mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
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:
parent
a8cc01bd0a
commit
7318f3365a
@ -168,6 +168,15 @@ export function createModel (builder: Builder): void {
|
|||||||
icon: chunter.icon.Chunter,
|
icon: chunter.icon.Chunter,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
navigatorModel: {
|
navigatorModel: {
|
||||||
|
specials: [
|
||||||
|
{
|
||||||
|
id: 'threads',
|
||||||
|
label: chunter.string.Threads,
|
||||||
|
icon: chunter.icon.Thread,
|
||||||
|
component: chunter.component.Threads,
|
||||||
|
position: 'top'
|
||||||
|
}
|
||||||
|
],
|
||||||
spaces: [
|
spaces: [
|
||||||
{
|
{
|
||||||
label: chunter.string.Channels,
|
label: chunter.string.Channels,
|
||||||
|
@ -26,6 +26,7 @@ export default mergeIds(chunterId, chunter, {
|
|||||||
component: {
|
component: {
|
||||||
CommentPresenter: '' as AnyComponent,
|
CommentPresenter: '' as AnyComponent,
|
||||||
ChannelPresenter: '' as AnyComponent,
|
ChannelPresenter: '' as AnyComponent,
|
||||||
|
Threads: '' as AnyComponent,
|
||||||
ThreadView: '' as AnyComponent
|
ThreadView: '' as AnyComponent
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import core, {
|
import core, {
|
||||||
AnyAttribute, ArrOf, AttachedDoc, Class, Client, Collection, Doc, DocumentQuery,
|
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'
|
} from '@anticrm/core'
|
||||||
import login from '@anticrm/login'
|
import login from '@anticrm/login'
|
||||||
import { getMetadata } from '@anticrm/platform'
|
import { getMetadata } from '@anticrm/platform'
|
||||||
@ -62,7 +62,7 @@ export class LiveQuery {
|
|||||||
query<T extends Doc>(
|
query<T extends Doc>(
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
callback: (result: T[]) => void,
|
callback: (result: FindResult<T>) => void,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
): void {
|
): void {
|
||||||
this.unsubscribe()
|
this.unsubscribe()
|
||||||
|
@ -386,10 +386,10 @@ describe('query', () => {
|
|||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
if (comment !== undefined) {
|
||||||
if (attempt > 0) {
|
if (attempt > 0) {
|
||||||
expect((comment as WithLookup<AttachedComment>).$lookup?.space?._id).toEqual(futureSpace._id)
|
expect(comment.$lookup?.space?._id).toEqual(futureSpace._id)
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toBeUndefined()
|
expect(comment.$lookup?.space).toBeUndefined()
|
||||||
attempt++
|
attempt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -433,10 +433,10 @@ describe('query', () => {
|
|||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
if (comment !== undefined) {
|
||||||
if (attempt > 0) {
|
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)
|
resolve(null)
|
||||||
} else {
|
} 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++
|
attempt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,7 +466,7 @@ describe('query', () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
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) {
|
if (attempt === childLength) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
@ -505,10 +505,10 @@ describe('query', () => {
|
|||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
if (comment !== undefined) {
|
||||||
if (attempt > 0) {
|
if (attempt > 0) {
|
||||||
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toBeUndefined()
|
expect(comment.$lookup?.space).toBeUndefined()
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
expect(((comment as WithLookup<AttachedComment>).$lookup?.space as Doc)?._id).toEqual(futureSpace)
|
expect((comment.$lookup?.space as Doc)?._id).toEqual(futureSpace)
|
||||||
attempt++
|
attempt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -546,10 +546,10 @@ describe('query', () => {
|
|||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
if (comment !== undefined) {
|
||||||
if (attempt > 0) {
|
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)
|
resolve(null)
|
||||||
} else {
|
} 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++
|
attempt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -586,7 +586,7 @@ describe('query', () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
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++
|
attempt++
|
||||||
}
|
}
|
||||||
if (attempt === childLength) {
|
if (attempt === childLength) {
|
||||||
@ -624,7 +624,7 @@ describe('query', () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
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) {
|
if (attempt > 0) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
@ -665,7 +665,7 @@ describe('query', () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
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) {
|
if (attempt > 0) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
@ -700,7 +700,7 @@ describe('query', () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
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) {
|
if (attempt > 0) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
|
@ -118,7 +118,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
|||||||
query<T extends Doc>(
|
query<T extends Doc>(
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
callback: (result: T[]) => void,
|
callback: (result: FindResult<T>) => void,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
): () => void {
|
): () => void {
|
||||||
const result = this.client.findAll(_class, query, options)
|
const result = this.client.findAll(_class, query, options)
|
||||||
|
@ -324,6 +324,7 @@ p:last-child { margin-block-end: 0; }
|
|||||||
|
|
||||||
.pl-2 { padding-left: .5rem; }
|
.pl-2 { padding-left: .5rem; }
|
||||||
.pl-4 { padding-left: 1rem; }
|
.pl-4 { padding-left: 1rem; }
|
||||||
|
.pl-8 { padding-left: 2rem; }
|
||||||
.pr-1 { padding-right: .25rem; }
|
.pr-1 { padding-right: .25rem; }
|
||||||
.pr-2 { padding-right: .5rem; }
|
.pr-2 { padding-right: .5rem; }
|
||||||
.pr-4 { padding-right: 1rem; }
|
.pr-4 { padding-right: 1rem; }
|
||||||
|
@ -8,4 +8,7 @@
|
|||||||
<symbol id="lock" viewBox="0 0 16 16">
|
<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"/>
|
<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>
|
||||||
|
<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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.4 KiB |
@ -27,6 +27,7 @@
|
|||||||
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
|
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
|
||||||
"Topic": "Topic",
|
"Topic": "Topic",
|
||||||
"Thread": "Thread",
|
"Thread": "Thread",
|
||||||
|
"Threads": "Threads",
|
||||||
"New": "New",
|
"New": "New",
|
||||||
"MarkUnread": "Mark unread",
|
"MarkUnread": "Mark unread",
|
||||||
"GetNewReplies": "Get notified about new replies",
|
"GetNewReplies": "Get notified about new replies",
|
||||||
@ -35,6 +36,8 @@
|
|||||||
"UnpinMessage": "Unpin message",
|
"UnpinMessage": "Unpin message",
|
||||||
"Pinned": "Pinned:",
|
"Pinned": "Pinned:",
|
||||||
"EditMessage": "Edit message",
|
"EditMessage": "Edit message",
|
||||||
"DeleteMessage": "Delete message"
|
"DeleteMessage": "Delete message",
|
||||||
|
"AndYou": "{participants, plural, =0 {Just you} other {and you}}",
|
||||||
|
"ShowMoreReplies": "Show {count} more replies"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -26,6 +26,7 @@
|
|||||||
"LastReply": "Последний ответ",
|
"LastReply": "Последний ответ",
|
||||||
"RepliesCount": "{replies, plural, =1 {# ответ} =2 {# ответа} =3 {# ответа} =4 {# ответа} other {# ответов}}",
|
"RepliesCount": "{replies, plural, =1 {# ответ} =2 {# ответа} =3 {# ответа} =4 {# ответа} other {# ответов}}",
|
||||||
"Thread": "Обсуждение",
|
"Thread": "Обсуждение",
|
||||||
|
"Threads": "Обсуждения",
|
||||||
"New": "Новое",
|
"New": "Новое",
|
||||||
"MarkUnread": "Отметить как непрочитанное",
|
"MarkUnread": "Отметить как непрочитанное",
|
||||||
"GetNewReplies": "Получать уведомления о новых ответах",
|
"GetNewReplies": "Получать уведомления о новых ответах",
|
||||||
@ -34,6 +35,8 @@
|
|||||||
"UnpinMessage": "Открепить сообщение",
|
"UnpinMessage": "Открепить сообщение",
|
||||||
"Pinned": "Закреплено:",
|
"Pinned": "Закреплено:",
|
||||||
"EditMessage": "Редактировать сообщение",
|
"EditMessage": "Редактировать сообщение",
|
||||||
"DeleteMessage": "Удалить сообщение"
|
"DeleteMessage": "Удалить сообщение",
|
||||||
|
"AndYou": "{participants, plural, =0 {Только вы} other {и вы}}",
|
||||||
|
"ShowMoreReplies": "{count, plural, =3 {Показать еще # ответа} =4 {Показать еще # ответа} other {Показать еще # ответов}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line
|
|||||||
loadMetadata(chunter.icon, {
|
loadMetadata(chunter.icon, {
|
||||||
Chunter: `${icons}#chunter`,
|
Chunter: `${icons}#chunter`,
|
||||||
Hashtag: `${icons}#hashtag`,
|
Hashtag: `${icons}#hashtag`,
|
||||||
|
Thread: `${icons}#thread`,
|
||||||
Lock: `${icons}#lock`
|
Lock: `${icons}#lock`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
let newMessagesPos: number = -1
|
let newMessagesPos: number = -1
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-col vScroll container" bind:this={div}>
|
<div class="flex-col vScroll" bind:this={div}>
|
||||||
{#if messages}
|
{#if messages}
|
||||||
{#each messages as message, i (message._id)}
|
{#each messages as message, i (message._id)}
|
||||||
{#if newMessagesPos === i}
|
{#if newMessagesPos === i}
|
||||||
@ -117,10 +117,3 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.container {
|
|
||||||
margin: 1rem 1rem 0;
|
|
||||||
padding: 1.5rem 1.5rem 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
<!--
|
<!--
|
||||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
// Copyright © 2021 Hardcore Engineering Inc.
|
//
|
||||||
//
|
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
// 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
|
// 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
|
// 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
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
//
|
//
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
@ -17,7 +16,7 @@
|
|||||||
import type { Channel } from '@anticrm/chunter'
|
import type { Channel } from '@anticrm/chunter'
|
||||||
import { Ref, Space } from '@anticrm/core'
|
import { Ref, Space } from '@anticrm/core'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { getCurrentLocation, Icon, navigate } from '@anticrm/ui'
|
import { getCurrentLocation, Icon, locationToUrl } from '@anticrm/ui'
|
||||||
import chunter from '../plugin'
|
import chunter from '../plugin'
|
||||||
|
|
||||||
export let value: Channel
|
export let value: Channel
|
||||||
@ -25,24 +24,28 @@
|
|||||||
|
|
||||||
$: icon = client.getHierarchy().getClass(value._class).icon
|
$: icon = client.getHierarchy().getClass(value._class).icon
|
||||||
|
|
||||||
function selectSpace (id: Ref<Space>) {
|
function getLink (id: Ref<Space>): string {
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
loc.path[1] = chunter.app.Chunter
|
loc.path[1] = chunter.app.Chunter
|
||||||
loc.path[2] = id
|
loc.path[2] = id
|
||||||
loc.path.length = 3
|
loc.path.length = 3
|
||||||
loc.fragment = undefined
|
loc.fragment = undefined
|
||||||
navigate(loc)
|
return locationToUrl(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: link = getLink(value._id)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#if value}
|
||||||
class="flex-row-center hover-trans"
|
<a
|
||||||
on:click={() => {
|
class="flex-presenter"
|
||||||
selectSpace(value._id)
|
href="{link}"
|
||||||
}}
|
>
|
||||||
>
|
<div class="icon">
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon {icon} size={'small'} />
|
<Icon {icon} size={'small'} />
|
||||||
{/if}
|
{/if}
|
||||||
{value.name}
|
</div>
|
||||||
</div>
|
<span class="label">{value.name}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
export let isNew: boolean = false
|
export let isNew: boolean = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full text-sm flex-center whitespace-nowrap mb-6" class:flex-reverse={reverse} class:new={isNew}>
|
<div class="w-full text-sm flex-center whitespace-nowrap" class:flex-reverse={reverse} class:new={isNew}>
|
||||||
<Label label={title} {params} />
|
<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 class:ml-4={!reverse} class:mr-4={reverse} class:line />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -33,7 +33,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--theme-chat-divider);
|
background-color: var(--theme-dialog-divider);
|
||||||
}
|
}
|
||||||
.new {
|
.new {
|
||||||
.line {
|
.line {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Attachment } from '@anticrm/attachment'
|
import { Attachment } from '@anticrm/attachment'
|
||||||
import { AttachmentList, AttachmentRefInput } from '@anticrm/attachment-resources'
|
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 { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||||
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
|
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
|
||||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||||
@ -35,7 +35,7 @@
|
|||||||
import Reactions from './Reactions.svelte'
|
import Reactions from './Reactions.svelte'
|
||||||
import Replies from './Replies.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 employees: Map<Ref<Employee>, Employee>
|
||||||
export let thread: boolean = false
|
export let thread: boolean = false
|
||||||
export let isPinned: boolean = false
|
export let isPinned: boolean = false
|
||||||
@ -81,19 +81,22 @@
|
|||||||
const deleteAction = {
|
const deleteAction = {
|
||||||
label: chunter.string.DeleteMessage,
|
label: chunter.string.DeleteMessage,
|
||||||
action: async () => {
|
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(c)
|
||||||
})
|
})
|
||||||
UnpinMessage(message)
|
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 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(subscribeAction)
|
||||||
actions.push(pinActions)
|
actions.push(pinActions)
|
||||||
|
|
||||||
|
menuShowed = true
|
||||||
showPopup(
|
showPopup(
|
||||||
Menu,
|
Menu,
|
||||||
{
|
{
|
||||||
@ -109,7 +112,10 @@
|
|||||||
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
|
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
ev.target as HTMLElement
|
ev.target as HTMLElement,
|
||||||
|
() => {
|
||||||
|
menuShowed = false
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +134,7 @@
|
|||||||
isEditing = false
|
isEditing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEmployee (message: WithLookup<Message>): Employee | undefined {
|
function getEmployee (message: WithLookup<ChunterMessage>): Employee | undefined {
|
||||||
const employee = (message.$lookup?.createBy as EmployeeAccount).employee
|
const employee = (message.$lookup?.createBy as EmployeeAccount).employee
|
||||||
if (employee !== undefined) {
|
if (employee !== undefined) {
|
||||||
return employees.get(employee)
|
return employees.get(employee)
|
||||||
@ -138,6 +144,9 @@
|
|||||||
function openThread () {
|
function openThread () {
|
||||||
dispatch('openThread', message._id)
|
dispatch('openThread', message._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: parentMessage = message as Message
|
||||||
|
$: hasReplies = (parentMessage?.replies?.length ?? 0) > 0
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -159,24 +168,16 @@
|
|||||||
<div class="text"><MessageViewer message={message.content} /></div>
|
<div class="text"><MessageViewer message={message.content} /></div>
|
||||||
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
|
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if reactions || message.replies}
|
{#if reactions || (!thread && hasReplies)}
|
||||||
<div class="footer flex-col">
|
<div class="footer flex-col">
|
||||||
<div>
|
{#if reactions}<Reactions />{/if}
|
||||||
{#if reactions}<Reactions />{/if}
|
{#if !thread && hasReplies}
|
||||||
</div>
|
<Replies message={parentMessage} on:click={openThread} />
|
||||||
{#if !thread}
|
|
||||||
<div>
|
|
||||||
{#if message.replies?.length}<Replies
|
|
||||||
replies={message.replies}
|
|
||||||
lastReply={message.lastReply}
|
|
||||||
on:click={openThread}
|
|
||||||
/>{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons" class:menuShowed>
|
||||||
<div class="tool">
|
<div class="tool">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={IconMoreH}
|
icon={IconMoreH}
|
||||||
@ -199,8 +200,7 @@
|
|||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 2rem;
|
padding: 2rem;
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
min-width: 2.25rem;
|
min-width: 2.25rem;
|
||||||
@ -229,6 +229,7 @@
|
|||||||
}
|
}
|
||||||
.text {
|
.text {
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
|
user-select: contain;
|
||||||
}
|
}
|
||||||
.attachments {
|
.attachments {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@ -247,8 +248,8 @@
|
|||||||
.buttons {
|
.buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
top: -0.5rem;
|
top: 0.5rem;
|
||||||
right: -0.5rem;
|
right: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -256,25 +257,18 @@
|
|||||||
.tool + .tool {
|
.tool + .tool {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.menuShowed {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .buttons {
|
&:hover > .buttons {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
&:hover::before {
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
&:hover {
|
||||||
position: absolute;
|
background-color: var(--board-card-bg-hover);
|
||||||
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>
|
</style>
|
||||||
|
@ -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");
|
// 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
|
// you may not use this file except in compliance with the License. You may
|
||||||
@ -13,15 +13,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Message } from '@anticrm/chunter'
|
||||||
import contact, { Employee } from '@anticrm/contact'
|
import contact, { Employee } from '@anticrm/contact'
|
||||||
import { Ref, Timestamp } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { Avatar, createQuery } from '@anticrm/presentation'
|
import { Avatar, createQuery } from '@anticrm/presentation'
|
||||||
import { Label, TimeSince } from '@anticrm/ui'
|
import { Label, TimeSince } from '@anticrm/ui'
|
||||||
import chunter from '../plugin'
|
import chunter from '../plugin'
|
||||||
|
|
||||||
export let replies: Ref<Employee>[] = []
|
export let message: Message
|
||||||
export let lastReply: Timestamp = new Date().getTime()
|
$: lastReply = message.lastReply ?? new Date().getTime()
|
||||||
$: employees = new Set(replies)
|
$: employees = new Set(message.replies)
|
||||||
|
|
||||||
const shown: number = 4
|
const shown: number = 4
|
||||||
let showReplies: Employee[] = []
|
let showReplies: Employee[] = []
|
||||||
@ -56,9 +57,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="whitespace-nowrap ml-2 mr-2 over-underline">
|
<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>
|
</div>
|
||||||
{#if replies.length > 1}
|
{#if (message.replies?.length ?? 0) > 1}
|
||||||
<div class="mr-1">
|
<div class="mr-1">
|
||||||
<Label label={chunter.string.LastReply} />
|
<Label label={chunter.string.LastReply} />
|
||||||
</div>
|
</div>
|
||||||
|
199
plugins/chunter-resources/src/components/Thread.svelte
Normal file
199
plugins/chunter-resources/src/components/Thread.svelte
Normal 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>
|
@ -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>
|
|
@ -26,7 +26,6 @@
|
|||||||
import chunter from '../plugin'
|
import chunter from '../plugin'
|
||||||
import ChannelSeparator from './ChannelSeparator.svelte'
|
import ChannelSeparator from './ChannelSeparator.svelte'
|
||||||
import MsgView from './Message.svelte'
|
import MsgView from './Message.svelte'
|
||||||
import ThreadComment from './ThreadComment.svelte'
|
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
@ -92,7 +91,7 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
comments = res
|
comments = res
|
||||||
newMessagesPos = newMessagesStart(comments)
|
newMessagesPos = newMessagesStart(comments, $lastViews)
|
||||||
notificationClient.updateLastView(id, chunter.class.Message)
|
notificationClient.updateLastView(id, chunter.class.Message)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -153,8 +152,8 @@
|
|||||||
}
|
}
|
||||||
let comments: ThreadMessage[] = []
|
let comments: ThreadMessage[] = []
|
||||||
|
|
||||||
function newMessagesStart (comments: ThreadMessage[]): number {
|
function newMessagesStart (comments: ThreadMessage[], lastViews: Map<Ref<Doc>, number>): number {
|
||||||
const lastView = $lastViews.get(_id)
|
const lastView = lastViews.get(_id)
|
||||||
if (lastView === undefined || lastView === -1) return -1
|
if (lastView === undefined || lastView === -1) return -1
|
||||||
for (let index = 0; index < comments.length; index++) {
|
for (let index = 0; index < comments.length; index++) {
|
||||||
const comment = comments[index]
|
const comment = comments[index]
|
||||||
@ -165,7 +164,7 @@
|
|||||||
|
|
||||||
$: markUnread($lastViews)
|
$: markUnread($lastViews)
|
||||||
function markUnread (lastViews: Map<Ref<Doc>, number>) {
|
function markUnread (lastViews: Map<Ref<Doc>, number>) {
|
||||||
const newPos = newMessagesStart(comments)
|
const newPos = newMessagesStart(comments, lastViews)
|
||||||
if (newPos !== -1 || newMessagesPos === -1) {
|
if (newPos !== -1 || newMessagesPos === -1) {
|
||||||
newMessagesPos = newPos
|
newMessagesPos = newPos
|
||||||
}
|
}
|
||||||
@ -194,11 +193,7 @@
|
|||||||
{#if newMessagesPos === i}
|
{#if newMessagesPos === i}
|
||||||
<ChannelSeparator title={chunter.string.New} line reverse isNew />
|
<ChannelSeparator title={chunter.string.New} line reverse isNew />
|
||||||
{/if}
|
{/if}
|
||||||
<ThreadComment
|
<MsgView message={comment} {employees} thread isPinned={pinnedIds.includes(comment._id)} />
|
||||||
message={comment}
|
|
||||||
{employees}
|
|
||||||
isPinned={pinnedIds.includes(comment._id)}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -231,10 +226,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.content {
|
|
||||||
margin: 1rem 1rem 0px;
|
|
||||||
padding: 1.5rem 1.5rem 0px;
|
|
||||||
}
|
|
||||||
.ref-input {
|
.ref-input {
|
||||||
margin: 1.25rem 2.5rem;
|
margin: 1.25rem 2.5rem;
|
||||||
}
|
}
|
||||||
|
55
plugins/chunter-resources/src/components/Threads.svelte
Normal file
55
plugins/chunter-resources/src/components/Threads.svelte
Normal 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>
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'var(--theme-caption-color)'
|
const fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
export let size: 'small' | 'medium' | 'large'
|
||||||
const fill: string = 'var(--theme-caption-color)'
|
const fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -30,6 +30,7 @@ import CommentsPresenter from './components/CommentsPresenter.svelte'
|
|||||||
import CreateChannel from './components/CreateChannel.svelte'
|
import CreateChannel from './components/CreateChannel.svelte'
|
||||||
import EditChannel from './components/EditChannel.svelte'
|
import EditChannel from './components/EditChannel.svelte'
|
||||||
import ThreadView from './components/ThreadView.svelte'
|
import ThreadView from './components/ThreadView.svelte'
|
||||||
|
import Threads from './components/Threads.svelte'
|
||||||
|
|
||||||
export { CommentsPresenter }
|
export { CommentsPresenter }
|
||||||
|
|
||||||
@ -40,28 +41,27 @@ async function MarkUnread (object: Message): Promise<void> {
|
|||||||
|
|
||||||
async function MarkCommentUnread (object: ThreadMessage): Promise<void> {
|
async function MarkCommentUnread (object: ThreadMessage): Promise<void> {
|
||||||
const client = NotificationClientImpl.getClient()
|
const client = NotificationClientImpl.getClient()
|
||||||
const value = object.modifiedOn - 1
|
await client.updateLastView(object.attachedTo, object.attachedToClass, object.createOn - 1, true)
|
||||||
await client.updateLastView(object.attachedTo, object.attachedToClass, value, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function SubscribeMessage (object: Message): Promise<void> {
|
async function SubscribeMessage (object: Message): Promise<void> {
|
||||||
const client = NotificationClientImpl.getClient()
|
const client = getClient()
|
||||||
await client.updateLastView(object._id, object._class, undefined, true)
|
const notificationClient = NotificationClientImpl.getClient()
|
||||||
}
|
if (client.getHierarchy().isDerived(object._class, chunter.class.ThreadMessage)) {
|
||||||
|
await notificationClient.updateLastView(object.attachedTo, object.attachedToClass, undefined, true)
|
||||||
async function SubscribeComment (object: ThreadMessage): Promise<void> {
|
} else {
|
||||||
const client = NotificationClientImpl.getClient()
|
await notificationClient.updateLastView(object._id, object._class, undefined, true)
|
||||||
await client.updateLastView(object.attachedTo, object.attachedToClass, undefined, true)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function UnsubscribeMessage (object: Message): Promise<void> {
|
async function UnsubscribeMessage (object: Message): Promise<void> {
|
||||||
const client = NotificationClientImpl.getClient()
|
const client = getClient()
|
||||||
await client.unsubscribe(object._id)
|
const notificationClient = NotificationClientImpl.getClient()
|
||||||
}
|
if (client.getHierarchy().isDerived(object._class, chunter.class.ThreadMessage)) {
|
||||||
|
await notificationClient.unsubscribe(object.attachedTo)
|
||||||
async function UnsubscribeComment (object: ThreadMessage): Promise<void> {
|
} else {
|
||||||
const client = NotificationClientImpl.getClient()
|
await notificationClient.unsubscribe(object._id)
|
||||||
await client.unsubscribe(object.attachedTo)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function PinMessage (message: ChunterMessage): Promise<void> {
|
async function PinMessage (message: ChunterMessage): Promise<void> {
|
||||||
@ -90,6 +90,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
CommentsPresenter,
|
CommentsPresenter,
|
||||||
ChannelPresenter,
|
ChannelPresenter,
|
||||||
EditChannel,
|
EditChannel,
|
||||||
|
Threads,
|
||||||
ThreadView
|
ThreadView
|
||||||
},
|
},
|
||||||
activity: {
|
activity: {
|
||||||
@ -101,9 +102,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
MarkUnread,
|
MarkUnread,
|
||||||
MarkCommentUnread,
|
MarkCommentUnread,
|
||||||
SubscribeMessage,
|
SubscribeMessage,
|
||||||
SubscribeComment,
|
|
||||||
UnsubscribeMessage,
|
UnsubscribeMessage,
|
||||||
UnsubscribeComment,
|
|
||||||
PinMessage,
|
PinMessage,
|
||||||
UnpinMessage
|
UnpinMessage
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,7 @@ export default mergeIds(chunterId, chunter, {
|
|||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
SubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
|
SubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
|
||||||
SubscribeComment: '' as Resource<(object: Doc) => Promise<void>>,
|
|
||||||
UnsubscribeMessage: '' 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>>,
|
PinMessage: '' as Resource<(object: Doc) => Promise<void>>,
|
||||||
UnpinMessage: '' 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,
|
Replies: '' as IntlString,
|
||||||
Topic: '' as IntlString,
|
Topic: '' as IntlString,
|
||||||
Thread: '' as IntlString,
|
Thread: '' as IntlString,
|
||||||
|
Threads: '' as IntlString,
|
||||||
RepliesCount: '' as IntlString,
|
RepliesCount: '' as IntlString,
|
||||||
LastReply: '' as IntlString,
|
LastReply: '' as IntlString,
|
||||||
New: '' as IntlString,
|
New: '' as IntlString,
|
||||||
@ -57,6 +56,8 @@ export default mergeIds(chunterId, chunter, {
|
|||||||
UnpinMessage: '' as IntlString,
|
UnpinMessage: '' as IntlString,
|
||||||
Pinned: '' as IntlString,
|
Pinned: '' as IntlString,
|
||||||
DeleteMessage: '' as IntlString,
|
DeleteMessage: '' as IntlString,
|
||||||
EditMessage: '' as IntlString
|
EditMessage: '' as IntlString,
|
||||||
|
AndYou: '' as IntlString,
|
||||||
|
ShowMoreReplies: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -90,6 +90,7 @@ export default plugin(chunterId, {
|
|||||||
icon: {
|
icon: {
|
||||||
Chunter: '' as Asset,
|
Chunter: '' as Asset,
|
||||||
Hashtag: '' as Asset,
|
Hashtag: '' as Asset,
|
||||||
|
Thread: '' as Asset,
|
||||||
Lock: '' as Asset
|
Lock: '' as Asset
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
|
Loading…
Reference in New Issue
Block a user