mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-12 05:27:56 +00:00
Init drafts and fix format
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
06747dc3c2
commit
a305452faf
@ -174,7 +174,7 @@ class Client {
|
||||
card,
|
||||
message,
|
||||
messageCreated,
|
||||
data: { },
|
||||
data: {},
|
||||
creator: this.getSocialId()
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
|
@ -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
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 &&
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
87
packages/ui-next/src/draft.ts
Normal file
87
packages/ui-next/src/draft.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
}
|
@ -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[]
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user