Init drafts and fix format

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

View File

@ -174,7 +174,7 @@ class Client {
card,
message,
messageCreated,
data: { },
data: {},
creator: this.getSocialId()
}
await this.sendEvent(event)

View File

@ -20,7 +20,6 @@
import { Card } from '@hcengineering/card'
import uiNext from '../../plugin'
import { toMarkup } from '../../utils'
import MessageInput from './MessageInput.svelte'
import Label from '../Label.svelte'
import MessageContentViewer from './MessageContentViewer.svelte'
@ -45,12 +44,12 @@
{#if compact}
<div class="message__body">
<div class="time-container">
<div class="message__time message--time_hoverable">
<div class="message__date">
{formatDate(message.created)}
<div class="message__time message--time_hoverable">
<div class="message__date">
{formatDate(message.created)}
</div>
</div>
</div>
</div>
<div class="message__content">
{#if !isEditing && message.content !== ''}
@ -61,7 +60,6 @@
<MessageInput
{card}
{message}
content={toMarkup(message.content)}
onCancel={() => {
isEditing = false
}}
@ -106,7 +104,6 @@
<MessageInput
{card}
{message}
content={toMarkup(message.content)}
onCancel={() => {
isEditing = false
}}

View File

@ -33,18 +33,16 @@
<ThreadMessageViewer {message} thread={message.thread} />
{:else if isActivityMessage(message)}
<ActivityMessageViewer {message} {card} />
{:else}
{#if message.removed}
<span class="overflow-label removed-label">
{:else if message.removed}
<span class="overflow-label removed-label">
<Label label={uiNext.string.MessageWasRemoved} />
</span>
{:else}
<MarkupMessageViewer message={toMarkup(message.content)} />
{/if}
</span>
{:else}
<MarkupMessageViewer message={toMarkup(message.content)} />
{/if}
<style lang="scss">
.removed-label {
color: var(--theme-text-placeholder-color);
}
</style>
</style>

View File

@ -14,8 +14,8 @@
-->
<script lang="ts">
import { Markup, RateLimiter } from '@hcengineering/core'
import { tick, createEventDispatcher, onDestroy } from 'svelte'
import { Markup, RateLimiter, Ref } from '@hcengineering/core'
import { tick, createEventDispatcher } from 'svelte'
import {
uploadFile,
deleteFile,
@ -23,7 +23,7 @@
getClient,
getFileMetadata
} from '@hcengineering/presentation'
import { Message } from '@hcengineering/communication-types'
import { Message, MessageID } from '@hcengineering/communication-types'
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
import { areEqualMarkups, isEmptyMarkup, EmptyMarkup } from '@hcengineering/text'
import { updateMyPresence } from '@hcengineering/presence-resources'
@ -35,11 +35,12 @@
import { defaultMessageInputActions, toMarkdown } from '../../utils'
import uiNext from '../../plugin'
import IconPlus from '../icons/IconPlus.svelte'
import { type TextInputAction, UploadedFile, type PresenceTyping } from '../../types'
import { type TextInputAction, UploadedFile, type PresenceTyping, MessageDraft } from '../../types'
import TypingPresenter from '../TypingPresenter.svelte'
import { getDraft, messageToDraft, saveDraft, getEmptyDraft, removeDraft } from '../../draft'
export let card: Card
export let message: Message | undefined = undefined
export let content: Markup | undefined = undefined
export let title: string = ''
export let onCancel: (() => void) | undefined = undefined
export let onSubmit: ((markdown: string, files: UploadedFile[]) => Promise<void>) | undefined = undefined
@ -50,26 +51,47 @@
const client = getClient()
const me = getCurrentEmployee()
let files: UploadedFile[] =
message?.files?.map((it) => ({
blobId: it.blobId,
type: it.type,
filename: it.filename,
size: it.size,
metadata: it.meta
})) ?? []
let prevCard: Ref<Card> | undefined = card._id
let prevMessage: MessageID | undefined = message?.id
let draft: MessageDraft = message != null ? messageToDraft(message) : getDraft(card._id)
let inputElement: HTMLInputElement
let progress = false
$: if (prevCard !== card._id) {
prevCard = card._id
initDraft()
}
$: if (prevMessage !== message?.id) {
prevMessage = message?.id
initDraft()
}
$: _saveDraft(draft)
function initDraft (): void {
draft = message != null ? messageToDraft(message) : getDraft(card._id)
}
function _saveDraft (draft): void {
if (message === undefined) {
saveDraft(card._id, draft)
}
}
async function handleSubmit (event: CustomEvent<Markup>): Promise<void> {
event.preventDefault()
event.stopPropagation()
const markup = event.detail
const filesToLoad = files
const filesToLoad = draft.files
files = []
draft = getEmptyDraft()
if (message === undefined) {
removeDraft(card._id)
}
const markdown = toMarkdown(markup)
@ -122,7 +144,7 @@
}
for (const file of message.files) {
if (files.find((it) => it.blobId === file.blobId)) continue
if (draft.files.find((it) => it.blobId === file.blobId)) continue
void deleteFile(file.blobId)
await communicationClient.removeFile(card._id, message.id, message.created, file.blobId)
}
@ -151,14 +173,19 @@
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
files.push({
blobId: uuid,
type: file.type,
filename: file.name,
size: file.size,
metadata
})
files = files
draft = {
...draft,
files: [
...draft.files,
{
blobId: uuid,
type: file.type,
filename: file.name,
size: file.size,
metadata
}
]
}
}
const attachAction: TextInputAction = {
@ -170,24 +197,16 @@
},
order: 1000
}
onDestroy(() => {
for (const file of files) {
const fromMessage = message?.files.some((it) => it.blobId === file.blobId)
if (!fromMessage) {
void deleteFile(file.blobId)
}
}
})
async function handleCancel (): Promise<void> {
onCancel?.()
for (const file of files) {
for (const file of draft.files) {
const fromMessage = message?.files.some((it) => it.blobId === file.blobId)
if (!fromMessage) {
void deleteFile(file.blobId)
}
}
files = []
draft = getEmptyDraft()
}
async function loadFiles (evt: ClipboardEvent): Promise<void> {
@ -241,6 +260,10 @@
let newMarkup: Markup | undefined = undefined
async function onUpdate (event: CustomEvent<Markup>): Promise<void> {
draft = {
...draft,
content: event.detail
}
if (message !== undefined) return
newMarkup = event.detail
if (!isEmptyMarkup(newMarkup)) {
@ -256,9 +279,9 @@
if (message === undefined) return files.length > 0
if (message.files.length !== files.length) return true
if (message.files.some((it) => !files.some((f) => f.blobId === it.blobId))) return true
if (newMarkup === undefined || content === undefined) return false
if (newMarkup === undefined || draft.content === undefined) return false
return !areEqualMarkups(content, newMarkup ?? EmptyMarkup)
return !areEqualMarkups(draft.content, newMarkup ?? EmptyMarkup)
}
</script>
@ -281,11 +304,11 @@
on:change={fileSelected}
/>
<TextInput
{content}
content={draft.content}
placeholder={title !== '' ? uiNext.string.MessageIn : undefined}
placeholderParams={title !== '' ? { title } : undefined}
loading={progress}
hasChanges={hasChanges(files, message)}
hasChanges={hasChanges(draft.files, message)}
actions={[...defaultMessageInputActions, attachAction]}
on:submit={handleSubmit}
on:update={onUpdate}
@ -293,9 +316,9 @@
onPaste={pasteAction}
>
<div slot="header" class="header">
{#if files.length > 0}
{#if draft.files.length > 0}
<div class="flex-row-center files-list scroll-divider-color flex-gap-2 mt-2">
{#each files as file (file.blobId)}
{#each draft.files as file (file.blobId)}
<div class="item flex">
<AttachmentPresenter
value={{
@ -309,7 +332,11 @@
removable
on:remove={(result) => {
if (result !== undefined) {
files = files.filter((it) => it.blobId !== file.blobId)
draft = {
...draft,
files: draft.files.filter((it) => it.blobId !== file.blobId)
}
if (!message?.files?.some((it) => it.blobId === file.blobId)) {
void deleteFile(file.blobId)
}

View File

@ -49,7 +49,7 @@
let isDeleted = false
let author: Person | undefined
$:isDeleted = message.removed || (message.type === MessageType.Thread && message.thread == null)
$: isDeleted = message.removed || (message.type === MessageType.Thread && message.thread == null)
$: void updateAuthor(message.creator)
function canEdit (): boolean {
@ -224,11 +224,9 @@
position: relative;
padding: 0.5rem 4rem;
transition: background-color 0s ease 0s;
&:hover:not(.noHover) {
background: var(--global-ui-BackgroundColor);
transition: background-color 0s ease 0.05s;
.message__actions {
visibility: visible;
}

View File

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

View File

@ -37,32 +37,32 @@
}
let isDeleted = false
$:isDeleted = (message.type === MessageType.Thread && message.thread == null) || message.removed
$: isDeleted = (message.type === MessageType.Thread && message.thread == null) || message.removed
</script>
<div class="message__body">
{#if !hideAvatar}
<div class="message__avatar">
{#if !isDeleted}
<PersonPreviewProvider value={author}>
<Avatar name={author?.name} person={author} size="x-small" />
</PersonPreviewProvider>
{:else }
<PersonPreviewProvider value={author}>
<Avatar name={author?.name} person={author} size="x-small" />
</PersonPreviewProvider>
{:else}
<Avatar icon={IconDelete} size="x-small" />
{/if}
{/if}
</div>
{/if}
{#if !isDeleted}
<div class="message__header">
<PersonPreviewProvider value={author}>
<div class="message__username">
{formatName(author?.name ?? '')}
<div class="message__header">
<PersonPreviewProvider value={author}>
<div class="message__username">
{formatName(author?.name ?? '')}
</div>
</PersonPreviewProvider>
<div class="message__date">
{formatDate(message.created)}
</div>
</PersonPreviewProvider>
<div class="message__date">
{formatDate(message.created)}
</div>
</div>
{/if}
<div class="message__text">
@ -70,10 +70,10 @@
</div>
</div>
{#if !isDeleted}
<div class="message__footer">
<MessageFooter {message} {replies} />
</div>
{/if}
<div class="message__footer">
<MessageFooter {message} {replies} />
</div>
{/if}
<style lang="scss">
.message__body {

View File

@ -52,19 +52,19 @@
threadCard = undefined
}
$:threadClass = threadCard?._class ?? thread?.threadType ?? '' as Ref<Class<Card>>
$: threadClass = threadCard?._class ?? thread?.threadType ?? ('' as Ref<Class<Card>>)
$: label = hierarchy.hasClass(threadClass) ? hierarchy.getClass(threadClass).label : undefined
$:isDeleted = isLoaded && threadCard == null
$: isDeleted = isLoaded && threadCard == null
</script>
<div class="thread-view">
{#if label && !isDeleted}
<div class="thread-type">
<span class="overflow-label">
<Label {label} />
<div class="thread-type">
<span class="overflow-label">
<Label {label} />
</span>
</div>
{/if}
</div>
{/if}
{#if threadCard}
<ObjectPresenter
objectId={threadCard._id}
@ -73,7 +73,7 @@
colorInherit
shouldShowAvatar={false}
/>
{:else if isDeleted}
{:else if isDeleted}
<div class="deletedText">
<Label label={uiNext.string.ThreadWasRemoved} />
</div>

View File

@ -0,0 +1,87 @@
// Copyright © 2025 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.
import { getCurrentAccount, type Ref } from '@hcengineering/core'
import { derived, get } from 'svelte/store'
import { type Location, location } from '@hcengineering/ui'
import { type Card } from '@hcengineering/card'
import { EmptyMarkup } from '@hcengineering/text'
import { type Message } from '@hcengineering/communication-types'
import { type MessageDraft } from './types'
import { toMarkup } from './utils'
export const locationWorkspaceStore = derived(location, (loc: Location) => loc.path[1])
function geLocalStorageKey (card: Ref<Card>): string | undefined {
const me = getCurrentAccount()
const workspace = get(locationWorkspaceStore) ?? ''
if (me == null || workspace === '') return undefined
return `${workspace}.${me.uuid}.${card}.message_draft.v1`
}
export function getEmptyDraft (): MessageDraft {
return {
content: EmptyMarkup,
files: []
}
}
export function getDraft (card: Ref<Card>): MessageDraft {
const key = geLocalStorageKey(card)
console.log('getDraft ket', key)
if (key === undefined) {
return getEmptyDraft()
}
const stored = localStorage.getItem(key)
console.log('getDraft stored', stored)
if (stored == null) {
return getEmptyDraft()
}
try {
const data = JSON.parse(stored)
return {
content: data.content,
files: data.files
}
} catch (e) {
console.error(e)
return getEmptyDraft()
}
}
export function removeDraft (card: Ref<Card>): void {
const key = geLocalStorageKey(card)
if (key === undefined) return
localStorage.removeItem(key)
}
export function saveDraft (card: Ref<Card>, draft: MessageDraft): void {
const key = geLocalStorageKey(card)
if (key === undefined) return
localStorage.setItem(key, JSON.stringify(draft))
}
export function messageToDraft (message: Message): MessageDraft {
return {
content: toMarkup(message.content),
files: message.files.map((it) => ({
blobId: it.blobId,
type: it.type,
filename: it.filename,
size: it.size,
metadata: it.meta
}))
}
}

View File

@ -17,7 +17,7 @@ import { type Asset, type IntlString } from '@hcengineering/platform'
import { type ComponentType } from 'svelte'
import { type TextEditorHandler } from '@hcengineering/text-editor'
import { type BlobID } from '@hcengineering/communication-types'
import type { BlobMetadata, Ref, Timestamp } from '@hcengineering/core'
import type { BlobMetadata, Markup, Ref, Timestamp } from '@hcengineering/core'
import type { Person } from '@hcengineering/contact'
export interface NavigationSection {
@ -69,3 +69,8 @@ export interface PresenceTyping {
person: Ref<Person>
lastTyping: Timestamp
}
export interface MessageDraft {
content: Markup
files: UploadedFile[]
}

View File

@ -24,7 +24,7 @@ import {
import { markupToJSON, jsonToMarkup, markupToText } from '@hcengineering/text'
import { showPopup } from '@hcengineering/ui'
import { markupToMarkdown, markdownToMarkup } from '@hcengineering/text-markdown'
import {MessageType, type Message } from '@hcengineering/communication-types'
import { MessageType, type Message } from '@hcengineering/communication-types'
import { getClient, getCommunicationClient } from '@hcengineering/presentation'
import { employeeByPersonIdStore } from '@hcengineering/contact-resources'
import cardPlugin, { type Card } from '@hcengineering/card'

View File

@ -21,7 +21,6 @@ import {
type MessageID,
type WorkspaceID,
SortingOrder,
type Message,
MessagesGroup,
BlobID
} from '@hcengineering/communication-types'
@ -183,7 +182,13 @@ async function applyPatchesToGroup (
return applyPatches(message, patches)
}
})
const blob = await uploadGroupFile(ctx, storage, workspace, parsedFile.metadata, updatedMessages.map(deserializeMessage))
const blob = await uploadGroupFile(
ctx,
storage,
workspace,
parsedFile.metadata,
updatedMessages.map(deserializeMessage)
)
await createGroup(
client,
group.card,