mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-15 10:12:07 -04:00
notifications: cleanup popup display logic
This commit is contained in:
@@ -16,6 +16,7 @@ PanelWindow {
|
|||||||
required property var notificationData
|
required property var notificationData
|
||||||
required property string notificationId
|
required property string notificationId
|
||||||
readonly property bool hasValidData: notificationData && notificationData.notification
|
readonly property bool hasValidData: notificationData && notificationData.notification
|
||||||
|
readonly property alias hovered: cardHoverHandler.hovered
|
||||||
property int screenY: 0
|
property int screenY: 0
|
||||||
property bool exiting: false
|
property bool exiting: false
|
||||||
property bool _isDestroying: false
|
property bool _isDestroying: false
|
||||||
@@ -48,7 +49,7 @@ PanelWindow {
|
|||||||
signal entered
|
signal entered
|
||||||
signal exitStarted
|
signal exitStarted
|
||||||
signal exitFinished
|
signal exitFinished
|
||||||
signal popupHeightChanged()
|
signal popupHeightChanged
|
||||||
|
|
||||||
function startExit() {
|
function startExit() {
|
||||||
if (exiting || _isDestroying) {
|
if (exiting || _isDestroying) {
|
||||||
@@ -428,14 +429,14 @@ PanelWindow {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: {
|
anchors.topMargin: {
|
||||||
if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) {
|
if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) {
|
||||||
const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2;
|
const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2;
|
||||||
return Math.max(0, headerSummary / 2 - popupIconSize / 2);
|
return Math.max(0, headerSummary / 2 - popupIconSize / 2);
|
||||||
|
}
|
||||||
|
if (descriptionExpanded)
|
||||||
|
return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2);
|
||||||
|
return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2);
|
||||||
}
|
}
|
||||||
if (descriptionExpanded)
|
|
||||||
return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2);
|
|
||||||
return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
imageSource: {
|
imageSource: {
|
||||||
if (!notificationData)
|
if (!notificationData)
|
||||||
|
|||||||
@@ -15,20 +15,14 @@ QtObject {
|
|||||||
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
|
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
|
||||||
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||||
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
|
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
|
||||||
property int maxTargetNotifications: 4
|
property var popupWindows: []
|
||||||
property var popupWindows: [] // strong refs to windows (live until exitFinished)
|
|
||||||
property var destroyingWindows: new Set()
|
property var destroyingWindows: new Set()
|
||||||
property var pendingDestroys: []
|
property var pendingDestroys: []
|
||||||
property int destroyDelayMs: 100
|
property int destroyDelayMs: 100
|
||||||
property var pendingCreates: []
|
|
||||||
property int createDelayMs: 50
|
|
||||||
property bool createBusy: false
|
|
||||||
property Component popupComponent
|
property Component popupComponent
|
||||||
|
|
||||||
popupComponent: Component {
|
popupComponent: Component {
|
||||||
NotificationPopup {
|
NotificationPopup {
|
||||||
onEntered: manager._onPopupEntered(this)
|
|
||||||
onExitStarted: manager._onPopupExitStarted(this)
|
|
||||||
onExitFinished: manager._onPopupExitFinished(this)
|
onExitFinished: manager._onPopupExitFinished(this)
|
||||||
onPopupHeightChanged: manager._onPopupHeightChanged(this)
|
onPopupHeightChanged: manager._onPopupHeightChanged(this)
|
||||||
}
|
}
|
||||||
@@ -74,36 +68,6 @@ QtObject {
|
|||||||
destroyTimer.restart();
|
destroyTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
property Timer createTimer: Timer {
|
|
||||||
interval: createDelayMs
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
onTriggered: manager._processCreateQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _processCreateQueue() {
|
|
||||||
createBusy = false;
|
|
||||||
if (pendingCreates.length === 0)
|
|
||||||
return;
|
|
||||||
const wrapper = pendingCreates.shift();
|
|
||||||
if (wrapper)
|
|
||||||
_doInsertNewestAtTop(wrapper);
|
|
||||||
if (pendingCreates.length > 0) {
|
|
||||||
createBusy = true;
|
|
||||||
createTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _scheduleCreate(wrapper) {
|
|
||||||
if (!wrapper)
|
|
||||||
return;
|
|
||||||
pendingCreates.push(wrapper);
|
|
||||||
if (!createBusy) {
|
|
||||||
createBusy = true;
|
|
||||||
createTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sweeper: Timer {
|
sweeper: Timer {
|
||||||
interval: 500
|
interval: 500
|
||||||
running: false
|
running: false
|
||||||
@@ -129,11 +93,10 @@ QtObject {
|
|||||||
}
|
}
|
||||||
if (toRemove.length) {
|
if (toRemove.length) {
|
||||||
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1);
|
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1);
|
||||||
_repositionAllActivePopups();
|
_repositionAll();
|
||||||
}
|
}
|
||||||
if (popupWindows.length === 0) {
|
if (popupWindows.length === 0)
|
||||||
sweeper.stop();
|
sweeper.stop();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,98 +108,29 @@ QtObject {
|
|||||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _canMakeRoomFor(wrapper) {
|
|
||||||
const activeWindows = _active();
|
|
||||||
if (activeWindows.length < maxTargetNotifications) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!wrapper || !wrapper.notification) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const incomingUrgency = wrapper.urgency || 0;
|
|
||||||
for (const p of activeWindows) {
|
|
||||||
if (!p.notificationData || !p.notificationData.notification) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const existingUrgency = p.notificationData.urgency || 0;
|
|
||||||
if (existingUrgency < incomingUrgency) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (existingUrgency === incomingUrgency) {
|
|
||||||
const timer = p.notificationData.timer;
|
|
||||||
if (timer && !timer.running) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _makeRoomForNew(wrapper) {
|
|
||||||
const activeWindows = _active();
|
|
||||||
if (activeWindows.length < maxTargetNotifications) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const toRemove = _selectPopupToRemove(activeWindows, wrapper);
|
|
||||||
if (toRemove && !toRemove.exiting) {
|
|
||||||
toRemove.notificationData.removedByLimit = true;
|
|
||||||
toRemove.notificationData.popup = false;
|
|
||||||
if (toRemove.notificationData.timer) {
|
|
||||||
toRemove.notificationData.timer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _selectPopupToRemove(activeWindows, incomingWrapper) {
|
|
||||||
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
|
||||||
const aUrgency = (a.notificationData) ? a.notificationData.urgency || 0 : 0;
|
|
||||||
const bUrgency = (b.notificationData) ? b.notificationData.urgency || 0 : 0;
|
|
||||||
if (aUrgency !== bUrgency) {
|
|
||||||
return aUrgency - bUrgency;
|
|
||||||
}
|
|
||||||
const aTimer = a.notificationData && a.notificationData.timer;
|
|
||||||
const bTimer = b.notificationData && b.notificationData.timer;
|
|
||||||
const aRunning = aTimer && aTimer.running;
|
|
||||||
const bRunning = bTimer && bTimer.running;
|
|
||||||
if (aRunning !== bRunning) {
|
|
||||||
return aRunning ? 1 : -1;
|
|
||||||
}
|
|
||||||
return b.screenY - a.screenY;
|
|
||||||
});
|
|
||||||
return sortedWindows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _sync(newWrappers) {
|
function _sync(newWrappers) {
|
||||||
for (const w of newWrappers) {
|
|
||||||
if (w && !_hasWindowFor(w)) {
|
|
||||||
insertNewestAtTop(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const p of popupWindows.slice()) {
|
for (const p of popupWindows.slice()) {
|
||||||
if (!_isValidWindow(p)) {
|
if (!_isValidWindow(p) || p.exiting)
|
||||||
continue;
|
continue;
|
||||||
}
|
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
|
||||||
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
|
|
||||||
p.notificationData.removedByLimit = true;
|
p.notificationData.removedByLimit = true;
|
||||||
p.notificationData.popup = false;
|
p.notificationData.popup = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (const w of newWrappers) {
|
||||||
|
if (w && !_hasWindowFor(w))
|
||||||
function insertNewestAtTop(wrapper) {
|
_insertAtTop(w);
|
||||||
if (!wrapper)
|
|
||||||
return;
|
|
||||||
if (createBusy || pendingCreates.length > 0) {
|
|
||||||
_scheduleCreate(wrapper);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
_doInsertNewestAtTop(wrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _doInsertNewestAtTop(wrapper) {
|
function _popupHeight(p) {
|
||||||
|
return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _insertAtTop(wrapper) {
|
||||||
if (!wrapper)
|
if (!wrapper)
|
||||||
return;
|
return;
|
||||||
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : "";
|
const notificationId = wrapper?.notification ? wrapper.notification.id : "";
|
||||||
const win = popupComponent.createObject(null, {
|
const win = popupComponent.createObject(null, {
|
||||||
"notificationData": wrapper,
|
"notificationData": wrapper,
|
||||||
"notificationId": notificationId,
|
"notificationId": notificationId,
|
||||||
@@ -250,85 +144,69 @@ QtObject {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
popupWindows.unshift(win);
|
popupWindows.unshift(win);
|
||||||
|
_repositionAll();
|
||||||
_repositionAllActivePopups();
|
|
||||||
|
|
||||||
createBusy = true;
|
|
||||||
createTimer.restart();
|
|
||||||
if (!sweeper.running)
|
if (!sweeper.running)
|
||||||
sweeper.start();
|
sweeper.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _active() {
|
function _repositionAll() {
|
||||||
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting);
|
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
|
||||||
}
|
|
||||||
|
|
||||||
function _bottom() {
|
const pinnedSlots = [];
|
||||||
let b = null;
|
for (const p of active) {
|
||||||
let maxY = -1;
|
if (!p.hovered)
|
||||||
for (const p of _active()) {
|
continue;
|
||||||
if (p.screenY > maxY) {
|
pinnedSlots.push({
|
||||||
maxY = p.screenY;
|
y: p.screenY,
|
||||||
b = p;
|
end: p.screenY + _popupHeight(p)
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
return b;
|
pinnedSlots.sort((a, b) => a.y - b.y);
|
||||||
}
|
|
||||||
|
|
||||||
function _onPopupEntered(p) {
|
let currentY = topMargin;
|
||||||
|
for (const win of active) {
|
||||||
|
if (win.hovered)
|
||||||
|
continue;
|
||||||
|
for (const slot of pinnedSlots) {
|
||||||
|
if (currentY >= slot.y - 1 && currentY < slot.end)
|
||||||
|
currentY = slot.end;
|
||||||
|
}
|
||||||
|
win.screenY = currentY;
|
||||||
|
currentY += _popupHeight(win);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onPopupHeightChanged(p) {
|
function _onPopupHeightChanged(p) {
|
||||||
if (!p || p.exiting || p._isDestroying)
|
if (!p || p.exiting || p._isDestroying)
|
||||||
return;
|
return;
|
||||||
_repositionAllActivePopups();
|
if (popupWindows.indexOf(p) === -1)
|
||||||
}
|
|
||||||
|
|
||||||
function _repositionAllActivePopups() {
|
|
||||||
const activeWindows = _active().sort((a, b) => a.screenY - b.screenY);
|
|
||||||
let currentY = topMargin;
|
|
||||||
for (const win of activeWindows) {
|
|
||||||
win.screenY = currentY;
|
|
||||||
const popupHeight = win.alignedHeight || win.implicitHeight;
|
|
||||||
currentY += popupHeight + popupSpacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onPopupExitStarted(p) {
|
|
||||||
if (!p)
|
|
||||||
return;
|
return;
|
||||||
_repositionAllActivePopups();
|
_repositionAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onPopupExitFinished(p) {
|
function _onPopupExitFinished(p) {
|
||||||
if (!p) {
|
if (!p)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const windowId = p.toString();
|
const windowId = p.toString();
|
||||||
if (destroyingWindows.has(windowId)) {
|
if (destroyingWindows.has(windowId))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
destroyingWindows.add(windowId);
|
destroyingWindows.add(windowId);
|
||||||
const i = popupWindows.indexOf(p);
|
const i = popupWindows.indexOf(p);
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
popupWindows.splice(i, 1);
|
popupWindows.splice(i, 1);
|
||||||
popupWindows = popupWindows.slice();
|
popupWindows = popupWindows.slice();
|
||||||
}
|
}
|
||||||
if (NotificationService.releaseWrapper && p.notificationData) {
|
if (NotificationService.releaseWrapper && p.notificationData)
|
||||||
NotificationService.releaseWrapper(p.notificationData);
|
NotificationService.releaseWrapper(p.notificationData);
|
||||||
}
|
|
||||||
_scheduleDestroy(p);
|
_scheduleDestroy(p);
|
||||||
Qt.callLater(() => destroyingWindows.delete(windowId));
|
Qt.callLater(() => destroyingWindows.delete(windowId));
|
||||||
_repositionAllActivePopups();
|
_repositionAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupAllWindows() {
|
function cleanupAllWindows() {
|
||||||
sweeper.stop();
|
sweeper.stop();
|
||||||
destroyTimer.stop();
|
destroyTimer.stop();
|
||||||
createTimer.stop();
|
|
||||||
pendingDestroys = [];
|
pendingDestroys = [];
|
||||||
pendingCreates = [];
|
|
||||||
createBusy = false;
|
|
||||||
for (const p of popupWindows.slice()) {
|
for (const p of popupWindows.slice()) {
|
||||||
if (p) {
|
if (p) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Singleton {
|
|||||||
|
|
||||||
property list<NotifWrapper> notificationQueue: []
|
property list<NotifWrapper> notificationQueue: []
|
||||||
property list<NotifWrapper> visibleNotifications: []
|
property list<NotifWrapper> visibleNotifications: []
|
||||||
property int maxVisibleNotifications: 3
|
property int maxVisibleNotifications: 4
|
||||||
property bool addGateBusy: false
|
property bool addGateBusy: false
|
||||||
property int enterAnimMs: 400
|
property int enterAnimMs: 400
|
||||||
property int seqCounter: 0
|
property int seqCounter: 0
|
||||||
@@ -253,7 +253,9 @@ Singleton {
|
|||||||
return timeStr;
|
return timeStr;
|
||||||
try {
|
try {
|
||||||
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
||||||
const weekday = date.toLocaleDateString(localeName, { weekday: "long" });
|
const weekday = date.toLocaleDateString(localeName, {
|
||||||
|
weekday: "long"
|
||||||
|
});
|
||||||
return weekday + ", " + timeStr;
|
return weekday + ", " + timeStr;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return timeStr;
|
return timeStr;
|
||||||
@@ -488,7 +490,7 @@ Singleton {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: addGate
|
id: addGate
|
||||||
interval: enterAnimMs + 50
|
interval: 80
|
||||||
running: false
|
running: false
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
@@ -694,7 +696,9 @@ Singleton {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
||||||
const weekday = time.toLocaleDateString(localeName, { weekday: "long" });
|
const weekday = time.toLocaleDateString(localeName, {
|
||||||
|
weekday: "long"
|
||||||
|
});
|
||||||
return `${weekday}, ${formatTime(time)}`;
|
return `${weekday}, ${formatTime(time)}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return formatTime(time);
|
return formatTime(time);
|
||||||
@@ -843,39 +847,54 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processQueue() {
|
property bool _processingQueue: false
|
||||||
if (addGateBusy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (popupsDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (SessionData.doNotDisturb) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (notificationQueue.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activePopupCount = visibleNotifications.filter(n => n && n.popup).length;
|
function processQueue() {
|
||||||
if (activePopupCount >= maxVisibleNotifications) {
|
if (addGateBusy || _processingQueue)
|
||||||
return;
|
return;
|
||||||
}
|
if (popupsDisabled)
|
||||||
|
return;
|
||||||
|
if (SessionData.doNotDisturb)
|
||||||
|
return;
|
||||||
|
if (notificationQueue.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_processingQueue = true;
|
||||||
|
|
||||||
const next = notificationQueue.shift();
|
const next = notificationQueue.shift();
|
||||||
if (!next)
|
if (!next) {
|
||||||
|
_processingQueue = false;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
next.seq = ++seqCounter;
|
next.seq = ++seqCounter;
|
||||||
visibleNotifications = [...visibleNotifications, next];
|
|
||||||
|
const activePopups = visibleNotifications.filter(n => n && n.popup);
|
||||||
|
let evicted = null;
|
||||||
|
if (activePopups.length >= maxVisibleNotifications) {
|
||||||
|
const unhovered = activePopups.filter(n => n.timer?.running);
|
||||||
|
const pool = unhovered.length > 0 ? unhovered : activePopups;
|
||||||
|
evicted = pool.reduce((min, n) => (n.seq < min.seq) ? n : min, pool[0]);
|
||||||
|
if (evicted)
|
||||||
|
evicted.removedByLimit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evicted) {
|
||||||
|
visibleNotifications = [...visibleNotifications.filter(n => n !== evicted), next];
|
||||||
|
} else {
|
||||||
|
visibleNotifications = [...visibleNotifications, next];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evicted)
|
||||||
|
evicted.popup = false;
|
||||||
next.popup = true;
|
next.popup = true;
|
||||||
|
|
||||||
if (next.timer.interval > 0) {
|
if (next.timer.interval > 0)
|
||||||
next.timer.start();
|
next.timer.start();
|
||||||
}
|
|
||||||
|
|
||||||
addGateBusy = true;
|
addGateBusy = true;
|
||||||
addGate.restart();
|
addGate.restart();
|
||||||
|
_processingQueue = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromVisibleNotifications(wrapper) {
|
function removeFromVisibleNotifications(wrapper) {
|
||||||
|
|||||||
Reference in New Issue
Block a user