From 052ee2af4debe435ed94776aec08b4040c38f3f5 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 30 Nov 2021 21:35:13 +0100 Subject: [PATCH] Feature/bring notifications back (#801) * Bring notifications menu back. * Cleanup some stuff. --- .../Notification/NotificationActionHandler.cs | 22 ++--- backend/i18n/frontend_en.json | 1 + backend/i18n/frontend_it.json | 1 + backend/i18n/frontend_nl.json | 1 + backend/i18n/frontend_zh.json | 1 + backend/i18n/source/frontend_en.json | 1 + .../Comments/Models/UpsertCommentDto.cs | 11 ++- .../comments/comment.component.scss | 12 +++ .../notifications-menu.component.html | 29 +++++++ .../notifications-menu.component.scss | 13 +++ .../internal/notifications-menu.component.ts | 86 ++++++++++++++++++- 11 files changed, 164 insertions(+), 14 deletions(-) diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 7aed0abc8..158be043b 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -33,7 +33,12 @@ namespace Squidex.Extensions.Actions.Notification { if (@event is EnrichedUserEventBase userEvent) { - var text = await FormatAsync(action.Text, @event); + var user = await userResolver.FindByIdOrEmailAsync(action.User); + + if (user == null) + { + throw new InvalidOperationException($"Cannot find user by '{action.User}'"); + } var actor = userEvent.Actor; @@ -42,16 +47,13 @@ namespace Squidex.Extensions.Actions.Notification actor = RefToken.Client(action.Client); } - var user = await userResolver.FindByIdOrEmailAsync(action.User); - - if (user == null) + var ruleJob = new CreateComment { - throw new InvalidOperationException($"Cannot find user by '{action.User}'"); - } - - var commentsId = DomainId.Create(user.Id); - - var ruleJob = new CreateComment { Actor = actor, CommentsId = commentsId, Text = text }; + Actor = actor, + CommentId = DomainId.NewGuid(), + CommentsId = DomainId.Create(user.Id), + Text = await FormatAsync(action.Text, @event) + }; if (!string.IsNullOrWhiteSpace(action.Url)) { diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index fa0f27499..f94ba78ab 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -607,6 +607,7 @@ "languages.updateFailed": "Failed to change language. Please reload.", "news.headline": "What's new?", "news.title": "New Features", + "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.", "plans.billingPortal": "Billing Portal", "plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index ccad631a0..6209894e8 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -607,6 +607,7 @@ "languages.updateFailed": "Non è stato possibile cambiare la lingua. Per favore ricarica.", "news.headline": "Che cosa c'è di nuovo?", "news.title": "Nuove funzionalità", + "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.", "plans.billingPortal": "Portale di fatturazione", "plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 7f3ad2615..807ffaced 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -607,6 +607,7 @@ "languages.updateFailed": "Het wijzigen van de taal is mislukt. Laad opnieuw.", "news.headline": "Wat is er nieuw?", "news.title": "Nieuwe functies", + "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.", "plans.billingPortal": "Factureringsportal", "plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index a7ed7f409..c0a652379 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -607,6 +607,7 @@ "languages.updateFailed": "更改语言失败。请重新加载。", "news.headline": "有什么新鲜事?", "news.title": "新功能", + "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "单击此按钮可订阅所有更改并接收推送通知。", "plans.billingPortal": "计费门户", "plans.billingPortalHint": "前往账单门户查看付款历史和订阅概览。", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index fa0f27499..f94ba78ab 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -607,6 +607,7 @@ "languages.updateFailed": "Failed to change language. Please reload.", "news.headline": "What's new?", "news.title": "New Features", + "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.", "plans.billingPortal": "Billing Portal", "plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.", diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs index 7b8608c5e..22f78f54f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs @@ -27,12 +27,19 @@ namespace Squidex.Areas.Api.Controllers.Comments.Models public CreateComment ToCreateCommand(DomainId commentsId) { - return SimpleMapper.Map(this, new CreateComment { CommentsId = commentsId }); + return SimpleMapper.Map(this, new CreateComment + { + CommentsId = commentsId + }); } public UpdateComment ToUpdateComment(DomainId commentsId, DomainId commentId) { - return SimpleMapper.Map(this, new UpdateComment { CommentsId = commentsId, CommentId = commentId }); + return SimpleMapper.Map(this, new UpdateComment + { + CommentsId = commentsId, + CommentId = commentId + }); } } } diff --git a/frontend/app/shared/components/comments/comment.component.scss b/frontend/app/shared/components/comments/comment.component.scss index 7fcdf87c8..7ae830baa 100644 --- a/frontend/app/shared/components/comments/comment.component.scss +++ b/frontend/app/shared/components/comments/comment.component.scss @@ -1,8 +1,12 @@ +/* stylelint-disable no-descending-specificity */ + .actions { @include absolute(-5px, -15px, auto, auto); background: $color-white; border: 0; + border-radius: 0; display: none; + width: auto; } .user-ref { @@ -45,6 +49,14 @@ } } +:host { + &:last-child { + .comment { + margin-bottom: 0; + } + } +} + :host ::ng-deep { p { &:last-child { diff --git a/frontend/app/shell/pages/internal/notifications-menu.component.html b/frontend/app/shell/pages/internal/notifications-menu.component.html index d601b17ea..5d16866e8 100644 --- a/frontend/app/shell/pages/internal/notifications-menu.component.html +++ b/frontend/app/shell/pages/internal/notifications-menu.component.html @@ -1,3 +1,32 @@ \ No newline at end of file diff --git a/frontend/app/shell/pages/internal/notifications-menu.component.scss b/frontend/app/shell/pages/internal/notifications-menu.component.scss index e69de29bb..5d9cb9fb2 100644 --- a/frontend/app/shell/pages/internal/notifications-menu.component.scss +++ b/frontend/app/shell/pages/internal/notifications-menu.component.scss @@ -0,0 +1,13 @@ +.dropdown-menu { + max-height: 500px; + min-height: 4rem; + overflow-y: scroll; + padding: 1.25rem; + padding-bottom: 1rem; + width: 300px; +} + +.badge { + @include absolute(-.5rem, 0, null, null); + font-size: 80%; +} \ No newline at end of file diff --git a/frontend/app/shell/pages/internal/notifications-menu.component.ts b/frontend/app/shell/pages/internal/notifications-menu.component.ts index d70a917b8..74f50a10d 100644 --- a/frontend/app/shell/pages/internal/notifications-menu.component.ts +++ b/frontend/app/shell/pages/internal/notifications-menu.component.ts @@ -5,13 +5,95 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; +import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; +import { AuthService, CommentDto, CommentsService, CommentsState, DialogService, fadeAnimation, LocalStoreService, ModalModel, ResourceOwner, UIOptions } from '@app/shared'; + +const CONFIG_KEY = 'notifications.version'; @Component({ selector: 'sqx-notifications-menu', styleUrls: ['./notifications-menu.component.scss'], templateUrl: './notifications-menu.component.html', + animations: [ + fadeAnimation, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class NotificationsMenuComponent { +export class NotificationsMenuComponent extends ResourceOwner implements OnInit { + public modalMenu = new ModalModel(); + + public commentsState: CommentsState; + + public versionRead = -1; + public versionReceived = -1; + + public userToken: string; + + public get unread() { + return Math.max(0, this.versionReceived - this.versionRead); + } + + public isNotifoConfigured: boolean; + + constructor(authService: AuthService, commentsService: CommentsService, dialogs: DialogService, uiOptions: UIOptions, + private readonly changeDetector: ChangeDetectorRef, + private readonly localStore: LocalStoreService, + ) { + super(); + + const notifoApiKey = authService.user?.notifoToken; + const notifoApiUrl = uiOptions.get('more.notifoApi'); + + this.isNotifoConfigured = !!notifoApiKey && !!notifoApiUrl; + + this.userToken = authService.user!.token; + + this.versionRead = localStore.getInt(CONFIG_KEY, -1); + this.versionReceived = this.versionRead; + + const commentsUrl = `users/${authService.user!.id}/notifications`; + + this.commentsState = + new CommentsState( + commentsUrl, + commentsService, + dialogs, + true, + this.versionRead); + } + + public ngOnInit() { + this.own( + this.modalMenu.isOpenChanges.pipe( + tap(_ => { + this.updateVersion(); + }), + )); + + this.own( + this.commentsState.versionNumber.pipe( + tap(version => { + this.versionReceived = version; + + this.updateVersion(); + + this.changeDetector.detectChanges(); + }))); + + this.own(timer(0, 4000).pipe(switchMap(() => this.commentsState.load(true).pipe(onErrorResumeNext())))); + } + + public trackByComment(_index: number, comment: CommentDto) { + return comment.id; + } + + private updateVersion() { + if (this.modalMenu.isOpen) { + this.versionRead = this.versionReceived; + + this.localStore.setInt(CONFIG_KEY, this.versionRead); + } + } }