Chunter: Direct messages (#1472)

Signed-off-by: Denis Bunakalya <denis.bunakalya@xored.com>
This commit is contained in:
Denis Bunakalya 2022-04-22 06:13:15 +03:00 committed by GitHub
parent ed0a747330
commit 2efc19044e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 441 additions and 105 deletions

View File

@ -16,12 +16,14 @@
import activity from '@anticrm/activity'
import type {
Backlink,
ChunterSpace,
Channel,
ChunterMessage,
Comment,
Message,
SavedMessages,
ThreadMessage
ThreadMessage,
DirectMessage
} from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact'
import type { Account, Class, Doc, Domain, Ref, Space, Timestamp } from '@anticrm/core'
@ -50,20 +52,27 @@ import preference, { TPreference } from '@anticrm/model-preference'
export const DOMAIN_CHUNTER = 'chunter' as Domain
export const DOMAIN_COMMENT = 'comment' as Domain
@Model(chunter.class.Channel, core.class.Space)
@UX(chunter.string.Channel, chunter.icon.Hashtag)
export class TChannel extends TSpace implements Channel {
@Model(chunter.class.ChunterSpace, core.class.Space)
export class TChunterSpace extends TSpace implements ChunterSpace {
@Prop(TypeTimestamp(), chunter.string.LastMessage)
lastMessage?: Timestamp
@Prop(ArrOf(TypeRef(chunter.class.ChunterMessage)), chunter.string.PinnedMessages)
pinned?: Ref<ChunterMessage>[]
}
@Model(chunter.class.Channel, chunter.class.ChunterSpace)
@UX(chunter.string.Channel, chunter.icon.Hashtag)
export class TChannel extends TChunterSpace implements Channel {
@Prop(TypeString(), chunter.string.Topic)
@Index(IndexKind.FullText)
topic?: string
}
@Model(chunter.class.DirectMessage, chunter.class.ChunterSpace)
@UX(chunter.string.DirectMessage, contact.icon.Person)
export class TDirectMessage extends TChunterSpace implements DirectMessage {}
@Model(chunter.class.ChunterMessage, core.class.AttachedDoc, DOMAIN_CHUNTER)
export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
@Prop(TypeMarkup(), chunter.string.Content)
@ -128,23 +137,49 @@ export class TSavedMessages extends TPreference implements SavedMessages {
}
export function createModel (builder: Builder): void {
builder.createModel(TChannel, TMessage, TThreadMessage, TChunterMessage, TComment, TBacklink, TSavedMessages)
builder.mixin(chunter.class.Channel, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: chunter.class.Message
}
builder.createModel(
TChunterSpace,
TChannel,
TMessage,
TThreadMessage,
TChunterMessage,
TComment,
TBacklink,
TDirectMessage,
TSavedMessages
)
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
spaceClasses.forEach((spaceClass) => {
builder.mixin(spaceClass, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: chunter.class.Message
}
})
builder.mixin(spaceClass, core.class.Class, notification.mixin.SpaceLastEdit, {
lastEditField: 'lastMessage'
})
builder.mixin(spaceClass, core.class.Class, view.mixin.ObjectEditor, {
editor: chunter.component.EditChannel
})
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.SpaceName, {
getName: chunter.function.GetDmName
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.AttributePresenter, {
presenter: chunter.component.DmPresenter
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.AttributePresenter, {
presenter: chunter.component.ChannelPresenter
})
builder.mixin(chunter.class.Channel, core.class.Class, notification.mixin.SpaceLastEdit, {
lastEditField: 'lastMessage'
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectEditor, {
editor: chunter.component.EditChannel
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.SpaceHeader, {
header: chunter.component.DmHeader
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.SpaceHeader, {
@ -302,6 +337,12 @@ export function createModel (builder: Builder): void {
spaceClass: chunter.class.Channel,
addSpaceLabel: chunter.string.CreateChannel,
createComponent: chunter.component.CreateChannel
},
{
label: chunter.string.DirectMessages,
spaceClass: chunter.class.DirectMessage,
addSpaceLabel: chunter.string.NewDirectMessage,
createComponent: chunter.component.CreateDirectMessage
}
],
aside: chunter.component.ThreadView

View File

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

View File

@ -20,7 +20,8 @@ import core, { TClass, TDoc } from '@anticrm/model-core'
import type { Asset, IntlString, Resource, Status } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import type {
Action, ActionTarget,
Action,
ActionTarget,
AttributeEditor,
AttributePresenter,
HTMLPresenter,
@ -32,6 +33,7 @@ import type {
ObjectValidator,
PreviewPresenter,
SpaceHeader,
SpaceName,
TextPresenter,
ViewAction,
ViewContext,
@ -67,7 +69,12 @@ export function createAction (
)
}
export function actionTarget (builder: Builder, action: Ref<Action>, target: Ref<Class<Doc>>, context: ViewContext): void {
export function actionTarget (
builder: Builder,
action: Ref<Action>,
target: Ref<Class<Doc>>,
context: ViewContext
): void {
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target,
action,
@ -116,6 +123,11 @@ export class TSpaceHeader extends TClass implements SpaceHeader {
header!: AnyComponent
}
@Mixin(view.mixin.SpaceName, core.class.Class)
export class TSpaceName extends TClass implements SpaceName {
getName!: Resource<(client: Client, space: Space) => Promise<string>>
}
@Mixin(view.mixin.ObjectValidator, core.class.Class)
export class TObjectValidator extends TClass implements ObjectValidator {
validator!: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status<{}>>>
@ -189,6 +201,7 @@ export function createModel (builder: Builder): void {
TObjectEditorHeader,
THTMLPresenter,
TSpaceHeader,
TSpaceName,
TTextPresenter,
TIgnoreActions,
TPreviewPresenter
@ -214,20 +227,34 @@ export function createModel (builder: Builder): void {
view.viewlet.Table
)
createAction(builder, view.action.Delete, view.string.Delete, view.actionImpl.Delete, { icon: view.icon.Delete, keyBinding: ['Meta + Backspace'] })
createAction(builder, view.action.Delete, view.string.Delete, view.actionImpl.Delete, {
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace']
})
actionTarget(builder, view.action.Delete, core.class.Doc, { mode: ['context', 'browser'], group: 'tools' })
createAction(builder, view.action.Move, view.string.Move, view.actionImpl.Move, { icon: view.icon.Move, singleInput: true })
createAction(builder, view.action.Move, view.string.Move, view.actionImpl.Move, {
icon: view.icon.Move,
singleInput: true
})
// Keyboard actions.
createAction(builder, view.action.MoveUp, view.string.MoveUp, view.actionImpl.MoveUp, { keyBinding: ['ArrowUp', 'keyK'] })
createAction(builder, view.action.MoveUp, view.string.MoveUp, view.actionImpl.MoveUp, {
keyBinding: ['ArrowUp', 'keyK']
})
actionTarget(builder, view.action.MoveUp, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.MoveDown, view.string.MoveDown, view.actionImpl.MoveDown, { keyBinding: ['ArrowDown', 'keyJ'] })
createAction(builder, view.action.MoveDown, view.string.MoveDown, view.actionImpl.MoveDown, {
keyBinding: ['ArrowDown', 'keyJ']
})
actionTarget(builder, view.action.MoveDown, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.MoveLeft, view.string.MoveLeft, view.actionImpl.MoveLeft, { keyBinding: ['ArrowLeft'] })
createAction(builder, view.action.MoveLeft, view.string.MoveLeft, view.actionImpl.MoveLeft, {
keyBinding: ['ArrowLeft']
})
actionTarget(builder, view.action.MoveLeft, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.MoveRight, view.string.MoveRight, view.actionImpl.MoveRight, { keyBinding: ['ArrowRight'] })
createAction(builder, view.action.MoveRight, view.string.MoveRight, view.actionImpl.MoveRight, {
keyBinding: ['ArrowRight']
})
actionTarget(builder, view.action.MoveRight, core.class.Doc, { mode: 'browser' })
builder.mixin(core.class.Space, core.class.Class, view.mixin.AttributePresenter, {
@ -235,19 +262,32 @@ export function createModel (builder: Builder): void {
})
// Selection stuff
createAction(builder, view.action.SelectItem, view.string.SelectItem, view.actionImpl.SelectItem, { keyBinding: ['keyX'] })
createAction(builder, view.action.SelectItem, view.string.SelectItem, view.actionImpl.SelectItem, {
keyBinding: ['keyX']
})
actionTarget(builder, view.action.SelectItem, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.SelectItemAll, view.string.SelectItemAll, view.actionImpl.SelectItemAll, { keyBinding: ['meta + keyA'] })
createAction(builder, view.action.SelectItemAll, view.string.SelectItemAll, view.actionImpl.SelectItemAll, {
keyBinding: ['meta + keyA']
})
actionTarget(builder, view.action.SelectItemAll, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.SelectItemNone, view.string.SelectItemNone, view.actionImpl.SelectItemNone, { keyBinding: ['escape'] })
createAction(builder, view.action.SelectItemNone, view.string.SelectItemNone, view.actionImpl.SelectItemNone, {
keyBinding: ['escape']
})
actionTarget(builder, view.action.SelectItemNone, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.ShowActions, view.string.ShowActions, view.actionImpl.ShowActions, { keyBinding: ['meta + keyk'] })
actionTarget(builder, view.action.ShowActions, core.class.Doc, { mode: ['workbench', 'browser', 'popup', 'panel', 'editor'] })
createAction(builder, view.action.ShowActions, view.string.ShowActions, view.actionImpl.ShowActions, {
keyBinding: ['meta + keyk']
})
actionTarget(builder, view.action.ShowActions, core.class.Doc, {
mode: ['workbench', 'browser', 'popup', 'panel', 'editor']
})
createAction(builder, view.action.ShowPreview, view.string.ShowPreview, view.actionImpl.ShowPreview, { keyBinding: ['Space'], singleInput: true })
createAction(builder, view.action.ShowPreview, view.string.ShowPreview, view.actionImpl.ShowPreview, {
keyBinding: ['Space'],
singleInput: true
})
actionTarget(builder, view.action.ShowPreview, core.class.Doc, { mode: 'browser' })
}

View File

@ -16,9 +16,6 @@
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
export const viewOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
}
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -3,17 +3,22 @@
"ApplicationLabelChunter": "Chat",
"LeftComment": "left a comment",
"Channels": "Channels",
"DirectMessages": "Direct messages",
"CreateChannel": "New Channel",
"NewDirectMessage": "New Direct Message",
"ChannelName": "Name",
"ChannelNamePlaceholder": "Channel",
"ChannelDescription": "Description",
"MakePrivate": "Make private",
"MakePrivateDescription": "Only members can see it",
"Channel": "Channel",
"DirectMessage": "Direct message",
"EditUpdate": "Save...",
"EditCancel": "Cancel",
"Comments" : "Comments",
"About": "About",
"Members": "Members",
"NoMembers": "No members",
"MentionedIn": "mentioned this ",
"ContactInfo": "Contact Info",
"Content": "Content",

View File

@ -3,17 +3,22 @@
"ApplicationLabelChunter": "Чат",
"LeftComment": "оставил(а) комментарий",
"Channels": "Каналы",
"DirectMessages": "Личные сообщения",
"CreateChannel": "Создать канал",
"NewDirectMessage": "Создать личное сообщение",
"ChannelName": "Название",
"ChannelNamePlaceholder": "Канал",
"ChannelDescription": "Описание",
"MakePrivate": "Сделать личным",
"MakePrivateDescription": "Только пользователи могут видеть это",
"Channel": "Канал ",
"DirectMessage": "Личное сообщение",
"EditUpdate": "Сохранить...",
"EditCancel": "Отменить",
"Comments" : "Комментарии",
"About": "Информация",
"Members": "Участники",
"NoMembers": "Нет участников",
"MentionedIn": "упомянул(а) ",
"ContactInfo": "Контактная информация",
"Content": "Содержимое",

View File

@ -74,7 +74,7 @@
(res) => {
messages = res
newMessagesPos = newMessagesStart(messages)
notificationClient.updateLastView(space, chunter.class.Channel)
notificationClient.updateLastView(space, chunter.class.ChunterSpace)
},
{
lookup: {

View File

@ -14,26 +14,16 @@
-->
<script lang="ts">
import type { Channel } from '@anticrm/chunter'
import { Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { getCurrentLocation, Icon, locationToUrl } from '@anticrm/ui'
import chunter from '../plugin'
import { Icon } from '@anticrm/ui'
import { getSpaceLink } from '../utils'
export let value: Channel
const client = getClient()
$: icon = client.getHierarchy().getClass(value._class).icon
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
return locationToUrl(loc)
}
$: link = getLink(value._id)
$: link = getSpaceLink(value._id)
</script>
{#if value}

View File

@ -40,7 +40,7 @@
space,
{
attachedTo: space,
attachedToClass: chunter.class.Channel,
attachedToClass: chunter.class.ChunterSpace,
collection: 'messages',
content: message,
createOn: 0,
@ -50,11 +50,11 @@
_id
)
tx.attributes.createOn = tx.modifiedOn
await notificationClient.updateLastView(space, chunter.class.Channel, tx.modifiedOn, true)
await notificationClient.updateLastView(space, chunter.class.ChunterSpace, tx.modifiedOn, true)
await client.tx(tx)
// Create an backlink to document
await createBacklinks(client, space, chunter.class.Channel, _id, message)
await createBacklinks(client, space, chunter.class.ChunterSpace, _id, message)
_id = generateId()
}
@ -68,7 +68,7 @@
const pinnedQuery = createQuery()
let pinnedIds: Ref<ChunterMessage>[] = []
pinnedQuery.query(
chunter.class.Channel,
chunter.class.ChunterSpace,
{ _id: space },
(res) => {
pinnedIds = res[0]?.pinned ?? []

View File

@ -0,0 +1,57 @@
<!--
// Copyright © 2022 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 { createEventDispatcher } from 'svelte'
import contact, { Employee } from '@anticrm/contact'
import core, { getCurrentAccount, Ref } from '@anticrm/core'
import { getClient, SpaceCreateCard, UserBoxList } from '@anticrm/presentation'
import chunter from '../plugin'
const dispatch = createEventDispatcher()
const client = getClient()
const myAccId = getCurrentAccount()._id
let employeeIds: Ref<Employee>[] = []
function createDirectMessage () {
client.findAll(contact.class.EmployeeAccount, { employee: { $in: employeeIds } }).then((employeeAccounts) => {
client.createDoc(chunter.class.DirectMessage, core.space.Space, {
name: '',
description: '',
private: true,
archived: false,
members: [myAccId, ...employeeAccounts.filter((ea) => ea._id !== myAccId).map((ea) => ea._id)]
})
})
}
</script>
<SpaceCreateCard
label={chunter.string.NewDirectMessage}
okAction={createDirectMessage}
canSave={employeeIds.length > 0}
on:close={() => {
dispatch('close')
}}
>
<UserBoxList
_class={contact.class.Employee}
label={chunter.string.Members}
noItems={chunter.string.NoMembers}
on:update={(evt) => (employeeIds = evt.detail)}
/>
</SpaceCreateCard>

View File

@ -0,0 +1,46 @@
<!--
// 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 { DirectMessage } from '@anticrm/chunter'
import type { Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { showPanel } from '@anticrm/ui'
import chunter from '../plugin'
import { classIcon, getDmName } from '../utils'
import Header from './Header.svelte'
export let spaceId: Ref<DirectMessage> | undefined
const client = getClient()
const query = createQuery()
let dm: DirectMessage | undefined
$: query.query(chunter.class.DirectMessage, { _id: spaceId }, (result) => {
dm = result[0]
})
async function onSpaceEdit (): Promise<void> {
if (dm === undefined) return
showPanel(chunter.component.EditChannel, dm._id, dm._class, 'right')
}
</script>
<div class="ac-header divide full">
{#if dm}
{#await getDmName(client, dm) then name}
<Header icon={classIcon(client, dm._class)} label={name} description={''} on:click={onSpaceEdit} />
{/await}
{/if}
</div>

View File

@ -0,0 +1,40 @@
<!--
// 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 { DirectMessage } from '@anticrm/chunter'
import { getClient } from '@anticrm/presentation'
import { Icon } from '@anticrm/ui'
import { getSpaceLink, getDmName } from '../utils'
export let dm: DirectMessage
const client = getClient()
$: icon = client.getHierarchy().getClass(dm._class).icon
$: link = getSpaceLink(dm._id)
</script>
{#if dm}
{#await getDmName(client, dm) then name}
<a class="flex-presenter" href={link}>
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="label">{name}</span>
</a>
{/await}
{/if}

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Channel } from '@anticrm/chunter'
import { ChunterSpace } from '@anticrm/chunter'
import type { Class, Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { createQuery, getClient, Members } from '@anticrm/presentation'
@ -25,10 +25,10 @@
import EditChannelDescriptionTab from './EditChannelDescriptionTab.svelte'
import EditChannelSettingsTab from './EditChannelSettingsTab.svelte'
export let _id: Ref<Channel>
export let _class: Ref<Class<Channel>>
export let _id: Ref<ChunterSpace>
export let _class: Ref<Class<ChunterSpace>>
let channel: Channel | undefined
let channel: ChunterSpace
const dispatch = createEventDispatcher()
@ -36,11 +36,15 @@
const clazz = client.getHierarchy().getClass(_class)
const query = createQuery()
$: query.query(chunter.class.Channel, { _id }, (result) => {
$: query.query(chunter.class.ChunterSpace, { _id }, (result) => {
channel = result[0]
})
const tabLabels: IntlString[] = [chunter.string.Channel, chunter.string.Members, chunter.string.Settings]
const tabLabels: IntlString[] = [
chunter.string.About,
chunter.string.Members,
...(_class === chunter.class.Channel ? [chunter.string.Settings] : [])
]
let selectedTabIndex = 0
</script>

View File

@ -16,13 +16,13 @@
<script lang="ts">
import attachment, { Attachment } from '@anticrm/attachment'
import { AttachmentPresenter } from '@anticrm/attachment-resources'
import { Channel } from '@anticrm/chunter'
import { ChunterSpace } from '@anticrm/chunter'
import { Doc, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Menu } from '@anticrm/view-resources'
import { showPopup, IconMoreV, Label } from '@anticrm/ui'
export let channel: Channel | undefined
export let channel: ChunterSpace | undefined
const query = createQuery()
let visibleAttachments: Attachment[] | undefined

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Channel } from '@anticrm/chunter'
import { Channel, ChunterSpace } from '@anticrm/chunter'
import { getCurrentAccount } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, EditBox } from '@anticrm/ui'
@ -22,14 +22,21 @@
import chunter from '../plugin'
import EditChannelDescriptionAttachments from './EditChannelDescriptionAttachments.svelte'
export let channel: Channel
export let channel: ChunterSpace
const client = getClient()
const dispatch = createEventDispatcher()
function isCommonChannel (channel?: ChunterSpace): channel is Channel {
return channel?._class === chunter.class.Channel
}
function onTopicChange (ev: Event) {
if (!isCommonChannel(channel)) {
return
}
const newTopic = (ev.target as HTMLInputElement).value
client.update(channel!, { topic: newTopic })
client.update(channel, { topic: newTopic })
}
function onDescriptionChange (ev: Event) {
@ -47,6 +54,7 @@
{#if channel}
<div class="flex-col flex-gap-3">
{#if isCommonChannel(channel)}
<EditBox
label={chunter.string.Topic}
bind:value={channel.topic}
@ -71,6 +79,7 @@
leaveChannel()
}}
/>
{/if}
<EditChannelDescriptionAttachments {channel} />
</div>
{/if}

View File

@ -13,7 +13,7 @@
const pinnedQuery = createQuery()
let pinnedIds: Ref<ChunterMessage>[] = []
pinnedQuery.query(
chunter.class.Channel,
chunter.class.ChunterSpace,
{ _id: space },
(res) => {
pinnedIds = res[0]?.pinned ?? []

View File

@ -15,7 +15,7 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { Channel, Message, ThreadMessage } from '@anticrm/chunter'
import type { ChunterSpace, 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'
@ -24,6 +24,7 @@
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import ChannelPresenter from './ChannelPresenter.svelte'
import DmPresenter from './DmPresenter.svelte'
import MsgView from './Message.svelte'
const client = getClient()
@ -152,22 +153,24 @@
await client.tx(tx)
// Create an backlink to document
await createBacklinks(client, parent.space, chunter.class.Channel, commentId, message)
await createBacklinks(client, parent.space, chunter.class.ChunterSpace, commentId, message)
commentId = generateId()
}
let comments: ThreadMessage[] = []
async function getChannel (_id: Ref<Channel>): Promise<Channel | undefined> {
return await client.findOne(chunter.class.Channel, { _id })
async function getChannel (_id: Ref<ChunterSpace>): Promise<ChunterSpace | undefined> {
return await client.findOne(chunter.class.ChunterSpace, { _id })
}
</script>
<div class="ml-8 mt-4">
{#if parent}
{#await getChannel(parent.space) then channel}
{#if channel}
{#if channel?._class === chunter.class.Channel}
<ChannelPresenter value={channel} />
{:else if channel}
<DmPresenter dm={channel} />
{/if}
{/await}
{#await getParticipants(comments, parent, employees) then participants}
@ -208,7 +211,7 @@
overflow: hidden;
margin: 1rem 1rem 0px;
background-color: var(--theme-border-modal);
border-radius: .75rem;
border-radius: 0.75rem;
border: 1px solid var(--theme-zone-border);
}

View File

@ -100,7 +100,7 @@
)
pinnedQuery.query(
chunter.class.Channel,
chunter.class.ChunterSpace,
{ _id: currentSpace },
(res) => {
pinnedIds = res[0]?.pinned ?? []
@ -153,7 +153,7 @@
await client.tx(tx)
// Create an backlink to document
await createBacklinks(client, currentSpace, chunter.class.Channel, commentId, message)
await createBacklinks(client, currentSpace, chunter.class.ChunterSpace, commentId, message)
commentId = generateId()
}

View File

@ -14,7 +14,7 @@
//
import core from '@anticrm/core'
import chunter, { Channel, ChunterMessage, Message, ThreadMessage } from '@anticrm/chunter'
import chunter, { ChunterSpace, Channel, ChunterMessage, Message, ThreadMessage } from '@anticrm/chunter'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { Resources } from '@anticrm/platform'
import { getClient, MessageBox } from '@anticrm/presentation'
@ -23,23 +23,28 @@ import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
import TxBacklinkReference from './components/activity/TxBacklinkReference.svelte'
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte'
import DmPresenter from './components/DmPresenter.svelte'
import ChannelView from './components/ChannelView.svelte'
import ChannelHeader from './components/ChannelHeader.svelte'
import DmHeader from './components/DmHeader.svelte'
import CommentInput from './components/CommentInput.svelte'
import CommentPresenter from './components/CommentPresenter.svelte'
import CommentsPresenter from './components/CommentsPresenter.svelte'
import CreateChannel from './components/CreateChannel.svelte'
import CreateDirectMessage from './components/CreateDirectMessage.svelte'
import EditChannel from './components/EditChannel.svelte'
import ThreadView from './components/ThreadView.svelte'
import Threads from './components/Threads.svelte'
import SavedMessages from './components/SavedMessages.svelte'
import preference from '@anticrm/preference'
import { getDmName } from './utils'
export { CommentsPresenter }
async function MarkUnread (object: Message): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.updateLastView(object.space, chunter.class.Channel, object.createOn - 1, true)
await client.updateLastView(object.space, chunter.class.ChunterSpace, object.createOn - 1, true)
}
async function MarkCommentUnread (object: ThreadMessage): Promise<void> {
@ -70,7 +75,7 @@ async function UnsubscribeMessage (object: Message): Promise<void> {
async function PinMessage (message: ChunterMessage): Promise<void> {
const client = getClient()
await client.updateDoc<Channel>(chunter.class.Channel, core.space.Space, message.space, {
await client.updateDoc<ChunterSpace>(chunter.class.ChunterSpace, core.space.Space, message.space, {
$push: { pinned: message._id }
})
}
@ -78,7 +83,7 @@ async function PinMessage (message: ChunterMessage): Promise<void> {
export async function UnpinMessage (message: ChunterMessage): Promise<void> {
const client = getClient()
await client.updateDoc<Channel>(chunter.class.Channel, core.space.Space, message.space, {
await client.updateDoc<ChunterSpace>(chunter.class.ChunterSpace, core.space.Space, message.space, {
$pull: { pinned: message._id }
})
}
@ -149,16 +154,22 @@ export default async (): Promise<Resources> => ({
component: {
CommentInput,
CreateChannel,
CreateDirectMessage,
ChannelHeader,
DmHeader,
ChannelView,
CommentPresenter,
CommentsPresenter,
ChannelPresenter,
DmPresenter,
EditChannel,
Threads,
ThreadView,
SavedMessages
},
function: {
GetDmName: getDmName
},
activity: {
TxCommentCreate,
TxBacklinkCreate,

View File

@ -14,7 +14,8 @@
//
import chunter, { chunterId } from '@anticrm/chunter'
import type { IntlString } from '@anticrm/platform'
import type { Client, Space } from '@anticrm/core'
import type { IntlString, Resource } from '@anticrm/platform'
import { mergeIds } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import { ViewAction } from '@anticrm/view'
@ -22,10 +23,15 @@ import { ViewAction } from '@anticrm/view'
export default mergeIds(chunterId, chunter, {
component: {
CreateChannel: '' as AnyComponent,
CreateDirectMessage: '' as AnyComponent,
ChannelHeader: '' as AnyComponent,
DmHeader: '' as AnyComponent,
ChannelView: '' as AnyComponent,
EditChannel: '' as AnyComponent
},
function: {
GetDmName: '' as Resource<(client: Client, space: Space) => Promise<string>>
},
actionImpl: {
SubscribeMessage: '' as ViewAction,
UnsubscribeMessage: '' as ViewAction,
@ -36,14 +42,19 @@ export default mergeIds(chunterId, chunter, {
},
string: {
Channel: '' as IntlString,
DirectMessage: '' as IntlString,
Channels: '' as IntlString,
DirectMessages: '' as IntlString,
CreateChannel: '' as IntlString,
NewDirectMessage: '' as IntlString,
ChannelName: '' as IntlString,
ChannelNamePlaceholder: '' as IntlString,
ChannelDescription: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString,
About: '' as IntlString,
Members: '' as IntlString,
NoMembers: '' as IntlString,
In: '' as IntlString,
Replies: '' as IntlString,
Topic: '' as IntlString,

View File

@ -1,6 +1,9 @@
import contact, { EmployeeAccount } from '@anticrm/contact'
import { Account, Class, Client, Obj, Ref } from '@anticrm/core'
import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
import { Account, Class, Client, Obj, Ref, Space, getCurrentAccount } from '@anticrm/core'
import { Asset } from '@anticrm/platform'
import { getCurrentLocation, locationToUrl } from '@anticrm/ui'
import chunter from './plugin'
export async function getUser (
client: Client,
@ -35,3 +38,28 @@ export function isToday (time: number): boolean {
export function classIcon (client: Client, _class: Ref<Class<Obj>>): Asset | undefined {
return client.getHierarchy().getClass(_class).icon
}
export async function getDmName (client: Client, dm: Space): Promise<string> {
const myAccId = getCurrentAccount()._id
const employeeAccounts = await client.findAll(contact.class.EmployeeAccount, {
_id: { $in: dm.members as Array<Ref<EmployeeAccount>> }
})
const name = (dm.members.length > 1 ? employeeAccounts.filter((a) => a._id !== myAccId) : employeeAccounts)
.map((a) => formatName(a.name))
.join(', ')
return name
}
export function getSpaceLink (id: Ref<Space>): string {
const loc = getCurrentLocation()
loc.path[1] = chunter.app.Chunter
loc.path[2] = id
loc.path.length = 3
loc.fragment = undefined
return locationToUrl(loc)
}

View File

@ -23,12 +23,23 @@ import { AnyComponent } from '@anticrm/ui'
/**
* @public
*/
export interface Channel extends Space {
export interface ChunterSpace extends Space {
lastMessage?: Timestamp
pinned?: Ref<ChunterMessage>[]
}
/**
* @public
*/
export interface Channel extends ChunterSpace {
topic?: string
}
/**
* @public
*/
export interface DirectMessage extends ChunterSpace {}
/**
* @public
*/
@ -113,8 +124,10 @@ export default plugin(chunterId, {
ThreadMessage: '' as Ref<Class<ThreadMessage>>,
Backlink: '' as Ref<Class<Backlink>>,
Comment: '' as Ref<Class<Comment>>,
ChunterSpace: '' as Ref<Class<ChunterSpace>>,
Channel: '' as Ref<Class<Channel>>,
SavedMessages: '' as Ref<Class<SavedMessages>>
SavedMessages: '' as Ref<Class<SavedMessages>>,
DirectMessage: '' as Ref<Class<DirectMessage>>
},
space: {
Backlinks: '' as Ref<Space>

View File

@ -59,6 +59,13 @@ export interface SpaceHeader extends Class<Doc> {
header: AnyComponent
}
/**
* @public
*/
export interface SpaceName extends Class<Doc> {
getName: Resource<(client: Client, space: Space) => Promise<string>>
}
/**
* @public
*/
@ -244,6 +251,7 @@ const view = plugin(viewId, {
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>,
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>,
SpaceHeader: '' as Ref<Mixin<SpaceHeader>>,
SpaceName: '' as Ref<Mixin<SpaceName>>,
IgnoreActions: '' as Ref<Mixin<IgnoreActions>>,
HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>,
TextPresenter: '' as Ref<Mixin<TextPresenter>>,

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Doc, Ref, SortingOrder, Space } from '@anticrm/core'
import core, { Doc, Ref, SortingOrder, Space, getCurrentAccount } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { Scroller } from '@anticrm/ui'
@ -34,6 +34,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
const myAccId = getCurrentAccount()._id
let spaces: Space[] = []
let starred: Space[] = []
@ -77,7 +78,7 @@
topSpecials = []
bottomSpecials = []
}
shownSpaces = spaces.filter((sp) => !sp.archived && !preferences.has(sp._id))
shownSpaces = spaces.filter((sp) => !sp.archived && !preferences.has(sp._id) && (!sp.members.length || sp.members.includes(myAccId)))
starred = spaces.filter((sp) => preferences.has(sp._id))
}

View File

@ -132,6 +132,20 @@
return lastView < value
}
async function getName (space: Space) {
const clazz = hierarchy.getClass(space._class)
const nameMixin = hierarchy.as(clazz, view.mixin.SpaceName)
if (nameMixin.getName) {
const getSpaceName = await getResource(nameMixin.getName);
const name = await getSpaceName(client, space)
return name
}
return space.name
}
function getParentActions(): Action[] {
return hasSpaceBrowser ? [browseSpaces, addSpace] : [addSpace]
}
@ -155,18 +169,20 @@
{/each}
</TreeNode>
{:else}
<TreeItem
indent={'ml-4'}
_id={space._id}
title={space.name}
icon={classIcon(client, space._class)}
selected={currentSpace === space._id}
actions={() => getActions(space)}
bold={isChanged(space, $lastViews)}
on:click={() => {
selectSpace(space._id)
}}
/>
{#await getName(space) then name}
<TreeItem
indent={'ml-4'}
_id={space._id}
title={name}
icon={classIcon(client, space._class)}
selected={currentSpace === space._id}
actions={() => getActions(space)}
bold={isChanged(space, $lastViews)}
on:click={() => {
selectSpace(space._id)
}}
/>
{/await}
{/if}
{/each}
</TreeNode>

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import chunter, { Channel, Comment, Message, ThreadMessage } from '@anticrm/chunter'
import chunter, { ChunterSpace, Comment, Message, ThreadMessage } from '@anticrm/chunter'
import { EmployeeAccount } from '@anticrm/contact'
import core, {
Class,
@ -39,7 +39,7 @@ import workbench from '@anticrm/workbench'
* @public
*/
export function channelHTMLPresenter (doc: Doc): string {
const channel = doc as Channel
const channel = doc as ChunterSpace
const front = getMetadata(login.metadata.FrontUrl) ?? ''
return `<a href="${front}/${workbench.component.WorkbenchApp}/${chunter.app.Chunter}/${channel._id}">${channel.name}</a>`
}
@ -48,7 +48,7 @@ export function channelHTMLPresenter (doc: Doc): string {
* @public
*/
export function channelTextPresenter (doc: Doc): string {
const channel = doc as Channel
const channel = doc as ChunterSpace
return `${channel.name}`
}
@ -145,12 +145,12 @@ export async function MessageCreate (tx: Tx, control: TriggerControl): Promise<T
const message = doc as Message
const channel = (await control.findAll(chunter.class.Channel, {
const channel = (await control.findAll(chunter.class.ChunterSpace, {
_id: message.space
}, { limit: 1 }))[0]
if (channel.lastMessage === undefined || channel.lastMessage < message.createOn) {
const res = control.txFactory.createTxUpdateDoc<Channel>(channel._class, channel.space, channel._id, {
const res = control.txFactory.createTxUpdateDoc<ChunterSpace>(channel._class, channel.space, channel._id, {
lastMessage: message.createOn
})
return [res]
@ -165,7 +165,7 @@ export async function MessageDelete (tx: Tx, control: TriggerControl): Promise<T
const hierarchy = control.hierarchy
if (tx._class !== core.class.TxCollectionCUD) return []
const rmTx = (tx as TxCollectionCUD<Channel, Message>).tx
const rmTx = (tx as TxCollectionCUD<ChunterSpace, Message>).tx
if (!hierarchy.isDerived(rmTx.objectClass, chunter.class.Message)) {
return []
}
@ -175,7 +175,7 @@ export async function MessageDelete (tx: Tx, control: TriggerControl): Promise<T
const message = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<Message>)
const channel = (await control.findAll(chunter.class.Channel, {
const channel = (await control.findAll(chunter.class.ChunterSpace, {
_id: message.space
}, { limit: 1 }))[0]
@ -185,7 +185,7 @@ export async function MessageDelete (tx: Tx, control: TriggerControl): Promise<T
})
const lastMessageDate = messages.reduce((maxDate, mess) => mess.createOn > maxDate ? mess.createOn : maxDate, 0)
const updateTx = control.txFactory.createTxUpdateDoc<Channel>(channel._class, channel.space, channel._id, {
const updateTx = control.txFactory.createTxUpdateDoc<ChunterSpace>(channel._class, channel.space, channel._id, {
lastMessage: lastMessageDate > 0 ? lastMessageDate : undefined
})

View File

@ -31,7 +31,17 @@ export default plugin(serverChunterId, {
ChunterTrigger: '' as Resource<TriggerFunc>
},
function: {
CommentRemove: '' as Resource<(doc: Doc, hiearachy: Hierarchy, findAll: <T extends Doc> (clazz: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>) => Promise<FindResult<T>>) => Promise<Doc[]>>,
CommentRemove: '' as Resource<
(
doc: Doc,
hiearachy: Hierarchy,
findAll: <T extends Doc>(
clazz: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<FindResult<T>>
) => Promise<Doc[]>
>,
ChannelHTMLPresenter: '' as Resource<(doc: Doc) => string>,
ChannelTextPresenter: '' as Resource<(doc: Doc) => string>
}