UBERF-6205: add real archive for notifications (#5385)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-04-17 21:43:51 +04:00 committed by GitHub
parent 55c1e745b7
commit a29e1701be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 459 additions and 184 deletions

View File

@ -243,6 +243,9 @@ export class TInboxNotification extends TDoc implements InboxNotification {
// @Index(IndexKind.Indexed)
isViewed!: boolean
@Prop(TypeBoolean(), core.string.Boolean)
archived?: boolean
title?: IntlString
body?: IntlString
intlParams?: Record<string, string | number>
@ -537,15 +540,29 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: notification.actionImpl.DeleteContextNotifications,
label: notification.string.Archive,
action: notification.actionImpl.ArchiveContextNotifications,
label: view.string.Archive,
icon: view.icon.CheckCircle,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: ['panel'], application: notification.app.Notification, group: 'remove' }
},
notification.action.DeleteContextNotifications
notification.action.ArchiveContextNotifications
)
createAction(
builder,
{
action: notification.actionImpl.UnarchiveContextNotifications,
label: view.string.UnArchive,
icon: view.icon.Circle,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: ['panel'], application: notification.app.Notification, group: 'remove' }
},
notification.action.UnarchiveContextNotifications
)
createAction(
@ -669,6 +686,7 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_NOTIFICATION,
indexes: [{ user: 1, archived: 1 }],
disabled: [{ modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { isViewed: 1 }, { hidden: 1 }]
})
}

View File

@ -76,7 +76,8 @@ export default mergeIds(notificationId, notification, {
PinDocNotifyContext: '' as ViewAction,
UnReadNotifyContext: '' as ViewAction,
ReadNotifyContext: '' as ViewAction,
DeleteContextNotifications: '' as ViewAction,
ArchiveContextNotifications: '' as ViewAction,
UnarchiveContextNotifications: '' as ViewAction,
ArchiveAll: '' as ViewAction,
ReadAll: '' as ViewAction,
UnreadAll: '' as ViewAction

View File

@ -31,6 +31,7 @@
export let disabled: boolean = false
export let loading: boolean = false
export let inheritColor: boolean = false
export let noSelection: boolean = false
export let items: DropdownIntlItem[]
export let params: Record<string, any> = {}
@ -46,7 +47,7 @@
function openPopup () {
if (!opened) {
opened = true
showPopup(ModernPopup, { items, selected, params }, element, (result) => {
showPopup(ModernPopup, { items, selected: noSelection ? undefined : selected, params }, element, (result) => {
if (result) {
selected = result
dispatch('selected', result)

View File

@ -3,7 +3,7 @@
// © 2023 Hardcore Engineering, Inc. All Rights Reserved.
// Licensed under the Eclipse Public License v2.0 (SPDX: EPL-2.0).
//
import type { Asset, IntlString } from '@hcengineering/platform'
import type { IntlString } from '@hcengineering/platform'
import Label from './Label.svelte'
export let title: string | undefined = undefined

View File

@ -38,7 +38,7 @@
"Mentioned": "Mentioned",
"You": "You",
"Mentions": "Mentions",
"MentionedYouIn": "Mentioned you in {title}",
"MentionedYouIn": "Mentioned you in",
"Messages": "Messages",
"Thread": "Thread",
"AddReaction": "Add reaction",

View File

@ -37,6 +37,6 @@
"Mentioned": "Mencionado",
"You": "Tú",
"Mentions": "Menciones",
"MentionedYouIn": "Has sido Mencionado en {title}"
"MentionedYouIn": "Has sido Mencionado en"
}
}

View File

@ -37,6 +37,6 @@
"Mentioned": "Mencionado",
"You": "Tu",
"Mentions": "Menções",
"MentionedYouIn": "Foste Mencionado em {title}"
"MentionedYouIn": "Foste Mencionado em"
}
}

View File

@ -38,7 +38,7 @@
"Mentioned": "Упомянул(а)",
"You": "Вы",
"Mentions": "Упоминания",
"MentionedYouIn": "Упомянул(а) вас в {title}",
"MentionedYouIn": "Упомянул(а) вас в",
"Messages": "Cообщения",
"Thread": "Обсуждение",
"AddReaction": "Добавить реакцию",

View File

@ -83,7 +83,7 @@
})
const excludedActions = [
notification.action.DeleteContextNotifications,
notification.action.ArchiveContextNotifications,
notification.action.UnReadNotifyContext,
notification.action.ReadNotifyContext
]

View File

@ -272,7 +272,7 @@ export async function removeActivityChannels (contexts: DocNotifyContext[]): Pro
try {
for (const context of contexts) {
const notifications = notificationsByContext.get(context._id) ?? []
await client.deleteNotifications(
await client.archiveNotifications(
ops,
notifications.map(({ _id }) => _id)
)
@ -293,7 +293,7 @@ export async function readActivityChannels (contexts: DocNotifyContext[]): Promi
try {
for (const context of contexts) {
const notifications = notificationsByContext.get(context._id) ?? []
await client.deleteNotifications(
await client.archiveNotifications(
ops,
notifications
.filter(({ _class }) => _class === notification.class.ActivityInboxNotification)

View File

@ -39,7 +39,7 @@ import activity, {
type DocUpdateMessage
} from '@hcengineering/activity'
import {
deleteContextNotifications,
archiveContextNotifications,
InboxNotificationsClientImpl,
isMentionNotification
} from '@hcengineering/notification-resources'
@ -396,7 +396,7 @@ export async function removeChannelAction (context?: DocNotifyContext): Promise<
const client = getClient()
await deleteContextNotifications(context)
await archiveContextNotifications(context)
await client.remove(context)
}

View File

@ -45,6 +45,7 @@
"StarDocument": "Star document",
"UnstarDocument": "Unstar document",
"Unsubscribe": "Unsubscribe",
"Push": "Push"
"Push": "Push",
"Unreads": "Unreads"
}
}

View File

@ -45,6 +45,7 @@
"StarDocument": "Добавить в избранное",
"UnstarDocument": "Удалить из избранного",
"Unsubscribe": "Отписаться",
"Push": "Push"
"Push": "Push",
"Unreads": "Непрочитанные"
}
}

View File

@ -26,11 +26,12 @@
import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
import NotifyContextIcon from './NotifyContextIcon.svelte'
import { deleteContextNotifications } from '../utils'
import { archiveContextNotifications, unarchiveContextNotifications } from '../utils'
export let value: DocNotifyContext
export let notifications: WithLookup<DisplayInboxNotification>[]
export let viewlets: ActivityNotificationViewlet[] = []
export let archived = false
const maxNotifications = 3
@ -67,6 +68,13 @@
{
object: value,
baseMenuClass: notification.class.DocNotifyContext,
excludedActions: archived
? [
notification.action.ArchiveContextNotifications,
notification.action.ReadNotifyContext,
notification.action.UnReadNotifyContext
]
: [notification.action.UnarchiveContextNotifications],
mode: 'panel'
},
ev.target as HTMLElement,
@ -83,13 +91,13 @@
isActionMenuOpened = false
}
let deletingPromise: Promise<any> | undefined = undefined
let archivingPromise: Promise<any> | undefined = undefined
async function checkContext (): Promise<void> {
await deletingPromise
deletingPromise = deleteContextNotifications(value)
await deletingPromise
deletingPromise = undefined
await archivingPromise
archivingPromise = archived ? unarchiveContextNotifications(value) : archiveContextNotifications(value)
await archivingPromise
archivingPromise = undefined
}
</script>
@ -125,10 +133,10 @@
<div class="actions clear-mins">
<div class="flex-center">
{#if deletingPromise !== undefined}
{#if archivingPromise !== undefined}
<Spinner size="small" />
{:else}
<CheckBox checked={false} kind="todo" size="medium" on:value={checkContext} />
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
{/if}
</div>
<ButtonIcon

View File

@ -1,48 +0,0 @@
<!--
// 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 { IconFilter, SelectPopup, eventToHTMLElement, showPopup, ButtonIcon } from '@hcengineering/ui'
import notification from '../plugin'
export let filter: 'all' | 'read' | 'unread' = 'all'
$: filters = [
{
id: 'all',
isSelected: filter === 'all',
label: notification.string.All
},
{
id: 'read',
isSelected: filter === 'read',
label: notification.string.Read
},
{
id: 'unread',
isSelected: filter === 'unread',
label: notification.string.Unread
}
]
function click (e: MouseEvent) {
showPopup(SelectPopup, { value: filters }, eventToHTMLElement(e), (res) => {
if (res) {
filter = res
}
})
}
</script>
<ButtonIcon icon={IconFilter} size="small" on:click={click} />

View File

@ -30,25 +30,16 @@
import { getActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
export let value: DisplayActivityInboxNotification
export let viewlets: ActivityNotificationViewlet[] = []
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const activityNotificationsStore = inboxClient.activityInboxNotifications
let viewlet: ActivityNotificationViewlet | undefined = undefined
let displayMessage: DisplayActivityMessage | undefined = undefined
let actions: Action[] = []
$: combinedNotifications = $activityNotificationsStore.filter(({ _id }) => value.combinedIds.includes(_id))
$: messages = combinedNotifications
.map((it) => it.$lookup?.attachedTo)
.filter((it): it is ActivityMessage => it !== undefined)
$: void updateDisplayMessage(messages)
$: void updateDisplayMessage(value.combinedMessages)
async function updateDisplayMessage (messages: ActivityMessage[]): Promise<void> {
const combinedMessages = await combineActivityMessages(sortActivityMessages(messages))

View File

@ -13,20 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import notification, {
import {
ActivityInboxNotification,
decodeObjectURI,
DocNotifyContext,
InboxNotification,
decodeObjectURI
InboxNotification
} from '@hcengineering/notification'
import { ActionContext, getClient } from '@hcengineering/presentation'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view'
import {
AnyComponent,
ButtonWithDropdown,
Component,
defineSeparators,
IconDropdown,
Label,
location as locationStore,
Location,
@ -36,25 +34,19 @@
TabList
} from '@hcengineering/ui'
import chunter, { ThreadMessage } from '@hcengineering/chunter'
import { IdMap, Ref } from '@hcengineering/core'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { isActivityMessageClass, isReactionMessage } from '@hcengineering/activity-resources'
import { get } from 'svelte/store'
import { translate } from '@hcengineering/platform'
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import Filter from '../Filter.svelte'
import {
archiveAll,
getDisplayInboxData,
isMentionNotification,
openInboxDoc,
readAll,
resolveLocation,
unreadAll
} from '../../utils'
import SettingsButton from './SettingsButton.svelte'
import { getDisplayInboxData, isMentionNotification, openInboxDoc, resolveLocation } from '../../utils'
import { InboxData, InboxNotificationsFilter } from '../../types'
import InboxGroupedListView from './InboxGroupedListView.svelte'
import notification from '../../plugin'
import InboxMenuButton from './InboxMenuButton.svelte'
export let visibleNav: boolean = true
export let navFloat: boolean = false
@ -62,6 +54,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const me = getCurrentAccount()
const inboxClient = InboxNotificationsClientImpl.getClient()
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
@ -69,15 +62,23 @@
const contextByDocStore = inboxClient.contextByDoc
const contextsStore = inboxClient.contexts
const archivedActivityNotificationsQuery = createQuery()
const archivedOtherNotificationsQuery = createQuery()
const allTab: TabItem = {
id: 'all',
labelIntl: notification.string.All
}
let showArchive = false
let archivedActivityNotifications: InboxNotification[] = []
let archivedOtherNotifications: InboxNotification[] = []
let archivedNotifications: InboxNotification[] = []
let inboxData: InboxData = new Map()
let filteredData: InboxData = new Map()
let filter: InboxNotificationsFilter = 'all'
let filter: InboxNotificationsFilter = (localStorage.getItem('inbox-filter') as InboxNotificationsFilter) ?? 'all'
let tabItems: TabItem[] = []
let selectedTabId: string = allTab.id
@ -88,9 +89,55 @@
let selectedMessage: ActivityMessage | undefined = undefined
$: void getDisplayInboxData($notificationsByContextStore).then((res) => {
inboxData = res
})
$: if (showArchive) {
archivedActivityNotificationsQuery.query(
notification.class.ActivityInboxNotification,
{ archived: true, user: me._id },
(res) => {
archivedActivityNotifications = res
},
{
lookup: {
attachedTo: activity.class.ActivityMessage
},
sort: {
createdOn: SortingOrder.Descending
},
limit: 1000
}
)
archivedOtherNotificationsQuery.query(
notification.class.InboxNotification,
{ _class: { $nin: [notification.class.ActivityInboxNotification] }, archived: true, user: me._id },
(res) => {
archivedOtherNotifications = res
},
{
sort: {
createdOn: SortingOrder.Descending
},
limit: 500
}
)
}
$: archivedNotifications = [...archivedActivityNotifications, ...archivedOtherNotifications].sort(
(n1, n2) => (n2.createdOn ?? n2.modifiedOn) - (n1.createdOn ?? n1.modifiedOn)
)
$: void updateInboxData($notificationsByContextStore, archivedNotifications, showArchive)
async function updateInboxData (
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>,
archivedNotifications: InboxNotification[],
showArchive: boolean
): Promise<void> {
if (showArchive) {
inboxData = await getDisplayInboxData(groupByArray(archivedNotifications, (it) => it.docNotifyContext))
} else {
inboxData = await getDisplayInboxData(notificationsByContext)
}
}
$: filteredData = filterData(filter, selectedTabId, inboxData, $contextByIdStore)
@ -262,8 +309,6 @@
switch (filter) {
case 'unread':
return notifications.filter(({ isViewed }) => !isViewed)
case 'read':
return notifications.filter(({ isViewed }) => isViewed)
case 'all':
return notifications
}
@ -317,21 +362,30 @@
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
])
async function dropdownItemSelected (id: 'archive' | 'read' | 'unread'): Promise<void> {
if (id == null) return
if (id === 'archive') {
void archiveAll()
}
if (id === 'read') {
void readAll()
}
if (id === 'unread') {
void unreadAll()
}
function onArchiveToggled (): void {
showArchive = !showArchive
selectedTabId = allTab.id
}
function onUnreadsToggled (): void {
filter = filter === 'unread' ? 'all' : 'unread'
localStorage.setItem('inbox-filter', filter)
}
$: items = [
{
id: 'unread',
on: filter === 'unread',
label: notification.string.Unreads,
onToggle: onUnreadsToggled
},
{
id: 'archive',
on: showArchive,
label: view.string.Archived,
onToggle: onArchiveToggled
}
]
</script>
<ActionContext
@ -353,35 +407,8 @@
<span class="title"><Label label={notification.string.Inbox} /></span>
</div>
<div class="flex flex-gap-2">
{#if inboxData.size > 0}
<ButtonWithDropdown
justify="left"
kind="regular"
label={notification.string.MarkReadAll}
icon={view.icon.Eye}
on:click={readAll}
dropdownItems={[
{
id: 'read',
icon: view.icon.Eye,
label: notification.string.MarkReadAll
},
{
id: 'unread',
icon: view.icon.EyeCrossed,
label: notification.string.MarkUnreadAll
},
{
id: 'archive',
icon: view.icon.CheckCircle,
label: notification.string.ArchiveAll
}
]}
dropdownIcon={IconDropdown}
on:dropdown-selected={(ev) => dropdownItemSelected(ev.detail)}
/>
{/if}
<Filter bind:filter />
<SettingsButton {items} />
<InboxMenuButton />
</div>
</div>
@ -390,7 +417,12 @@
</div>
<Scroller padding="0">
<InboxGroupedListView data={filteredData} selectedContext={selectedContextId} on:click={selectContext} />
<InboxGroupedListView
data={filteredData}
selectedContext={selectedContextId}
archived={showArchive}
on:click={selectContext}
/>
</Scroller>
</div>
<Separator name="inbox" float={navFloat ? 'navigator' : true} index={0} />

View File

@ -25,11 +25,12 @@
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import DocNotifyContextCard from '../DocNotifyContextCard.svelte'
import { deleteContextNotifications } from '../../utils'
import { archiveContextNotifications, unarchiveContextNotifications } from '../../utils'
import { InboxData } from '../../types'
export let data: InboxData
export let selectedContext: Ref<DocNotifyContext> | undefined
export let archived = false
const client = getClient()
const dispatch = createEventDispatcher()
@ -83,7 +84,11 @@
const contextId = displayData[listSelection]?.[0]
const context = $contextByIdStore.get(contextId)
void deleteContextNotifications(context)
if (archived) {
void unarchiveContextNotifications(context)
} else {
void archiveContextNotifications(context)
}
}
if (key.code === 'Enter') {
key.preventDefault()
@ -121,6 +126,7 @@
<DocNotifyContextCard
value={context}
notifications={contextNotifications}
{archived}
{viewlets}
on:click={(event) => {
dispatch('click', event.detail)

View File

@ -0,0 +1,61 @@
<!--
// 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 view from '@hcengineering/view'
import { ButtonMenu, IconMoreV } from '@hcengineering/ui'
import notification from '../../plugin'
import { archiveAll, readAll, unreadAll } from '../../utils'
async function onSelect (id?: 'archive' | 'read' | 'unread'): Promise<void> {
if (id == null) return
if (id === 'archive') {
void archiveAll()
}
if (id === 'read') {
void readAll()
}
if (id === 'unread') {
void unreadAll()
}
}
</script>
<ButtonMenu
size="small"
noSelection
icon={IconMoreV}
items={[
{
id: 'read',
icon: view.icon.Eye,
label: notification.string.MarkReadAll
},
{
id: 'unread',
icon: view.icon.EyeCrossed,
label: notification.string.MarkUnreadAll
},
{
id: 'archive',
icon: view.icon.CheckCircle,
label: notification.string.ArchiveAll
}
]}
on:selected={(ev) => onSelect(ev.detail)}
/>

View File

@ -0,0 +1,28 @@
<!--
// Copyright © 2024 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 { eventToHTMLElement, showPopup, ButtonIcon, IconSettings } from '@hcengineering/ui'
import SettingsPopup from './SettingsPopup.svelte'
import { SettingItem } from '../../types'
export let items: SettingItem[] = []
function click (e: MouseEvent): void {
showPopup(SettingsPopup, { items }, eventToHTMLElement(e))
}
</script>
<ButtonIcon icon={IconSettings} size="small" on:click={click} />

View File

@ -0,0 +1,107 @@
<!--
// 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 { SettingItem } from '../../types'
export let items: SettingItem[] = []
let popupElement: HTMLDivElement | undefined = undefined
const dispatch = createEventDispatcher()
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()
items[selection]?.onToggle()
items = items.map((item, index) => {
if (index === selection) {
return { ...item, on: !item.on }
}
return item
})
return true
}
return false
}
const manager = createFocusManager()
$: if (popupElement) {
popupElement.focus()
}
</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={item.on} size="small" on:change={item.onToggle} />
</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>

View File

@ -34,6 +34,7 @@ import notification, {
} from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { derived, get, writable } from 'svelte/store'
import { isActivityNotification } from './utils'
/**
* @public
@ -103,6 +104,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
notification.class.InboxNotification,
{
_class: { $nin: [notification.class.ActivityInboxNotification] },
archived: { $ne: true },
user: getCurrentAccount()._id
},
(result: InboxNotification[]) => {
@ -118,6 +120,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
this.activityInboxNotificationsQuery.query(
notification.class.ActivityInboxNotification,
{
archived: { $ne: true },
user: getCurrentAccount()._id
},
(result: ActivityInboxNotification[]) => {
@ -249,28 +252,29 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
}
async deleteNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {
async archiveNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {
const inboxNotifications = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id))
for (const notification of inboxNotifications) {
await client.remove(notification)
await client.update(notification, { archived: true })
}
}
async deleteAllNotifications (): Promise<void> {
const doneOp = await getClient().measure('deleteAllNotifications')
async archiveAllNotifications (): Promise<void> {
const doneOp = await getClient().measure('archiveAllNotifications')
const ops = getClient().apply(generateId())
try {
const inboxNotifications = await ops.findAll(
notification.class.InboxNotification,
{
user: getCurrentAccount()._id
user: getCurrentAccount()._id,
archived: { $ne: true }
},
{ projection: { _id: 1, _class: 1, space: 1 } }
)
const contexts = get(this.contexts) ?? []
for (const notification of inboxNotifications) {
await ops.removeDoc(notification._class, notification.space, notification._id)
await ops.updateDoc(notification._class, notification.space, notification._id, { archived: true })
}
for (const context of contexts) {
@ -291,7 +295,8 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
notification.class.InboxNotification,
{
user: getCurrentAccount()._id,
isViewed: { $ne: true }
isViewed: { $ne: true },
archived: { $ne: true }
},
{ projection: { _id: 1, _class: 1, space: 1 } }
)
@ -317,9 +322,13 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
notification.class.InboxNotification,
{
user: getCurrentAccount()._id,
isViewed: true
isViewed: true,
archived: { $ne: true }
},
{ projection: { _id: 1, _class: 1, space: 1 } }
{
projection: { _id: 1, _class: 1, space: 1, docNotifyContext: 1 },
sort: { createdOn: SortingOrder.Ascending }
}
)
const contexts = get(this.contexts) ?? []
@ -327,7 +336,17 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
await ops.updateDoc(notification._class, notification.space, notification._id, { isViewed: false })
}
for (const context of contexts) {
await ops.update(context, { lastViewedTimestamp: 0 })
const firstUnread = inboxNotifications.find(
(it) => it.docNotifyContext === context._id && isActivityNotification(it)
)
if (firstUnread === undefined) {
continue
}
const lastViewedTimestamp = (firstUnread.createdOn ?? firstUnread.modifiedOn) - 1
await ops.update(context, { lastViewedTimestamp })
}
} finally {
await ops.commit()

View File

@ -38,12 +38,13 @@ import {
canUnReadNotifyContext,
readNotifyContext,
unReadNotifyContext,
deleteContextNotifications,
archiveContextNotifications,
hasInboxNotifications,
archiveAll,
readAll,
unreadAll,
checkPermission
checkPermission,
unarchiveContextNotifications
} from './utils'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -86,7 +87,8 @@ export default async (): Promise<Resources> => ({
UnpinDocNotifyContext: unpinDocNotifyContext,
ReadNotifyContext: readNotifyContext,
UnReadNotifyContext: unReadNotifyContext,
DeleteContextNotifications: deleteContextNotifications,
ArchiveContextNotifications: archiveContextNotifications,
UnarchiveContextNotifications: unarchiveContextNotifications,
ArchiveAll: archiveAll,
ReadAll: readAll,
UnreadAll: unreadAll

View File

@ -34,6 +34,7 @@ export default mergeIds(notificationId, notification, {
Activity: '' as IntlString,
People: '' as IntlString,
Read: '' as IntlString,
Unread: '' as IntlString
Unread: '' as IntlString,
Unreads: '' as IntlString
}
})

View File

@ -1,6 +1,14 @@
import type { Ref } from '@hcengineering/core'
import type { DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import type { IntlString } from '@hcengineering/platform'
export type InboxNotificationsFilter = 'all' | 'read' | 'unread'
export type InboxNotificationsFilter = 'all' | 'unread'
export type InboxData = Map<Ref<DocNotifyContext>, DisplayInboxNotification[]>
export interface SettingItem {
id: string
on: boolean
label: IntlString
onToggle: () => void
}

View File

@ -143,23 +143,23 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
/**
* @public
*/
export async function deleteContextNotifications (doc?: DocNotifyContext): Promise<void> {
export async function archiveContextNotifications (doc?: DocNotifyContext): Promise<void> {
if (doc === undefined) {
return
}
const doneOp = await getClient().measure('deleteContextNotifications')
const doneOp = await getClient().measure('archiveContextNotifications')
const ops = getClient().apply(doc._id)
try {
const notifications = await ops.findAll(
notification.class.InboxNotification,
{ docNotifyContext: doc._id },
{ docNotifyContext: doc._id, archived: { $ne: true } },
{ projection: { _id: 1, _class: 1, space: 1 } }
)
for (const notification of notifications) {
await ops.removeDoc(notification._class, notification.space, notification._id)
await ops.updateDoc(notification._class, notification.space, notification._id, { archived: true, isViewed: true })
}
await ops.update(doc, { lastViewedTimestamp: Date.now() })
} finally {
@ -168,6 +168,33 @@ export async function deleteContextNotifications (doc?: DocNotifyContext): Promi
}
}
/**
* @public
*/
export async function unarchiveContextNotifications (doc?: DocNotifyContext): Promise<void> {
if (doc === undefined) {
return
}
const doneOp = await getClient().measure('unarchiveContextNotifications')
const ops = getClient().apply(doc._id)
try {
const notifications = await ops.findAll(
notification.class.InboxNotification,
{ docNotifyContext: doc._id, archived: true },
{ projection: { _id: 1, _class: 1, space: 1 } }
)
for (const notification of notifications) {
await ops.updateDoc(notification._class, notification.space, notification._id, { archived: false })
}
} finally {
await ops.commit()
await doneOp()
}
}
enum OpWithMe {
Add = 'add',
Remove = 'remove'
@ -259,7 +286,7 @@ export async function archiveAll (): Promise<void> {
'top',
(result?: boolean) => {
if (result === true) {
void client.deleteAllNotifications()
void client.archiveAllNotifications()
}
}
)
@ -300,10 +327,6 @@ export async function getDisplayInboxNotifications (
continue
}
if (filter === 'read' && !notification.isViewed) {
continue
}
if (isActivityNotification(notification)) {
activityNotifications.push(notification)
} else {
@ -344,7 +367,10 @@ export async function getDisplayInboxNotifications (
const displayNotification = {
...activityNotification,
combinedIds: activityNotifications.filter(({ attachedTo }) => ids.includes(attachedTo)).map(({ _id }) => _id)
combinedIds: activityNotifications.filter(({ attachedTo }) => ids.includes(attachedTo)).map(({ _id }) => _id),
combinedMessages: activityNotifications
.map((a) => a.$lookup?.attachedTo)
.filter((m): m is ActivityMessage => m !== undefined)
}
result.push(displayNotification)
@ -353,7 +379,8 @@ export async function getDisplayInboxNotifications (
if (activityNotification !== undefined) {
result.push({
...activityNotification,
combinedIds: [activityNotification._id]
combinedIds: [activityNotification._id],
combinedMessages: [message]
})
}
}

View File

@ -252,6 +252,7 @@ export interface InboxNotification extends Doc {
body?: IntlString
intlParams?: Record<string, string | number>
intlParamsNotLocalized?: Record<string, IntlString>
archived?: boolean
}
export interface ActivityInboxNotification extends InboxNotification {
@ -278,6 +279,7 @@ export interface MentionInboxNotification extends CommonInboxNotification {
export interface DisplayActivityInboxNotification extends ActivityInboxNotification {
combinedIds: Ref<ActivityInboxNotification>[]
combinedMessages: ActivityMessage[]
}
export type DisplayInboxNotification = DisplayActivityInboxNotification | InboxNotification
@ -315,8 +317,8 @@ export interface InboxNotificationsClient {
readMessages: (client: TxOperations, ids: Ref<ActivityMessage>[]) => Promise<void>
readNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
unreadNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
deleteNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
deleteAllNotifications: () => Promise<void>
archiveNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
archiveAllNotifications: () => Promise<void>
readAllNotifications: () => Promise<void>
unreadAllNotifications: () => Promise<void>
}
@ -401,7 +403,8 @@ const notification = plugin(notificationId, {
UnpinDocNotifyContext: '' as Ref<Action>,
UnReadNotifyContext: '' as Ref<Action>,
ReadNotifyContext: '' as Ref<Action>,
DeleteContextNotifications: '' as Ref<Action>
ArchiveContextNotifications: '' as Ref<Action>,
UnarchiveContextNotifications: '' as Ref<Action>
},
icon: {
Notifications: '' as Asset,

View File

@ -144,4 +144,7 @@
<path d="M1 9C1 8.72386 1.22386 8.5 1.5 8.5C1.77614 8.5 2 8.72386 2 9V10C2 10.5523 2.44772 11 3 11H11C11.5523 11 12 10.5523 12 10V9C12 8.72386 12.2239 8.5 12.5 8.5C12.7761 8.5 13 8.72386 13 9V10C13 11.1046 12.1046 12 11 12H3C1.89543 12 1 11.1046 1 10V9Z" />
<path d="M14 6C14 5.72386 13.7761 5.5 13.5 5.5H0.5C0.223858 5.5 0 5.72386 0 6C0 6.27614 0.223857 6.5 0.5 6.5H13.5C13.7761 6.5 14 6.27614 14 6Z" />
</symbol>
<symbol id="circle" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 28.0002C22.6274 28.0002 28 22.6277 28 16.0002C28 9.37283 22.6274 4.00024 16 4.00024C9.37258 4.00024 4 9.37283 4 16.0002C4 22.6277 9.37258 28.0002 16 28.0002ZM16 30.0002C23.732 30.0002 30 23.7322 30 16.0002C30 8.26826 23.732 2.00024 16 2.00024C8.26801 2.00024 2 8.26826 2 16.0002C2 23.7322 8.26801 30.0002 16 30.0002Z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -120,6 +120,7 @@
"UnArchive": "Unarchive",
"Pin": "Pin",
"Unpin": "Unpin",
"Archived": "Archived",
"MoreActions": "More actions"
}
}

View File

@ -117,6 +117,7 @@
"UnArchive": "Разархивировать",
"Pin": "Закрепить",
"Unpin": "Открепить",
"Archived": "Архивированные",
"MoreActions": "Больше действий"
}
}

View File

@ -50,5 +50,6 @@ loadMetadata(view.icon, {
Image: `${icons}#image`,
Table2: `${icons}#table2`,
CodeBlock: `${icons}#code-block`,
SeparatorLine: `${icons}#separator-line`
SeparatorLine: `${icons}#separator-line`,
Circle: `${icons}#circle`
})

View File

@ -43,7 +43,6 @@ export default mergeIds(viewId, view, {
DeletePopupNoPermissionLabel: '' as IntlString,
DeletePopupCreatorLabel: '' as IntlString,
DeletePopupOwnerLabel: '' as IntlString,
Archive: '' as IntlString,
ArchiveConfirm: '' as IntlString,
Assignees: '' as IntlString,
Labels: '' as IntlString,

View File

@ -193,8 +193,10 @@ const view = plugin(viewId, {
NoGrouping: '' as IntlString,
Type: '' as IntlString,
UnArchive: '' as IntlString,
Archive: '' as IntlString,
Save: '' as IntlString,
PublicView: '' as IntlString,
Archived: '' as IntlString,
MoreActions: '' as IntlString
},
icon: {
@ -230,7 +232,8 @@ const view = plugin(viewId, {
Image: '' as Asset,
Table2: '' as Asset,
CodeBlock: '' as Asset,
SeparatorLine: '' as Asset
SeparatorLine: '' as Asset,
Circle: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,