mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 14:05:05 +02:00
Combine into one application chat, inbox and home (#9865)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
<!-- 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 { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Card, type MasterTag } from '@hcengineering/card'
|
||||
import core, { DocumentQuery, type Ref, SortingOrder } from '@hcengineering/core'
|
||||
import ui, { Label, Scroller, Loading } from '@hcengineering/ui'
|
||||
import card from '@hcengineering/card'
|
||||
|
||||
import HomeCardPresenter from './FeedCardPresenter.svelte'
|
||||
import NewCardForm from './NewCardForm.svelte'
|
||||
|
||||
const cardsQuery = createQuery()
|
||||
const limitStep = 50
|
||||
const client = getClient()
|
||||
|
||||
export let _class: Ref<MasterTag> | undefined = undefined
|
||||
export let query: DocumentQuery<Card>
|
||||
|
||||
let divScroll: HTMLDivElement
|
||||
let limit = limitStep
|
||||
let cards: Card[] = []
|
||||
let total = -1
|
||||
let isLoading = true
|
||||
let classQuery: Record<string, unknown> = {}
|
||||
|
||||
// Get descendants for _class using model hierarchy
|
||||
$: if (_class !== undefined) {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const descendants = hierarchy.getDescendants(_class)
|
||||
const allClasses = [_class, ...descendants]
|
||||
classQuery = allClasses.length > 1 ? { _class: { $in: allClasses } } : { _class }
|
||||
} else {
|
||||
classQuery = {}
|
||||
}
|
||||
|
||||
$: resultQuery = { ...query, ...classQuery }
|
||||
$: cardsQuery.query(
|
||||
card.class.Card,
|
||||
resultQuery,
|
||||
(res) => {
|
||||
cards = res
|
||||
total = res.total
|
||||
isLoading = false
|
||||
},
|
||||
{
|
||||
sort: { modifiedOn: SortingOrder.Descending },
|
||||
limit,
|
||||
total: true,
|
||||
lookup: {
|
||||
space: core.class.Space
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: hasNextPage = total > cards.length
|
||||
|
||||
function onScroll (): void {
|
||||
if (divScroll != null && hasNextPage && !isLoading) {
|
||||
const isAtBottom = divScroll.scrollTop + divScroll.clientHeight >= divScroll.scrollHeight - 400
|
||||
if (isAtBottom) {
|
||||
isLoading = true
|
||||
limit += limitStep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFormatDateId (timestamp: number): string {
|
||||
const now = new Date()
|
||||
const date = new Date(timestamp)
|
||||
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
|
||||
const yesterdayStart = todayStart - 24 * 60 * 60 * 1000
|
||||
|
||||
const thisWeekStart = new Date(now)
|
||||
const day = thisWeekStart.getDay() === 0 ? 7 : thisWeekStart.getDay()
|
||||
|
||||
thisWeekStart.setDate(thisWeekStart.getDate() - (day - 1))
|
||||
thisWeekStart.setHours(0, 0, 0, 0)
|
||||
|
||||
const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime()
|
||||
const thisYearStart = new Date(now.getFullYear(), 0, 1).getTime()
|
||||
|
||||
if (date.getTime() >= todayStart) {
|
||||
return 'today'
|
||||
} else if (date.getTime() >= yesterdayStart) {
|
||||
return 'yesterday'
|
||||
} else if (date.getTime() >= thisWeekStart.getTime()) {
|
||||
return 'thisWeek'
|
||||
} else if (date.getTime() >= thisMonthStart) {
|
||||
return 'thisMonth'
|
||||
} else if (date.getTime() >= thisYearStart) {
|
||||
return 'thisYear'
|
||||
} else {
|
||||
return date.getFullYear().toString()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scroller bind:divScroll {onScroll} padding="2rem 4rem">
|
||||
<div class="home">
|
||||
<NewCardForm type={_class} />
|
||||
<div class="body flex-gap-2">
|
||||
{#each cards as card, index}
|
||||
{@const previousCard = cards[index - 1]}
|
||||
{@const currentDate = card.modifiedOn}
|
||||
{@const previousDate = previousCard?.modifiedOn}
|
||||
{@const currentDateId = getFormatDateId(currentDate)}
|
||||
{@const previousDateId = previousDate != null ? getFormatDateId(previousDate) : undefined}
|
||||
{#if currentDateId !== previousDateId}
|
||||
<div class="date">
|
||||
{#if currentDateId === 'today'}
|
||||
<Label label={ui.string.Today} />
|
||||
{:else if currentDateId === 'yesterday'}
|
||||
<Label label={ui.string.Yesterday} />
|
||||
{:else if currentDateId === 'thisWeek'}
|
||||
<Label label={ui.string.ThisWeek} />
|
||||
{:else if currentDateId === 'thisMonth'}
|
||||
<Label label={ui.string.ThisMonth} />
|
||||
{:else if currentDateId === 'thisYear'}
|
||||
<Label label={ui.string.ThisYear} />
|
||||
{:else}
|
||||
{currentDateId}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<HomeCardPresenter {card} />
|
||||
{/each}
|
||||
{#if isLoading}
|
||||
<div class="flex-center pb-2">
|
||||
<Loading />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--global-secondary-TextColor);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<!-- 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 cardPlugin, { Card } from '@hcengineering/card'
|
||||
import { createMessagesQuery, IconForward } from '@hcengineering/presentation'
|
||||
import core, { PersonId, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { CardID, Message, Label as CardLabel } from '@hcengineering/communication-types'
|
||||
import { Avatar, getPersonByPersonIdStore, PersonPreviewProvider } from '@hcengineering/contact-resources'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { MessagePresenter, labelsStore, MessagePreview } from '@hcengineering/communication-resources'
|
||||
import { Button, IconMoreH, tooltip } from '@hcengineering/ui'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import CardTagsColored from './CardTagsColored.svelte'
|
||||
import CardPathPresenter from './CardPathPresenter.svelte'
|
||||
import CardTimestamp from './CardTimestamp.svelte'
|
||||
|
||||
import { openCardInSidebar } from '../utils'
|
||||
|
||||
import SystemAvatar from '@hcengineering/contact-resources/src/components/SystemAvatar.svelte'
|
||||
|
||||
export let card: WithLookup<Card>
|
||||
export let isCompact = false
|
||||
export let isComfortable2 = false
|
||||
|
||||
const messagesQuery = createMessagesQuery()
|
||||
|
||||
let message: Message | undefined = undefined
|
||||
|
||||
let socialId: PersonId | undefined = undefined
|
||||
let person: Person | undefined = undefined
|
||||
|
||||
$: messagesQuery.query(
|
||||
{ card: card._id, strict: true, attachments: true, reactions: true, limit: 1, order: SortingOrder.Descending },
|
||||
(res) => {
|
||||
const msgs = res.getResult().reverse()
|
||||
message = msgs[msgs.length - 1]
|
||||
}
|
||||
)
|
||||
|
||||
$: socialId = message?.creator ?? card.modifiedBy
|
||||
$: personStore = getPersonByPersonIdStore([socialId])
|
||||
$: person = $personStore.get(socialId)
|
||||
|
||||
function hasNewMessages (labels: CardLabel[], cardId: CardID): boolean {
|
||||
return labels.some((it) => (it.labelId as string) === cardPlugin.label.NewMessages && it.cardId === cardId)
|
||||
}
|
||||
let isActionsOpened = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="card" on:click|stopPropagation|preventDefault={() => openCardInSidebar(card._id, card)}>
|
||||
<div class="card__avatar">
|
||||
{#if socialId !== core.account.System}
|
||||
<PersonPreviewProvider value={person}>
|
||||
<Avatar name={person?.name} {person} size="medium" />
|
||||
</PersonPreviewProvider>
|
||||
{:else}
|
||||
<SystemAvatar size="medium" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
<div class="card__header">
|
||||
<div class="flex-presenter">
|
||||
{#if hasNewMessages($labelsStore, card._id)}
|
||||
<div class="flex pr-1">
|
||||
<span class="notifyMarker" />
|
||||
</div>
|
||||
{/if}
|
||||
<span
|
||||
class="card__title overflow-label"
|
||||
use:tooltip={{ label: getEmbeddedLabel(card.title), textAlign: 'left' }}
|
||||
>
|
||||
{card.title}
|
||||
</span>
|
||||
</div>
|
||||
<CardTimestamp date={card.modifiedOn} />
|
||||
{#if !isComfortable2}
|
||||
<div class="flex-presenter flex-gap-0-5 tags-container">
|
||||
<CardPathPresenter {card} />
|
||||
<IconForward size={'small'} />
|
||||
<div class="card__tags">
|
||||
<CardTagsColored value={card} collapsable fullWidth />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card__message">
|
||||
{#if message}
|
||||
{#if isCompact}
|
||||
<MessagePreview {card} {message} colorInherit />
|
||||
{:else}
|
||||
<MessagePresenter {card} {message} hideHeader hideAvatar readonly padding="0" thread={false} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card__parent" class:wrap={isComfortable2}>
|
||||
{#if isComfortable2}
|
||||
<div class="flex-presenter flex-gap-0-5 tags-container">
|
||||
<CardPathPresenter {card} />
|
||||
<IconForward size={'small'} />
|
||||
<div class="card__tags mr-2">
|
||||
<CardTagsColored value={card} collapsable fullWidth />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !isCompact}
|
||||
<div class="card__actions" class:opened={isActionsOpened}>
|
||||
<Button
|
||||
icon={IconMoreH}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind="icon"
|
||||
dataId="btnMoreActions"
|
||||
on:click={(e) => {
|
||||
isActionsOpened = true
|
||||
showMenu(e, { object: card }, () => {
|
||||
isActionsOpened = false
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.375rem 1rem;
|
||||
|
||||
gap: 0.75rem;
|
||||
min-height: 4.75rem;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--global-ui-hover-BackgroundColor);
|
||||
|
||||
.card__actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 2rem;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--global-primary-TextColor);
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin-top: 0.625rem;
|
||||
}
|
||||
|
||||
&__message {
|
||||
display: flex;
|
||||
min-height: 1.375rem;
|
||||
color: var(--global-secondary-TextColor);
|
||||
}
|
||||
|
||||
&__parent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
padding-left: 0.5rem;
|
||||
visibility: hidden;
|
||||
|
||||
&.opened {
|
||||
visibility: visible;
|
||||
}
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.notifyMarker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-color: var(--global-higlight-Color);
|
||||
|
||||
min-width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
.tags-container {
|
||||
min-width: 14rem;
|
||||
max-width: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -39,9 +39,10 @@
|
||||
|
||||
const threadMasterTag = chat.masterTag.Thread
|
||||
|
||||
export let type: Ref<MasterTag> = threadMasterTag
|
||||
|
||||
let title: string = ''
|
||||
let space: Ref<CardSpace> | undefined = undefined
|
||||
let type: Ref<MasterTag> = threadMasterTag
|
||||
let description = EmptyMarkup
|
||||
let _id = generateId<Card>()
|
||||
let isExpanded = false
|
||||
|
||||
@@ -17,13 +17,27 @@
|
||||
import { ObjectBox } from '@hcengineering/view-resources'
|
||||
import view, { ViewletDescriptor } from '@hcengineering/view'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import card from '../../../plugin'
|
||||
|
||||
export let value: Ref<ViewletDescriptor> | undefined = undefined
|
||||
export let withSingleViews: boolean = false
|
||||
export let label: IntlString
|
||||
$: supportedTypes = withSingleViews
|
||||
? [view.viewlet.Table, view.viewlet.List, view.viewlet.Tree, view.viewlet.MasterDetail, view.viewlet.Document]
|
||||
: [view.viewlet.Table, view.viewlet.List, view.viewlet.Tree, view.viewlet.MasterDetail]
|
||||
? [
|
||||
view.viewlet.Table,
|
||||
view.viewlet.List,
|
||||
view.viewlet.Tree,
|
||||
view.viewlet.MasterDetail,
|
||||
card.viewlet.CardFeedDescriptor,
|
||||
view.viewlet.Document
|
||||
]
|
||||
: [
|
||||
view.viewlet.Table,
|
||||
view.viewlet.List,
|
||||
view.viewlet.Tree,
|
||||
view.viewlet.MasterDetail,
|
||||
card.viewlet.CardFeedDescriptor
|
||||
]
|
||||
</script>
|
||||
|
||||
<ObjectBox
|
||||
|
||||
Reference in New Issue
Block a user