Группировка однотипных уведомлений

При активной работе интерфейса — частых запросах к серверу, массовых действиях пользователя или потоковых событиях — возникает ситуация, когда однотипные уведомления начинают накапливаться. Это приводит к визуальному шуму, снижению читаемости интерфейса и ухудшению пользовательского опыта.

Типичные примеры:

  • множественные уведомления об успешном сохранении
  • повторяющиеся ошибки соединения
  • уведомления о добавлении элементов в список

Группировка уведомлений решает эту проблему, объединяя однотипные сообщения в одно, с обновлением содержимого или счетчика.


Базовый подход к группировке

Библиотека Toastify не содержит встроенного механизма группировки, поэтому реализация строится на уровне логики приложения.

Основная идея:

  • отслеживать уже показанные уведомления
  • предотвращать создание новых, если аналогичное уже существует
  • обновлять содержимое существующего уведомления

Простейший способ — использовать объект или Map для хранения активных уведомлений.

const activeToasts = new Map();

Уникальные ключи уведомлений

Для группировки необходимо определить, какие уведомления считаются однотипными. Это достигается через уникальный ключ.

Примеры ключей:

  • "save-success"
  • "network-error"
  • "item-added"

Ключ формируется на основе типа события и, при необходимости, дополнительных параметров.

function getToastKey(type, context) {
    return `${type}-${context || ''}`;
}

Создание или обновление уведомления

Логика:

  1. Проверить наличие уведомления по ключу
  2. Если нет — создать новое
  3. Если есть — обновить содержимое
function showGroupedToast(key, message) {
    if (activeToasts.has(key)) {
        const toast = activeToasts.get(key);
        toast.element.textContent = message;
    } else {
        const toast = Toastify({
            text: message,
            duration: 3000,
            close: true,
            gravity: "top",
            position: "right",
            stopOnFocus: true,
            callback: function() {
                activeToasts.delete(key);
            }
        });

        toast.showToast();
        activeToasts.set(key, toast);
    }
}

Счетчик повторений

Более продвинутый вариант — отображение количества повторений.

Хранение состояния:

const toastCounters = new Map();

Логика обновления:

function showCountingToast(key, baseMessage) {
    let count = toastCounters.get(key) || 0;
    count++;

    toastCounters.set(key, count);

    const message = `${baseMessage} (${count})`;

    if (activeToasts.has(key)) {
        const toast = activeToasts.get(key);
        toast.element.textContent = message;
    } else {
        const toast = Toastify({
            text: message,
            duration: 3000,
            close: true,
            gravity: "top",
            position: "right",
            callback: function() {
                activeToasts.delete(key);
                toastCounters.delete(key);
            }
        });

        toast.showToast();
        activeToasts.set(key, toast);
    }
}

Продление времени жизни уведомления

При повторных событиях важно не только обновлять текст, но и продлевать отображение уведомления.

Проблема: Toastify не предоставляет встроенного метода “reset таймера”.

Решение:

  • удалять текущее уведомление
  • создавать новое с обновленным временем
function refreshToast(key, message) {
    if (activeToasts.has(key)) {
        const oldToast = activeToasts.get(key);
        oldToast.hideToast();
        activeToasts.delete(key);
    }

    const toast = Toastify({
        text: message,
        duration: 3000,
        gravity: "top",
        position: "right",
        callback: function() {
            activeToasts.delete(key);
        }
    });

    toast.showToast();
    activeToasts.set(key, toast);
}

Группировка по типам событий

Часто удобно группировать уведомления по категориям:

  • успех (success)
  • ошибка (error)
  • предупреждение (warning)
  • информация (info)
function showTypedToast(type, message) {
    const key = `toast-${type}`;

    showCountingToast(key, message);
}

Это позволяет:

  • объединять все ошибки в одно уведомление
  • отдельно отслеживать успешные операции

Группировка с разными уровнями детализации

Иногда требуется более гибкая логика:

Полная группировка

Все уведомления одного типа объединяются:

key = "error";

Частичная группировка

Учитывается контекст:

key = `error-${endpoint}`;

Без группировки

Каждое уведомление уникально:

key = Date.now();

Ограничение количества уведомлений

Дополнительно к группировке можно ограничить максимальное число уведомлений:

const MAX_TOASTS = 3;

function limitToasts() {
    if (activeToasts.size >= MAX_TOASTS) {
        const firstKey = activeToasts.keys().next().value;
        activeToasts.get(firstKey).hideToast();
        activeToasts.delete(firstKey);
    }
}

Использование:

limitToasts();
showGroupedToast("info", "Новое сообщение");

Очередь уведомлений

Альтернативный подход — не обновлять, а выстраивать очередь:

const queue = [];
let isShowing = false;

function processQueue() {
    if (queue.length === 0) {
        isShowing = false;
        return;
    }

    isShowing = true;
    const message = queue.shift();

    Toastify({
        text: message,
        duration: 2000,
        callback: processQueue
    }).showToast();
}

function enqueueToast(message) {
    queue.push(message);
    if (!isShowing) {
        processQueue();
    }
}

Комбинированный подход

На практике часто используется сочетание методов:

  • группировка одинаковых событий
  • счетчик повторений
  • ограничение количества
  • очередь для разных типов

Пример:

function smartToast(type, message) {
    const key = `toast-${type}`;

    limitToasts();
    showCountingToast(key, message);
}

Визуальное различие сгруппированных уведомлений

Для повышения читаемости:

  • добавление счетчика
  • изменение цвета при увеличении количества
  • выделение жирным
const message = `<strong>${baseMessage}</strong> (${count})`;

Toastify({
    text: message,
    escapeMarkup: false
}).showToast();

Очистка состояния

Важно удалять данные о уведомлениях после их исчезновения:

callback: function() {
    activeToasts.delete(key);
    toastCounters.delete(key);
}

Без этого:

  • накапливаются “мертвые” ссылки
  • нарушается логика повторного отображения

Особенности и ограничения

  • Toastify не поддерживает обновление DOM напрямую через API — требуется доступ к toast.element
  • нет встроенной системы идентификаторов уведомлений
  • управление жизненным циклом полностью лежит на разработчике

Практические сценарии применения

1. Повторные ошибки сети

  • одно уведомление
  • увеличивающийся счетчик

2. Массовые действия (например, загрузка файлов)

  • уведомление “Загружено файлов: N”

3. Системные уведомления

  • группировка по уровню важности

4. Логи в интерфейсе

  • компактное отображение вместо потока сообщений

Расширение функциональности

Возможные улучшения:

  • debounce перед показом уведомления
  • объединение сообщений в список внутри одного toast
  • добавление кнопки “развернуть”
  • хранение истории уведомлений

Пример debounce:

let timeout;

function debouncedToast(key, message) {
    clearTimeout(timeout);

    timeout = setTimeout(() => {
        showGroupedToast(key, message);
    }, 300);
}

Архитектурный подход

Для крупных приложений группировка выносится в отдельный модуль:

class ToastManager {
    constructor() {
        this.active = new Map();
        this.counters = new Map();
    }

    show(key, message) {
        let count = (this.counters.get(key) || 0) + 1;
        this.counters.set(key, count);

        const text = `${message} (${count})`;

        if (this.active.has(key)) {
            this.active.get(key).element.textContent = text;
        } else {
            const toast = Toastify({
                text,
                duration: 3000,
                callback: () => {
                    this.active.delete(key);
                    this.counters.delete(key);
                }
            });

            toast.showToast();
            this.active.set(key, toast);
        }
    }
}

Такой подход:

  • централизует управление
  • упрощает поддержку
  • делает поведение предсказуемым