mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 22:15:04 +02:00
Add new chat fixes (#8687)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
||||
type RequestEvent,
|
||||
type ResponseEvent,
|
||||
type UpdateNotificationContextEvent,
|
||||
type AddCollaboratorsEvent,
|
||||
MessageRequestEventType,
|
||||
NotificationRequestEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
@@ -181,6 +182,16 @@ class Client {
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async addCollaborators (card: CardID, cardType: CardType, collaborators: AccountID[]): Promise<void> {
|
||||
const event: AddCollaboratorsEvent = {
|
||||
type: NotificationRequestEventType.AddCollaborators,
|
||||
card,
|
||||
cardType,
|
||||
collaborators
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createFile (
|
||||
card: CardID,
|
||||
message: MessageID,
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
//MessageInput
|
||||
--next-message-input-color-background: var(--color-global-dark-gray);
|
||||
--next-message-input-color-stroke: var(--color-global-charcoal-black);
|
||||
|
||||
//Message
|
||||
--next-message-hover-color-background: var(--color-huly-off-white-5)
|
||||
}
|
||||
|
||||
|
||||
@@ -165,4 +168,8 @@
|
||||
//MessageInput
|
||||
--next-message-input-color-background: var(--color-global-light-gray);
|
||||
--next-message-input-color-stroke: var(--color-global-white-100);
|
||||
|
||||
|
||||
//Message
|
||||
--next-message-hover-color-background: var(--color-huly-dark-grey-5)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} v {time}",
|
||||
"YearAt": "{year} v {time}",
|
||||
"Yesterday": "Včera",
|
||||
"YesterdayAt": "Včera v {time}"
|
||||
"YesterdayAt": "Včera v {time}",
|
||||
"AndMore": "a {count} dalších",
|
||||
"IsTyping": "{count, plural, =1 {píše} other {píší}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} um {time}",
|
||||
"YearAt": "{year} um {time}",
|
||||
"Yesterday": "Gestern",
|
||||
"YesterdayAt": "Gestern um {time}"
|
||||
"YesterdayAt": "Gestern um {time}",
|
||||
"AndMore": "und {count} weitere",
|
||||
"IsTyping": "{count, plural, =1 {schreibt} other {schreiben}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} at {time}",
|
||||
"YearAt": "{year} at {time}",
|
||||
"Yesterday": "Yesterday",
|
||||
"YesterdayAt": "Yesterday at {time}"
|
||||
"YesterdayAt": "Yesterday at {time}",
|
||||
"AndMore": "and {count} more",
|
||||
"IsTyping": "{count, plural, =1 {is typing} other {are typing}}..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} a las {time}",
|
||||
"YearAt": "{year} a las {time}",
|
||||
"Yesterday": "Ayer",
|
||||
"YesterdayAt": "Ayer a las {time}"
|
||||
"YesterdayAt": "Ayer a las {time}",
|
||||
"AndMore": "y {count} más",
|
||||
"IsTyping": "{count, plural, =1 {está escribiendo} other {están escribiendo}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} à {time}",
|
||||
"YearAt": "{year} à {time}",
|
||||
"Yesterday": "Hier",
|
||||
"YesterdayAt": "Hier à {time}"
|
||||
"YesterdayAt": "Hier à {time}",
|
||||
"AndMore": "et {count} autres",
|
||||
"IsTyping": "{count, plural, =1 {est en train d'écrire} other {écrivent}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} alle {time}",
|
||||
"YearAt": "{year} alle {time}",
|
||||
"Yesterday": "Ieri",
|
||||
"YesterdayAt": "Ieri alle {time}"
|
||||
"YesterdayAt": "Ieri alle {time}",
|
||||
"AndMore": "e {count} altri",
|
||||
"IsTyping": "{count, plural, =1 {sta scrivendo} other {stanno scrivendo}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday}・{time}",
|
||||
"YearAt": "{year}・{time}",
|
||||
"Yesterday": "昨日",
|
||||
"YesterdayAt": "昨日・{time}"
|
||||
"YesterdayAt": "昨日・{time}",
|
||||
"AndMore": "および{count}件の他",
|
||||
"IsTyping": "{count, plural, =1 {入力中} other {入力中}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} às {time}",
|
||||
"YearAt": "{year} às {time}",
|
||||
"Yesterday": "Ontem",
|
||||
"YesterdayAt": "Ontem às {time}"
|
||||
"YesterdayAt": "Ontem às {time}",
|
||||
"AndMore": "e mais {count}",
|
||||
"IsTyping": "{count, plural, =1 {está digitando} other {estão digitando}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
"WeekdayAt": "{weekday} в {time}",
|
||||
"YearAt": "{year} в {time}",
|
||||
"Yesterday": "Вчера",
|
||||
"YesterdayAt": "Вчера в {time}"
|
||||
"YesterdayAt": "Вчера в {time}",
|
||||
"AndMore": "и еще {count}",
|
||||
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"WeekdayAt": "{weekday} 于 {time}",
|
||||
"YearAt": "{year} 于 {time}",
|
||||
"Yesterday": "昨天",
|
||||
"YesterdayAt": "昨天 于 {time}"
|
||||
"YesterdayAt": "昨天 于 {time}",
|
||||
"AndMore": "和其他 {count} 条",
|
||||
"IsTyping": "{count, plural, =1 {正在输入} other {正在输入}}"
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,8 @@
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/view": "^0.6.13",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/presence": "^0.6.0",
|
||||
"@hcengineering/presence-resources": "^0.6.0",
|
||||
"@tiptap/core": "^2.11.7",
|
||||
"@tiptap/pm": "^2.11.7",
|
||||
"fast-equals": "^5.2.2",
|
||||
|
||||
@@ -44,10 +44,12 @@
|
||||
</script>
|
||||
|
||||
<div class="date-separator" class:sticky>
|
||||
<div class="date-separator__date">
|
||||
{#await formatDate(date, $themeStore.language) then date}
|
||||
{date}
|
||||
{/await}
|
||||
<div class="date-separator__content">
|
||||
<div class="date-separator__date">
|
||||
{#await formatDate(date, $themeStore.language) then date}
|
||||
{date}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,10 +57,7 @@
|
||||
.date-separator {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
border-bottom: 1px solid var(--next-divider-color);
|
||||
padding: 0 2rem;
|
||||
background: var(--next-panel-color-background);
|
||||
|
||||
&.sticky {
|
||||
@@ -68,10 +67,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.date-separator__content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--next-divider-color);
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0;
|
||||
}
|
||||
|
||||
.date-separator__date {
|
||||
color: var(--next-text-color-secondary);
|
||||
font-size: 0.813rem;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { tooltip } from '@hcengineering/ui'
|
||||
import { PersonId } from '@hcengineering/core'
|
||||
|
||||
import ReactionsTooltip from './ReactionsTooltip.svelte'
|
||||
import Icon from './Icon.svelte'
|
||||
import { IconSize, IconComponent } from '../types'
|
||||
|
||||
@@ -23,13 +27,18 @@
|
||||
export let count: number | undefined = undefined
|
||||
export let selected: boolean = false
|
||||
export let active: boolean = false
|
||||
export let socialIds: PersonId[] = []
|
||||
</script>
|
||||
|
||||
<!--TODO: add users tooltip-->
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="reaction" class:selected class:active on:click>
|
||||
<div
|
||||
class="reaction"
|
||||
class:selected
|
||||
class:active
|
||||
on:click
|
||||
use:tooltip={{ component: ReactionsTooltip, props: { socialIds } }}
|
||||
>
|
||||
<div class="reaction__emoji" class:foreground={icon != null}>
|
||||
{#if icon}
|
||||
<Icon {icon} size={iconSize} />
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<ReactionPresenter
|
||||
{emoji}
|
||||
selected={reactions.some((it) => me.socialIds.includes(it.creator))}
|
||||
socialIds={reactions.map((it) => it.creator)}
|
||||
count={reactions.length}
|
||||
on:click={() => dispatch('click', emoji)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- 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. -->
|
||||
|
||||
<script lang="ts">
|
||||
import { PersonId } from '@hcengineering/core'
|
||||
import { ObjectPresenter } from '@hcengineering/view-resources'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { personRefByPersonIdStore } from '@hcengineering/contact-resources'
|
||||
|
||||
export let socialIds: PersonId[] = []
|
||||
|
||||
$: persons = new Set(socialIds.map((user) => $personRefByPersonIdStore.get(user)))
|
||||
</script>
|
||||
|
||||
<div class="m-2 flex-col flex-gap-2">
|
||||
{#each persons as person (person)}
|
||||
<ObjectPresenter objectId={person} _class={contact.class.Person} disabled />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -20,6 +20,7 @@
|
||||
import { handler, registerFocus } from '@hcengineering/ui'
|
||||
import { FocusPosition } from '@tiptap/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { EditorView } from '@tiptap/pm/view'
|
||||
import {
|
||||
EmojiExtension,
|
||||
IsEmptyContentExtension,
|
||||
@@ -45,6 +46,7 @@
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
export let autofocus: FocusPosition = false
|
||||
export let onCancel: (() => void) | undefined = undefined
|
||||
export let onPaste: ((view: EditorView, event: ClipboardEvent) => boolean) | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -148,9 +150,11 @@
|
||||
{boundary}
|
||||
canEmbedFiles={false}
|
||||
canEmbedImages={false}
|
||||
dropcursor={false}
|
||||
editorAttributes={{ class: 'next-text-editor-view' }}
|
||||
{placeholder}
|
||||
{placeholderParams}
|
||||
{onPaste}
|
||||
on:content={(ev) => {
|
||||
if (canSubmit(loading, content, hasNonTextContent)) {
|
||||
dispatch('submit', ev.detail)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { getName, Person, getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { IdMap, notEmpty } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { presenceByObjectId } from '@hcengineering/presence-resources'
|
||||
import { CardID } from '@hcengineering/communication-types'
|
||||
|
||||
import uiNext from '../plugin'
|
||||
import { type PresenceTyping } from '../types'
|
||||
|
||||
export let cardId: CardID
|
||||
const typingDelay = 2000
|
||||
const maxTypingPersons = 3
|
||||
const me = getCurrentEmployee()
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
|
||||
let typingPersonsLabel: string = ''
|
||||
let typingPersonsCount = 0
|
||||
let moreCount: number = 0
|
||||
|
||||
let typing: PresenceTyping[] = []
|
||||
$: presence = $presenceByObjectId.get(cardId) ?? []
|
||||
$: typing = presence.map((p) => p.presence.typing).filter(notEmpty)
|
||||
|
||||
$: updateTypingPersons($personByIdStore, typing)
|
||||
|
||||
function updateTypingPersons (personById: IdMap<Person>, typingInfo: PresenceTyping[]): void {
|
||||
const now = Date.now()
|
||||
const personIds = new Set(
|
||||
typingInfo.filter((info) => info.person !== me && now - info.lastTyping < typingDelay).map((info) => info.person)
|
||||
)
|
||||
const names = Array.from(personIds)
|
||||
.map((personId) => personById.get(personId))
|
||||
.filter((person): person is Person => person !== undefined)
|
||||
.map((person) => getName(hierarchy, person))
|
||||
.sort((name1, name2) => name1.localeCompare(name2))
|
||||
|
||||
typingPersonsCount = names.length
|
||||
typingPersonsLabel = names.slice(0, maxTypingPersons).join(', ')
|
||||
moreCount = Math.max(names.length - maxTypingPersons, 0)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
updateTypingPersons($personByIdStore, typing)
|
||||
}, typingDelay)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<span class="root h-4 mt-1 mb-1 ml-0-5 overflow-label">
|
||||
{#if typingPersonsLabel !== ''}
|
||||
<span class="fs-bold">
|
||||
{typingPersonsLabel}
|
||||
</span>
|
||||
{#if moreCount > 0}
|
||||
<span class="ml-1"><Label label={uiNext.string.AndMore} params={{ count: moreCount }} /></span>
|
||||
{/if}
|
||||
<span class="ml-1"><Label label={uiNext.string.IsTyping} params={{ count: typingPersonsCount }} /></span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { IconSize } from '../../types'
|
||||
|
||||
export let size: IconSize = 'small'
|
||||
</script>
|
||||
|
||||
<svg class="next-svg__{size}" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M2.75571 10.3721C2.67505 8.16892 3.82845 6.21211 5.51842 4.81037C7.21886 3.39994 9.35454 2.64423 11.05 2.76018C11.9385 2.82094 12.8149 2.98182 13.6797 3.19064C14.7797 3.51647 16.6649 4.60069 17.7603 6.12395C18.6923 7.42004 19.0162 8.74543 19.0911 9.14216L20.5651 8.86401C20.4666 8.34186 20.0827 6.78423 18.9781 5.2482C17.6529 3.40536 15.4516 2.14469 14.0834 1.74586C14.0069 1.72679 13.9314 1.70778 13.8565 1.68894C12.9854 1.46987 12.2042 1.27339 11.127 1.26199C8.39779 1.23312 6.48237 2.06199 4.5608 3.65584C2.62441 5.26196 1.1593 7.62561 1.25622 10.4132C1.25812 12.2248 1.64051 13.5796 2.21557 14.6627C2.69647 15.5686 3.29897 16.2569 3.85452 16.8334L3.05433 19.7384C2.88102 20.3679 3.51935 20.8887 4.09404 20.6353L7.40538 19.1733C7.87457 19.3317 8.33732 19.4723 8.8072 19.586L9.16015 18.1281C8.54145 17.9784 7.93959 17.7727 7.3393 17.5628L4.91459 18.6333L5.53026 16.3982L5.22549 16.0898C4.60039 15.4573 3.9989 14.8229 3.54043 13.9594C3.08809 13.1073 2.75622 11.9931 2.75622 10.3996V10.3859L2.75571 10.3721Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M22.2645 18.4346C23.8127 15.061 21.4542 11.3919 18.3526 10.5041C16.8345 10.0007 15.1435 10.203 13.7238 10.8708C12.2987 11.5412 11.0662 12.7196 10.5459 14.2602C9.54062 17.2366 11.4209 20.8545 14.6395 21.8814C15.9617 22.3032 17.5346 22.1569 18.1927 21.9491C18.431 22.0161 18.7158 22.1372 19.0197 22.2664C19.7954 22.5963 20.6952 22.979 21.2615 22.6468C21.5475 22.479 21.6604 22.1705 21.6471 21.8546C21.6294 21.4301 21.573 21.0054 21.5167 20.5813C21.4805 20.3088 21.4443 20.0367 21.4185 19.7651C21.7475 19.356 22.0453 18.9123 22.2645 18.4346ZM11.9671 14.7402C12.3308 13.6633 13.226 12.7627 14.3623 12.2282C15.4988 11.6936 16.7959 11.564 17.8928 11.932L17.9099 11.9377L17.9273 11.9426C20.3341 12.6228 21.9859 15.4235 20.9068 17.7968C20.633 18.3376 20.2858 18.8072 19.8658 19.2409C19.9138 19.791 19.9655 20.3406 20.03 20.889C19.7776 20.8166 19.5238 20.7496 19.2701 20.6826C18.8945 20.5833 18.5189 20.4841 18.148 20.3677L17.9034 20.4622C16.9745 20.8209 15.9738 20.7129 15.0414 20.436C12.3639 19.7416 11.2065 16.9918 11.9671 14.7402Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,108 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { showPopup, EmojiPopup } from '@hcengineering/ui'
|
||||
import { Message } from '@hcengineering/communication-types'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import IconEmoji from '../icons/IconEmoji.svelte'
|
||||
import IconMessageMultiple from '../icons/IconMessageMultiple.svelte'
|
||||
import IconPen from '../icons/IconPen.svelte'
|
||||
import uiNext from '../../plugin'
|
||||
import { toggleReaction } from '../../utils'
|
||||
import { Action } from '../../types'
|
||||
import Button from '../Button.svelte'
|
||||
|
||||
export let message: Message
|
||||
export let editable: boolean = true
|
||||
export let isOpened: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getActions (): Action[] {
|
||||
const actions: Action[] = [
|
||||
{
|
||||
id: 'emoji',
|
||||
label: uiNext.string.Emoji,
|
||||
icon: IconEmoji,
|
||||
order: 10,
|
||||
action: (event: MouseEvent): void => {
|
||||
isOpened = true
|
||||
showPopup(
|
||||
EmojiPopup,
|
||||
{},
|
||||
event.target as HTMLElement,
|
||||
async (result) => {
|
||||
isOpened = false
|
||||
const emoji = result?.emoji
|
||||
if (emoji == null) {
|
||||
return
|
||||
}
|
||||
|
||||
await toggleReaction(message, emoji)
|
||||
},
|
||||
() => {
|
||||
isOpened = false
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'reply',
|
||||
label: uiNext.string.Reply,
|
||||
icon: IconMessageMultiple,
|
||||
order: 20,
|
||||
action: (): void => {
|
||||
dispatch('reply')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (editable) {
|
||||
actions.unshift({
|
||||
id: 'edit',
|
||||
label: uiNext.string.Edit,
|
||||
icon: IconPen,
|
||||
order: 30,
|
||||
action: () => {
|
||||
dispatch('edit')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return actions.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
const actions: Action[] = getActions()
|
||||
</script>
|
||||
|
||||
<div class="message-actions-panel">
|
||||
{#each actions as action (action.id)}
|
||||
<Button icon={action.icon} iconSize="medium" tooltip={{ label: action.label }} on:click={action.action} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.message-actions-panel {
|
||||
display: flex;
|
||||
background: var(--next-background-color);
|
||||
border: 1px solid var(--next-border-color);
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
gap: 0.25rem;
|
||||
box-shadow: 0.5rem 0.75rem 1rem 0.25rem var(--color-huly-dark-grey-25);
|
||||
}
|
||||
</style>
|
||||
@@ -20,12 +20,17 @@
|
||||
import { uploadFile, deleteFile, getCommunicationClient } from '@hcengineering/presentation'
|
||||
import { CardID, CardType, Message } from '@hcengineering/communication-types'
|
||||
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { updateMyPresence } from '@hcengineering/presence-resources'
|
||||
import { ThrottledCaller } from '@hcengineering/ui'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
|
||||
import TextInput from '../TextInput.svelte'
|
||||
import { defaultMessageInputActions, toMarkdown } from '../../utils'
|
||||
import uiNext from '../../plugin'
|
||||
import IconPlus from '../icons/IconPlus.svelte'
|
||||
import { type TextInputAction, UploadedFile } from '../../types'
|
||||
import { type TextInputAction, UploadedFile, type PresenceTyping } from '../../types'
|
||||
import TypingPresenter from '../TypingPresenter.svelte'
|
||||
|
||||
export let cardId: CardID | undefined = undefined
|
||||
export let cardType: CardType | undefined = undefined
|
||||
@@ -36,14 +41,16 @@
|
||||
export let onCancel: (() => void) | undefined = undefined
|
||||
export let onSubmit: ((markdown: string, files: UploadedFile[]) => Promise<void>) | undefined = undefined
|
||||
|
||||
const throttle = new ThrottledCaller(500)
|
||||
const dispatch = createEventDispatcher()
|
||||
const communicationClient = getCommunicationClient()
|
||||
const me = getCurrentEmployee()
|
||||
|
||||
let files: UploadedFile[] = []
|
||||
let inputElement: HTMLInputElement
|
||||
|
||||
let progress = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const communicationClient = getCommunicationClient()
|
||||
|
||||
async function handleSubmit (event: CustomEvent<Markup>): Promise<void> {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -57,6 +64,7 @@
|
||||
|
||||
if (onSubmit !== undefined) {
|
||||
await onSubmit(markdown, filesToLoad)
|
||||
return
|
||||
}
|
||||
|
||||
if (message === undefined) {
|
||||
@@ -145,53 +153,129 @@
|
||||
}
|
||||
files = []
|
||||
}
|
||||
|
||||
async function loadFiles (evt: ClipboardEvent): Promise<void> {
|
||||
progress = true
|
||||
const files = (evt.clipboardData?.files ?? []) as File[]
|
||||
|
||||
for (const file of files) {
|
||||
await addFile(file)
|
||||
}
|
||||
|
||||
progress = false
|
||||
}
|
||||
|
||||
function pasteAction (_: any, evt: ClipboardEvent): boolean {
|
||||
let target: HTMLElement | null = evt.target as HTMLElement
|
||||
let allowed = false
|
||||
while (target != null) {
|
||||
target = target.parentElement
|
||||
if (target === inputElement) {
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
if (!allowed) {
|
||||
return false
|
||||
}
|
||||
const hasFiles = Array.from(evt.clipboardData?.items ?? []).some((i) => i.kind === 'file')
|
||||
|
||||
if (hasFiles) {
|
||||
void loadFiles(evt)
|
||||
return true
|
||||
}
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
async function fileDrop (e: DragEvent): Promise<void> {
|
||||
const list = e.dataTransfer?.files
|
||||
const limiter = new RateLimiter(10)
|
||||
|
||||
if (list === undefined || list.length === 0) return
|
||||
progress = true
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) {
|
||||
await limiter.add(() => addFile(file))
|
||||
}
|
||||
}
|
||||
await limiter.waitProcessing()
|
||||
progress = false
|
||||
}
|
||||
|
||||
async function onUpdate (event: CustomEvent<Markup>): Promise<void> {
|
||||
if (cardId === undefined || cardType === undefined) return
|
||||
if (message !== undefined) return
|
||||
const markup = event.detail
|
||||
if (!isEmptyMarkup(markup)) {
|
||||
throttle.call(() => {
|
||||
const room = { objectId: cardId, objectClass: cardType }
|
||||
const typing: PresenceTyping = { person: me, lastTyping: Date.now() }
|
||||
updateMyPresence(room, { typing })
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
disabled={inputElement == null}
|
||||
multiple
|
||||
type="file"
|
||||
name="file"
|
||||
id="file"
|
||||
style="display: none"
|
||||
on:change={fileSelected}
|
||||
/>
|
||||
<TextInput
|
||||
{content}
|
||||
{placeholder}
|
||||
{placeholderParams}
|
||||
loading={progress}
|
||||
hasNonTextContent={files.length > 0}
|
||||
actions={[...defaultMessageInputActions, attachAction]}
|
||||
on:submit={handleSubmit}
|
||||
onCancel={onCancel ? handleCancel : undefined}
|
||||
<div
|
||||
class="flex-col no-print w-full"
|
||||
on:dragover|preventDefault={() => {}}
|
||||
on:dragleave={() => {}}
|
||||
on:drop|preventDefault|stopPropagation={fileDrop}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
{#if files.length > 0}
|
||||
<div class="flex-row-center files-list scroll-divider-color flex-gap-2">
|
||||
{#each files as file (file.blobId)}
|
||||
<div class="item flex">
|
||||
<AttachmentPresenter
|
||||
value={{
|
||||
file: file.blobId,
|
||||
name: file.filename,
|
||||
type: file.type
|
||||
}}
|
||||
removable
|
||||
on:remove={(result) => {
|
||||
if (result !== undefined) {
|
||||
files = files.filter((it) => it.blobId !== file.blobId)
|
||||
void deleteFile(file.blobId)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</TextInput>
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
disabled={inputElement == null}
|
||||
multiple
|
||||
type="file"
|
||||
name="file"
|
||||
id="file"
|
||||
style="display: none"
|
||||
on:change={fileSelected}
|
||||
/>
|
||||
<TextInput
|
||||
{content}
|
||||
{placeholder}
|
||||
{placeholderParams}
|
||||
loading={progress}
|
||||
hasNonTextContent={files.length > 0}
|
||||
actions={[...defaultMessageInputActions, attachAction]}
|
||||
on:submit={handleSubmit}
|
||||
on:update={onUpdate}
|
||||
onCancel={onCancel ? handleCancel : undefined}
|
||||
onPaste={pasteAction}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
{#if files.length > 0}
|
||||
<div class="flex-row-center files-list scroll-divider-color flex-gap-2">
|
||||
{#each files as file (file.blobId)}
|
||||
<div class="item flex">
|
||||
<AttachmentPresenter
|
||||
value={{
|
||||
file: file.blobId,
|
||||
name: file.filename,
|
||||
type: file.type,
|
||||
size: file.size
|
||||
}}
|
||||
removable
|
||||
on:remove={(result) => {
|
||||
if (result !== undefined) {
|
||||
files = files.filter((it) => it.blobId !== file.blobId)
|
||||
void deleteFile(file.blobId)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</TextInput>
|
||||
</div>
|
||||
|
||||
{#if cardId && message === undefined}
|
||||
<TypingPresenter {cardId} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
import uiNext from '../../plugin'
|
||||
import MessageReplies from './MessageReplies.svelte'
|
||||
import { toMarkup, toggleReaction } from '../../utils'
|
||||
import MessageActionsPanel from './MessageActionsPanel.svelte'
|
||||
import IconEmoji from '../icons/IconEmoji.svelte'
|
||||
import IconMessageMultiple from '../icons/IconMessageMultiple.svelte'
|
||||
import IconPen from '../icons/IconPen.svelte'
|
||||
|
||||
export let card: Card
|
||||
export let message: Message
|
||||
@@ -123,6 +127,7 @@
|
||||
const actions: MenuAction[] = [
|
||||
{
|
||||
label: uiNext.string.Emoji,
|
||||
icon: IconEmoji,
|
||||
action: async (): Promise<void> => {
|
||||
showPopup(
|
||||
EmojiPopup,
|
||||
@@ -142,6 +147,7 @@
|
||||
},
|
||||
{
|
||||
label: uiNext.string.Reply,
|
||||
icon: IconMessageMultiple,
|
||||
action: async (): Promise<void> => {
|
||||
dispatch('reply', message)
|
||||
}
|
||||
@@ -151,6 +157,7 @@
|
||||
if (canEdit()) {
|
||||
actions.unshift({
|
||||
label: uiNext.string.Edit,
|
||||
icon: IconPen,
|
||||
action: handleEdit
|
||||
})
|
||||
}
|
||||
@@ -158,12 +165,31 @@
|
||||
showPopup(Menu, { actions }, getEventPositionElement(event), () => {})
|
||||
}
|
||||
}
|
||||
|
||||
let isActionsOpened = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!--TODO: remove or improve on:contextmenu-->
|
||||
<div class="message" id={message.id.toString()} on:contextmenu={handleContextMenu}>
|
||||
<div
|
||||
class="message"
|
||||
id={message.id.toString()}
|
||||
on:contextmenu={handleContextMenu}
|
||||
class:active={isActionsOpened && !isEditing}
|
||||
>
|
||||
{#if !isEditing}
|
||||
<div class="message__actions" class:opened={isActionsOpened}>
|
||||
<MessageActionsPanel
|
||||
{message}
|
||||
editable={canEdit()}
|
||||
bind:isOpened={isActionsOpened}
|
||||
on:edit={handleEdit}
|
||||
on:reply={() => {
|
||||
dispatch('reply', message)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="message__body">
|
||||
<div class="message__avatar">
|
||||
<PersonPreviewProvider value={author}>
|
||||
@@ -187,7 +213,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if !isEditing}
|
||||
<div class="message__text overflow-label">
|
||||
<div class="message__text">
|
||||
<MessageContentViewer {message} {card} />
|
||||
</div>
|
||||
{:else}
|
||||
@@ -216,7 +242,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if replies && message.thread && message.thread.repliesCount > 0}
|
||||
<div class="message__replies">
|
||||
<div class="message__replies overflow-label">
|
||||
<MessageReplies
|
||||
count={message.thread.repliesCount}
|
||||
lastReply={message.thread.lastReply}
|
||||
@@ -233,7 +259,19 @@
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 1rem 2rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--next-message-hover-color-background);
|
||||
.message__actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--next-message-hover-color-background);
|
||||
}
|
||||
}
|
||||
|
||||
.message__body {
|
||||
@@ -242,6 +280,7 @@
|
||||
gap: 0.75rem;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message__avatar {
|
||||
@@ -308,12 +347,29 @@
|
||||
|
||||
.message__replies {
|
||||
padding-top: 0.5rem;
|
||||
margin-left: 2.75rem;
|
||||
margin-left: 2.25rem;
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message__files {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
overflow-x: auto;
|
||||
margin-left: 2.75rem;
|
||||
}
|
||||
|
||||
.message__actions {
|
||||
position: absolute;
|
||||
top: -0.75rem;
|
||||
right: 1rem;
|
||||
visibility: hidden;
|
||||
|
||||
&.opened {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -125,40 +125,49 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="replies" on:click>
|
||||
<div class="replies__count">
|
||||
<Label label={uiNext.string.RepliesCount} params={{ count }} />
|
||||
</div>
|
||||
<span class="text overflow-label">
|
||||
<span class="replies__count">
|
||||
<Label label={uiNext.string.RepliesCount} params={{ count }} />
|
||||
</span>
|
||||
|
||||
<div class="replies__last-reply">
|
||||
<Label label={uiNext.string.LastReply} />
|
||||
{displayDate}
|
||||
</div>
|
||||
<span class="replies__last-reply">
|
||||
<Label label={uiNext.string.LastReply} />
|
||||
<span class="lower">
|
||||
{displayDate}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.replies {
|
||||
display: flex;
|
||||
padding: 8px 4px;
|
||||
padding: 0.5rem 0.5rem;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1 0 0;
|
||||
border-radius: 8px;
|
||||
gap: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-huly-off-white-5);
|
||||
background: var(--next-button-menu-ghost-background-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.replies__count {
|
||||
color: var(--next-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.replies__last-reply {
|
||||
color: var(--next-text-color-tertiary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.messages-group__messages {
|
||||
@@ -69,7 +68,5 @@
|
||||
align-items: flex-start;
|
||||
flex: 1 0 0;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
$: action = message.data.action
|
||||
</script>
|
||||
|
||||
<span class="flex-center flex-gap-1">
|
||||
<span class="flex flex-gap-1 overflow-label">
|
||||
{#if clazz.icon}
|
||||
<span class="icon mr-1">
|
||||
<Icon icon={clazz.icon} size="small" />
|
||||
@@ -54,7 +54,7 @@
|
||||
disabled={action === 'remove'}
|
||||
accent={true}
|
||||
component={objectPanel?.component ?? view.component.EditDoc}
|
||||
shrink={0}
|
||||
shrink={1}
|
||||
>
|
||||
{card.title}
|
||||
</DocNavLink>
|
||||
|
||||
@@ -45,7 +45,9 @@ export const uiNext = plugin(uiNextId, {
|
||||
WeekdayAt: '' as IntlString,
|
||||
YearAt: '' as IntlString,
|
||||
Yesterday: '' as IntlString,
|
||||
YesterdayAt: '' as IntlString
|
||||
YesterdayAt: '' as IntlString,
|
||||
AndMore: '' as IntlString,
|
||||
IsTyping: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ 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 { Ref, Timestamp } from '@hcengineering/core'
|
||||
import type { Person } from '@hcengineering/contact'
|
||||
|
||||
export interface NavigationSection {
|
||||
id: string
|
||||
@@ -85,3 +87,17 @@ export interface UploadedFile {
|
||||
filename: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
id: string
|
||||
label: IntlString
|
||||
icon: IconComponent
|
||||
action: (event: MouseEvent) => void
|
||||
order: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface PresenceTyping {
|
||||
person: Ref<Person>
|
||||
lastTyping: Timestamp
|
||||
}
|
||||
|
||||
@@ -153,7 +153,12 @@
|
||||
weekInfoFirstDay
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={docWidth} bind:innerHeight={docHeight} />
|
||||
<svelte:window
|
||||
bind:innerWidth={docWidth}
|
||||
bind:innerHeight={docHeight}
|
||||
on:dragover|preventDefault
|
||||
on:drop|preventDefault
|
||||
/>
|
||||
|
||||
<Theme>
|
||||
<div id="ui-root" class:mobile-theme={isMobile}>
|
||||
|
||||
@@ -177,7 +177,6 @@
|
||||
.chat {
|
||||
background: var(--next-background-color);
|
||||
border-color: var(--next-border-color);
|
||||
font-family: 'Inter Display', sans-serif;
|
||||
}
|
||||
|
||||
.chat__navigator {
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { UploadedFile } from '@hcengineering/ui-next'
|
||||
|
||||
import ChatInput from './ChatInput.svelte'
|
||||
|
||||
export let card: Card
|
||||
export let card: Card | undefined = undefined
|
||||
export let height: number | undefined = undefined
|
||||
export let onSubmit: ((markdown: string, files: UploadedFile[]) => Promise<void>) | undefined = undefined
|
||||
|
||||
let _height: number | undefined | null
|
||||
|
||||
@@ -30,7 +32,7 @@
|
||||
</script>
|
||||
|
||||
<div class="chat__footer" bind:clientHeight={_height}>
|
||||
<ChatInput {card} />
|
||||
<ChatInput {card} {onSubmit} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -39,7 +41,8 @@
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 12px;
|
||||
padding: 1.25rem 0.75rem;
|
||||
padding-bottom: 0;
|
||||
|
||||
border-top: 1px solid var(--next-divider-color);
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { Header, Icon } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { PresenceAvatars } from '@hcengineering/presence-resources'
|
||||
|
||||
export let card: Card | undefined = undefined
|
||||
export let canClose: boolean = false
|
||||
@@ -46,6 +47,12 @@
|
||||
>{card?.title ?? title}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="presence">
|
||||
{#if card}
|
||||
<PresenceAvatars object={card} size="x-small" limit={5} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Header>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -14,17 +14,19 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { MessageInput } from '@hcengineering/ui-next'
|
||||
import { MessageInput, UploadedFile } from '@hcengineering/ui-next'
|
||||
import { Card } from '@hcengineering/card'
|
||||
|
||||
import chat from '../plugin'
|
||||
|
||||
export let card: Card
|
||||
export let card: Card | undefined
|
||||
export let onSubmit: ((markdown: string, files: UploadedFile[]) => Promise<void>) | undefined = undefined
|
||||
</script>
|
||||
|
||||
<MessageInput
|
||||
cardId={card._id}
|
||||
cardType={card._class}
|
||||
cardId={card?._id}
|
||||
cardType={card?._class}
|
||||
placeholder={chat.string.MessageIn}
|
||||
placeholderParams={{ title: card.title }}
|
||||
placeholderParams={{ title: card?.title ?? '' }}
|
||||
{onSubmit}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { NotificationContext } from '@hcengineering/communication-types'
|
||||
import { Presence } from '@hcengineering/presence-resources'
|
||||
|
||||
import ChatHeader from './ChatHeader.svelte'
|
||||
import ChatBody from './ChatBody.svelte'
|
||||
@@ -27,6 +28,7 @@
|
||||
let footerHeight: number | undefined = undefined
|
||||
</script>
|
||||
|
||||
<Presence object={card} />
|
||||
<ChatHeader {card} />
|
||||
{#key card._id}
|
||||
<ChatBody {card} {footerHeight} {context} />
|
||||
|
||||
@@ -21,12 +21,15 @@
|
||||
import { MessagePresenter, MessageInput, Divider, UploadedFile } from '@hcengineering/ui-next'
|
||||
import { fillDefaults, MarkupBlobRef, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { employeeByPersonIdStore } from '@hcengineering/contact-resources'
|
||||
import { getEmployeeBySocialId } from '@hcengineering/contact'
|
||||
|
||||
import { ChatWidgetData } from '../types'
|
||||
import chat from '../plugin'
|
||||
import ChatHeader from './ChatHeader.svelte'
|
||||
import ChatBody from './ChatBody.svelte'
|
||||
import { createThreadTitle } from '../utils'
|
||||
import ChatFooter from './ChatFooter.svelte'
|
||||
|
||||
export let widget: Widget | undefined
|
||||
export let widgetState: WidgetState | undefined
|
||||
@@ -105,6 +108,8 @@
|
||||
|
||||
let threadId: CardID | undefined = message.thread?.thread
|
||||
if (threadId == null) {
|
||||
const author =
|
||||
$employeeByPersonIdStore.get(message.creator) ?? (await getEmployeeBySocialId(client, message.creator))
|
||||
const lastOne = await client.findOne(cardPlugin.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
const title = createThreadTitle(message, parentCard)
|
||||
const data = fillDefaults(
|
||||
@@ -120,6 +125,9 @@
|
||||
threadId = (await client.createDoc(chat.masterTag.Thread, cardPlugin.space.Default, data)) as CardID
|
||||
|
||||
await communicationClient.createThread(card, message.id, message.created, threadId)
|
||||
if (author?.active === true && author?.personUuid !== undefined) {
|
||||
await communicationClient.addCollaborators(threadId, chat.masterTag.Thread, [author.personUuid])
|
||||
}
|
||||
}
|
||||
|
||||
if (threadId != null) {
|
||||
@@ -129,23 +137,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
let footerHeight: number | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if widget && data && data.message}
|
||||
<div class="chat-widget" style:width style:height>
|
||||
<ChatHeader card={threadCard} icon={chat.icon.Thread} title={data.name} canClose on:close />
|
||||
{#if message && parentCard}
|
||||
<div style:padding="1rem">
|
||||
<MessagePresenter {message} card={parentCard} replies={false} />
|
||||
</div>
|
||||
<div class="mt-4" />
|
||||
<MessagePresenter {message} card={parentCard} replies={false} />
|
||||
<Divider />
|
||||
|
||||
<div class="messages">
|
||||
<ChatBody card={threadCard} bottomStart={false} showDates={false} overlyColor="var(--next-background-color)" />
|
||||
</div>
|
||||
<div style:padding="1rem">
|
||||
<MessageInput onSubmit={handleSubmit} />
|
||||
<ChatBody
|
||||
card={threadCard}
|
||||
bottomStart={false}
|
||||
showDates={false}
|
||||
overlyColor="var(--next-background-color)"
|
||||
{footerHeight}
|
||||
/>
|
||||
</div>
|
||||
<ChatFooter card={threadCard} bind:height={footerHeight} onSubmit={handleSubmit} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -158,7 +170,6 @@
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--next-background-color);
|
||||
font-family: 'Inter Display', sans-serif;
|
||||
}
|
||||
|
||||
.messages {
|
||||
@@ -167,4 +178,9 @@
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--next-divider-color);
|
||||
padding: 1.25rem 1rem 0 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -401,6 +401,23 @@ export const personByPersonIdStore: Readable<Map<PersonId, Person>> = derived(
|
||||
return new Map(mapped)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* [PersonId (social ID) => Employee] mapping
|
||||
*/
|
||||
export const employeeByPersonIdStore: Readable<Map<PersonId, Employee>> = derived(
|
||||
[personByPersonIdStore, employeeByIdStore],
|
||||
([personByPersonId, employeeById]) => {
|
||||
const mapped = Array.from(personByPersonId.entries())
|
||||
.map(([personId, person]) => {
|
||||
const employee = employeeById.get(person._id as Ref<Employee>)
|
||||
if (employee === undefined) return undefined
|
||||
return [personId, employee] as const
|
||||
})
|
||||
.filter(notEmpty)
|
||||
return new Map(mapped)
|
||||
}
|
||||
)
|
||||
/**
|
||||
* [string (social key) => Employee] mapping
|
||||
*/
|
||||
|
||||
@@ -302,6 +302,14 @@ export async function getPersonBySocialId (client: Client, socialIdString: Perso
|
||||
return await client.findOne(contact.class.Person, { _id: socialId?.attachedTo, _class: socialId?.attachedToClass })
|
||||
}
|
||||
|
||||
export async function getEmployeeBySocialId (client: Client, socialIdString: PersonId): Promise<Employee | undefined> {
|
||||
const socialId = await client.findOne(contact.class.SocialIdentity, { _id: socialIdString as SocialIdentityRef })
|
||||
|
||||
if (socialId === undefined) return undefined
|
||||
|
||||
return await client.findOne(contact.mixin.Employee, { _id: socialId.attachedTo as Ref<Employee> })
|
||||
}
|
||||
|
||||
export async function getPersonRefBySocialId (
|
||||
client: Client,
|
||||
socialIdString: PersonId
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
{boundary}
|
||||
{canEmbedFiles}
|
||||
{canEmbedImages}
|
||||
dropcursor={false}
|
||||
on:content={(ev) => {
|
||||
if (canSubmit) {
|
||||
dispatch('message', ev.detail)
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
export let autofocus: FocusPosition = false
|
||||
export let canEmbedFiles = true
|
||||
export let canEmbedImages = true
|
||||
export let dropcursor: false | undefined = undefined
|
||||
export let onPaste: ((view: EditorView, event: ClipboardEvent) => boolean) | undefined = undefined
|
||||
|
||||
let element: HTMLElement
|
||||
@@ -167,6 +168,7 @@
|
||||
(await getEditorKit()).configure({
|
||||
mode: 'compact',
|
||||
file: canEmbedFiles ? {} : false,
|
||||
dropcursor,
|
||||
image: canEmbedImages
|
||||
? {
|
||||
toolbar: {
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface DefaultKitOptions {
|
||||
levels?: Level[]
|
||||
}
|
||||
history?: false
|
||||
dropcursor?: false
|
||||
}
|
||||
|
||||
export const DefaultKit = Extension.create<DefaultKitOptions>({
|
||||
@@ -59,7 +60,8 @@ export const DefaultKit = Extension.create<DefaultKitOptions>({
|
||||
heading: this.options.heading,
|
||||
history: this.options.history,
|
||||
bulletList: false,
|
||||
orderedList: false
|
||||
orderedList: false,
|
||||
dropcursor: this.options.dropcursor
|
||||
}),
|
||||
ListItem.extend({ group: 'listItems' }),
|
||||
BulletList.extend({ content: 'listItems+' }),
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
flex-direction: row;
|
||||
min-width: 25rem;
|
||||
border-radius: 0 var(--medium-BorderRadius) var(--medium-BorderRadius) 0;
|
||||
border-bottom: 1px solid transparent; // adjust the side panel body height to match the main panel
|
||||
|
||||
&.mini {
|
||||
justify-content: flex-end;
|
||||
|
||||
Reference in New Issue
Block a user