Add remove message action

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina Fefelova 2025-05-22 09:24:54 +04:00
parent 26f01bb27d
commit 06747dc3c2
No known key found for this signature in database
GPG Key ID: 750D35EF042F0690
18 changed files with 132 additions and 24 deletions

View File

@ -167,6 +167,19 @@ class Client {
await this.sendEvent(event)
}
async removeMessage (card: CardID, message: MessageID, messageCreated: Date): Promise<void> {
const event: CreatePatchEvent = {
type: MessageRequestEventType.CreatePatch,
patchType: PatchType.remove,
card,
message,
messageCreated,
data: { },
creator: this.getSocialId()
}
await this.sendEvent(event)
}
async createReaction (card: CardID, message: MessageID, messageCreated: Date, reaction: string): Promise<void> {
const event: CreateReactionEvent = {
type: MessageRequestEventType.CreateReaction,

View File

@ -28,6 +28,10 @@
"Yesterday": "Včera",
"YesterdayAt": "Včera v {time}",
"AndMore": "a {count} dalších",
"IsTyping": "{count, plural, =1 {píše} other {píší}}"
"IsTyping": "{count, plural, =1 {píše} other {píší}}",
"Loading": "Načítání...",
"MessageIn": "Zpráva #{title}",
"ThreadWasDeleted": "Tato vlákno byla smazána.",
"MessageWasRemoved": "Tato zpráva byla odstraněna."
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "Gestern",
"YesterdayAt": "Gestern um {time}",
"AndMore": "und {count} weitere",
"IsTyping": "{count, plural, =1 {schreibt} other {schreiben}}"
"IsTyping": "{count, plural, =1 {schreibt} other {schreiben}}",
"Loading": "Laden...",
"MessageIn": "Nachricht #{title}",
"ThreadWasDeleted": "Dieser Thread wurde gelöscht.",
"MessageWasRemoved": "Diese Nachricht wurde entfernt."
}
}

View File

@ -30,6 +30,8 @@
"AndMore": "and {count} more",
"IsTyping": "{count, plural, =1 {is typing} other {are typing}}...",
"Loading": "Loading...",
"MessageIn": "Message #{title}"
"MessageIn": "Message #{title}",
"ThreadWasRemoved": "This thread was removed.",
"MessageWasRemoved": "This message was removed."
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "Ayer",
"YesterdayAt": "Ayer a las {time}",
"AndMore": "y {count} más",
"IsTyping": "{count, plural, =1 {está escribiendo} other {están escribiendo}}"
"IsTyping": "{count, plural, =1 {está escribiendo} other {están escribiendo}}",
"Loading": "Cargando...",
"MessageIn": "Mensaje #{title}",
"ThreadWasDeleted": "Este hilo fue eliminado.",
"MessageWasRemoved": "Este mensaje fue eliminado."
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "Hier",
"YesterdayAt": "Hier à {time}",
"AndMore": "et {count} autres",
"IsTyping": "{count, plural, =1 {est en train d'écrire} other {écrivent}}"
"IsTyping": "{count, plural, =1 {est en train d'écrire} other {écrivent}}",
"Loading": "Chargement...",
"MessageIn": "Message #{title}",
"ThreadWasDeleted": "Ce fil de discussion a été supprimé.",
"MessageWasRemoved": "Ce message a été supprimé."
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "Ieri",
"YesterdayAt": "Ieri alle {time}",
"AndMore": "e {count} altri",
"IsTyping": "{count, plural, =1 {sta scrivendo} other {stanno scrivendo}}"
"IsTyping": "{count, plural, =1 {sta scrivendo} other {stanno scrivendo}}",
"Loading": "Caricamento...",
"MessageIn": "Messaggio #{title}",
"ThreadWasDeleted": "Questo thread è stato eliminato.",
"MessageWasRemoved": "Questo messaggio è stato rimosso."
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "昨日",
"YesterdayAt": "昨日・{time}",
"AndMore": "および{count}件の他",
"IsTyping": "{count, plural, =1 {入力中} other {入力中}}"
"IsTyping": "{count, plural, =1 {入力中} other {入力中}}",
"Loading": "読み込み中...",
"MessageIn": "メッセージ #{title}",
"ThreadWasDeleted": "このスレッドは削除されました。",
"MessageWasRemoved": "このメッセージは削除されました。"
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "Ontem",
"YesterdayAt": "Ontem às {time}",
"AndMore": "e mais {count}",
"IsTyping": "{count, plural, =1 {está digitando} other {estão digitando}}"
"IsTyping": "{count, plural, =1 {está digitando} other {estão digitando}}",
"Loading": "Carregando...",
"MessageIn": "Mensagem #{title}",
"ThreadWasDeleted": "Este thread foi excluído.",
"MessageWasRemoved": "Este mensagem foi removida."
}
}

View File

@ -27,6 +27,10 @@
"Yesterday": "Вчера",
"YesterdayAt": "Вчера в {time}",
"AndMore": "и еще {count}",
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}..."
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}...",
"Loading": "Загрузка...",
"MessageIn": "Сообщение #{title}",
"ThreadWasDeleted": "Этот поток был удален.",
"MessageWasRemoved": "Это сообщение было удалено."
}
}

View File

@ -28,6 +28,10 @@
"Yesterday": "昨天",
"YesterdayAt": "昨天 于 {time}",
"AndMore": "和其他 {count} 条",
"IsTyping": "{count, plural, =1 {正在输入} other {正在输入}}"
"IsTyping": "{count, plural, =1 {正在输入} other {正在输入}}",
"Loading": "加载中...",
"MessageIn": "消息 #{title}",
"ThreadWasDeleted": "此线程已被删除。",
"MessageWasRemoved": "此消息已被删除。"
}
}

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { showPopup, ButtonIcon } from '@hcengineering/ui'
import ui, { showPopup, ButtonIcon, IconDelete } from '@hcengineering/ui'
import { Message } from '@hcengineering/communication-types'
import { createEventDispatcher } from 'svelte'
import emojiPlugin from '@hcengineering/emoji'
@ -30,6 +30,7 @@
export let canReply: boolean = true
export let canReact: boolean = true
export let isOpened: boolean = false
export let canRemove: boolean = false
const dispatch = createEventDispatcher()
@ -89,6 +90,18 @@
})
}
if (canRemove) {
actions.push({
id: 'remove',
label: ui.string.Remove,
icon: IconDelete,
order: 999,
action: () => {
dispatch('remove')
}
})
}
return actions.sort((a, b) => a.order - b.order)
}

View File

@ -17,11 +17,13 @@
import { MessageViewer as MarkupMessageViewer } from '@hcengineering/presentation'
import { Message, MessageType } from '@hcengineering/communication-types'
import { Card } from '@hcengineering/card'
import { Label } from '@hcengineering/ui'
import ActivityMessageViewer from './ActivityMessageViewer.svelte'
import { toMarkup } from '../../utils'
import { isActivityMessage } from '../../activity'
import ThreadMessageViewer from './ThreadMessageViewer.svelte'
import uiNext from '../../plugin'
export let card: Card
export let message: Message
@ -29,10 +31,20 @@
{#if message.type === MessageType.Thread || message.thread != null}
<ThreadMessageViewer {message} thread={message.thread} />
{:else if message.type === MessageType.Message}
<MarkupMessageViewer message={toMarkup(message.content)} />
{:else if isActivityMessage(message)}
<ActivityMessageViewer {message} {card} />
{:else}
<MarkupMessageViewer message={toMarkup(message.content)} />
{#if message.removed}
<span class="overflow-label removed-label">
<Label label={uiNext.string.MessageWasRemoved} />
</span>
{:else}
<MarkupMessageViewer message={toMarkup(message.content)} />
{/if}
{/if}
<style lang="scss">
.removed-label {
color: var(--theme-text-placeholder-color);
}
</style>

View File

@ -15,10 +15,10 @@
<script lang="ts">
import { getPersonBySocialId, Person } from '@hcengineering/contact'
import { getClient } from '@hcengineering/presentation'
import { getClient, getCommunicationClient } from '@hcengineering/presentation'
import { Card } from '@hcengineering/card'
import { getCurrentAccount } from '@hcengineering/core'
import { getEventPositionElement, showPopup, Action as MenuAction } from '@hcengineering/ui'
import ui, { getEventPositionElement, showPopup, Action as MenuAction, IconDelete } from '@hcengineering/ui'
import { personByPersonIdStore } from '@hcengineering/contact-resources'
import type { SocialID } from '@hcengineering/communication-types'
import { Message, MessageType } from '@hcengineering/communication-types'
@ -42,11 +42,14 @@
export let hideAvatar: boolean = false
const client = getClient()
const communicationClient = getCommunicationClient()
const me = getCurrentAccount()
let isEditing = false
let isDeleted = false
let author: Person | undefined
$:isDeleted = message.removed || (message.type === MessageType.Thread && message.thread == null)
$: void updateAuthor(message.creator)
function canEdit (): boolean {
@ -57,6 +60,13 @@
return me.socialIds.includes(message.creator)
}
function canRemove (): boolean {
if (!editable) return false
if (message.type !== MessageType.Message) return false
return me.socialIds.includes(message.creator)
}
function canReply (): boolean {
return message.type === MessageType.Message || message.type === MessageType.Thread
}
@ -74,6 +84,12 @@
isEditing = true
}
async function handleRemove (): Promise<void> {
if (!canRemove()) return
message.removed = true
await communicationClient.removeMessage(message.card, message.id, message.created)
}
function isInside (x: number, y: number, rect: DOMRect): boolean {
return x >= rect.left && y >= rect.top && x <= rect.right && y <= rect.bottom
}
@ -147,6 +163,14 @@
})
}
if (canRemove()) {
actions.push({
label: ui.string.Remove,
icon: IconDelete,
action: handleRemove
})
}
showPopup(Menu, { actions }, getEventPositionElement(event), () => {})
}
}
@ -161,19 +185,21 @@
<div
class="message"
id={`${message.id}`}
on:contextmenu={editable && !isEditing ? handleContextMenu : undefined}
on:contextmenu={editable && !isEditing && !isDeleted ? handleContextMenu : undefined}
class:active={isActionsOpened && !isEditing}
class:noHover={!editable}
style:padding
>
{#if !isEditing && editable}
{#if !isEditing && editable && !isDeleted}
<div class="message__actions" class:opened={isActionsOpened}>
<MessageActionsPanel
{message}
editable={canEdit()}
canReply={canReply()}
canRemove={canRemove()}
bind:isOpened={isActionsOpened}
on:edit={handleEdit}
on:remove={handleRemove}
on:reply={() => {
void replyToThread(message, card)
}}
@ -181,7 +207,7 @@
</div>
{/if}
{#if isThread || message.type === MessageType.Activity}
{#if isThread || message.type === MessageType.Activity || message.removed}
<OneRowMessageBody {message} {card} {author} {replies} {hideAvatar} />
{:else}
<MessageBody {message} {card} {author} bind:isEditing {compact} {replies} {hideAvatar} />

View File

@ -47,7 +47,8 @@
<div class="messages-group__messages">
{#each messages as message, index (message.id)}
{@const previousMessage = messages[index - 1]}
{@const compact =
{@const compact = !message.removed &&
!previousMessage?.removed &&
previousMessage !== undefined &&
previousMessage.creator === message.creator &&
previousMessage.type === message.type &&

View File

@ -37,7 +37,7 @@
}
let isDeleted = false
$:isDeleted = message.type === MessageType.Thread && message.thread == null
$:isDeleted = (message.type === MessageType.Thread && message.thread == null) || message.removed
</script>
<div class="message__body">

View File

@ -19,7 +19,8 @@
import cardPlugin, { Card } from '@hcengineering/card'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { Label } from '@hcengineering/ui'
import {Class, type Ref} from '@hcengineering/core'
import { Class, type Ref } from '@hcengineering/core'
import uiNext from '../../plugin'
export let message: Message
export let thread: Thread | undefined
@ -59,7 +60,9 @@
<div class="thread-view">
{#if label && !isDeleted}
<div class="thread-type">
<span class="overflow-label">
<Label {label} />
</span>
</div>
{/if}
{#if threadCard}
@ -72,7 +75,7 @@
/>
{:else if isDeleted}
<div class="deletedText">
This thread was deleted.
<Label label={uiNext.string.ThreadWasRemoved} />
</div>
{/if}
</div>

View File

@ -49,7 +49,9 @@ export const uiNext = plugin(uiNextId, {
AndMore: '' as IntlString,
IsTyping: '' as IntlString,
Loading: '' as IntlString,
MessageIn: '' as IntlString
MessageIn: '' as IntlString,
ThreadWasRemoved: '' as IntlString,
MessageWasRemoved: '' as IntlString
}
})