mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-22 01:25:00 +02:00
Chat and inbox ui updates (#9580)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -175,9 +175,11 @@ const listConfig: (BuildModelKey | string)[] = [
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{
|
||||
key: '',
|
||||
presenter: view.component.RolePresenter,
|
||||
presenter: card.component.CardTagsColored,
|
||||
label: card.string.Tags,
|
||||
props: { fullSize: true },
|
||||
props: {
|
||||
showType: false
|
||||
},
|
||||
displayProps: { key: 'tags', fixed: 'right' }
|
||||
},
|
||||
{
|
||||
@@ -230,10 +232,10 @@ const favoritesViewletConfig: (BuildModelKey | string)[] = [
|
||||
},
|
||||
key: '$lookup.attachedTo',
|
||||
label: card.string.Tags,
|
||||
presenter: view.component.RolePresenter,
|
||||
props: {
|
||||
fullSize: true
|
||||
}
|
||||
showType: false
|
||||
},
|
||||
presenter: card.component.CardTagsColored
|
||||
},
|
||||
{
|
||||
key: '$lookup.attachedTo',
|
||||
@@ -285,7 +287,14 @@ export function createSystemType (
|
||||
config: [
|
||||
{ key: '', props: { shrink: true } },
|
||||
'_class',
|
||||
{ key: '', presenter: view.component.RolePresenter, label: card.string.Tags, props: { fullSize: true } },
|
||||
{
|
||||
key: '',
|
||||
presenter: card.component.CardTagsColored,
|
||||
label: card.string.Tags,
|
||||
props: {
|
||||
showType: false
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
presenter: card.component.LabelsPresenter,
|
||||
@@ -519,7 +528,14 @@ export function createModel (builder: Builder): void {
|
||||
config: [
|
||||
'',
|
||||
'_class',
|
||||
{ key: '', presenter: view.component.RolePresenter, label: card.string.Tags, props: { fullSize: true } },
|
||||
{
|
||||
key: '',
|
||||
presenter: card.component.CardTagsColored,
|
||||
label: card.string.Tags,
|
||||
props: {
|
||||
showType: false
|
||||
}
|
||||
},
|
||||
'modifiedOn'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
import CardTagColored from './CardTagColored.svelte'
|
||||
|
||||
export let value: Card
|
||||
export let showTags: boolean = true
|
||||
export let showType: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@@ -37,9 +39,13 @@
|
||||
</script>
|
||||
|
||||
<div class="tags-container">
|
||||
<CardTagColored labelIntl={type.label} color={type.background} />
|
||||
{#if tags.length > 0}
|
||||
<div class="divider" />
|
||||
{#if showType}
|
||||
<CardTagColored labelIntl={type.label} color={type.background} />
|
||||
{/if}
|
||||
{#if tags.length > 0 && showTags}
|
||||
{#if showType}
|
||||
<div class="divider" />
|
||||
{/if}
|
||||
{#each tags as tag}
|
||||
<CardTagColored labelIntl={tag.label} color={tag.background} />
|
||||
{/each}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import card, { Card, CardSection, CardViewDefaults } from '@hcengineering/card'
|
||||
import { Component, Loading, Scroller } from '@hcengineering/ui'
|
||||
import { Component, languageStore, Loading, ModernButton, Scroller } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { NotificationContext } from '@hcengineering/communication-types'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
@@ -24,7 +24,7 @@
|
||||
import { Heading } from '@hcengineering/text-editor'
|
||||
|
||||
import { CardSectionAction } from '../types'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
|
||||
export let doc: Card
|
||||
export let context: NotificationContext | undefined = undefined
|
||||
@@ -58,11 +58,18 @@
|
||||
let timer: any
|
||||
let hideBar: boolean = false
|
||||
|
||||
let bottomOffset: number = 0
|
||||
|
||||
function onNavigationClick (heading: Heading): void {
|
||||
selectedToc = heading
|
||||
navigate()
|
||||
}
|
||||
|
||||
function getBottomOffset (): number {
|
||||
if (scrollDiv == null) return 0
|
||||
return Math.max(0, Math.floor(scrollDiv.scrollHeight - scrollDiv.scrollTop - scrollDiv.clientHeight))
|
||||
}
|
||||
|
||||
function navigate (): void {
|
||||
if (selectedToc === undefined) return
|
||||
const sectionId = selectedToc.group as Ref<CardSection>
|
||||
@@ -188,6 +195,8 @@
|
||||
if (selectedSectionId != null && selectedSectionId !== selectedToc?.group) {
|
||||
selectedToc = toc.find((it) => it.group === selectedSectionId)
|
||||
}
|
||||
|
||||
bottomOffset = getBottomOffset()
|
||||
}
|
||||
|
||||
async function filterSections (s: CardSection[], doc: Card): Promise<void> {
|
||||
@@ -210,6 +219,33 @@
|
||||
$: void filterSections(_sections, doc)
|
||||
$: updateToc(sections, subTocBySection)
|
||||
$: showOverlay = !isScrollInitialized || Object.values(sectionOverlays).some((it) => it)
|
||||
|
||||
function handleLasTocScroll (): void {
|
||||
selectedToc = toc[toc.length - 1]
|
||||
const sectionId = selectedToc.group as Ref<CardSection>
|
||||
|
||||
const ref = sectionRef[sectionId]
|
||||
if (ref?.scrollDown) {
|
||||
ref.scrollDown(selectedToc.id)
|
||||
}
|
||||
}
|
||||
|
||||
function canScrollDown (): boolean {
|
||||
const lastSection = sections[sections.length - 1]
|
||||
if (lastSection === undefined) return false
|
||||
const ref = sectionRef[lastSection._id]
|
||||
if (ref === undefined) return false
|
||||
|
||||
return ref.canScrollDown?.() ?? false
|
||||
}
|
||||
|
||||
async function getDownButtonTitle (item: Heading, lang: string): Promise<string> {
|
||||
if (item.titleIntl) {
|
||||
const label = await translate(item.titleIntl, {}, lang)
|
||||
return '↓ ' + label
|
||||
}
|
||||
return '↓ ' + (item.title ?? '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent-content__container columns relative">
|
||||
@@ -274,6 +310,14 @@
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
{#if toc.length > 0 && (bottomOffset > 400 || canScrollDown())}
|
||||
{@const lastToc = toc[toc.length - 1]}
|
||||
{#await getDownButtonTitle(lastToc, $languageStore) then title}
|
||||
<div class="down-button">
|
||||
<ModernButton {title} shape="round" size="small" kind="primary" on:click={handleLasTocScroll} />
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -316,4 +360,26 @@
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.down-button {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
bottom: 0.5rem;
|
||||
animation: 0.5s fadeIn;
|
||||
animation-fill-mode: forwards;
|
||||
visibility: hidden;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
99% {
|
||||
visibility: hidden;
|
||||
}
|
||||
100% {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
},
|
||||
key: '',
|
||||
label: card.string.Tags,
|
||||
presenter: view.component.RolePresenter,
|
||||
presenter: card.component.CardTagsColored,
|
||||
props: {
|
||||
fullSize: true
|
||||
showType: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let list: MessagesList | undefined = undefined
|
||||
|
||||
let position: MessagesNavigationAnchors =
|
||||
navigation === MessagesNavigationAnchors.LatestMessages
|
||||
? MessagesNavigationAnchors.LatestMessages
|
||||
@@ -49,6 +51,16 @@
|
||||
position = id
|
||||
}
|
||||
|
||||
export function scrollDown (): void {
|
||||
shouldScrollToStart = false
|
||||
position = MessagesNavigationAnchors.LatestMessages
|
||||
list?.scrollDown()
|
||||
}
|
||||
|
||||
export function canScrollDown (): boolean {
|
||||
return list?.canScrollDown() ?? false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (active && navigation === MessagesNavigationAnchors.LatestMessages) {
|
||||
position = MessagesNavigationAnchors.LatestMessages
|
||||
@@ -60,6 +72,7 @@
|
||||
{#if scrollDiv != null && isContextLoaded && contentDiv != null}
|
||||
<div class="section-messages">
|
||||
<MessagesList
|
||||
bind:this={list}
|
||||
card={doc}
|
||||
{readonly}
|
||||
context={notificationContext}
|
||||
|
||||
@@ -52,8 +52,10 @@
|
||||
|
||||
const scrollToNewThreshold = 50
|
||||
|
||||
const initialLastView = context?.lastView
|
||||
const initialLastUpdate = context?.lastUpdate
|
||||
let initialLastView = context?.lastView
|
||||
let initialLastUpdate = context?.lastUpdate
|
||||
|
||||
let shouldScrollToEnd = false
|
||||
|
||||
let separatorDiv: HTMLDivElement | null | undefined = undefined
|
||||
|
||||
@@ -76,9 +78,18 @@
|
||||
let topOffset: number = 0
|
||||
|
||||
const limit = $deviceInfo.isMobile ? 25 : 50
|
||||
const unread = initialLastView != null && initialLastUpdate != null && initialLastUpdate > initialLastView
|
||||
let queryDef = getBaseQuery()
|
||||
|
||||
export function scrollDown (): void {
|
||||
shouldScrollToEnd = true
|
||||
position = MessagesNavigationAnchors.LatestMessages
|
||||
|
||||
reinit(position, true)
|
||||
}
|
||||
|
||||
export function canScrollDown (): boolean {
|
||||
return window !== undefined && window.hasNextPage()
|
||||
}
|
||||
$: if (
|
||||
(context?.lastView?.getTime() ?? 0) >= (context?.lastUpdate?.getTime() ?? 0) &&
|
||||
(notifications?.length ?? 0) > 0 &&
|
||||
@@ -156,8 +167,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function reinit (position: MessagesNavigationAnchors): void {
|
||||
if (prevPosition === position) {
|
||||
function reinit (position: MessagesNavigationAnchors, force = false): void {
|
||||
if (prevPosition === position && !force) {
|
||||
return
|
||||
}
|
||||
prevPosition = position
|
||||
@@ -191,7 +202,10 @@
|
||||
limit
|
||||
}
|
||||
}
|
||||
const order = unread ? SortingOrder.Ascending : SortingOrder.Descending
|
||||
initialLastView = context?.lastView
|
||||
initialLastUpdate = context?.lastUpdate
|
||||
const unread = initialLastView != null && initialLastUpdate != null && initialLastUpdate > initialLastView
|
||||
const order = unread && !shouldScrollToEnd ? SortingOrder.Ascending : SortingOrder.Descending
|
||||
return {
|
||||
card: card._id,
|
||||
replies: true,
|
||||
@@ -200,7 +214,7 @@
|
||||
links: true,
|
||||
order,
|
||||
limit,
|
||||
from: unread && initialLastView != null ? initialLastView : undefined
|
||||
from: unread && !shouldScrollToEnd && initialLastView != null ? initialLastView : undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,12 +245,12 @@
|
||||
return bottomOffset <= 200
|
||||
}
|
||||
|
||||
function loadMore (): void {
|
||||
function loadMore (direction: 'up' | 'down'): void {
|
||||
if (window === undefined || !isScrollInitialized) return
|
||||
|
||||
if (shouldLoadPrevPage() && window.hasPrevPage()) {
|
||||
if (shouldLoadPrevPage() && window.hasPrevPage() && direction === 'up') {
|
||||
void loadPrevPage()
|
||||
} else if (shouldLoadNextPage() && window.hasNextPage()) {
|
||||
} else if (shouldLoadNextPage() && window.hasNextPage() && direction === 'down') {
|
||||
void loadNextPage()
|
||||
}
|
||||
}
|
||||
@@ -312,13 +326,19 @@
|
||||
}
|
||||
}
|
||||
let rafId: any | null = null
|
||||
let lastScrollTop: number = 0
|
||||
|
||||
function handleScroll (): void {
|
||||
if (rafId !== null) return
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const top = scrollDiv.scrollTop
|
||||
const direction = top > lastScrollTop ? 'down' : 'up'
|
||||
|
||||
lastScrollTop = top
|
||||
bottomOffset = getBottomOffset()
|
||||
topOffset = getTopOffset()
|
||||
updateShouldScrollToNew()
|
||||
loadMore()
|
||||
loadMore(direction)
|
||||
checkPositionOnScroll()
|
||||
void readAll()
|
||||
rafId = null
|
||||
@@ -531,11 +551,15 @@
|
||||
|
||||
const separatorIndex =
|
||||
initialLastView !== undefined
|
||||
? messages.findIndex(({ created, creator }) => !me.socialIds.includes(creator) && created > initialLastView)
|
||||
? messages.findIndex(
|
||||
({ created, creator }) =>
|
||||
initialLastView != null && !me.socialIds.includes(creator) && created > initialLastView
|
||||
)
|
||||
: -1
|
||||
|
||||
if (separatorIndex === -1) {
|
||||
if (separatorIndex === -1 || shouldScrollToEnd) {
|
||||
await tick() // Wait for the DOM to update
|
||||
shouldScrollToEnd = false
|
||||
scrollToBottom(true)
|
||||
shouldScrollToNew = true
|
||||
atBottom = true
|
||||
@@ -551,7 +575,9 @@
|
||||
if (separatorDiv != null) {
|
||||
await tick() // Wait for the DOM to update
|
||||
scrollToWithOffset(scrollDiv, separatorDiv, 80)
|
||||
isScrollInitialized = true
|
||||
setTimeout(() => {
|
||||
isScrollInitialized = true
|
||||
}, 10)
|
||||
updateShouldScrollToNew()
|
||||
bottomOffset = getBottomOffset()
|
||||
topOffset = getTopOffset()
|
||||
|
||||
@@ -23,25 +23,20 @@
|
||||
Location,
|
||||
restoreLocation,
|
||||
Component,
|
||||
Label,
|
||||
ModernButton,
|
||||
IconDelete,
|
||||
closePanel,
|
||||
getCurrentLocation
|
||||
} from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { getClient, getCommunicationClient } from '@hcengineering/presentation'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { inboxId } from '@hcengineering/inbox'
|
||||
import view from '@hcengineering/view'
|
||||
import { NotificationContext } from '@hcengineering/communication-types'
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
|
||||
import InboxNavigation from './InboxNavigation.svelte'
|
||||
import { getCardIdFromLocation, navigateToCard } from '../location'
|
||||
import inbox from '../plugin'
|
||||
import InboxHeader from './InboxHeader.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const communicationClient = getCommunicationClient()
|
||||
|
||||
let replacedPanelElement: HTMLElement
|
||||
let card: Card | undefined = undefined
|
||||
@@ -95,20 +90,6 @@
|
||||
|
||||
$: $deviceInfo.replacedPanel = replacedPanelElement
|
||||
onDestroy(() => ($deviceInfo.replacedPanel = undefined))
|
||||
|
||||
let clearing = false
|
||||
async function clearInbox (): Promise<void> {
|
||||
if (clearing) return
|
||||
try {
|
||||
clearing = true
|
||||
const contexts = await communicationClient.findNotificationContexts({ order: SortingOrder.Ascending })
|
||||
await Promise.all(contexts.map((context) => communicationClient.removeNotificationContext(context.id)))
|
||||
clearing = false
|
||||
} catch (e) {
|
||||
clearing = false
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyPanels-container inbox">
|
||||
@@ -120,19 +101,7 @@
|
||||
class:fly={$deviceInfo.navigator.float}
|
||||
>
|
||||
<div class="antiPanel-wrap__content hulyNavPanel-container">
|
||||
<div class="hulyNavPanel-header withButton small inbox-header">
|
||||
<span class="overflow-label"><Label label={inbox.string.Inbox} /></span>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<ModernButton
|
||||
label={inbox.string.ClearAll}
|
||||
icon={IconDelete}
|
||||
size="small"
|
||||
iconSize="small"
|
||||
on:click={clearInbox}
|
||||
loading={clearing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InboxHeader />
|
||||
<div class="antiPanel-wrap__content hulyNavPanel-container">
|
||||
<InboxNavigation {card} on:select={selectCard} />
|
||||
</div>
|
||||
@@ -175,8 +144,4 @@
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.inbox-header {
|
||||
border-bottom: 1px solid var(--theme-navpanel-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 1rem 0.5rem 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--global-ui-BorderColor);
|
||||
min-height: 5.625rem;
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<!-- 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 { SortingOrder } from '@hcengineering/core'
|
||||
import { IconDelete, IconSettings, Label, ModernButton, showPopup, eventToHTMLElement } from '@hcengineering/ui'
|
||||
import { getCommunicationClient } from '@hcengineering/presentation'
|
||||
|
||||
import inbox from '../plugin'
|
||||
import InboxViewSettings from './InboxViewSettings.svelte'
|
||||
|
||||
const communicationClient = getCommunicationClient()
|
||||
|
||||
let clearing = false
|
||||
async function clearInbox (): Promise<void> {
|
||||
if (clearing) return
|
||||
try {
|
||||
clearing = true
|
||||
const contexts = await communicationClient.findNotificationContexts({ order: SortingOrder.Ascending })
|
||||
await Promise.all(contexts.map((context) => communicationClient.removeNotificationContext(context.id)))
|
||||
clearing = false
|
||||
} catch (e) {
|
||||
clearing = false
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function click (e: MouseEvent): void {
|
||||
showPopup(InboxViewSettings, {}, eventToHTMLElement(e))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyNavPanel-header withButton small inbox-header">
|
||||
<span class="overflow-label"><Label label={inbox.string.Inbox} /></span>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<ModernButton
|
||||
label={inbox.string.ClearAll}
|
||||
icon={IconDelete}
|
||||
size="small"
|
||||
iconSize="small"
|
||||
on:click={clearInbox}
|
||||
loading={clearing}
|
||||
/>
|
||||
<ModernButton icon={IconSettings} on:click={click} size="small" iconSize="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.inbox-header {
|
||||
border-bottom: 1px solid var(--theme-navpanel-border);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { createFocusManager, FocusHandler, Label, ListView, ModernToggle, resizeObserver } from '@hcengineering/ui'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
import { updateViewSetting, isViewSettingEnabled, viewSettingsStore, hideUserNamesSettingId } from '../settings'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let popupElement: HTMLDivElement | undefined = undefined
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: hideUserNamesSettingId,
|
||||
label: getEmbeddedLabel('Hide user names')
|
||||
}
|
||||
]
|
||||
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
export function onKeydown (key: KeyboardEvent): boolean {
|
||||
if (key.code === 'Tab') {
|
||||
dispatch('close')
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
return true
|
||||
}
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection - 1)
|
||||
return true
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection + 1)
|
||||
return true
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
const item = items[selection]
|
||||
if (item != null) {
|
||||
onToggle(item.id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const manager = createFocusManager()
|
||||
|
||||
$: if (popupElement) {
|
||||
popupElement.focus()
|
||||
}
|
||||
|
||||
function onToggle (id: string): void {
|
||||
updateViewSetting(id, !isViewSettingEnabled($viewSettingsStore, id))
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="selectPopup"
|
||||
bind:this={popupElement}
|
||||
tabindex="0"
|
||||
use:resizeObserver={() => {
|
||||
dispatch('changeContent')
|
||||
}}
|
||||
on:keydown={onKeydown}
|
||||
>
|
||||
<div class="menu-space" />
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
<ListView bind:this={list} count={items.length} bind:selection on:changeContent={() => dispatch('changeContent')}>
|
||||
<svelte:fragment slot="item" let:item={itemId}>
|
||||
{@const item = items[itemId]}
|
||||
<div class="menu-item withList w-full container">
|
||||
<Label label={item.label} />
|
||||
<ModernToggle
|
||||
checked={isViewSettingEnabled($viewSettingsStore, item.id)}
|
||||
size="small"
|
||||
on:change={() => {
|
||||
onToggle(item.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-space" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
{#if notification.message}
|
||||
<div class="reaction-notification">
|
||||
<PreviewTemplate socialId={content.creator} date={notification.created} color="secondary">
|
||||
<PreviewTemplate socialId={content.creator} date={notification.created} color="secondary" showSeparator={false}>
|
||||
<Label label={inbox.string.ReactedToYourMessage} />
|
||||
</PreviewTemplate>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
getPersonByPersonId
|
||||
} from '@hcengineering/contact-resources'
|
||||
import { SocialID } from '@hcengineering/communication-types'
|
||||
import { isViewSettingEnabled, hideUserNamesSettingId, viewSettingsStore } from '../../settings'
|
||||
|
||||
export let tooltipLabel: IntlString | undefined = undefined
|
||||
export let person: Person | undefined = undefined
|
||||
@@ -32,10 +33,11 @@
|
||||
export let kind: 'default' | 'column' = 'default'
|
||||
export let padding: string | undefined = undefined
|
||||
export let fixHeight: boolean = false
|
||||
export let showSeparator: boolean = true
|
||||
|
||||
let clientWidth: number
|
||||
|
||||
$: showPersonName = clientWidth > 300
|
||||
$: showPersonName = clientWidth > 300 && !isViewSettingEnabled($viewSettingsStore, hideUserNamesSettingId)
|
||||
|
||||
let _person: Person | undefined
|
||||
$: void updatePerson(socialId, person)
|
||||
@@ -95,9 +97,11 @@
|
||||
{/if}
|
||||
</span>
|
||||
{#if showPersonName && _person}
|
||||
<span class="message-preview-template__name overflow-label">{formatName(_person?.name ?? '')}</span>
|
||||
<span class="message-preview-template__name overflow-label {color}">{formatName(_person?.name ?? '')}</span>
|
||||
{/if}
|
||||
{#if showSeparator}
|
||||
<span class="message-preview-template__separator"> • </span>
|
||||
{/if}
|
||||
<span class="message-preview-template__separator"> • </span>
|
||||
</span>
|
||||
</PersonPreviewProvider>
|
||||
|
||||
@@ -188,6 +192,10 @@
|
||||
&__name {
|
||||
font-weight: 500;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.secondary {
|
||||
color: var(--global-secondary-TextColor);
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { get, writable } from 'svelte/store'
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import { location } from '@hcengineering/ui'
|
||||
|
||||
export interface ViewSetting {
|
||||
id: string
|
||||
on: boolean
|
||||
}
|
||||
|
||||
export const hideUserNamesSettingId = 'view-settings.hide-user-names'
|
||||
|
||||
export const viewSettingsStore = writable<ViewSetting[]>([])
|
||||
|
||||
export function isViewSettingEnabled (settings: ViewSetting[], id: string): boolean {
|
||||
const index = settings.findIndex((it) => it.id === id)
|
||||
if (index === -1) return false
|
||||
return settings[index].on
|
||||
}
|
||||
|
||||
export function updateViewSetting (id: string, on: boolean): void {
|
||||
const settings = get(viewSettingsStore)
|
||||
const index = settings.findIndex((it) => it.id === id)
|
||||
if (index === -1) {
|
||||
viewSettingsStore.set([...settings, { id, on }])
|
||||
} else {
|
||||
viewSettingsStore.set(settings.map((it) => (it.id === id ? { ...it, on } : it)))
|
||||
}
|
||||
}
|
||||
|
||||
let prevWorkspace: string | undefined
|
||||
|
||||
location.subscribe((loc) => {
|
||||
const workspace = loc.path[1]
|
||||
if (prevWorkspace !== workspace) {
|
||||
initViewSettings()
|
||||
prevWorkspace = workspace
|
||||
}
|
||||
})
|
||||
|
||||
viewSettingsStore.subscribe((settings) => {
|
||||
const key = geLocalStorageKey()
|
||||
if (key === undefined) return
|
||||
localStorage.setItem(key, JSON.stringify(settings))
|
||||
})
|
||||
|
||||
function geLocalStorageKey (): string | undefined {
|
||||
const me = getCurrentAccount()
|
||||
const loc = get(location)
|
||||
const workspace = loc.path[1] ?? ''
|
||||
if (me == null || workspace === '') return undefined
|
||||
return `${workspace}.${me.uuid}.inbox.view-settings.v1`
|
||||
}
|
||||
|
||||
function initViewSettings (): void {
|
||||
const key = geLocalStorageKey()
|
||||
if (key === undefined) return
|
||||
|
||||
const stored = localStorage.getItem(key)
|
||||
|
||||
if (stored == null) {
|
||||
viewSettingsStore.set([])
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(stored)
|
||||
Array.isArray(data) ? viewSettingsStore.set(data as ViewSetting[]) : viewSettingsStore.set([])
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
viewSettingsStore.set([])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user