mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-29 04:55:03 +02:00
UBERF-4360: rewrite chat (#4265)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -24,5 +24,5 @@
|
||||
</script>
|
||||
|
||||
{#if extension}
|
||||
<Component is={extension.components[kind]} {props} on:close on:open />
|
||||
<Component is={extension.components[kind]} {props} on:close on:open on:submit />
|
||||
{/if}
|
||||
|
||||
@@ -30,12 +30,17 @@
|
||||
export let object: Doc
|
||||
export let isNewestFirst = false
|
||||
|
||||
let filtered: ActivityMessage[]
|
||||
const allId = activity.ids.AllFilter
|
||||
const client = getClient()
|
||||
|
||||
let filtered: ActivityMessage[]
|
||||
let filters: ActivityMessagesFilter[] = []
|
||||
|
||||
const saved = localStorage.getItem('activity-filter')
|
||||
let selectedFiltersRefs: Ref<Doc>[] | 'All' =
|
||||
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
|
||||
|
||||
let selectedFiltersRefs: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = saved
|
||||
? (JSON.parse(saved) as Ref<ActivityMessagesFilter>[])
|
||||
: allId
|
||||
let selectedFilters: ActivityMessagesFilter[] = []
|
||||
|
||||
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
|
||||
@@ -45,14 +50,14 @@
|
||||
filters = res
|
||||
|
||||
if (saved !== null && saved !== undefined) {
|
||||
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved)
|
||||
if (temp !== 'All' && Array.isArray(temp)) {
|
||||
const temp: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = JSON.parse(saved)
|
||||
if (temp !== allId && Array.isArray(temp)) {
|
||||
selectedFiltersRefs = temp.filter((it) => filters.findIndex((f) => it === f._id) > -1)
|
||||
if (selectedFiltersRefs.length === 0) {
|
||||
selectedFiltersRefs = 'All'
|
||||
selectedFiltersRefs = allId
|
||||
}
|
||||
} else {
|
||||
selectedFiltersRefs = 'All'
|
||||
selectedFiltersRefs = allId
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -69,9 +74,9 @@
|
||||
isNewestFirst = res.value
|
||||
return
|
||||
}
|
||||
const selected = res.value as Ref<Doc>[]
|
||||
const selected = res.value as Ref<ActivityMessagesFilter>[]
|
||||
const isAll = selected.length === filters.length || selected.length === 0
|
||||
if (res.action === 'select') selectedFiltersRefs = isAll ? 'All' : selected
|
||||
if (res.action === 'select') selectedFiltersRefs = isAll ? allId : selected
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -81,14 +86,14 @@
|
||||
async function updateFilterActions (
|
||||
messages: ActivityMessage[],
|
||||
filters: ActivityMessagesFilter[],
|
||||
selected: Ref<Doc>[] | 'All',
|
||||
selected: Ref<Doc>[] | Ref<ActivityMessagesFilter>,
|
||||
sortOrder: SortingOrder
|
||||
): Promise<void> {
|
||||
const sortedMessages = sortActivityMessages(messages, sortOrder).sort(({ isPinned }) =>
|
||||
isPinned && sortOrder === SortingOrder.Ascending ? -1 : 1
|
||||
)
|
||||
|
||||
if (selected === 'All') {
|
||||
if (selected === allId) {
|
||||
filtered = sortedMessages
|
||||
|
||||
dispatch('update', filtered)
|
||||
@@ -126,9 +131,9 @@
|
||||
<div
|
||||
class="tag-icon"
|
||||
on:click={() => {
|
||||
if (selectedFiltersRefs !== 'All') {
|
||||
if (selectedFiltersRefs !== allId && Array.isArray(selectedFiltersRefs)) {
|
||||
const ids = selectedFiltersRefs.filter((it) => it !== filter._id)
|
||||
selectedFiltersRefs = ids.length > 0 ? ids : 'All'
|
||||
selectedFiltersRefs = ids.length > 0 ? ids : allId
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Class, Doc, Ref, SortingOrder, isOtherDay, Timestamp } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, {
|
||||
ActivityExtension,
|
||||
ActivityMessage,
|
||||
ActivityMessagesFilter,
|
||||
DisplayActivityMessage,
|
||||
type DisplayDocUpdateMessage
|
||||
} from '@hcengineering/activity'
|
||||
import notification, { InboxNotificationsClient } from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import ActivityMessagePresenter from './activity-message/ActivityMessagePresenter.svelte'
|
||||
import { combineActivityMessages, filterActivityMessages, getClosestDateSelectorDate } from '../activityMessagesUtils'
|
||||
import ActivityMessagesSeparator from './activity-message/ActivityMessagesSeparator.svelte'
|
||||
import JumpToDateSelector from './JumpToDateSelector.svelte'
|
||||
import ActivityExtensionComponent from './ActivityExtension.svelte'
|
||||
|
||||
export let _class: Ref<Class<ActivityMessage>> = activity.class.ActivityMessage
|
||||
export let object: Doc
|
||||
export let isLoading = false
|
||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||
export let scrollElement: HTMLDivElement | undefined = undefined
|
||||
export let startFromBottom = false
|
||||
export let filter: Ref<ActivityMessagesFilter> | undefined = undefined
|
||||
export let withDates: boolean = true
|
||||
export let collection: string | undefined = undefined
|
||||
export let showEmbedded = false
|
||||
export let skipLabels = false
|
||||
export let lastViewedTimestamp: Timestamp | undefined = undefined
|
||||
|
||||
const dateSelectorHeight = 30
|
||||
const headerHeight = 50
|
||||
|
||||
const client = getClient()
|
||||
const messagesQuery = createQuery()
|
||||
|
||||
let prevMessagesLength = 0
|
||||
let messages: DisplayActivityMessage[] = []
|
||||
let displayMessages: DisplayActivityMessage[] = []
|
||||
let filters: ActivityMessagesFilter[] = []
|
||||
let extensions: ActivityExtension[] = []
|
||||
|
||||
let separatorElement: HTMLDivElement | undefined = undefined
|
||||
let separatorPosition: number | undefined = undefined
|
||||
let showDateSelector = false
|
||||
let selectedDate: Timestamp | undefined = undefined
|
||||
|
||||
let isViewportInitialized = false
|
||||
|
||||
let inboxClient: InboxNotificationsClient | undefined = undefined
|
||||
|
||||
getResource(notification.function.GetInboxNotificationsClient).then((getClientFn) => {
|
||||
inboxClient = getClientFn()
|
||||
})
|
||||
|
||||
$: client.findAll(activity.class.ActivityExtension, { ofClass: object._class }).then((res) => {
|
||||
extensions = res
|
||||
})
|
||||
|
||||
client.findAll(activity.class.ActivityMessagesFilter, {}).then((res) => {
|
||||
filters = res
|
||||
})
|
||||
|
||||
$: messagesQuery.query(
|
||||
_class,
|
||||
{ attachedTo: object._id },
|
||||
(result: ActivityMessage[]) => {
|
||||
prevMessagesLength = messages.length
|
||||
messages = combineActivityMessages(result)
|
||||
isLoading = false
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
createdOn: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function scrollToBottom (afterScrollFn?: () => void) {
|
||||
setTimeout(() => {
|
||||
if (scrollElement !== undefined) {
|
||||
scrollElement?.scrollTo(0, scrollElement.scrollHeight)
|
||||
afterScrollFn?.()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function scrollToSeparator (afterScrollFn?: () => void) {
|
||||
setTimeout(() => {
|
||||
if (separatorElement) {
|
||||
separatorElement.scrollIntoView()
|
||||
afterScrollFn?.()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function handleJumpToDate (e: CustomEvent) {
|
||||
const date = e.detail.date
|
||||
|
||||
if (!date || !scrollElement) {
|
||||
return
|
||||
}
|
||||
|
||||
let closestDate = getClosestDateSelectorDate(date, scrollElement)
|
||||
|
||||
if (!closestDate) {
|
||||
return
|
||||
}
|
||||
|
||||
if (closestDate < date) {
|
||||
closestDate = undefined
|
||||
} else {
|
||||
scrollToDate(closestDate)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToDate (date: Timestamp) {
|
||||
let offset = date && document.getElementById(date.toString())?.offsetTop
|
||||
|
||||
if (!offset || !scrollElement) {
|
||||
return
|
||||
}
|
||||
|
||||
offset = offset - headerHeight - dateSelectorHeight / 2
|
||||
|
||||
scrollElement.scrollTo({ left: 0, top: offset })
|
||||
}
|
||||
|
||||
function handleScroll () {
|
||||
updateSelectedDate()
|
||||
readViewportMessages()
|
||||
}
|
||||
|
||||
function readViewportMessages () {
|
||||
if (!isViewportInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
const containerRect = scrollElement?.getBoundingClientRect()
|
||||
|
||||
if (containerRect === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const messagesToRead: DisplayActivityMessage[] = []
|
||||
|
||||
for (const message of displayMessages) {
|
||||
const msgElement = document.getElementById(message._id)
|
||||
|
||||
if (!msgElement) {
|
||||
continue
|
||||
}
|
||||
|
||||
const messageRect = msgElement.getBoundingClientRect()
|
||||
|
||||
if (messageRect.top >= containerRect.top && messageRect.bottom - messageRect.height / 2 <= containerRect.bottom) {
|
||||
messagesToRead.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
readMessage(messagesToRead)
|
||||
}
|
||||
|
||||
function readMessage (messages: DisplayActivityMessage[]) {
|
||||
if (inboxClient === undefined || messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const allIds = messages
|
||||
.map((message) => {
|
||||
const combined =
|
||||
message._class === activity.class.DocUpdateMessage
|
||||
? (message as DisplayDocUpdateMessage)?.combinedMessagesIds
|
||||
: undefined
|
||||
|
||||
return [message._id, ...(combined ?? [])]
|
||||
})
|
||||
.flat()
|
||||
|
||||
inboxClient.readMessages(allIds)
|
||||
|
||||
const notifyContext = get(inboxClient.docNotifyContextByDoc).get(object._id)
|
||||
const lastTimestamp = messages[messages.length - 1].createdOn ?? 0
|
||||
|
||||
if (notifyContext !== undefined && (notifyContext.lastViewedTimestamp ?? 0) < lastTimestamp) {
|
||||
client.update(notifyContext, { lastViewedTimestamp: lastTimestamp })
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedDate () {
|
||||
if (!scrollElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const clientRect = scrollElement.getBoundingClientRect()
|
||||
const dateSelectors = scrollElement.getElementsByClassName('dateSelector')
|
||||
const firstVisibleDateElement = Array.from(dateSelectors)
|
||||
.reverse()
|
||||
.find((child) => {
|
||||
if (child?.nodeType === Node.ELEMENT_NODE) {
|
||||
const rect = child?.getBoundingClientRect()
|
||||
if (rect.top - dateSelectorHeight / 2 <= clientRect.top + dateSelectorHeight) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (!firstVisibleDateElement) {
|
||||
return
|
||||
}
|
||||
|
||||
showDateSelector = clientRect.top - firstVisibleDateElement.getBoundingClientRect().top > -dateSelectorHeight / 2
|
||||
selectedDate = parseInt(firstVisibleDateElement.id)
|
||||
}
|
||||
|
||||
$: filterActivityMessages(messages, filters, object._class, filter).then((filteredMessages) => {
|
||||
displayMessages = filteredMessages
|
||||
})
|
||||
|
||||
function getNewPosition (displayMessages: ActivityMessage[], lastViewedTimestamp?: Timestamp): number | undefined {
|
||||
if (displayMessages.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (separatorPosition !== undefined) {
|
||||
return separatorPosition
|
||||
}
|
||||
|
||||
if (lastViewedTimestamp === undefined) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (lastViewedTimestamp === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const lastViewedMessageIdx = displayMessages.findIndex((message, index) => {
|
||||
const createdOn = message.createdOn ?? 0
|
||||
const nextCreatedOn = displayMessages[index + 1]?.createdOn ?? 0
|
||||
|
||||
return lastViewedTimestamp >= createdOn && lastViewedTimestamp < nextCreatedOn
|
||||
})
|
||||
|
||||
return lastViewedMessageIdx !== -1 ? lastViewedMessageIdx + 1 : -1
|
||||
}
|
||||
|
||||
$: separatorPosition = getNewPosition(displayMessages, lastViewedTimestamp)
|
||||
$: initializeViewport(scrollElement, separatorElement, separatorPosition)
|
||||
|
||||
function initializeViewport (
|
||||
scrollElement?: HTMLDivElement,
|
||||
separatorElement?: HTMLDivElement,
|
||||
separatorPosition?: number
|
||||
) {
|
||||
if (separatorPosition === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (separatorPosition < 0 && scrollElement) {
|
||||
scrollToBottom(markViewportInitialized)
|
||||
} else if (separatorElement) {
|
||||
scrollToSeparator(markViewportInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
function markViewportInitialized () {
|
||||
// We should mark viewport as initialized when scroll is finished
|
||||
setTimeout(() => {
|
||||
isViewportInitialized = true
|
||||
readViewportMessages()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function handleMessageSent () {
|
||||
scrollToBottom(markViewportInitialized)
|
||||
}
|
||||
|
||||
$: if (isViewportInitialized && messages.length > prevMessagesLength) {
|
||||
setTimeout(() => {
|
||||
readViewportMessages()
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !isLoading}
|
||||
<div class="flex-col vScroll" bind:this={scrollElement} on:scroll={handleScroll}>
|
||||
{#if startFromBottom}
|
||||
<div class="grower" />
|
||||
{/if}
|
||||
{#if showDateSelector}
|
||||
<div class="ml-2 pr-2 fixed">
|
||||
<JumpToDateSelector {selectedDate} fixed on:jumpToDate={handleJumpToDate} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each displayMessages as message, index}
|
||||
{#if index === separatorPosition}
|
||||
<ActivityMessagesSeparator bind:element={separatorElement} title={activity.string.New} line reverse isNew />
|
||||
{/if}
|
||||
|
||||
{#if withDates && (index === 0 || isOtherDay(message.createdOn ?? 0, displayMessages[index - 1].createdOn ?? 0))}
|
||||
<JumpToDateSelector selectedDate={message.createdOn} on:jumpToDate={handleJumpToDate} />
|
||||
{/if}
|
||||
|
||||
<ActivityMessagePresenter
|
||||
value={message}
|
||||
skipLabel={skipLabels}
|
||||
{showEmbedded}
|
||||
isHighlighted={message._id === selectedMessageId}
|
||||
shouldScroll={selectedMessageId === message._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ref-input">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
{extensions}
|
||||
props={{ object, boundary: scrollElement, collection }}
|
||||
on:submit={handleMessageSent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grower {
|
||||
flex-grow: 10;
|
||||
flex-shrink: 5;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ref-input {
|
||||
margin: 1.25rem 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -15,15 +15,17 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Label, resizeObserver, CheckBox, MiniToggle } from '@hcengineering/ui'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { CheckBox, Label, MiniToggle, resizeObserver } from '@hcengineering/ui'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
|
||||
import activity from '../plugin'
|
||||
|
||||
export let selectedFiltersRefs: Ref<Doc>[] | 'All' = 'All'
|
||||
export let selectedFiltersRefs: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = activity.ids.AllFilter
|
||||
export let filters: ActivityMessagesFilter[] = []
|
||||
|
||||
const allId = activity.ids.AllFilter
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
@@ -31,20 +33,19 @@
|
||||
interface ActionMenu {
|
||||
label: IntlString
|
||||
checked: boolean
|
||||
value: Ref<Doc> | 'All'
|
||||
value: Ref<ActivityMessagesFilter>
|
||||
}
|
||||
let menu: ActionMenu[] = [
|
||||
{
|
||||
label: activity.string.All,
|
||||
checked: true,
|
||||
value: 'All'
|
||||
}
|
||||
]
|
||||
filters.map((fl) => menu.push({ label: fl.label, checked: false, value: fl._id }))
|
||||
if (selectedFiltersRefs !== 'All') {
|
||||
selectedFiltersRefs.forEach((fl) => {
|
||||
const index = menu.findIndex((el) => el.value === fl)
|
||||
if (index !== -1) menu[index].checked = true
|
||||
|
||||
let menu: ActionMenu[] = []
|
||||
|
||||
filters.map(({ label, _id }) => menu.push({ label, checked: _id === allId, value: _id }))
|
||||
|
||||
if (Array.isArray(selectedFiltersRefs)) {
|
||||
selectedFiltersRefs.forEach((filterId) => {
|
||||
const index = menu.findIndex(({ value }) => value === filterId)
|
||||
if (index !== -1) {
|
||||
menu[index].checked = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,31 +80,31 @@
|
||||
|
||||
const checkAll = () => {
|
||||
menu.forEach((el, i) => (el.checked = i === 0))
|
||||
selectedFiltersRefs = 'All'
|
||||
selectedFiltersRefs = allId
|
||||
}
|
||||
|
||||
const uncheckAll = () => {
|
||||
menu.forEach((el) => (el.checked = true))
|
||||
const temp = filters.map((fl) => fl._id as Ref<Doc>)
|
||||
selectedFiltersRefs = temp
|
||||
selectedFiltersRefs = filters.map(({ _id }) => _id)
|
||||
}
|
||||
|
||||
const selectRow = (n: number) => {
|
||||
if (n === 0) {
|
||||
if (selectedFiltersRefs === 'All') uncheckAll()
|
||||
if (selectedFiltersRefs === allId) uncheckAll()
|
||||
else checkAll()
|
||||
} else {
|
||||
if (selectedFiltersRefs === 'All') {
|
||||
if (selectedFiltersRefs === allId) {
|
||||
menu[n].checked = true
|
||||
selectedFiltersRefs = [menu[n].value as Ref<Doc>]
|
||||
selectedFiltersRefs = [menu[n].value]
|
||||
} else if (menu[n].checked) {
|
||||
if (menu.filter((el) => el.checked).length === 2) checkAll()
|
||||
else {
|
||||
else if (Array.isArray(selectedFiltersRefs)) {
|
||||
menu[n].checked = false
|
||||
selectedFiltersRefs = selectedFiltersRefs.filter((fl) => fl !== menu[n].value)
|
||||
}
|
||||
} else {
|
||||
} else if (Array.isArray(selectedFiltersRefs)) {
|
||||
menu[n].checked = true
|
||||
selectedFiltersRefs.push(menu[n].value as Ref<Doc>)
|
||||
selectedFiltersRefs.push(menu[n].value)
|
||||
}
|
||||
}
|
||||
menu = menu
|
||||
@@ -152,7 +153,7 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex-center justify-end mr-3 pointer-events-none">
|
||||
<CheckBox checked={item.checked} symbol={selectedFiltersRefs !== 'All' && i === 0 ? 'minus' : 'check'} />
|
||||
<CheckBox checked={item.checked} symbol={selectedFiltersRefs !== allId && i === 0 ? 'minus' : 'check'} />
|
||||
</div>
|
||||
<span class="overflow-label">
|
||||
<Label label={item.label} />
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { getDay, Timestamp } from '@hcengineering/core'
|
||||
import { DateRangePopup, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let selectedDate: Timestamp | undefined
|
||||
export let fixed: boolean = false
|
||||
|
||||
let div: HTMLDivElement | undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: time = selectedDate ? getDay(selectedDate) : undefined
|
||||
|
||||
$: isCurrentYear = time ? new Date(time).getFullYear() === new Date().getFullYear() : undefined
|
||||
</script>
|
||||
|
||||
<div id={fixed ? '' : time?.toString()} class="flex-center clear-mins dateSelector">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
bind:this={div}
|
||||
class="border-radius-4 over-underline dateSelectorButton clear-mins"
|
||||
on:click={() => {
|
||||
showPopup(DateRangePopup, {}, div, (v) => {
|
||||
if (v) {
|
||||
v.setHours(0, 0, 0, 0)
|
||||
dispatch('jumpToDate', { date: v.getTime() })
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{#if time}
|
||||
{new Date(time).toLocaleDateString('default', {
|
||||
weekday: 'short',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: isCurrentYear ? undefined : 'numeric'
|
||||
})}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.dateSelector {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin: 0.25rem 0;
|
||||
|
||||
&:not(:first-child)::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--theme-divider-color);
|
||||
}
|
||||
.dateSelectorButton {
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: max-content;
|
||||
background-color: var(--theme-list-row-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Person } from '@hcengineering/contact'
|
||||
import { personByIdStore, Avatar } from '@hcengineering/contact-resources'
|
||||
import { Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { getLocation, Label, navigate, TimeSince } from '@hcengineering/ui'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import notification, {
|
||||
ActivityInboxNotification,
|
||||
DocNotifyContext,
|
||||
InboxNotification,
|
||||
InboxNotificationsClient
|
||||
} from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import activity from '../plugin'
|
||||
|
||||
export let message: ActivityMessage
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const maxDisplayPersons = 5
|
||||
|
||||
$: lastReply = message.lastReply ?? new Date().getTime()
|
||||
$: persons = new Set(message.repliedPersons)
|
||||
|
||||
let inboxClient: InboxNotificationsClient | undefined = undefined
|
||||
|
||||
getResource(notification.function.GetInboxNotificationsClient).then((getClientFn) => {
|
||||
inboxClient = getClientFn()
|
||||
})
|
||||
|
||||
let displayPersons: Person[] = []
|
||||
|
||||
$: docNotifyContextByDocStore = inboxClient?.docNotifyContextByDoc
|
||||
$: inboxNotificationsByContextStore = inboxClient?.inboxNotificationsByContext
|
||||
|
||||
$: hasNew = hasNewReplies(message, $docNotifyContextByDocStore, $inboxNotificationsByContextStore)
|
||||
$: updateQuery(persons, $personByIdStore)
|
||||
|
||||
function hasNewReplies (
|
||||
message: ActivityMessage,
|
||||
notifyContexts?: Map<Ref<Doc>, DocNotifyContext>,
|
||||
inboxNotificationsByContext?: Map<Ref<DocNotifyContext>, WithLookup<InboxNotification>[]>
|
||||
): boolean {
|
||||
const context: DocNotifyContext | undefined = notifyContexts?.get(message._id)
|
||||
|
||||
if (context === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (inboxNotificationsByContext?.get(context._id) ?? [])
|
||||
.filter((notification) => {
|
||||
const activityNotifications = notification as ActivityInboxNotification
|
||||
return activityNotifications.attachedToClass !== activity.class.DocUpdateMessage
|
||||
})
|
||||
.some(({ isViewed }) => !isViewed)
|
||||
}
|
||||
|
||||
function updateQuery (personIds: Set<Ref<Person>>, personById: IdMap<Person>) {
|
||||
displayPersons = Array.from(personIds)
|
||||
.map((id) => personById.get(id))
|
||||
.filter((person): person is Person => person !== undefined)
|
||||
.slice(0, maxDisplayPersons - 1)
|
||||
}
|
||||
|
||||
function handleReply (e: any) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
if (onReply) {
|
||||
onReply()
|
||||
return
|
||||
}
|
||||
const loc = getLocation()
|
||||
loc.path[4] = message._id
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex-row-center container cursor-pointer" on:click={handleReply}>
|
||||
<div class="flex-row-center">
|
||||
<div class="avatars">
|
||||
{#each displayPersons as person}
|
||||
<Avatar size="x-small" avatar={person.avatar} name={person.name} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if persons.size > maxDisplayPersons}
|
||||
<div class="plus">
|
||||
+{persons.size - maxDisplayPersons}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="whitespace-nowrap ml-2 mr-2 over-underline repliesCount">
|
||||
<Label label={activity.string.RepliesCount} params={{ replies: message.replies ?? 0 }} />
|
||||
</div>
|
||||
{#if hasNew}
|
||||
<div class="notifyMarker" />
|
||||
{/if}
|
||||
<div class="lastReply">
|
||||
<Label label={activity.string.LastReply} />
|
||||
</div>
|
||||
<div class="time">
|
||||
<TimeSince value={lastReply} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: fit-content;
|
||||
margin-left: -0.5rem;
|
||||
|
||||
.plus {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.repliesCount {
|
||||
color: var(--theme-link-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.lastReply {
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.notifyMarker {
|
||||
margin-right: 0.25rem;
|
||||
width: 0.425rem;
|
||||
height: 0.425rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--button-border-hover);
|
||||
background-color: var(--theme-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { ActionIcon } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { ActivityMessage, SavedMessage } from '@hcengineering/activity'
|
||||
import preference from '@hcengineering/preference'
|
||||
|
||||
import Bookmark from './icons/Bookmark.svelte'
|
||||
|
||||
export let object: ActivityMessage
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
|
||||
let savedMessage: SavedMessage | undefined = undefined
|
||||
|
||||
$: query.query(activity.class.SavedMessage, { attachedTo: object._id }, (res) => {
|
||||
savedMessage = res[0]
|
||||
})
|
||||
|
||||
async function toggleSaveMessage (): Promise<void> {
|
||||
if (savedMessage !== undefined) {
|
||||
await client.remove(savedMessage)
|
||||
savedMessage = undefined
|
||||
} else {
|
||||
await client.createDoc(activity.class.SavedMessage, preference.space.Preference, {
|
||||
attachedTo: object._id
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionIcon
|
||||
icon={Bookmark}
|
||||
iconProps={savedMessage ? { fill: '#3265cb' } : undefined}
|
||||
size="medium"
|
||||
action={toggleSaveMessage}
|
||||
/>
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
$: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>)
|
||||
@@ -65,7 +65,7 @@
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{withActions}
|
||||
viewlet={undefined}
|
||||
{onClick}
|
||||
>
|
||||
|
||||
+15
-4
@@ -16,7 +16,7 @@
|
||||
import { DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import view from '@hcengineering/view'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { Action, Component } from '@hcengineering/ui'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
|
||||
export let value: DisplayActivityMessage
|
||||
@@ -25,8 +25,14 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let skipLabel = false
|
||||
export let actions: Action[] = []
|
||||
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@@ -40,12 +46,17 @@
|
||||
props={{
|
||||
value,
|
||||
showNotify,
|
||||
skipLabel,
|
||||
isHighlighted,
|
||||
isSelected,
|
||||
shouldScroll,
|
||||
embedded,
|
||||
hasActionsMenu,
|
||||
onClick
|
||||
withActions,
|
||||
showEmbedded,
|
||||
hideReplies,
|
||||
actions,
|
||||
onClick,
|
||||
onReply
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
+34
-22
@@ -30,6 +30,8 @@
|
||||
import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte'
|
||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||
import PinMessageAction from './PinMessageAction.svelte'
|
||||
import Replies from '../Replies.svelte'
|
||||
import SaveMessageAction from '../SaveMessageAction.svelte'
|
||||
|
||||
export let message: DisplayActivityMessage
|
||||
export let parentMessage: DisplayActivityMessage | undefined
|
||||
@@ -43,8 +45,11 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
let allActionIds: string[] = []
|
||||
@@ -53,9 +58,10 @@
|
||||
let extensions: ActivityMessageExtension[] = []
|
||||
let isActionMenuOpened = false
|
||||
|
||||
$: void getActions(client, message, activity.class.ActivityMessage).then((res) => {
|
||||
allActionIds = res.map(({ _id }) => _id)
|
||||
})
|
||||
$: withActions &&
|
||||
getActions(client, message, activity.class.ActivityMessage).then((res) => {
|
||||
allActionIds = res.map(({ _id }) => _id)
|
||||
})
|
||||
|
||||
function scrollToMessage (): void {
|
||||
if (element != null && shouldScroll) {
|
||||
@@ -101,7 +107,7 @@
|
||||
|
||||
$: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined
|
||||
$: withActionMenu =
|
||||
!embedded && hasActionsMenu && (actions.length > 0 || allActionIds.some((id) => !excludedActions.includes(id)))
|
||||
withActions && !embedded && (actions.length > 0 || allActionIds.some((id) => !excludedActions.includes(id)))
|
||||
</script>
|
||||
|
||||
{#if !isHidden}
|
||||
@@ -151,32 +157,38 @@
|
||||
|
||||
<slot name="content" />
|
||||
|
||||
{#if !hideReplies && message.replies && message.replies > 0}
|
||||
<div class="mt-2" />
|
||||
<Replies {message} {onReply} />
|
||||
{/if}
|
||||
<ActivityMessageExtensionComponent kind="footer" {extensions} props={{ object: message }} />
|
||||
|
||||
<ReactionsPresenter object={message} />
|
||||
|
||||
{#if parentMessage}
|
||||
{#if parentMessage && showEmbedded}
|
||||
<div class="mt-2" />
|
||||
<ActivityMessagePresenter value={parentMessage} embedded />
|
||||
<ActivityMessagePresenter value={parentMessage} embedded hideReplies withActions={false} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions clear-mins flex flex-gap-2 items-center"
|
||||
class:menuShowed={isActionMenuOpened || message.isPinned}
|
||||
class:opened={isActionMenuOpened || message.isPinned}
|
||||
>
|
||||
<AddReactionAction object={message} />
|
||||
<PinMessageAction object={message} />
|
||||
{#if withActions}
|
||||
<AddReactionAction object={message} />
|
||||
<PinMessageAction object={message} />
|
||||
<SaveMessageAction object={message} />
|
||||
|
||||
<ActivityMessageExtensionComponent
|
||||
kind="action"
|
||||
{extensions}
|
||||
props={{ object: message }}
|
||||
on:close={handleActionMenuClosed}
|
||||
on:open={handleActionMenuOpened}
|
||||
/>
|
||||
{#if withActionMenu}
|
||||
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
|
||||
<ActivityMessageExtensionComponent
|
||||
kind="action"
|
||||
{extensions}
|
||||
props={{ object: message }}
|
||||
on:close={handleActionMenuClosed}
|
||||
on:open={handleActionMenuOpened}
|
||||
/>
|
||||
{#if withActionMenu}
|
||||
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,8 +207,8 @@
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0.75rem 0.75rem 0.75rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
@@ -228,7 +240,7 @@
|
||||
right: 0.75rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
|
||||
&.menuShowed {
|
||||
&.opened {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 type { IntlString } from '@hcengineering/platform'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
export let title: IntlString
|
||||
export let line: boolean = false
|
||||
export let params: any = undefined
|
||||
export let reverse: boolean = false
|
||||
export let isNew: boolean = false
|
||||
export let element: HTMLDivElement | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full text-sm flex-center whitespace-nowrap"
|
||||
class:flex-reverse={reverse}
|
||||
class:new={isNew}
|
||||
bind:this={element}
|
||||
>
|
||||
<div class:ml-8={!reverse} class:mr-4={reverse}><Label label={title} {params} /></div>
|
||||
<div class:ml-4={!reverse} class:mr-4={reverse} class:line />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.line {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.new {
|
||||
.line {
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
color: var(--highlight-red);
|
||||
}
|
||||
</style>
|
||||
+11
-3
@@ -25,7 +25,7 @@
|
||||
import core, { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, ShowMore } from '@hcengineering/ui'
|
||||
import { Component, ShowMore, Action } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
|
||||
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
|
||||
@@ -42,8 +42,12 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let actions: Action[] = []
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@@ -146,9 +150,13 @@
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{withActions}
|
||||
{viewlet}
|
||||
{showEmbedded}
|
||||
{hideReplies}
|
||||
{actions}
|
||||
{onClick}
|
||||
{onReply}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
{#if viewlet?.labelComponent}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
// Copyright © 2023 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.2,18c-0.1,0-0.2,0-0.3-0.1l-6-4l-6,4c-0.2,0.1-0.4,0.1-0.5,0c-0.2-0.1-0.3-0.3-0.3-0.4V4.2 C3.2,3,4.3,2,5.5,2h8.9c1.3,0,2.3,1,2.3,2.2v13.3c0,0.2-0.1,0.4-0.3,0.4C16.4,18,16.3,18,16.2,18z M10,12.8c0.1,0,0.2,0,0.3,0.1 l5.5,3.6V4.2c0-0.6-0.6-1.2-1.3-1.2H5.5C4.8,3,4.2,3.5,4.2,4.2v12.4l5.5-3.6C9.8,12.9,9.9,12.8,10,12.8z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -16,12 +16,11 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { ActionIcon, EmojiPopup, IconEmoji, showPopup } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { Reaction } from '@hcengineering/activity'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
|
||||
|
||||
import { updateDocReactions } from '../../utils'
|
||||
|
||||
export let object: Doc | undefined = undefined
|
||||
export let object: ActivityMessage | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@@ -38,16 +37,10 @@
|
||||
|
||||
function openEmojiPalette (ev: Event) {
|
||||
dispatch('open')
|
||||
showPopup(
|
||||
EmojiPopup,
|
||||
{},
|
||||
ev.target as HTMLElement,
|
||||
(emoji: string) => {
|
||||
updateDocReactions(client, reactions, object, emoji)
|
||||
dispatch('close')
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji: string) => {
|
||||
updateDocReactions(client, reactions, object, emoji)
|
||||
dispatch('close')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { Reaction } from '@hcengineering/activity'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
|
||||
|
||||
import Reactions from './Reactions.svelte'
|
||||
import { updateDocReactions } from '../../utils'
|
||||
|
||||
export let object: Doc | undefined
|
||||
export let object: ActivityMessage | undefined
|
||||
|
||||
const client = getClient()
|
||||
const reactionsQuery = createQuery()
|
||||
@@ -38,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if reactions.length}
|
||||
{#if object !== undefined && reactions.length > 0}
|
||||
<div class="footer flex-col p-inline contrast mt-2">
|
||||
<Reactions {reactions} {object} on:click={handleClick} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user