1/* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {Inject, Injectable, NgZone} from '@angular/core'; 18import {MatSnackBar} from '@angular/material/snack-bar'; 19import {assertDefined} from 'common/assert_utils'; 20import {NotificationType, UserNotification} from 'messaging/user_notification'; 21import {SnackBarComponent} from './snack_bar_component'; 22 23type Messages = string[]; 24 25@Injectable({providedIn: 'root'}) 26export class SnackBarOpener { 27 private static CROP_THRESHOLD = 5; 28 private isOpen = false; 29 private queue: Messages[] = []; 30 31 constructor( 32 @Inject(NgZone) private ngZone: NgZone, 33 @Inject(MatSnackBar) private snackBar: MatSnackBar, 34 ) {} 35 36 onNotifications(notifications: UserNotification[]) { 37 const messages = this.convertNotificationsToMessages(notifications); 38 39 if (messages.length === 0) { 40 return; 41 } 42 43 if (this.isOpen) { 44 this.queue.push(messages); 45 return; 46 } 47 48 this.displayMessages(messages); 49 } 50 51 private convertNotificationsToMessages( 52 notifications: UserNotification[], 53 ): Messages { 54 const messages: Messages = []; 55 56 const warnings = notifications.filter( 57 (n) => n.getNotificationType() === NotificationType.WARNING, 58 ); 59 const groups = this.groupNotificationsByDescriptor(warnings); 60 61 for (const groupedWarnings of groups) { 62 const countUsed = Math.min( 63 groupedWarnings.length, 64 SnackBarOpener.CROP_THRESHOLD, 65 ); 66 const countCropped = groupedWarnings.length - countUsed; 67 68 groupedWarnings.slice(0, countUsed).forEach((warning) => { 69 messages.push(warning.getMessage()); 70 }); 71 72 if (countCropped > 0) { 73 messages.push( 74 `... (cropped ${countCropped} '${groupedWarnings[0].getDescriptor()}' message${ 75 countCropped > 1 ? 's' : '' 76 })`, 77 ); 78 } 79 } 80 81 return messages; 82 } 83 84 private groupNotificationsByDescriptor( 85 warnings: UserNotification[], 86 ): Set<UserNotification[]> { 87 const groups = new Map<string, UserNotification[]>(); 88 89 warnings.forEach((warning) => { 90 if (groups.get(warning.getDescriptor()) === undefined) { 91 groups.set(warning.getDescriptor(), []); 92 } 93 assertDefined(groups.get(warning.getDescriptor())).push(warning); 94 }); 95 96 return new Set(groups.values()); 97 } 98 99 private displayMessages(messages: Messages) { 100 this.ngZone.run(() => { 101 // The snackbar needs to be opened within ngZone, 102 // otherwise it will first display on the left and then will jump to the center 103 this.isOpen = true; 104 const ref = this.snackBar.openFromComponent(SnackBarComponent, { 105 data: messages, 106 duration: 5000 * messages.length, 107 }); 108 ref.afterDismissed().subscribe(() => { 109 this.isOpen = false; 110 const next = this.queue.shift(); 111 if (next !== undefined) { 112 this.displayMessages(next); 113 } 114 }); 115 }); 116 } 117} 118