mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 20:27:43 +02:00
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:
+1
-1
Submodule communication updated: ffed29fc6b...90b0ea297d
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "Gesamtstimmen",
|
||||
"Voted": "Abgestimmt",
|
||||
"VotedFor": "Für abgestimmt",
|
||||
"RevokedVote": "Stimme widerrufen"
|
||||
"RevokedVote": "Stimme widerrufen",
|
||||
"AnonymousQuiz": "Anonyme quiz"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "Total votes",
|
||||
"Voted": "Voted",
|
||||
"VotedFor": "Voted for",
|
||||
"RevokedVote": "Revoked vote"
|
||||
"RevokedVote": "Revoked vote",
|
||||
"AnonymousQuiz": "Anonymous quiz"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "Votos totales",
|
||||
"Voted": "Votado",
|
||||
"VotedFor": "Votado por",
|
||||
"RevokedVote": "Revocar voto"
|
||||
"RevokedVote": "Revocar voto",
|
||||
"AnonymousQuiz": "Quiz anónimo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "Voti totali",
|
||||
"Voted": "Votato",
|
||||
"VotedFor": "Votato per",
|
||||
"RevokedVote": "Revoca il voto"
|
||||
"RevokedVote": "Revoca il voto",
|
||||
"AnonymousQuiz": "Quiz anonimo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "合計投票数",
|
||||
"Voted": "投票済み",
|
||||
"VotedFor": "投票したユーザ",
|
||||
"RevokedVote": "投票を取り消し"
|
||||
"RevokedVote": "投票を取り消し",
|
||||
"AnonymousQuiz": "匿名クイズ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "Total de votos",
|
||||
"Voted": "Votado",
|
||||
"VotedFor": "Votado para",
|
||||
"RevokedVote": "Revogar voto"
|
||||
"RevokedVote": "Revogar voto",
|
||||
"AnonymousQuiz": "Quiz anônimo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"TotalVotes": "Всего голосов",
|
||||
"Voted": "Проголосовавшие",
|
||||
"VotedFor": "Голосовал(а) за",
|
||||
"RevokedVote": "Отменил(а) голос"
|
||||
"RevokedVote": "Отменил(а) голос",
|
||||
"AnonymousQuiz": "Анонимная викторина"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+13
-9
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user