Threads special (#1399)

* Draft

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>

* Threads special

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-04-14 22:55:56 +06:00 committed by GitHub
parent a8cc01bd0a
commit 7318f3365a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 387 additions and 386 deletions

View File

@ -168,6 +168,15 @@ export function createModel (builder: Builder): void {
icon: chunter.icon.Chunter,
hidden: false,
navigatorModel: {
specials: [
{
id: 'threads',
label: chunter.string.Threads,
icon: chunter.icon.Thread,
component: chunter.component.Threads,
position: 'top'
}
],
spaces: [
{
label: chunter.string.Channels,

View File

@ -26,6 +26,7 @@ export default mergeIds(chunterId, chunter, {
component: {
CommentPresenter: '' as AnyComponent,
ChannelPresenter: '' as AnyComponent,
Threads: '' as AnyComponent,
ThreadView: '' as AnyComponent
},
action: {

View File

@ -16,7 +16,7 @@
import core, {
AnyAttribute, ArrOf, AttachedDoc, Class, Client, Collection, Doc, DocumentQuery,
FindOptions, getCurrentAccount, Ref, RefTo, Tx, TxOperations, TxResult
FindOptions, FindResult, getCurrentAccount, Ref, RefTo, Tx, TxOperations, TxResult
} from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
@ -62,7 +62,7 @@ export class LiveQuery {
query<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
callback: (result: T[]) => void,
callback: (result: FindResult<T>) => void,
options?: FindOptions<T>
): void {
this.unsubscribe()

View File

@ -386,10 +386,10 @@ describe('query', () => {
const comment = result[0]
if (comment !== undefined) {
if (attempt > 0) {
expect((comment as WithLookup<AttachedComment>).$lookup?.space?._id).toEqual(futureSpace._id)
expect(comment.$lookup?.space?._id).toEqual(futureSpace._id)
resolve(null)
} else {
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toBeUndefined()
expect(comment.$lookup?.space).toBeUndefined()
attempt++
}
}
@ -433,10 +433,10 @@ describe('query', () => {
const comment = result[0]
if (comment !== undefined) {
if (attempt > 0) {
expect(((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space?._id).toEqual(futureSpace._id)
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space?._id).toEqual(futureSpace._id)
resolve(null)
} else {
expect(((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
attempt++
}
}
@ -466,7 +466,7 @@ describe('query', () => {
(result) => {
const comment = result[0]
if (comment !== undefined) {
expect(((comment as WithLookup<AttachedComment>).$lookup as any)?.comments).toHaveLength(attempt++)
expect((comment.$lookup as any)?.comments).toHaveLength(attempt++)
}
if (attempt === childLength) {
resolve(null)
@ -505,10 +505,10 @@ describe('query', () => {
const comment = result[0]
if (comment !== undefined) {
if (attempt > 0) {
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toBeUndefined()
expect(comment.$lookup?.space).toBeUndefined()
resolve(null)
} else {
expect(((comment as WithLookup<AttachedComment>).$lookup?.space as Doc)?._id).toEqual(futureSpace)
expect((comment.$lookup?.space as Doc)?._id).toEqual(futureSpace)
attempt++
}
}
@ -546,10 +546,10 @@ describe('query', () => {
const comment = result[0]
if (comment !== undefined) {
if (attempt > 0) {
expect(((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
resolve(null)
} else {
expect((((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Doc)?._id).toEqual(futureSpace)
expect(((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Doc)?._id).toEqual(futureSpace)
attempt++
}
}
@ -586,7 +586,7 @@ describe('query', () => {
(result) => {
const comment = result[0]
if (comment !== undefined) {
expect(((comment as WithLookup<AttachedComment>).$lookup as any)?.comments).toHaveLength(childLength - attempt)
expect((comment.$lookup as any)?.comments).toHaveLength(childLength - attempt)
attempt++
}
if (attempt === childLength) {
@ -624,7 +624,7 @@ describe('query', () => {
(result) => {
const comment = result[0]
if (comment !== undefined) {
expect(((comment as WithLookup<AttachedComment>).$lookup?.space as Space).name).toEqual(attempt.toString())
expect((comment.$lookup?.space as Space).name).toEqual(attempt.toString())
}
if (attempt > 0) {
resolve(null)
@ -665,7 +665,7 @@ describe('query', () => {
(result) => {
const comment = result[0]
if (comment !== undefined) {
expect((((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Space).name).toEqual(attempt.toString())
expect(((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Space).name).toEqual(attempt.toString())
}
if (attempt > 0) {
resolve(null)
@ -700,7 +700,7 @@ describe('query', () => {
(result) => {
const comment = result[0]
if (comment !== undefined) {
expect((((comment as WithLookup<AttachedComment>).$lookup as any)?.comments[0] as AttachedComment).message).toEqual(attempt.toString())
expect(((comment.$lookup as any)?.comments[0] as AttachedComment).message).toEqual(attempt.toString())
}
if (attempt > 0) {
resolve(null)

View File

@ -118,7 +118,7 @@ export class LiveQuery extends TxProcessor implements Client {
query<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
callback: (result: T[]) => void,
callback: (result: FindResult<T>) => void,
options?: FindOptions<T>
): () => void {
const result = this.client.findAll(_class, query, options)

View File

@ -324,6 +324,7 @@ p:last-child { margin-block-end: 0; }
.pl-2 { padding-left: .5rem; }
.pl-4 { padding-left: 1rem; }
.pl-8 { padding-left: 2rem; }
.pr-1 { padding-right: .25rem; }
.pr-2 { padding-right: .5rem; }
.pr-4 { padding-right: 1rem; }

View File

@ -8,4 +8,7 @@
<symbol id="lock" viewBox="0 0 16 16">
<path d="M12,7.1h-0.7V5.4c0-1.8-1.5-3.3-3.3-3.3c-1.8,0-3.3,1.5-3.3,3.3v1.7H4c-0.8,0-1.5,0.7-1.5,1.5v4.1c0,0.8,0.7,1.5,1.5,1.5h8 c0.8,0,1.5-0.7,1.5-1.5V8.6C13.5,7.8,12.8,7.1,12,7.1z M5.7,5.4c0-1.2,1-2.3,2.3-2.3s2.3,1,2.3,2.3v1.7H5.7V5.4z M12.5,12.7 c0,0.3-0.2,0.5-0.5,0.5H4c-0.3,0-0.5-0.2-0.5-0.5V8.6c0-0.3,0.2-0.5,0.5-0.5h8c0.3,0,0.5,0.2,0.5,0.5V12.7z"/>
</symbol>
<symbol id="thread" viewBox="0 0 16 16">
<path d="M8,14.5c-0.9,0-1.9-0.2-2.7-0.6c-0.2-0.1-0.5-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.7,0.2c-0.5,0.2-1.2,0.4-1.7-0.1 c-0.5-0.5-0.3-1.2-0.1-1.7c0.1-0.3,0.2-0.5,0.2-0.7c0-0.2-0.1-0.4-0.2-0.7C1,8.3,1.5,5.3,3.4,3.4C4.6,2.2,6.3,1.5,8,1.5 s3.4,0.7,4.6,1.9c2.5,2.5,2.5,6.7,0,9.2l0,0C11.4,13.8,9.7,14.5,8,14.5z M4.6,12.7c0.4,0,0.7,0.1,1,0.3c2.1,1,4.6,0.5,6.2-1.1 c2.1-2.1,2.1-5.6,0-7.8c-1-1-2.4-1.6-3.9-1.6c-1.5,0-2.9,0.6-3.9,1.6C2.5,5.7,2,8.2,3,10.3c0.1,0.4,0.3,0.7,0.3,1.1 c0,0.4-0.1,0.7-0.2,1C3,12.6,2.9,13,2.9,13.1C3,13.2,3.4,13,3.6,12.9C3.9,12.8,4.3,12.7,4.6,12.7z M12.2,12.2L12.2,12.2L12.2,12.2z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -27,6 +27,7 @@
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
"Topic": "Topic",
"Thread": "Thread",
"Threads": "Threads",
"New": "New",
"MarkUnread": "Mark unread",
"GetNewReplies": "Get notified about new replies",
@ -35,6 +36,8 @@
"UnpinMessage": "Unpin message",
"Pinned": "Pinned:",
"EditMessage": "Edit message",
"DeleteMessage": "Delete message"
"DeleteMessage": "Delete message",
"AndYou": "{participants, plural, =0 {Just you} other {and you}}",
"ShowMoreReplies": "Show {count} more replies"
}
}

View File

@ -26,6 +26,7 @@
"LastReply": "Последний ответ",
"RepliesCount": "{replies, plural, =1 {# ответ} =2 {# ответа} =3 {# ответа} =4 {# ответа} other {# ответов}}",
"Thread": "Обсуждение",
"Threads": "Обсуждения",
"New": "Новое",
"MarkUnread": "Отметить как непрочитанное",
"GetNewReplies": "Получать уведомления о новых ответах",
@ -34,6 +35,8 @@
"UnpinMessage": "Открепить сообщение",
"Pinned": "Закреплено:",
"EditMessage": "Редактировать сообщение",
"DeleteMessage": "Удалить сообщение"
"DeleteMessage": "Удалить сообщение",
"AndYou": "{participants, plural, =0 {Только вы} other {и вы}}",
"ShowMoreReplies": "{count, plural, =3 {Показать еще # ответа} =4 {Показать еще # ответа} other {Показать еще # ответов}}"
}
}

View File

@ -20,6 +20,7 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(chunter.icon, {
Chunter: `${icons}#chunter`,
Hashtag: `${icons}#hashtag`,
Thread: `${icons}#thread`,
Lock: `${icons}#lock`
})

View File

@ -107,7 +107,7 @@
let newMessagesPos: number = -1
</script>
<div class="flex-col vScroll container" bind:this={div}>
<div class="flex-col vScroll" bind:this={div}>
{#if messages}
{#each messages as message, i (message._id)}
{#if newMessagesPos === i}
@ -117,10 +117,3 @@
{/each}
{/if}
</div>
<style lang="scss">
.container {
margin: 1rem 1rem 0;
padding: 1.5rem 1.5rem 0px;
}
</style>

View File

@ -1,15 +1,14 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Copyright © 2022 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.
-->
@ -17,7 +16,7 @@
import type { Channel } from '@anticrm/chunter'
import { Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { getCurrentLocation, Icon, navigate } from '@anticrm/ui'
import { getCurrentLocation, Icon, locationToUrl } from '@anticrm/ui'
import chunter from '../plugin'
export let value: Channel
@ -25,24 +24,28 @@
$: icon = client.getHierarchy().getClass(value._class).icon
function selectSpace (id: Ref<Space>) {
function getLink (id: Ref<Space>): string {
const loc = getCurrentLocation()
loc.path[1] = chunter.app.Chunter
loc.path[2] = id
loc.path.length = 3
loc.fragment = undefined
navigate(loc)
return locationToUrl(loc)
}
$: link = getLink(value._id)
</script>
<div
class="flex-row-center hover-trans"
on:click={() => {
selectSpace(value._id)
}}
>
{#if icon}
<Icon {icon} size={'small'} />
{/if}
{value.name}
</div>
{#if value}
<a
class="flex-presenter"
href="{link}"
>
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="label">{value.name}</span>
</a>
{/if}

View File

@ -23,8 +23,8 @@
export let isNew: boolean = false
</script>
<div class="w-full text-sm flex-center whitespace-nowrap mb-6" class:flex-reverse={reverse} class:new={isNew}>
<Label label={title} {params} />
<div class="w-full text-sm flex-center whitespace-nowrap" class:flex-reverse={reverse} class:new={isNew}>
<div class:ml-8={!reverse} class:mr-4={reverse}><Label label={title} {params} /></div>
<div class:ml-4={!reverse} class:mr-4={reverse} class:line />
</div>
@ -33,7 +33,7 @@
position: relative;
width: 100%;
height: 1px;
background-color: var(--theme-chat-divider);
background-color: var(--theme-dialog-divider);
}
.new {
.line {

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Attachment } from '@anticrm/attachment'
import { AttachmentList, AttachmentRefInput } from '@anticrm/attachment-resources'
import type { Message } from '@anticrm/chunter'
import type { ChunterMessage, Message } from '@anticrm/chunter'
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
@ -35,7 +35,7 @@
import Reactions from './Reactions.svelte'
import Replies from './Replies.svelte'
export let message: WithLookup<Message>
export let message: WithLookup<ChunterMessage>
export let employees: Map<Ref<Employee>, Employee>
export let thread: boolean = false
export let isPinned: boolean = false
@ -81,19 +81,22 @@
const deleteAction = {
label: chunter.string.DeleteMessage,
action: async () => {
(await client.findAll(chunter.class.ThreadMessage, {attachedTo: message._id})).forEach(c => {
(await client.findAll(chunter.class.ThreadMessage, { attachedTo: message._id as Ref<Message> })).forEach(c => {
UnpinMessage(c)
})
UnpinMessage(message)
await client.remove(message)
await client.removeDoc(message._class, message.space, message._id)
}
}
let menuShowed = false
const showMenu = async (ev: Event): Promise<void> => {
const actions = await getActions(client, message, chunter.class.Message)
const actions = await getActions(client, message, message._class)
actions.push(subscribeAction)
actions.push(pinActions)
menuShowed = true
showPopup(
Menu,
{
@ -109,7 +112,10 @@
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
]
},
ev.target as HTMLElement
ev.target as HTMLElement,
() => {
menuShowed = false
}
)
}
@ -128,7 +134,7 @@
isEditing = false
}
function getEmployee (message: WithLookup<Message>): Employee | undefined {
function getEmployee (message: WithLookup<ChunterMessage>): Employee | undefined {
const employee = (message.$lookup?.createBy as EmployeeAccount).employee
if (employee !== undefined) {
return employees.get(employee)
@ -138,6 +144,9 @@
function openThread () {
dispatch('openThread', message._id)
}
$: parentMessage = message as Message
$: hasReplies = (parentMessage?.replies?.length ?? 0) > 0
</script>
<div class="container">
@ -159,24 +168,16 @@
<div class="text"><MessageViewer message={message.content} /></div>
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
{/if}
{#if reactions || message.replies}
{#if reactions || (!thread && hasReplies)}
<div class="footer flex-col">
<div>
{#if reactions}<Reactions />{/if}
</div>
{#if !thread}
<div>
{#if message.replies?.length}<Replies
replies={message.replies}
lastReply={message.lastReply}
on:click={openThread}
/>{/if}
</div>
{#if reactions}<Reactions />{/if}
{#if !thread && hasReplies}
<Replies message={parentMessage} on:click={openThread} />
{/if}
</div>
{/if}
</div>
<div class="buttons">
<div class="buttons" class:menuShowed>
<div class="tool">
<ActionIcon
icon={IconMoreH}
@ -199,8 +200,7 @@
.container {
position: relative;
display: flex;
margin-bottom: 2rem;
z-index: 1;
padding: 2rem;
.avatar {
min-width: 2.25rem;
@ -229,6 +229,7 @@
}
.text {
line-height: 150%;
user-select: contain;
}
.attachments {
margin-top: 1rem;
@ -247,8 +248,8 @@
.buttons {
position: absolute;
visibility: hidden;
top: -0.5rem;
right: -0.5rem;
top: 0.5rem;
right: 1rem;
display: flex;
flex-direction: row-reverse;
user-select: none;
@ -256,25 +257,18 @@
.tool + .tool {
margin-right: 0.5rem;
}
&.menuShowed {
visibility: visible;
}
}
&: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: 0.75rem;
z-index: -1;
&:hover {
background-color: var(--board-card-bg-hover);
}
}
</style>

View File

@ -1,5 +1,5 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
// Copyright © 2022 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
@ -13,15 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import { Message } from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact'
import { Ref, Timestamp } from '@anticrm/core'
import { Ref } from '@anticrm/core'
import { Avatar, createQuery } from '@anticrm/presentation'
import { Label, TimeSince } from '@anticrm/ui'
import chunter from '../plugin'
export let replies: Ref<Employee>[] = []
export let lastReply: Timestamp = new Date().getTime()
$: employees = new Set(replies)
export let message: Message
$: lastReply = message.lastReply ?? new Date().getTime()
$: employees = new Set(message.replies)
const shown: number = 4
let showReplies: Employee[] = []
@ -56,9 +57,9 @@
{/if}
</div>
<div class="whitespace-nowrap ml-2 mr-2 over-underline">
<Label label={chunter.string.RepliesCount} params={{ replies: replies.length }} />
<Label label={chunter.string.RepliesCount} params={{ replies: message.replies?.length ?? 0 }} />
</div>
{#if replies.length > 1}
{#if (message.replies?.length ?? 0) > 1}
<div class="mr-1">
<Label label={chunter.string.LastReply} />
</div>

View File

@ -0,0 +1,199 @@
<!--
// Copyright © 2022 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 attachment from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { Channel, Message, ThreadMessage } from '@anticrm/chunter'
import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import core, { FindOptions, generateId, getCurrentAccount, Ref, SortingOrder, TxFactory } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { createQuery, getClient } from '@anticrm/presentation'
import { Label } from '@anticrm/ui'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import ChannelPresenter from './ChannelPresenter.svelte'
import MsgView from './Message.svelte'
const client = getClient()
const query = createQuery()
const messageQuery = createQuery()
export let _id: Ref<Message>
let parent: Message | undefined
let commentId = generateId() as Ref<ThreadMessage>
const notificationClient = NotificationClientImpl.getClient()
const lookup = {
_id: { attachments: attachment.class.Attachment },
createBy: core.class.Account
}
let showAll = false
let total = 0
$: updateQuery(_id)
$: updateThreadQuery(_id, showAll)
function updateQuery (id: Ref<Message>) {
messageQuery.query(
chunter.class.Message,
{
_id: id
},
(res) => (parent = res[0]),
{
lookup: {
_id: { attachments: attachment.class.Attachment },
createBy: core.class.Account
}
}
)
}
function updateThreadQuery (id: Ref<Message>, showAll: boolean) {
const options: FindOptions<ThreadMessage> = {
lookup,
sort: {
createOn: SortingOrder.Descending
}
}
if (!showAll) {
options.limit = 4
}
query.query(
chunter.class.ThreadMessage,
{
attachedTo: id
},
(res) => {
total = res.total
if (!showAll && res.total > 4) {
comments = res.splice(0, 2).reverse()
} else {
comments = res.reverse()
}
notificationClient.updateLastView(id, chunter.class.Message)
},
options
)
}
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 getParticipants (comments: ThreadMessage[], parent: Message | undefined, employees: Map<Ref<Employee>, Employee>): Promise<string[]> {
const refs = new Set(comments.map((p) => p.createBy))
if (parent !== undefined) {
refs.add(parent.createBy)
}
refs.delete(getCurrentAccount()._id)
const accounts = await client.findAll(contact.class.EmployeeAccount, { _id: { $in: Array.from(refs) as Ref<EmployeeAccount>[] } })
const res: string[] = []
for (const account of accounts) {
const employee = employees.get(account.employee)
if (employee !== undefined) {
res.push(formatName(employee.name))
}
}
return res
}
async function onMessage (event: CustomEvent) {
if (parent === undefined) return
const { message, attachments } = event.detail
const me = getCurrentAccount()._id
const txFactory = new TxFactory(me)
const tx = txFactory.createTxCreateDoc<ThreadMessage>(
chunter.class.ThreadMessage,
parent.space,
{
attachedTo: _id,
attachedToClass: chunter.class.Message,
collection: 'replies',
content: message,
createBy: me,
createOn: 0,
attachments
},
commentId
)
tx.attributes.createOn = tx.modifiedOn
await notificationClient.updateLastView(_id, chunter.class.Message, tx.modifiedOn, true)
await client.tx(tx)
// Create an backlink to document
await createBacklinks(client, parent.space, chunter.class.Channel, commentId, message)
commentId = generateId()
}
let comments: ThreadMessage[] = []
async function getChannel (_id: Ref<Channel>): Promise<Channel | undefined> {
return await client.findOne(chunter.class.Channel, { _id })
}
</script>
<div class="ml-8 mt-4">
{#if parent}
{#await getChannel(parent.space) then channel}
{#if channel}
<ChannelPresenter value={channel} />
{/if}
{/await}
{#await getParticipants(comments, parent, employees) then participants}
{participants.join(', ')}
<Label label={chunter.string.AndYou} params={{ participants: participants.length }} />
{/await}
{/if}
</div>
<div class="flex-col content">
{#if parent}
<MsgView message={parent} {employees} thread />
{#if total > comments.length}
<div class="label pb-2 pt-2 pl-8 over-underline" on:click={() => { showAll = true }}><Label label={chunter.string.ShowMoreReplies} params={{ count: total - comments.length }} /></div>
{/if}
{#each comments as comment (comment._id)}
<MsgView message={comment} {employees} thread />
{/each}
<div class="mr-4 ml-4 mb-4 mt-2">
<AttachmentRefInput space={parent.space} _class={chunter.class.Comment} objectId={commentId} on:message={onMessage} />
</div>
{/if}
</div>
<style lang="scss">
.content {
margin: 1rem 1rem 0px;
background-color: var(--theme-border-modal);
border-radius: 0.75rem;
border: 1px solid var(--theme-zone-border);
}
.label:hover {
background-color: var(--board-card-bg-hover);
}
</style>

View File

@ -1,257 +0,0 @@
<!--
// 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 { Attachment } from '@anticrm/attachment'
import { AttachmentList, AttachmentRefInput } from '@anticrm/attachment-resources'
import type { ThreadMessage } from '@anticrm/chunter'
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { getResource } from '@anticrm/platform'
import { Avatar, getClient, MessageViewer } from '@anticrm/presentation'
import { ActionIcon, IconMoreH, Menu, showPopup } from '@anticrm/ui'
import { Action } from '@anticrm/view'
import { getActions } from '@anticrm/view-resources'
import { UnpinMessage } from '../index';
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 message: WithLookup<ThreadMessage>
export let employees: Map<Ref<Employee>, Employee>
export let isPinned: boolean = false
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
const client = getClient()
const reactions: boolean = false
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
$: subscribed = ($lastViews.get(message.attachedTo) ?? -1) > -1
$: subscribeAction = subscribed
? ({
label: chunter.string.TurnOffReplies,
action: chunter.actionImpl.UnsubscribeComment
} as Action)
: ({
label: chunter.string.GetNewReplies,
action: chunter.actionImpl.SubscribeComment
} as Action)
$: pinActions = isPinned
? ({
label: chunter.string.UnpinMessage,
action: chunter.actionImpl.UnpinMessage
} as Action)
: ({
label: chunter.string.PinMessage,
action: chunter.actionImpl.PinMessage
} as Action)
$: isEditing = false;
const editAction = {
label: chunter.string.EditMessage,
action: () => isEditing = true
}
const deleteAction = {
label: chunter.string.DeleteMessage,
action: async () => {
await client.removeDoc(message._class, message.space, message._id)
UnpinMessage(message)
}
}
const showMenu = async (ev: Event): Promise<void> => {
const actions = await getActions(client, message, chunter.class.ThreadMessage)
actions.push(subscribeAction)
actions.push(pinActions)
showPopup(
Menu,
{
actions: [
...actions.map((a) => ({
label: a.label,
icon: a.icon,
action: async () => {
const impl = await getResource(a.action)
await impl(message)
}
})),
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
]
},
ev.target as HTMLElement
)
}
async function onMessageEdit (event: CustomEvent) {
const { message: newContent, attachments: newAttachments } = event.detail
if (newContent !== message.content || newAttachments !== attachments) {
await client.update(
message,
{
content: newContent,
attachments: newAttachments
}
)
}
isEditing = false
}
$: employee = getEmployee(message)
function getEmployee (comment: WithLookup<ThreadMessage>): Employee | undefined {
const employee = (comment.$lookup?.createBy as EmployeeAccount)?.employee
if (employee !== undefined) {
return employees.get(employee)
}
}
</script>
<div class="container">
<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 isEditing}
<AttachmentRefInput
space={message.space}
_class={chunter.class.Comment}
objectId={message._id}
content={message.content}
on:message={onMessageEdit}
/>
{:else}
<div class="text"><MessageViewer message={message.content} /></div>
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
{/if}
{#if reactions}
<div class="footer">
<div><Reactions /></div>
</div>
{/if}
</div>
<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: 0.25rem;
span {
margin-left: 0.5rem;
font-weight: 400;
font-size: 0.875rem;
line-height: 1.125rem;
opacity: 0.4;
}
}
.text {
line-height: 150%;
}
.attachments {
margin-top: 1rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
height: 2rem;
margin-top: 0.5rem;
user-select: none;
div + div {
margin-left: 1rem;
}
}
}
.buttons {
position: absolute;
visibility: hidden;
top: -0.5rem;
right: -0.5rem;
display: flex;
flex-direction: row-reverse;
user-select: none;
.tool + .tool {
margin-right: 0.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: 0.75rem;
z-index: -1;
}
}
</style>

View File

@ -26,7 +26,6 @@
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()
@ -92,7 +91,7 @@
},
(res) => {
comments = res
newMessagesPos = newMessagesStart(comments)
newMessagesPos = newMessagesStart(comments, $lastViews)
notificationClient.updateLastView(id, chunter.class.Message)
},
{
@ -153,8 +152,8 @@
}
let comments: ThreadMessage[] = []
function newMessagesStart (comments: ThreadMessage[]): number {
const lastView = $lastViews.get(_id)
function newMessagesStart (comments: ThreadMessage[], lastViews: Map<Ref<Doc>, number>): number {
const lastView = lastViews.get(_id)
if (lastView === undefined || lastView === -1) return -1
for (let index = 0; index < comments.length; index++) {
const comment = comments[index]
@ -165,7 +164,7 @@
$: markUnread($lastViews)
function markUnread (lastViews: Map<Ref<Doc>, number>) {
const newPos = newMessagesStart(comments)
const newPos = newMessagesStart(comments, lastViews)
if (newPos !== -1 || newMessagesPos === -1) {
newMessagesPos = newPos
}
@ -194,11 +193,7 @@
{#if newMessagesPos === i}
<ChannelSeparator title={chunter.string.New} line reverse isNew />
{/if}
<ThreadComment
message={comment}
{employees}
isPinned={pinnedIds.includes(comment._id)}
/>
<MsgView message={comment} {employees} thread isPinned={pinnedIds.includes(comment._id)} />
{/each}
{/if}
</div>
@ -231,10 +226,6 @@
}
}
}
.content {
margin: 1rem 1rem 0px;
padding: 1.5rem 1.5rem 0px;
}
.ref-input {
margin: 1.25rem 2.5rem;
}

View File

@ -0,0 +1,55 @@
<!--
// Copyright © 2022 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 type { Message } from '@anticrm/chunter'
import { getCurrentAccount, Ref, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Label, Scroller } from '@anticrm/ui'
import chunter from '../plugin'
import Thread from './Thread.svelte'
const query = createQuery()
const me = getCurrentAccount()._id
let threads: Ref<Message>[] = []
query.query(chunter.class.ThreadMessage, {
createBy: me
}, (res) => {
const ids = new Set(res.map((c) => c.attachedTo))
threads = Array.from(ids)
}, {
sort: {
createOn: SortingOrder.Descending
}
})
</script>
<div class="ac-header full divide">
<div class="ac-header__wrap-title">
<span class="ac-header__title"><Label label={chunter.string.Threads} /></span>
</div>
</div>
<Scroller>
{#each threads as thread (thread)}
<div class="item"><Thread _id={thread}/></div>
{/each}
</Scroller>
<style lang="scss">
.item + .item {
margin-top: 3rem;
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'var(--theme-caption-color)'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">

View File

@ -15,7 +15,7 @@
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'var(--theme-caption-color)'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">

View File

@ -30,6 +30,7 @@ import CommentsPresenter from './components/CommentsPresenter.svelte'
import CreateChannel from './components/CreateChannel.svelte'
import EditChannel from './components/EditChannel.svelte'
import ThreadView from './components/ThreadView.svelte'
import Threads from './components/Threads.svelte'
export { CommentsPresenter }
@ -40,28 +41,27 @@ async function MarkUnread (object: Message): Promise<void> {
async function MarkCommentUnread (object: ThreadMessage): Promise<void> {
const client = NotificationClientImpl.getClient()
const value = object.modifiedOn - 1
await client.updateLastView(object.attachedTo, object.attachedToClass, value, true)
await client.updateLastView(object.attachedTo, object.attachedToClass, object.createOn - 1, true)
}
async function SubscribeMessage (object: Message): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.updateLastView(object._id, object._class, undefined, true)
}
async function SubscribeComment (object: ThreadMessage): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.updateLastView(object.attachedTo, object.attachedToClass, undefined, true)
const client = getClient()
const notificationClient = NotificationClientImpl.getClient()
if (client.getHierarchy().isDerived(object._class, chunter.class.ThreadMessage)) {
await notificationClient.updateLastView(object.attachedTo, object.attachedToClass, undefined, true)
} else {
await notificationClient.updateLastView(object._id, object._class, undefined, true)
}
}
async function UnsubscribeMessage (object: Message): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.unsubscribe(object._id)
}
async function UnsubscribeComment (object: ThreadMessage): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.unsubscribe(object.attachedTo)
const client = getClient()
const notificationClient = NotificationClientImpl.getClient()
if (client.getHierarchy().isDerived(object._class, chunter.class.ThreadMessage)) {
await notificationClient.unsubscribe(object.attachedTo)
} else {
await notificationClient.unsubscribe(object._id)
}
}
async function PinMessage (message: ChunterMessage): Promise<void> {
@ -90,6 +90,7 @@ export default async (): Promise<Resources> => ({
CommentsPresenter,
ChannelPresenter,
EditChannel,
Threads,
ThreadView
},
activity: {
@ -101,9 +102,7 @@ export default async (): Promise<Resources> => ({
MarkUnread,
MarkCommentUnread,
SubscribeMessage,
SubscribeComment,
UnsubscribeMessage,
UnsubscribeComment,
PinMessage,
UnpinMessage
}

View File

@ -28,9 +28,7 @@ export default mergeIds(chunterId, chunter, {
},
actionImpl: {
SubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
SubscribeComment: '' as Resource<(object: Doc) => Promise<void>>,
UnsubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
UnsubscribeComment: '' as Resource<(object: Doc) => Promise<void>>,
PinMessage: '' as Resource<(object: Doc) => Promise<void>>,
UnpinMessage: '' as Resource<(object: Doc) => Promise<void>>
},
@ -48,6 +46,7 @@ export default mergeIds(chunterId, chunter, {
Replies: '' as IntlString,
Topic: '' as IntlString,
Thread: '' as IntlString,
Threads: '' as IntlString,
RepliesCount: '' as IntlString,
LastReply: '' as IntlString,
New: '' as IntlString,
@ -57,6 +56,8 @@ export default mergeIds(chunterId, chunter, {
UnpinMessage: '' as IntlString,
Pinned: '' as IntlString,
DeleteMessage: '' as IntlString,
EditMessage: '' as IntlString
EditMessage: '' as IntlString,
AndYou: '' as IntlString,
ShowMoreReplies: '' as IntlString
}
})

View File

@ -90,6 +90,7 @@ export default plugin(chunterId, {
icon: {
Chunter: '' as Asset,
Hashtag: '' as Asset,
Thread: '' as Asset,
Lock: '' as Asset
},
component: {