mirror of https://github.com/vizality/vizality
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
660 lines
20 KiB
660 lines
20 KiB
/**
|
|
* The notifications API is meant for sending various types of notifications,
|
|
* including toasts and notices.
|
|
* @module Notifications
|
|
* @memberof API
|
|
* @namespace API.Notifications
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
/**
|
|
* Vizality notice object.
|
|
* @typedef VizalityNotice
|
|
* @property {string} [id] Notice ID
|
|
* @property {string} message Notice message
|
|
* @property {string} [color='blurple'] Notice background color
|
|
* @property {Function} [callback] Callback function triggered when the notice closes
|
|
* @property {Array<NoticeButton>} [buttons] Notice buttons
|
|
* @property {string} caller Addon ID of notice sender. This property is set automatically.
|
|
*/
|
|
|
|
/**
|
|
* Vizality notice button.
|
|
* @typedef NoticeButton
|
|
* @property {Function} [onClick=()=> vizality.api.notifications.closeNotice(noticeId)] Button click executor. Closes the notice by default.
|
|
* @property {string} text Button text
|
|
*/
|
|
|
|
/**
|
|
* Vizality toast object.
|
|
* @typedef VizalityToast
|
|
* @property {string} [id] Toast ID
|
|
* @property {boolean} [showCloseButton=true] Toast close button
|
|
* @property {boolean|string|ToastIcon} [icon='InfoFilled'] Toast icon / image
|
|
* @property {string|ReactElement} header Toast header
|
|
* @property {string|ReactElement} [content] Toast content
|
|
* @property {Array<ToastButton>} [buttons] Toast buttons
|
|
* @property {boolean|number} [timeout] Time in milliseconds until the toast auto-closes, or a boolean of whether it should auto-close. This overrides the default user setting on a per-toast basis.
|
|
* @property {number} [delay] Delay toast appearing (in milliseconds)
|
|
* @property {Function} [callback] Callback function triggered when the toast closes
|
|
* @property {string} caller Addon ID of toast sender. This property is set automatically.
|
|
*/
|
|
|
|
/**
|
|
* Vizality toast icon.
|
|
* @typedef ToastIcon
|
|
* @property {string} [name='InfoFilled'] Icon name
|
|
* @property {string} [size='40'] Icon size in pixels
|
|
* @property {URL} [url] Icon external URL
|
|
* @property {string} [color] Icon color
|
|
*/
|
|
|
|
/**
|
|
* Vizality toast button.
|
|
* @typedef ToastButton
|
|
* @property {string} [size='small'] Button size
|
|
* @property {string} [look='filled'] Button appearance
|
|
* @property {string} [color='white'] Button color
|
|
* @property {Function} [onClick=()=> vizality.api.notifications.closeToast(toastId)] Button click executor. Closes the toast by default.
|
|
* @property {string} text Button text
|
|
*/
|
|
|
|
import { assertString, isString, isUrl, toSnakeCase } from '@vizality/util/string';
|
|
import { assertObject, isObject } from '@vizality/util/object';
|
|
import { assertNumber, isNumber } from '@vizality/util/number';
|
|
import { assertArray } from '@vizality/util/array';
|
|
import { isComponent } from '@vizality/util/react';
|
|
import { toast as _toast } from 'react-toastify';
|
|
import { getCaller } from '@vizality/util/file';
|
|
import { getModule } from '@vizality/webpack';
|
|
import React, { isValidElement } from 'react';
|
|
import { Events } from '@vizality/constants';
|
|
import { Icon } from '@vizality/components';
|
|
import { API } from '@vizality/entities';
|
|
|
|
/**
|
|
* All currently active/pending notifications.
|
|
*/
|
|
const notifications = {
|
|
notices: [],
|
|
toasts: []
|
|
};
|
|
|
|
/**
|
|
* @extends API
|
|
* @extends Events
|
|
*/
|
|
export default class Notifications extends API {
|
|
/**
|
|
* Shuts down the API, removing all listeners and stored objects.
|
|
*/
|
|
stop () {
|
|
this.closeAllNotices();
|
|
this.closeAllToasts();
|
|
this.removeAllListeners();
|
|
delete vizality.api.notifications;
|
|
}
|
|
|
|
/**
|
|
* Sends a notice (in the form of a banner at the top of the client).
|
|
* @param {VizalityNotice} notice Notice to send
|
|
* @emits Notifications#Events.VIZALITY_NOTICE_SEND
|
|
*/
|
|
sendNotice (notice) {
|
|
try {
|
|
assertObject(notice);
|
|
assertString(notice.message);
|
|
notice.caller = getCaller();
|
|
if (notice.id) {
|
|
assertString(notice.id);
|
|
if (this.isNotice(notice.id)) {
|
|
throw new Error(`Notice "${notice.id}" is already active!`);
|
|
}
|
|
} else {
|
|
notice.id = `${toSnakeCase(notice.caller).toUpperCase()}_NOTICE_${this.getNoticesByCaller(notice.caller)?.length + 1 || '1'}`;
|
|
}
|
|
if (notice.callback && typeof notice.callback !== 'function') {
|
|
throw new TypeError('Notice "callback" property value must be a function!');
|
|
}
|
|
if (notice.color) {
|
|
assertString(notice.color);
|
|
}
|
|
if (notice.buttons) {
|
|
assertArray(notice.buttons);
|
|
notice.buttons.forEach(button => {
|
|
assertString(button.text);
|
|
if (button.onClick) {
|
|
if (typeof button.onClick !== 'function') {
|
|
throw new TypeError('Notice button "onClick" property value must be a function!');
|
|
}
|
|
} else {
|
|
button.onClick = () => this.closeNotice(notice.id);
|
|
}
|
|
});
|
|
}
|
|
notifications.notices.push(notice);
|
|
this.emit(Events.VIZALITY_NOTICE_SEND, notice.id);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('sendNotice'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a toast notification.
|
|
* @param {VizalityToast|React.Component|function(): React.Element} toast Toast to send
|
|
* @emits Notifications#Events.VIZALITY_TOAST_SEND
|
|
*/
|
|
sendToast (toast) {
|
|
try {
|
|
if (!isComponent(toast) && !isObject(toast)) {
|
|
throw new Error('Toast must be either a React component or an object!');
|
|
}
|
|
if (isComponent(toast) || isValidElement(toast)) {
|
|
const component = toast;
|
|
toast = {};
|
|
toast.custom = component;
|
|
}
|
|
assertObject(toast);
|
|
toast.caller = getCaller();
|
|
if (toast.id) {
|
|
assertString(toast.id);
|
|
if (this.isToast(toast.id)) {
|
|
throw new Error(`Toast "${toast.id}" already exists!`);
|
|
}
|
|
} else {
|
|
toast.id = `${toSnakeCase(toast.caller).toUpperCase()}_TOAST_${this.getToastsByCaller(toast.caller)?.length + 1 || '1'}`;
|
|
}
|
|
if (toast.callback && typeof toast.callback !== 'function') {
|
|
throw new TypeError('Toast "callback" property value must be a function!');
|
|
}
|
|
if (toast.hasOwnProperty('showCloseButton')) {
|
|
if (typeof toast.showCloseButton !== 'boolean') {
|
|
throw new TypeError('Toast "showCloseButton" property value must be a boolean!');
|
|
}
|
|
} else {
|
|
toast.showCloseButton = true;
|
|
}
|
|
if (toast.delay) {
|
|
assertNumber(toast.delay);
|
|
}
|
|
if (toast.icon) {
|
|
if (isString(toast.icon)) {
|
|
if (isUrl(toast.icon)) {
|
|
const url = toast.icon;
|
|
toast.icon = {};
|
|
toast.icon.url = url;
|
|
} else {
|
|
if (Icon.Names.includes(toast.icon)) {
|
|
const name = toast.icon;
|
|
toast.icon = {};
|
|
toast.icon.name = name;
|
|
} else {
|
|
throw new Error(`Icon "${toast.icon}" asset not found!`);
|
|
}
|
|
}
|
|
} else if (isObject(toast.icon)) {
|
|
/**
|
|
* Check if an icon name property is provided.
|
|
*/
|
|
if (toast.icon.name) {
|
|
/**
|
|
* Make sure the icon name is a string.
|
|
*/
|
|
if (!isString(toast.icon.name)) {
|
|
throw new Error(`Toast icon "name" property value must be a string!`);
|
|
}
|
|
/**
|
|
* Delete the icon URL if one is provided, because the name overrides it.
|
|
*/
|
|
delete toast.icon.url;
|
|
} else {
|
|
/**
|
|
* Check if an icon URL is provided.
|
|
*/
|
|
if (toast.icon.url) {
|
|
/**
|
|
* Make sure the icon URL is a valid URL.
|
|
*/
|
|
if (!isUrl(toast.icon.url)) {
|
|
throw new Error(`Toast icon "url" property value must be a URL!`);
|
|
}
|
|
/**
|
|
* Use the icon name default.
|
|
*/
|
|
} else {
|
|
toast.icon.name = 'InfoFilled';
|
|
}
|
|
}
|
|
if (toast.icon.size) {
|
|
assertString(toast.icon.size);
|
|
}
|
|
if (toast.icon.color && toast.icon.color !== 'currentColor') {
|
|
assertString(toast.icon.color);
|
|
}
|
|
} else {
|
|
throw new TypeError(`Toast "icon" property value must be a boolean, string, or object!`);
|
|
}
|
|
}
|
|
if (toast.buttons) {
|
|
assertArray(toast.buttons);
|
|
toast.buttons.forEach(button => {
|
|
if (button.onClick) {
|
|
if (typeof button.onClick !== 'function') {
|
|
throw new TypeError('Toast button "onClick" property value must be a function!');
|
|
}
|
|
} else {
|
|
button.onClick = () => this.closeToast(toast.id);
|
|
}
|
|
if (!button.text || !isString(button.text)) {
|
|
throw new TypeError('Toast button "text" property value must be a string!');
|
|
}
|
|
/**
|
|
* Checks types and set defaults for button properties.
|
|
*/
|
|
if (button.size) {
|
|
assertString(button.size);
|
|
} else {
|
|
button.size = 'small';
|
|
}
|
|
if (button.look) {
|
|
assertString(button.look);
|
|
} else {
|
|
button.look = 'filled';
|
|
}
|
|
if (button.color) {
|
|
assertString(button.color);
|
|
} else {
|
|
button.color = 'white';
|
|
};
|
|
});
|
|
}
|
|
if (toast.hasOwnProperty('timeout') && (!isNumber(toast.timeout) && typeof toast.timeout !== 'boolean')) {
|
|
throw new TypeError('Toast "timeout" property value must be a number or boolean!');
|
|
}
|
|
assertString(toast.id);
|
|
notifications.toasts.push(toast);
|
|
this.emit(Events.VIZALITY_TOAST_SEND, toast.id);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('sendToast'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a notice is active.
|
|
* @param {string} noticeId Notice ID
|
|
* @returns {boolean} Whether a notice with a given ID is active
|
|
*/
|
|
isNotice (noticeId) {
|
|
try {
|
|
assertString(noticeId);
|
|
return notifications.notices.some(notice => notice.id === noticeId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('isNotice'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a toast is currently active.
|
|
* @param {string} toastId Toast ID
|
|
* @returns {boolean} Whether a toast with a given ID is active
|
|
*/
|
|
isToastActive (toastId) {
|
|
try {
|
|
assertString(toastId);
|
|
return Boolean(_toast.isActive(toastId));
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('isToast'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a toast is in queue to be sent.
|
|
* @param {string} toastId Toast ID
|
|
* @returns {boolean} Whether a toast with a given ID is queued
|
|
*/
|
|
isToastQueued (toastId) {
|
|
try {
|
|
assertString(toastId);
|
|
return Boolean(this.isToast(toastId) && !this.isToastActive(toastId));
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('isToast'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a toast is currently active or is in queue to be sent.
|
|
* @param {string} toastId Toast ID
|
|
* @returns {boolean} Whether a toast with a given ID is active or queued
|
|
*/
|
|
isToast (toastId) {
|
|
try {
|
|
assertString(toastId);
|
|
return notifications.toasts.some(toast => toast.id === toastId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('isToast'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the first notice found matching a given filter.
|
|
* @param {Function} filter Function to use to filter notices by
|
|
* @returns {object|null} Notice matching a given filter
|
|
*/
|
|
getNotice (filter) {
|
|
try {
|
|
if (!filter?.length) return null;
|
|
return notifications.notices.find(filter);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getNotice'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the first toast found matching a given filter.
|
|
* @param {Function} filter Function to use to filter toasts by
|
|
* @returns {object|null} Toast matching a given filter
|
|
*/
|
|
getToast (filter) {
|
|
try {
|
|
if (!filter?.length) return null;
|
|
return notifications.toasts.find(filter);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getToast'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a notice matching a given ID.
|
|
* @param {string} noticeId Notice ID
|
|
* @returns {object|null} Notice matching a given ID
|
|
*/
|
|
getNoticeById (noticeId) {
|
|
try {
|
|
assertString(noticeId);
|
|
return notifications.notices.find(notice => notice.id === noticeId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getNoticeById'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a toast matching a given ID.
|
|
* @param {string} toastId Toast ID
|
|
* @returns {object|null} Toast matching a given ID
|
|
*/
|
|
getToastById (toastId) {
|
|
try {
|
|
assertString(toastId);
|
|
return notifications.toasts.find(toast => toast.id === toastId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getToastById'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all notices found matching a given filter.
|
|
* @param {Function} filter Function to use to filter notices by
|
|
* @returns {Array<object|null>} Notices matching a given filter
|
|
*/
|
|
getNotices (filter) {
|
|
try {
|
|
if (!filter?.length) return null;
|
|
return notifications.notices.filter(filter);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getNotices'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all toasts found matching a given filter.
|
|
* @param {Function} filter Function to use to filter toasts by
|
|
* @returns {Array<object|null>} Toasts matching a given filter
|
|
*/
|
|
getToasts (filter) {
|
|
try {
|
|
if (!filter?.length) return null;
|
|
return notifications.toasts.filter(filter);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getToasts'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all notices matching a given caller.
|
|
* @param {string} addonId Addon ID
|
|
* @returns {Array<object|null>} Notices matching a given caller
|
|
*/
|
|
getNoticesByCaller (addonId) {
|
|
try {
|
|
assertString(addonId);
|
|
return notifications.notices.filter(notice => notice.caller === addonId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getNoticesByCaller'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all toasts matching a given caller.
|
|
* @param {string} addonId Addon ID
|
|
* @returns {Array<object|null>} Toasts matching a given caller
|
|
*/
|
|
getToastsByCaller (addonId) {
|
|
try {
|
|
assertString(addonId);
|
|
return notifications.toasts.filter(toast => toast.caller === addonId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getToastsByCaller'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all notices.
|
|
* @returns {Array<object|null>} All notices
|
|
*/
|
|
getAllNotices () {
|
|
try {
|
|
return notifications.notices;
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getAllNotices'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all toasts.
|
|
* @returns {Array<object|null>} All toasts
|
|
*/
|
|
getAllToasts () {
|
|
try {
|
|
return notifications.toasts;
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getAllToasts'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all active toasts.
|
|
* @returns {Array<object|null>} All active toasts
|
|
*/
|
|
getAllActiveToasts () {
|
|
try {
|
|
const activeToasts = [];
|
|
notifications.toasts.forEach(toast => {
|
|
if (this.isToastActive(toast.id)) {
|
|
activeToasts.push(toast);
|
|
}
|
|
});
|
|
return activeToasts;
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getAllActiveToasts'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all queued toasts.
|
|
* @returns {Array<object|null>} All queued toasts
|
|
*/
|
|
getAllQueuedToasts () {
|
|
try {
|
|
const queuedToasts = [];
|
|
notifications.toasts.forEach(toast => {
|
|
if (this.isToastQueued(toast.id)) {
|
|
queuedToasts.push(toast);
|
|
}
|
|
});
|
|
return queuedToasts;
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getAllQueuedToasts'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes a notice.
|
|
* @param {string} noticeId Notice ID
|
|
* @emits Notifications#Events.VIZALITY_NOTICE_CLOSE
|
|
*/
|
|
closeNotice (noticeId) {
|
|
try {
|
|
assertString(noticeId);
|
|
const notice = this.getNoticeById(noticeId);
|
|
notifications.notices = this.getNotices(notice => notice.id !== noticeId);
|
|
if (typeof notice?.callback === 'function') {
|
|
try {
|
|
notice.callback();
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeNotice'), `There was a problem invoking notice "${noticeId}" callback!`, err);
|
|
}
|
|
}
|
|
this.emit(Events.VIZALITY_NOTICE_CLOSE, noticeId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeNotice'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes a toast.
|
|
* @param {string} toastId Toast ID
|
|
* @emits Notifications#Events.VIZALITY_TOAST_CLOSE
|
|
*/
|
|
closeToast (toastId) {
|
|
try {
|
|
assertString(toastId);
|
|
const toast = this.getToastById(toastId);
|
|
if (!toast) return;
|
|
notifications.toasts = this.getToasts(toast => toast.id !== toastId);
|
|
_toast.dismiss(toastId);
|
|
if (typeof toast?.callback === 'function') {
|
|
try {
|
|
toast.callback();
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeToast'), `There was a problem invoking toast "${toastId}" callback!`, err);
|
|
}
|
|
}
|
|
this.emit(Events.VIZALITY_TOAST_CLOSE, toastId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeToast'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes all notices sent by a given caller.
|
|
* @param {string} addonId Addon ID
|
|
* @emits Notifications#Events.VIZALITY_NOTICE_CLOSE_ALL_BY_CALLER
|
|
*/
|
|
closeNoticesByCaller (addonId) {
|
|
try {
|
|
assertString(addonId);
|
|
notifications.notices
|
|
.filter(notice => notice.caller === addonId)
|
|
.forEach(notice => this.closeNotice(notice.id));
|
|
this.emit(Events.VIZALITY_NOTICE_REMOVE_ALL_BY_CALLER, addonId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeNoticesByCaller'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes all toasts sent by a given caller.
|
|
* @param {string} addonId Addon ID
|
|
* @emits Notifications#Events.VIZALITY_TOAST_CLOSE_ALL_BY_CALLER
|
|
*/
|
|
closeToastsByCaller (addonId) {
|
|
try {
|
|
assertString(addonId);
|
|
notifications.toasts
|
|
.filter(toast => toast.caller === addonId)
|
|
.forEach(toast => this.closeToast(toast.id));
|
|
this.emit(Events.VIZALITY_TOAST_REMOVE_ALL_BY_CALLER, addonId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeToastsByCaller'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes all active notices.
|
|
* @emits Notifications#Events.VIZALITY_NOTICE_CLOSE_ALL
|
|
*/
|
|
closeAllNotices () {
|
|
try {
|
|
notifications.notices.forEach(notice => this.closeNotice(notice.id));
|
|
// Dismiss all notices sent by Discord as well
|
|
getModule(m => m.default?.dismiss)?.default?.dismiss();
|
|
this.emit(Events.VIZALITY_NOTICE_CLOSE_ALL);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeAllNotices'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes all active (currently visible) toasts. If no toast limit is set, this will be functionally
|
|
* equivalent to the `closeAllToasts` method.
|
|
* @emits Notifications#Events.VIZALITY_TOAST_CLOSE_ALL_ACTIVE
|
|
*/
|
|
closeAllActiveToasts () {
|
|
try {
|
|
notifications.toasts.forEach(toast => {
|
|
if (this.isToastActive(toast.id)) {
|
|
notifications.toasts = this.getToasts(t => t.id !== toast.id);
|
|
}
|
|
});
|
|
_toast.dismiss();
|
|
this.emit(Events.VIZALITY_TOAST_CLOSE_ALL_ACTIVE);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeAllActiveToasts'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes all queued toasts. If no toast limit is set, this method will simply emit
|
|
* an event.
|
|
* @emits Notifications#Events.VIZALITY_TOAST_CLOSE_ALL_QUEUED
|
|
*/
|
|
closeAllQueuedToasts () {
|
|
try {
|
|
notifications.toasts.forEach(toast => {
|
|
if (this.isToastQueued(toast.id)) {
|
|
notifications.toasts = this.getToasts(t => t.id !== toast.id);
|
|
}
|
|
});
|
|
_toast.clearWaitingQueue();
|
|
this.emit(Events.VIZALITY_TOAST_CLOSE_ALL_QUEUED);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeAllQueuedToasts'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes all active toasts.
|
|
* @emits Notifications#Events.VIZALITY_TOAST_CLOSE_ALL
|
|
*/
|
|
closeAllToasts () {
|
|
try {
|
|
_toast.clearWaitingQueue();
|
|
_toast.dismiss();
|
|
notifications.toasts = [];
|
|
this.emit(Events.VIZALITY_TOAST_CLOSE_ALL);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('closeAllToasts'), err);
|
|
}
|
|
}
|
|
}
|