diff --git a/projects/orcid-registry-ui/src/lib/components/permission-notifications/permission-notifications.component.ts b/projects/orcid-registry-ui/src/lib/components/permission-notifications/permission-notifications.component.ts index e79c0085b5..a83d9a7660 100644 --- a/projects/orcid-registry-ui/src/lib/components/permission-notifications/permission-notifications.component.ts +++ b/projects/orcid-registry-ui/src/lib/components/permission-notifications/permission-notifications.component.ts @@ -1,4 +1,10 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' import { DomSanitizer } from '@angular/platform-browser' import { NgClass, @@ -45,6 +51,7 @@ export interface RegistryNotificationActionEvent { @Component({ selector: 'orcid-registry-permission-notifications', standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ NgClass, NgFor, diff --git a/src/app/core/inbox/permission-notifications.service.spec.ts b/src/app/core/inbox/permission-notifications.service.spec.ts index ec59d1c092..63a4416fbb 100644 --- a/src/app/core/inbox/permission-notifications.service.spec.ts +++ b/src/app/core/inbox/permission-notifications.service.spec.ts @@ -1,8 +1,9 @@ import { TestBed } from '@angular/core/testing' -import { of } from 'rxjs' +import { of, throwError } from 'rxjs' import { PermissionNotificationsService } from './permission-notifications.service' import { InboxService } from './inbox.service' import { InboxNotificationPermission } from '../../types/notifications.endpoint' +import { AccountTrustedOrganizationsService } from '../account-trusted-organizations/account-trusted-organizations.service' function createPermissionNotification( overrides: Partial = {} @@ -28,17 +29,27 @@ function createPermissionNotification( describe('PermissionNotificationsService', () => { let service: PermissionNotificationsService let inboxSpy: jasmine.SpyObj + let trustedOrgsSpy: jasmine.SpyObj beforeEach(() => { inboxSpy = jasmine.createSpyObj('InboxService', [ 'getUnreadCount', 'fetchNotificationsIncremental', ]) + trustedOrgsSpy = jasmine.createSpyObj( + 'AccountTrustedOrganizationsService', + ['get'] + ) + trustedOrgsSpy.get.and.returnValue(of([])) TestBed.configureTestingModule({ providers: [ PermissionNotificationsService, { provide: InboxService, useValue: inboxSpy }, + { + provide: AccountTrustedOrganizationsService, + useValue: trustedOrgsSpy, + }, ], }) service = TestBed.inject(PermissionNotificationsService) @@ -96,6 +107,70 @@ describe('PermissionNotificationsService', () => { expect(result[0].source.sourceClientId.path).toBe('client-a') expect(result[1].source.sourceClientId.path).toBe('client-b') expect(inboxSpy.fetchNotificationsIncremental).toHaveBeenCalledWith(false) + expect(trustedOrgsSpy.get).toHaveBeenCalled() + done() + }) + }) + + it('should filter out notifications from already trusted clientIds', (done) => { + const perm1 = createPermissionNotification({ + putCode: 1, + sentDate: 2000, + source: { + sourceClientId: { path: 'client-a' } as any, + sourceName: { content: 'Org A' }, + }, + }) + const perm2 = createPermissionNotification({ + putCode: 2, + sentDate: 1000, + source: { + sourceClientId: { path: 'client-b' } as any, + sourceName: { content: 'Org B' }, + }, + }) + + trustedOrgsSpy.get.and.returnValue(of([{ clientId: 'client-a' } as any])) + inboxSpy.getUnreadCount.and.returnValue(of(5)) + inboxSpy.fetchNotificationsIncremental.and.returnValue( + of({ total: 5, notifications: [perm1, perm2], done: true }) + ) + + service.loadUnreadPermissionNotifications(3).subscribe((result) => { + expect(result.length).toBe(1) + expect(result[0].source.sourceClientId.path).toBe('client-b') + done() + }) + }) + + it('should not fail if trusted-orgs lookup fails (no filtering applied)', (done) => { + const perm1 = createPermissionNotification({ + putCode: 1, + sentDate: 2000, + source: { + sourceClientId: { path: 'client-a' } as any, + sourceName: { content: 'Org A' }, + }, + }) + const perm2 = createPermissionNotification({ + putCode: 2, + sentDate: 1000, + source: { + sourceClientId: { path: 'client-b' } as any, + sourceName: { content: 'Org B' }, + }, + }) + + trustedOrgsSpy.get.and.returnValue(throwError(() => new Error('fail'))) + inboxSpy.getUnreadCount.and.returnValue(of(5)) + inboxSpy.fetchNotificationsIncremental.and.returnValue( + of({ total: 5, notifications: [perm1, perm2], done: true }) + ) + + service.loadUnreadPermissionNotifications(3).subscribe((result) => { + expect(result.length).toBe(2) + expect(result[0].source.sourceClientId.path).toBe('client-a') + expect(result[1].source.sourceClientId.path).toBe('client-b') done() }) }) diff --git a/src/app/core/inbox/permission-notifications.service.ts b/src/app/core/inbox/permission-notifications.service.ts index 0778f70208..be3d152691 100644 --- a/src/app/core/inbox/permission-notifications.service.ts +++ b/src/app/core/inbox/permission-notifications.service.ts @@ -1,17 +1,28 @@ import { Injectable } from '@angular/core' import { Observable, of } from 'rxjs' -import { first, last, map, switchMap, takeWhile } from 'rxjs/operators' +import { + catchError, + first, + last, + map, + switchMap, + takeWhile, +} from 'rxjs/operators' import { InboxService } from './inbox.service' import { InboxNotification, InboxNotificationPermission, } from '../../types/notifications.endpoint' +import { AccountTrustedOrganizationsService } from '../account-trusted-organizations/account-trusted-organizations.service' @Injectable({ providedIn: 'root', }) export class PermissionNotificationsService { - constructor(private _inbox: InboxService) {} + constructor( + private _inbox: InboxService, + private _trustedOrgs: AccountTrustedOrganizationsService + ) {} /** * Load unread permission notifications, one per client (deduplicated by source client), @@ -28,29 +39,34 @@ export class PermissionNotificationsService { return of([]) } const limit = Math.min(maxToScan, unreadCount) - return this._inbox.fetchNotificationsIncremental(false).pipe( - takeWhile( - (ev: { - total: number - notifications: InboxNotification[] - done: boolean - }) => { - const grouped = this.groupUnreadPermissionByClient( - ev.notifications - ) - return ( - grouped.length < maxItems && - !ev.done && - ev.notifications.length < limit + return this.getTrustedOrgClientIds().pipe( + switchMap((trustedClientIds) => + this._inbox.fetchNotificationsIncremental(false).pipe( + takeWhile( + (ev: { + total: number + notifications: InboxNotification[] + done: boolean + }) => { + const grouped = this.groupUnreadPermissionByClient( + ev.notifications, + trustedClientIds + ) + return ( + grouped.length < maxItems && + !ev.done && + ev.notifications.length < limit + ) + }, + true + ), + last(), + map((ev: { notifications: InboxNotification[] }) => + this.groupUnreadPermissionByClient( + ev?.notifications ?? [], + trustedClientIds + ).slice(0, maxItems) ) - }, - true - ), - last(), - map((ev: { notifications: InboxNotification[] }) => - this.groupUnreadPermissionByClient(ev?.notifications ?? []).slice( - 0, - maxItems ) ) ) @@ -58,8 +74,23 @@ export class PermissionNotificationsService { ) } + private getTrustedOrgClientIds(): Observable> { + return this._trustedOrgs.get().pipe( + first(), + map((orgs) => { + const ids = (orgs || []) + .map((o) => o?.clientId) + .filter((id): id is string => typeof id === 'string' && id.length > 0) + return new Set(ids) + }), + // If the trusted-orgs call fails, just don’t filter. + catchError(() => of(new Set())) + ) + } + private groupUnreadPermissionByClient( - notifications: InboxNotification[] + notifications: InboxNotification[], + trustedClientIds: Set ): InboxNotificationPermission[] { const unreadPermission = (notifications || []) .filter( @@ -71,6 +102,7 @@ export class PermissionNotificationsService { for (const n of unreadPermission) { const clientId = n?.source?.sourceClientId?.path if (!clientId) continue + if (trustedClientIds?.has(clientId)) continue if (!byClient.has(clientId)) byClient.set(clientId, n) } return Array.from(byClient.values()).sort( diff --git a/src/app/core/togglz/togglz-flags.enum.ts b/src/app/core/togglz/togglz-flags.enum.ts deleted file mode 100644 index 9898b5bdba..0000000000 --- a/src/app/core/togglz/togglz-flags.enum.ts +++ /dev/null @@ -1,16 +0,0 @@ -export enum TogglzFlag { - WORDPRESS_HOME_PAGE = 'WORDPRESS_HOME_PAGE', - HEADER_COMPACT = 'HEADER_COMPACT', - OAUTH_AUTHORIZATION = 'OAUTH_AUTHORIZATION', - OAUTH2_AUTHORIZATION = 'OAUTH2_AUTHORIZATION', - OAUTH_SIGNIN = 'OAUTH_SIGNIN', - EMAIL_DOMAINS_UI = 'EMAIL_DOMAINS_UI', - FEATURED_WORKS_UI = 'FEATURED_WORKS_UI', - NEW_RELIC_BROWSER_MONITORING = 'NEW_RELIC_BROWSER_MONITORING', - LOGIN_DOMAINS_INTERSTITIAL = 'LOGIN_DOMAINS_INTERSTITIAL', - OAUTH_DOMAINS_INTERSTITIAL = 'OAUTH_DOMAINS_INTERSTITIAL', - LOGIN_AFFILIATION_INTERSTITIAL = 'LOGIN_AFFILIATION_INTERSTITIAL', - OAUTH_AFFILIATION_INTERSTITIAL = 'OAUTH_AFFILIATION_INTERSTITIAL', - MAINTENANCE_MESSAGE = 'MAINTENANCE_MESSAGE', - FEATURED_AFFILIATIONS = 'FEATURED_AFFILIATIONS', -} diff --git a/src/app/record/components/top-bar/top-bar.component.ts b/src/app/record/components/top-bar/top-bar.component.ts index 7b93535eff..ec83f6f7c3 100644 --- a/src/app/record/components/top-bar/top-bar.component.ts +++ b/src/app/record/components/top-bar/top-bar.component.ts @@ -3,7 +3,7 @@ import { UserRecord } from '../../../types/record.local' import { PlatformInfo, PlatformInfoService } from '../../../cdk/platform-info' import { ModalNameComponent } from './modals/modal-name/modal-name.component' import { ModalBiographyComponent } from './modals/modal-biography/modal-biography.component' -import { takeUntil } from 'rxjs/operators' +import { take, takeUntil } from 'rxjs/operators' import { Subject } from 'rxjs' import { UserService } from '../../../core' import { RecordService } from '../../../core/record/record.service' @@ -25,6 +25,8 @@ import { import { InboxNotificationPermission } from 'src/app/types/notifications.endpoint' import { first } from 'rxjs/operators' import { Router } from '@angular/router' +import { TogglzService } from '../../../core/togglz/togglz.service' +import { TogglzFlag } from '../../../types/config.endpoint' @Component({ selector: 'app-top-bar', @@ -68,8 +70,8 @@ export class TopBarComponent implements OnInit, OnDestroy { ariaLabelName: string - permissionPanelTitle = $localize`:@@topBar.permissionNotificationsTitle:Unread permission notifications` - permissionPanelSubtitle = $localize`:@@topBar.permissionNotificationsSubtitle:You have updates waiting for your review.` + permissionPanelTitle = $localize`:@@topBar.permissionNotificationsTitle:Organizations want to connect` + permissionPanelSubtitle = $localize`:@@topBar.permissionNotificationsSubtitle:Connecting with trusted organizations helps keep your ORCID record up-to-date.` permissionPanelNotifications: RegistryPermissionNotification[] = [] private permissionPanelRaw: InboxNotificationPermission[] = [] private _permissionPanelLoadStarted = false @@ -84,6 +86,7 @@ export class TopBarComponent implements OnInit, OnDestroy { private _recordEmails: RecordEmailsService, private _verificationEmailModalService: VerificationEmailModalService, private _router: Router, + private _togglz: TogglzService, @Inject(WINDOW) private window: Window ) { _platform @@ -149,15 +152,22 @@ export class TopBarComponent implements OnInit, OnDestroy { ) } - // Load permission notifications panel once (only for logged-in private record views) - if ( - !this.isPublicRecord && - !this.recordWithIssues && - !this._permissionPanelLoadStarted - ) { - this._permissionPanelLoadStarted = true - this.loadUnreadPermissionNotifications() - } + // Load permission notifications panel once (only for logged-in private record views) and the togglz flag is active + this._togglz + .getStateOf(TogglzFlag.PERMISSION_NOTIFICATIONS) + .pipe(take(1)) + .subscribe((value) => { + if (value) { + if ( + !this.isPublicRecord && + !this.recordWithIssues && + !this._permissionPanelLoadStarted + ) { + this._permissionPanelLoadStarted = true + this.loadUnreadPermissionNotifications() + } + } + }) }) } @@ -202,7 +212,7 @@ export class TopBarComponent implements OnInit, OnDestroy { .subscribe((grouped) => { this.permissionPanelRaw = grouped this.permissionPanelNotifications = grouped.map((n) => { - const orgName = n?.source?.sourceName?.content || '' + const orgName = n?.sourceDescription || '' const escaped = orgName .replace(/&/g, '&') .replace(/LR - Unread permission notifications + Organizations want to connect src/app/record/components/top-bar/top-bar.component.ts 71 @@ -13955,7 +13955,7 @@ LR - You have updates waiting for your review. + Connecting with trusted organizations helps keep your ORCID record up-to-date. src/app/record/components/top-bar/top-bar.component.ts 72 diff --git a/src/locale/messages.rl.xlf b/src/locale/messages.rl.xlf index 785f027ad4..c2050b6f55 100644 --- a/src/locale/messages.rl.xlf +++ b/src/locale/messages.rl.xlf @@ -13947,7 +13947,7 @@ RL - Unread permission notifications + Organizations want to connect src/app/record/components/top-bar/top-bar.component.ts 71 @@ -13955,7 +13955,7 @@ RL - You have updates waiting for your review. + Connecting with trusted organizations helps keep your ORCID record up-to-date. src/app/record/components/top-bar/top-bar.component.ts 72 diff --git a/src/locale/messages.xlf b/src/locale/messages.xlf index 670d2344e1..4a5201f65b 100644 --- a/src/locale/messages.xlf +++ b/src/locale/messages.xlf @@ -12533,14 +12533,14 @@ - Unread permission notifications + Organizations want to connect src/app/record/components/top-bar/top-bar.component.ts 71 - You have updates waiting for your review. + Connecting with trusted organizations helps keep your ORCID record up-to-date. src/app/record/components/top-bar/top-bar.component.ts 72 diff --git a/src/locale/messages.xx.xlf b/src/locale/messages.xx.xlf index 7f1adc4a69..850b871f0d 100644 --- a/src/locale/messages.xx.xlf +++ b/src/locale/messages.xx.xlf @@ -13947,7 +13947,7 @@ X - Unread permission notifications + Organizations want to connect src/app/record/components/top-bar/top-bar.component.ts 71 @@ -13955,7 +13955,7 @@ X - You have updates waiting for your review. + Connecting with trusted organizations helps keep your ORCID record up-to-date. src/app/record/components/top-bar/top-bar.component.ts 72