Migrate thread messages (#1284)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-04-06 12:16:01 +06:00 committed by GitHub
parent 68774ff1ab
commit 83b28a61e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 164 additions and 58 deletions

View File

@ -14,16 +14,16 @@
//
import activity from '@anticrm/activity'
import type { Backlink, Channel, Comment, Message } from '@anticrm/chunter'
import type { Account, Class, Doc, Domain, Ref, Timestamp } from '@anticrm/core'
import type { Backlink, Channel, ChunterMessage, Comment, Message, ThreadMessage } from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact'
import type { Account, Class, Doc, Domain, Ref, Space, Timestamp } from '@anticrm/core'
import { IndexKind } from '@anticrm/core'
import { ArrOf, Builder, Collection, Index, Model, Prop, TypeMarkup, TypeRef, TypeTimestamp, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import core, { TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import core, { TAttachedDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import chunter from './plugin'
import contact, { Employee } from '@anticrm/contact'
import notification from '@anticrm/model-notification'
export const DOMAIN_CHUNTER = 'chunter' as Domain
@ -36,8 +36,8 @@ export class TChannel extends TSpace implements Channel {
lastMessage?: Timestamp
}
@Model(chunter.class.Message, core.class.Doc, DOMAIN_CHUNTER)
export class TMessage extends TDoc implements Message {
@Model(chunter.class.ChunterMessage, core.class.AttachedDoc, DOMAIN_CHUNTER)
export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
@Prop(TypeMarkup(), chunter.string.Content)
@Index(IndexKind.FullText)
content!: string
@ -45,12 +45,6 @@ export class TMessage extends TDoc implements Message {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
@Prop(ArrOf(TypeRef(contact.class.Employee)), chunter.string.Replies)
replies?: Ref<Employee>[]
@Prop(TypeTimestamp(), chunter.string.LastReply)
lastReply?: Timestamp
@Prop(TypeRef(core.class.Account), chunter.string.CreateBy)
createBy!: Ref<Account>
@ -58,6 +52,26 @@ export class TMessage extends TDoc implements Message {
createOn!: Timestamp
}
@Model(chunter.class.ThreadMessage, chunter.class.ChunterMessage)
export class TThreadMessage extends TChunterMessage implements ThreadMessage {
declare attachedTo: Ref<Message>
declare attachedToClass: Ref<Class<Message>>
}
@Model(chunter.class.Message, chunter.class.ChunterMessage)
export class TMessage extends TChunterMessage implements Message {
declare attachedTo: Ref<Space>
declare attachedToClass: Ref<Class<Space>>
@Prop(ArrOf(TypeRef(contact.class.Employee)), chunter.string.Replies)
replies?: Ref<Employee>[]
@Prop(TypeTimestamp(), chunter.string.LastReply)
lastReply?: Timestamp
}
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@UX(chunter.string.Comment)
export class TComment extends TAttachedDoc implements Comment {
@ -77,7 +91,7 @@ export class TBacklink extends TComment implements Backlink {
}
export function createModel (builder: Builder): void {
builder.createModel(TChannel, TMessage, TComment, TBacklink)
builder.createModel(TChannel, TMessage, TThreadMessage, TChunterMessage, TComment, TBacklink)
builder.mixin(chunter.class.Channel, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: chunter.class.Message
@ -130,11 +144,8 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: chunter.class.Comment,
action: chunter.action.MarkCommentUnread,
query: {
attachedToClass: chunter.class.Message
}
target: chunter.class.ThreadMessage,
action: chunter.action.MarkCommentUnread
})
builder.createDoc(workbench.class.Application, core.space.Model, {

View File

@ -13,10 +13,10 @@
// limitations under the License.
//
import { Message } from '@anticrm/chunter'
import type { Client, Ref } from '@anticrm/core'
import core, { TxOperations } from '@anticrm/core'
import { Comment, Message, ThreadMessage } from '@anticrm/chunter'
import core, { Client, Doc, DOMAIN_TX, Ref, TxCreateDoc, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { DOMAIN_CHUNTER, DOMAIN_COMMENT } from './index'
import chunter from './plugin'
export async function createDeps (client: Client): Promise<void> {
@ -69,8 +69,80 @@ export async function setCreate (client: TxOperations): Promise<void> {
await Promise.all(promises)
}
export async function migrateMessages (client: MigrationClient): Promise<void> {
const messages = await client.find(DOMAIN_CHUNTER, {
_class: chunter.class.Message,
attachedTo: { $exists: false }
})
for (const message of messages) {
await client.update(DOMAIN_CHUNTER, {
_id: message._id
}, {
attachedTo: message.space,
attachedToClass: chunter.class.Channel,
collection: 'messages'
})
}
const txes = await client.find<TxCreateDoc<Doc>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectClass: chunter.class.Message
})
for (const tx of txes) {
await client.update(DOMAIN_TX, {
_id: tx._id
}, {
'attributes.attachedTo': tx.objectSpace,
'attributes.attachedToClass': chunter.class.Channel,
'attributes.collection': 'messages'
})
}
}
export async function migrateThreadMessages (client: MigrationClient): Promise<void> {
const messages = await client.find<Comment>(DOMAIN_COMMENT, {
_class: chunter.class.Comment,
attachedToClass: chunter.class.Message
})
for (const message of messages) {
await client.delete(DOMAIN_COMMENT, message._id)
await client.create<ThreadMessage>(DOMAIN_CHUNTER, {
attachedTo: message.attachedTo as Ref<Message>,
attachedToClass: message.attachedToClass,
attachments: message.attachments,
content: message.message,
collection: message.collection,
_class: chunter.class.ThreadMessage,
space: message.space,
modifiedOn: message.modifiedOn,
modifiedBy: message.modifiedBy,
createBy: message.modifiedBy,
createOn: message.modifiedOn,
_id: message._id as string as Ref<ThreadMessage>
})
}
const txes = await client.find<TxCreateDoc<Comment>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectClass: chunter.class.Comment,
'attributes.attachedToClass': chunter.class.Message
})
for (const tx of txes) {
await client.update(DOMAIN_TX, {
_id: tx._id
}, {
objectClass: chunter.class.ThreadMessage,
'attributes.createBy': tx.modifiedBy,
'attributes.createOn': tx.modifiedOn,
'attributes.content': tx.attributes.message
})
}
}
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await migrateMessages(client)
await migrateThreadMessages(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)

View File

@ -97,7 +97,7 @@
function markUnread (lastViews: Map<Ref<Doc>, number>) {
if (messages === undefined) return
const newPos = newMessagesStart(messages)
if (newPos < newMessagesPos || newMessagesPos === -1) {
if (newPos !== -1 || newMessagesPos === -1) {
newMessagesPos = newPos
}
}

View File

@ -36,6 +36,9 @@
const me = getCurrentAccount()._id
const txFactory = new TxFactory(me)
const tx = txFactory.createTxCreateDoc<Message>(_class, space, {
attachedTo: space,
attachedToClass: chunter.class.Channel,
collection: 'messages',
content: message,
createOn: 0,
createBy: me,

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Attachment } from '@anticrm/attachment'
import { AttachmentList } from '@anticrm/attachment-resources'
import type { Comment } from '@anticrm/chunter'
import type { ThreadMessage } from '@anticrm/chunter'
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import { Ref, WithLookup } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
@ -31,10 +31,10 @@
import Emoji from './icons/Emoji.svelte'
import Reactions from './Reactions.svelte'
export let comment: WithLookup<Comment>
export let message: WithLookup<ThreadMessage>
export let employees: Map<Ref<Employee>, Employee>
$: attachments = (comment.$lookup?.attachments ?? []) as Attachment[]
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
const client = getClient()
@ -42,7 +42,7 @@
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
$: subscribed = ($lastViews.get(comment.attachedTo) ?? -1) > -1
$: subscribed = ($lastViews.get(message.attachedTo) ?? -1) > -1
$: subscribeAction = subscribed
? ({
label: chunter.string.TurnOffReplies,
@ -54,7 +54,7 @@
} as Action)
const showMenu = async (ev: Event): Promise<void> => {
const actions = await getActions(client, comment, chunter.class.Comment)
const actions = await getActions(client, message, chunter.class.ThreadMessage)
actions.push(subscribeAction)
showPopup(
Menu,
@ -65,7 +65,7 @@
icon: a.icon,
action: async () => {
const impl = await getResource(a.action)
await impl(comment)
await impl(message)
}
}))
]
@ -74,10 +74,10 @@
)
}
$: employee = getEmployee(comment)
$: employee = getEmployee(message)
function getEmployee (comment: WithLookup<Comment>): Employee | undefined {
const employee = (comment.$lookup?.modifiedBy as EmployeeAccount)?.employee
function getEmployee (comment: WithLookup<ThreadMessage>): Employee | undefined {
const employee = (comment.$lookup?.createBy as EmployeeAccount)?.employee
if (employee !== undefined) {
return employees.get(employee)
}
@ -89,10 +89,10 @@
<div class="message">
<div class="header">
{#if employee}{formatName(employee.name)}{/if}
<span>{getTime(comment.modifiedOn)}</span>
<span>{getTime(message.createOn)}</span>
</div>
<div class="text"><MessageViewer message={comment.message} /></div>
{#if comment.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
<div class="text"><MessageViewer message={message.content} /></div>
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
{#if reactions}
<div class="footer">
<div><Reactions /></div>

View File

@ -15,7 +15,7 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { Comment, Message } from '@anticrm/chunter'
import type { ThreadMessage, Message } from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact'
import core, { Doc, generateId, getCurrentAccount, Ref, Space, TxFactory } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
@ -34,9 +34,9 @@
const dispatch = createEventDispatcher()
export let _id: Ref<Message>
export let space: Ref<Space>
export let currentSpace: Ref<Space>
let message: Message | undefined
let commentId = generateId()
let commentId = generateId() as Ref<ThreadMessage>
let div: HTMLDivElement | undefined
let autoscroll: boolean = false
@ -54,7 +54,7 @@
const lookup = {
_id: { attachments: attachment.class.Attachment },
modifiedBy: core.class.Account
createBy: core.class.Account
}
$: updateQueries(_id)
@ -75,7 +75,7 @@
)
query.query(
chunter.class.Comment,
chunter.class.ThreadMessage,
{
attachedTo: id
},
@ -108,35 +108,37 @@
const { message, attachments } = event.detail
const me = getCurrentAccount()._id
const txFactory = new TxFactory(me)
const tx = txFactory.createTxCreateDoc(
chunter.class.Comment,
space,
const tx = txFactory.createTxCreateDoc<ThreadMessage>(
chunter.class.ThreadMessage,
currentSpace,
{
attachedTo: _id,
attachedToClass: chunter.class.Message,
collection: 'replies',
message,
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, space, chunter.class.Channel, commentId, message)
await createBacklinks(client, currentSpace, chunter.class.Channel, commentId, message)
commentId = generateId()
}
let comments: Comment[] = []
let comments: ThreadMessage[] = []
function newMessagesStart (comments: Comment[]): number {
function newMessagesStart (comments: ThreadMessage[]): number {
const lastView = $lastViews.get(_id)
if (lastView === undefined || lastView === -1) return -1
for (let index = 0; index < comments.length; index++) {
const comment = comments[index]
if (comment.modifiedOn > lastView) return index
if (comment.createOn > lastView) return index
}
return -1
}
@ -144,7 +146,7 @@
$: markUnread($lastViews)
function markUnread (lastViews: Map<Ref<Doc>, number>) {
const newPos = newMessagesStart(comments)
if (newPos < newMessagesPos || newMessagesPos === -1) {
if (newPos !== -1 || newMessagesPos === -1) {
newMessagesPos = newPos
}
}
@ -172,12 +174,12 @@
{#if newMessagesPos === i}
<ChannelSeparator title={chunter.string.New} line reverse isNew />
{/if}
<ThreadComment {comment} {employees} />
<ThreadComment message={comment} {employees} />
{/each}
{/if}
</div>
<div class="ref-input">
<AttachmentRefInput {space} _class={chunter.class.Comment} objectId={commentId} on:message={onMessage} />
<AttachmentRefInput space={currentSpace} _class={chunter.class.Comment} objectId={commentId} on:message={onMessage} />
</div>
<style lang="scss">

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import chunter, { Comment, Message } from '@anticrm/chunter'
import chunter, { Comment, Message, ThreadMessage } from '@anticrm/chunter'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { Resources } from '@anticrm/platform'
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
@ -34,7 +34,7 @@ async function MarkUnread (object: Message): Promise<void> {
await client.updateLastView(object.space, chunter.class.Channel, object.createOn - 1, true)
}
async function MarkCommentUnread (object: Comment): Promise<void> {
async function MarkCommentUnread (object: ThreadMessage): Promise<void> {
const client = NotificationClientImpl.getClient()
const value = object.modifiedOn - 1
await client.updateLastView(object.attachedTo, object.attachedToClass, value, true)
@ -45,7 +45,7 @@ async function SubscribeMessage (object: Message): Promise<void> {
await client.updateLastView(object._id, object._class, undefined, true)
}
async function SubscribeComment (object: Comment): Promise<void> {
async function SubscribeComment (object: ThreadMessage): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.updateLastView(object.attachedTo, object.attachedToClass, undefined, true)
}
@ -55,7 +55,7 @@ async function UnsubscribeMessage (object: Message): Promise<void> {
await client.unsubscribe(object._id)
}
async function UnsubscribeComment (object: Comment): Promise<void> {
async function UnsubscribeComment (object: ThreadMessage): Promise<void> {
const client = NotificationClientImpl.getClient()
await client.unsubscribe(object.attachedTo)
}

View File

@ -29,15 +29,31 @@ export interface Channel extends Space {
/**
* @public
*/
export interface Message extends Doc {
export interface ChunterMessage extends AttachedDoc {
content: string
attachments?: number
replies?: Ref<Employee>[]
lastReply?: Timestamp
createBy: Ref<Account>
createOn: Timestamp
}
/**
* @public
*/
export interface ThreadMessage extends ChunterMessage {
attachedTo: Ref<Message>
attachedToClass: Ref<Class<Message>>
}
/**
* @public
*/
export interface Message extends ChunterMessage {
attachedTo: Ref<Space>
attachedToClass: Ref<Class<Space>>
replies?: Ref<Employee>[]
lastReply?: Timestamp
}
/**
* @public
*/
@ -80,6 +96,8 @@ export default plugin(chunterId, {
},
class: {
Message: '' as Ref<Class<Message>>,
ChunterMessage: '' as Ref<Class<ChunterMessage>>,
ThreadMessage: '' as Ref<Class<ThreadMessage>>,
Backlink: '' as Ref<Class<Backlink>>,
Comment: '' as Ref<Class<Comment>>,
Channel: '' as Ref<Class<Channel>>