Improve notifications count (#9598)

* Update communication

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Show real unread notifications count

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Small ui changes

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

---------

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-07-25 00:11:47 +07:00
committed by GitHub
parent 790595a26c
commit ed3e8c0b6b
26 changed files with 154 additions and 77 deletions
+2 -2
View File
@@ -134,8 +134,8 @@ export function configureNotifications (): void {
const isCommunicationEnabled = getMetadata(communication.metadata.Enabled) ?? false
if (isCommunicationEnabled) {
notificationsCountQuery.query({ read: false, limit: 1000 }, res => {
newUnreadNotifications = res.getResult().length
notificationsCountQuery.query({ read: false, limit: 1, strict: true, total: true }, res => {
newUnreadNotifications = res.getTotal()
if (preferences.showUnreadCounter) {
electronAPI.setBadge(prevUnViewdNotificationsCount + newUnreadNotifications)
+4 -3
View File
@@ -60,7 +60,8 @@ import {
type AttachmentID,
type AttachmentData,
type AttachmentParams,
type AttachmentUpdateData
type AttachmentUpdateData,
type WithTotal
} from '@hcengineering/communication-types'
import core, {
generateId,
@@ -368,9 +369,9 @@ class Client {
).value
}
async findNotifications (params: FindNotificationsParams, queryId?: number): Promise<Notification[]> {
async findNotifications (params: FindNotificationsParams, queryId?: number): Promise<WithTotal<Notification>> {
return (
await this.connection.domainRequest<Notification[]>(COMMUNICATION, {
await this.connection.domainRequest<WithTotal<Notification>>(COMMUNICATION, {
findNotifications: { params, queryId }
})
).value
@@ -15,7 +15,7 @@
<script lang="ts">
import { Card, FavoriteCard, MasterTag } from '@hcengineering/card'
import core, { Class, Ref } from '@hcengineering/core'
import core, { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte'
import { IconMoreV, NavItem, Action, ButtonIcon } from '@hcengineering/ui'
@@ -38,7 +38,6 @@
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let activeAction: string | undefined = undefined
@@ -81,8 +80,6 @@
}
]
$: clazz = hierarchy.getClass(card._class) as Class<Card> & { color?: number }
function getCardTitle (card: Card): string {
if ((card?.parentInfo?.length ?? 0) === 0) return card.title
const parent = card.parentInfo[card.parentInfo.length - 1]
@@ -133,15 +130,11 @@
</svelte:fragment>
<svelte:fragment slot="notify">
{#if context && (context?.notifications?.length ?? 0) > 0}
{#if context && (context.totalNotifications ?? 0) > 0}
<div class="antiHSpacer" />
<div class="notify">
<div class="notifyMarker">
{#if (context.notifications?.length ?? 0) > 9}
{9}+
{:else}
{context.notifications?.length ?? 0}
{/if}
{context.totalNotifications}
</div>
</div>
<div class="antiHSpacer" />
@@ -155,7 +148,7 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 1rem;
min-width: 1rem;
height: 1rem;
}
.notifyMarker {
@@ -163,12 +156,13 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 50%;
border-radius: 0.5rem;
font-weight: 700;
background-color: var(--global-higlight-Color);
color: var(--global-on-accent-TextColor);
width: 1rem;
min-width: 1rem;
height: 1rem;
font-size: 0.5rem;
padding: 0 0.25rem;
}
</style>
@@ -75,7 +75,8 @@
read: false,
type: NotificationType.Message,
order: SortingOrder.Descending,
limit: 10
limit: 1,
total: true
}
},
(res) => {
@@ -108,7 +108,8 @@
type: NotificationType.Message,
order: SortingOrder.Descending,
read: false,
limit: 10
limit: 1,
total: true
}
},
(res) => {
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Celkem hlasů",
"Voted": "Hlasováno",
"VotedFor": "Hlasováno pro",
"RevokedVote": "Zrušeno hlasování"
"RevokedVote": "Zrušeno hlasování",
"AnonymousQuiz": "Anonymní test"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Gesamtstimmen",
"Voted": "Abgestimmt",
"VotedFor": "Für abgestimmt",
"RevokedVote": "Stimme widerrufen"
"RevokedVote": "Stimme widerrufen",
"AnonymousQuiz": "Anonyme quiz"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Total votes",
"Voted": "Voted",
"VotedFor": "Voted for",
"RevokedVote": "Revoked vote"
"RevokedVote": "Revoked vote",
"AnonymousQuiz": "Anonymous quiz"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Votos totales",
"Voted": "Votado",
"VotedFor": "Votado por",
"RevokedVote": "Revocar voto"
"RevokedVote": "Revocar voto",
"AnonymousQuiz": "Quiz anónimo"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Total des votes",
"Voted": "A voté",
"VotedFor": "A voté pour",
"RevokedVote": "Révoquer le vote"
"RevokedVote": "Révoquer le vote",
"AnonymousQuiz": "Quiz anonyme"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Voti totali",
"Voted": "Votato",
"VotedFor": "Votato per",
"RevokedVote": "Revoca il voto"
"RevokedVote": "Revoca il voto",
"AnonymousQuiz": "Quiz anonimo"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "合計投票数",
"Voted": "投票済み",
"VotedFor": "投票したユーザ",
"RevokedVote": "投票を取り消し"
"RevokedVote": "投票を取り消し",
"AnonymousQuiz": "匿名クイズ"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Total de votos",
"Voted": "Votado",
"VotedFor": "Votado para",
"RevokedVote": "Revogar voto"
"RevokedVote": "Revogar voto",
"AnonymousQuiz": "Quiz anônimo"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "Всего голосов",
"Voted": "Проголосовавшие",
"VotedFor": "Голосовал(а) за",
"RevokedVote": "Отменил(а) голос"
"RevokedVote": "Отменил(а) голос",
"AnonymousQuiz": "Анонимная викторина"
}
}
+2 -1
View File
@@ -84,6 +84,7 @@
"TotalVotes": "总投票数",
"Voted": "已投票",
"VotedFor": "投票于",
"RevokedVote": "撤销投票"
"RevokedVote": "撤销投票",
"AnonymousQuiz": "匿名测验"
}
}
@@ -355,6 +355,18 @@
separatorDate = messages[separatorIndex].created
}
function readAllReactions (): void {
if (reactionNotifications.length === 0) return
for (const notification of reactionNotifications) {
void communicationClient.updateNotifications(
notification.contextId,
{
id: notification.id
},
true
)
}
}
function readViewport (isAppFocused: boolean): void {
if (!isAppFocused || context == null || window == null) return
@@ -566,6 +578,9 @@
bottomOffset = getBottomOffset()
topOffset = getTopOffset()
dispatch('loaded')
if (shouldScrollToEnd) {
readAllReactions()
}
return
}
@@ -12,7 +12,7 @@
<!-- limitations under the License. -->
<script lang="ts">
import { CheckBox } from '@hcengineering/ui'
import { CheckBox, Loading } from '@hcengineering/ui'
import { getCurrentAccount, WithLookup } from '@hcengineering/core'
import { Poll, PollAnswer } from '@hcengineering/communication'
import { createEventDispatcher } from 'svelte'
@@ -21,6 +21,7 @@
export let option: PollOption
export let result: WithLookup<Poll> | undefined
export let isLoading: boolean
export let privateAnswers: PollAnswer[]
export let answer: string | undefined
export let anonymous: boolean
@@ -96,15 +97,19 @@
<div class="poll-option">
<div class="poll-option__answer">
<span class="option_checkbox">
<CheckBox
checked={false}
kind="todo"
size="small"
disabled={!started || ended}
on:value={() => {
dispatch('toggle')
}}
/>
{#if !isLoading}
<CheckBox
checked={false}
kind="todo"
size="small"
disabled={!started || ended}
on:value={() => {
dispatch('toggle')
}}
/>
{:else}
<Loading size="small" />
{/if}
</span>
<span class="option_label">
{option.label}
@@ -30,16 +30,23 @@
export let applet: Applet
export let attachment: AppletAttachment<PollConfig>
let result: Poll | undefined = undefined
const query = createQuery()
const privateAnswersQuery = createQuery()
let result: Poll | undefined = undefined
let privateAnswers: PollAnswer[] = []
let isLoadingPoll = true
let isLoadingPrivateAnswers = true
$: isLoading = isLoadingPoll || isLoadingPrivateAnswers
$: query.query(
communication.type.Poll,
{ _id: attachment.params.id },
(res) => {
result = res[0] as Poll
isLoadingPoll = false
},
{ limit: 1 }
)
@@ -52,8 +59,11 @@
},
(res) => {
privateAnswers = res
isLoadingPrivateAnswers = false
}
)
} else {
isLoadingPrivateAnswers = false
}
$: params = attachment.params
@@ -257,7 +267,9 @@
{params.question}
</div>
<div class="poll-type">
{#if params.anonymous}
{#if params.anonymous && params.quiz}
<Label label={communication.string.AnonymousQuiz} />
{:else if params.anonymous}
<Label label={communication.string.AnonymousVoting} />
{:else if params.quiz}
<Label label={communication.string.Quiz} />
@@ -279,6 +291,7 @@
<PollOptionPresenter
{option}
bind:result
{isLoading}
isVoted={voted}
answer={params.quizAnswer}
{started}
@@ -23,19 +23,23 @@
$: voteLabel = addedVote?.options?.map((it) => it.label).join(', ') ?? ''
</script>
<span class="icon mr-1"> <Icon icon={communication.icon.Poll} size="small" /> </span>
{#if addedVote}
<span class="overflow-label ml-1">
<span class="container overflow-label">
<span class="icon mr-1"> <Icon icon={communication.icon.Poll} size="small" /> </span>
{#if addedVote}
<Label label={communication.string.VotedFor} />
<span class="strong" title={voteLabel}>{voteLabel}</span>
</span>
{:else}
<span class="overflow-label ml-1">
<span class="strong overflow-label flex-shrink" title={voteLabel}>{voteLabel}</span>
{:else}
<Label label={communication.string.RevokedVote} />
</span>
{/if}
{/if}
</span>
<style lang="scss">
.container {
display: inline-flex;
gap: 0.5rem;
white-space: nowrap;
flex-shrink: 1;
}
.strong {
font-weight: 500;
}
+2 -1
View File
@@ -85,7 +85,8 @@ export default plugin(communicationId, {
TotalVotes: '' as IntlString,
Voted: '' as IntlString,
VotedFor: '' as IntlString,
RevokedVote: '' as IntlString
RevokedVote: '' as IntlString,
AnonymousQuiz: '' as IntlString
},
ids: {
CardMessagesSection: '' as Ref<CardSection>,
@@ -13,10 +13,10 @@
-->
<script lang="ts">
import { NotificationContext, Notification } from '@hcengineering/communication-types'
import { NotificationContext } from '@hcengineering/communication-types'
import { Card } from '@hcengineering/card'
import { createEventDispatcher } from 'svelte'
import { getCommunicationClient } from '@hcengineering/presentation'
import { createNotificationsQuery, getCommunicationClient } from '@hcengineering/presentation'
import { CheckBox, Spinner } from '@hcengineering/ui'
import { AccountRole, getCurrentAccount } from '@hcengineering/core'
@@ -31,8 +31,13 @@
const dispatch = createEventDispatcher()
const communicationClient = getCommunicationClient()
let displayNotifications: Notification[] = []
$: displayNotifications = (context.notifications ?? []).filter((it) => it.message != null).slice(0, 3)
const notificationsQuery = createNotificationsQuery()
let total = 0
notificationsQuery.query({ limit: 1, total: true, read: false, strict: true, context: context.id }, (res) => {
total = res.getTotal()
})
let isRemoving = false
async function handleToggle (): Promise<void> {
@@ -49,7 +54,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="inbox-card" class:selected on:click={() => dispatch('select', { context, card })}>
<div class="inbox-card__header">
<InboxCardIcon {card} count={context.notifications?.filter((x) => !x.read)?.length ?? 0} />
<InboxCardIcon {card} count={total ?? 0} />
<div class="inbox-card__labels">
<span class="inbox-card__title overflow-label clear-mins" title={card.title}>
{card.title}
@@ -68,7 +73,7 @@
<div class="inbox-card__content">
<div class="inbox-card__notifications">
{#each displayNotifications as notification}
{#each context.notifications ?? [] as notification}
<div class="inbox-card__notification">
<div class="inbox-card__marker" />
<InboxNotification {notification} {card} />
@@ -51,8 +51,9 @@
&__marker {
position: absolute;
top: -0.25rem;
right: -0.25rem;
top: -0.375rem;
right: 0;
transform: translateX(calc(100% - 0.875rem));
display: flex;
align-items: center;
justify-content: center;
@@ -50,7 +50,7 @@
notifications: {
message: true,
order: SortingOrder.Descending,
limit: 10
limit: 3
},
order: SortingOrder.Descending,
limit
@@ -16,17 +16,11 @@
export let count: number = 0
export let kind: 'primary' | 'simple' = 'primary'
export let size: 'xx-small' | 'x-small' | 'small' | 'medium' = 'small'
const maxNumber = 9
</script>
{#if kind === 'primary' && count > 0}
<div class="notifyMarker {size} {kind}">
{#if count > maxNumber}
{maxNumber}+
{:else}
{count}
{/if}
{count}
</div>
{/if}
@@ -40,8 +34,9 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 50%;
border-radius: 0.5rem;
font-weight: 500;
padding: 0 0.25rem;
&.simple,
&.primary {
@@ -50,23 +45,23 @@
}
&.xx-small {
width: 0.5rem;
min-width: 0.5rem;
height: 0.5rem;
}
&.x-small {
width: 0.75rem;
min-width: 0.75rem;
height: 0.75rem;
}
&.small {
width: 1rem;
min-width: 1rem;
height: 1rem;
font-size: 0.5rem;
}
&.medium {
width: 1.25rem;
min-width: 1.25rem;
height: 1.25rem;
font-size: 0.625rem;
}
+34 -3
View File
@@ -53,14 +53,34 @@ export interface HelloResponse extends Response<any> {
useCompression?: boolean
}
function isTotalArray (value: any): value is { total?: number, lookupMap?: Record<string, any> } & any[] {
return Array.isArray(value) && ((value as any).total !== undefined || (value as any).lookupMap !== undefined)
}
export function rpcJSONReplacer (key: string, value: any): any {
if (Array.isArray(value) && ((value as any).total !== undefined || (value as any).lookupMap !== undefined)) {
if (isTotalArray(value)) {
return {
dataType: 'TotalArray',
total: (value as any).total,
lookupMap: (value as any).lookupMap,
total: value.total,
lookupMap: value.lookupMap,
value: [...value]
}
} else if (
typeof value === 'object' &&
value !== null &&
'domain' in value &&
typeof value.domain === 'string' &&
'value' in value &&
isTotalArray(value.value)
) {
return {
...value,
value: {
dataType: 'TotalArray',
total: value.value.total,
lookupMap: value.value.lookupMap,
value: [...value.value]
}
}
} else {
return value ?? null
}
@@ -70,6 +90,17 @@ export function rpcJSONReceiver (key: string, value: any): any {
if (typeof value === 'object' && value !== null) {
if (value.dataType === 'TotalArray') {
return Object.assign(value.value, { total: value.total, lookupMap: value.lookupMap })
} else if (
'domain' in value &&
typeof value.domain === 'string' &&
'value' in value &&
value.value != null &&
value.value.dataType === 'TotalArray'
) {
return {
...value,
value: Object.assign(value.value.value, { total: value.value.total, lookupMap: value.value.lookupMap })
}
}
}
return value