mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
handle notif spam better
This commit is contained in:
@@ -39,6 +39,7 @@ DankModal {
|
|||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
notificationModalOpen = true
|
notificationModalOpen = true
|
||||||
|
NotificationService.onOverlayOpen()
|
||||||
open()
|
open()
|
||||||
modalKeyboardController.reset()
|
modalKeyboardController.reset()
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ DankModal {
|
|||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
notificationModalOpen = false
|
notificationModalOpen = false
|
||||||
|
NotificationService.onOverlayClose()
|
||||||
close()
|
close()
|
||||||
modalKeyboardController.reset()
|
modalKeyboardController.reset()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ DankPopout {
|
|||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
NotificationService.disablePopups(true)
|
NotificationService.onOverlayOpen()
|
||||||
// Set up keyboard controller when content is loaded
|
// Set up keyboard controller when content is loaded
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
if (contentLoader.item) {
|
if (contentLoader.item) {
|
||||||
@@ -79,7 +79,7 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
NotificationService.disablePopups(false)
|
NotificationService.onOverlayClose()
|
||||||
// Reset keyboard state when closing
|
// Reset keyboard state when closing
|
||||||
keyboardController.keyboardNavigationActive = false
|
keyboardController.keyboardNavigationActive = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ PanelWindow {
|
|||||||
function forceExit() {
|
function forceExit() {
|
||||||
if (_isDestroying)
|
if (_isDestroying)
|
||||||
return
|
return
|
||||||
|
|
||||||
_isDestroying = true
|
_isDestroying = true
|
||||||
exiting = true
|
exiting = true
|
||||||
visible = false
|
visible = false
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ QtObject {
|
|||||||
property var modelData
|
property var modelData
|
||||||
property int topMargin: 0
|
property int topMargin: 0
|
||||||
property int baseNotificationHeight: 120
|
property int baseNotificationHeight: 120
|
||||||
property int maxTargetNotifications: 3
|
property int maxTargetNotifications: 4
|
||||||
property var popupWindows: [] // strong refs to windows (live until exitFinished)
|
property var popupWindows: [] // strong refs to windows (live until exitFinished)
|
||||||
property var destroyingWindows: new Set()
|
property var destroyingWindows: new Set()
|
||||||
property Component popupComponent
|
property Component popupComponent
|
||||||
@@ -34,50 +34,31 @@ QtObject {
|
|||||||
property Timer sweeper
|
property Timer sweeper
|
||||||
|
|
||||||
sweeper: Timer {
|
sweeper: Timer {
|
||||||
interval: 2000
|
interval: 500
|
||||||
running: false // Not running by default
|
running: false
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
let toRemove = []
|
let toRemove = []
|
||||||
for (let p of popupWindows) {
|
for (let p of popupWindows) {
|
||||||
if (!p) {
|
if (!p) { toRemove.push(p); continue }
|
||||||
toRemove.push(p)
|
const isZombie =
|
||||||
continue
|
p.status === Component.Null ||
|
||||||
}
|
(!p.visible && !p.exiting) ||
|
||||||
const isZombie = p.status === Component.Null || (!p.visible
|
(!p.notificationData && !p._isDestroying) ||
|
||||||
&& !p.exiting)
|
(!p.hasValidData && !p._isDestroying)
|
||||||
|| (!p.notificationData && !p._isDestroying)
|
|
||||||
|| (!p.hasValidData && !p._isDestroying)
|
|
||||||
if (isZombie) {
|
if (isZombie) {
|
||||||
|
|
||||||
toRemove.push(p)
|
toRemove.push(p)
|
||||||
if (p.forceExit) {
|
if (p.forceExit) p.forceExit()
|
||||||
p.forceExit()
|
else if (p.destroy) { try { p.destroy() } catch(e) {} }
|
||||||
} else if (p.destroy) {
|
|
||||||
try {
|
|
||||||
p.destroy()
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toRemove.length > 0) {
|
if (toRemove.length) {
|
||||||
for (let zombie of toRemove) {
|
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1)
|
||||||
const i = popupWindows.indexOf(zombie)
|
const survivors = _active().sort((a,b)=>a.screenY-b.screenY)
|
||||||
if (i !== -1)
|
for (var k=0; k<survivors.length; ++k)
|
||||||
popupWindows.splice(i, 1)
|
|
||||||
}
|
|
||||||
popupWindows = popupWindows.slice()
|
|
||||||
const survivors = _active().sort((a, b) => {
|
|
||||||
return a.screenY - b.screenY
|
|
||||||
})
|
|
||||||
for (var k = 0; k < survivors.length; ++k) {
|
|
||||||
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (popupWindows.length === 0)
|
if (popupWindows.length === 0) sweeper.stop()
|
||||||
sweeper.stop()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +75,82 @@ QtObject {
|
|||||||
&& p.hasValidData
|
&& p.hasValidData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _canMakeRoomFor(wrapper) {
|
||||||
|
const activeWindows = _active()
|
||||||
|
if (activeWindows.length < maxTargetNotifications)
|
||||||
|
return true
|
||||||
|
|
||||||
|
if (!wrapper || !wrapper.notification)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const incomingUrgency = wrapper.notification.urgency || 0
|
||||||
|
|
||||||
|
for (let p of activeWindows) {
|
||||||
|
if (!p.notificationData || !p.notificationData.notification)
|
||||||
|
continue
|
||||||
|
|
||||||
|
const existingUrgency = p.notificationData.notification.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 incomingUrgency = (incomingWrapper && incomingWrapper.notification)
|
||||||
|
? incomingWrapper.notification.urgency || 0 : 0
|
||||||
|
|
||||||
|
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
||||||
|
const aUrgency = (a.notificationData && a.notificationData.notification)
|
||||||
|
? a.notificationData.notification.urgency || 0 : 0
|
||||||
|
const bUrgency = (b.notificationData && b.notificationData.notification)
|
||||||
|
? b.notificationData.notification.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 (let w of newWrappers) {
|
for (let w of newWrappers) {
|
||||||
if (w && !_hasWindowFor(w))
|
if (w && !_hasWindowFor(w))
|
||||||
insertNewestAtTop(w)
|
insertNewestAtTop(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let p of popupWindows.slice()) {
|
for (let p of popupWindows.slice()) {
|
||||||
if (!_isValidWindow(p))
|
if (!_isValidWindow(p))
|
||||||
continue
|
continue
|
||||||
@@ -116,6 +168,7 @@ QtObject {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let p of popupWindows) {
|
for (let p of popupWindows) {
|
||||||
if (!_isValidWindow(p))
|
if (!_isValidWindow(p))
|
||||||
continue
|
continue
|
||||||
@@ -145,8 +198,6 @@ QtObject {
|
|||||||
popupWindows.push(win)
|
popupWindows.push(win)
|
||||||
if (!sweeper.running)
|
if (!sweeper.running)
|
||||||
sweeper.start()
|
sweeper.start()
|
||||||
|
|
||||||
_maybeStartOverflow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _active() {
|
function _active() {
|
||||||
@@ -168,64 +219,7 @@ QtObject {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
function _maybeStartOverflow() {
|
|
||||||
const activeWindows = _active()
|
|
||||||
if (activeWindows.length <= maxTargetNotifications + 1)
|
|
||||||
return
|
|
||||||
|
|
||||||
const expiredCandidates = activeWindows.filter(p => {
|
|
||||||
if (!p.notificationData
|
|
||||||
|| !p.notificationData.notification)
|
|
||||||
return false
|
|
||||||
if (p.notificationData.notification.urgency === 2)
|
|
||||||
return false
|
|
||||||
|
|
||||||
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
|
|
||||||
if (timeoutMs === 0)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return !p.notificationData.timer.running
|
|
||||||
}).sort(
|
|
||||||
(a, b) => b.screenY - a.screenY)
|
|
||||||
|
|
||||||
if (expiredCandidates.length > 0) {
|
|
||||||
const toRemove = expiredCandidates[0]
|
|
||||||
if (toRemove && !toRemove.exiting) {
|
|
||||||
toRemove.notificationData.removedByLimit = true
|
|
||||||
toRemove.notificationData.popup = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutCandidates = activeWindows.filter(p => {
|
|
||||||
if (!p.notificationData
|
|
||||||
|| !p.notificationData.notification)
|
|
||||||
return false
|
|
||||||
if (p.notificationData.notification.urgency === 2)
|
|
||||||
return false
|
|
||||||
|
|
||||||
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
|
|
||||||
return timeoutMs > 0
|
|
||||||
}).sort((a, b) => {
|
|
||||||
const aTimeout = a.notificationData.timer ? a.notificationData.timer.interval : 5000
|
|
||||||
const bTimeout = b.notificationData.timer ? b.notificationData.timer.interval : 5000
|
|
||||||
if (aTimeout !== bTimeout)
|
|
||||||
return aTimeout - bTimeout
|
|
||||||
return b.screenY - a.screenY
|
|
||||||
})
|
|
||||||
|
|
||||||
if (timeoutCandidates.length > 0) {
|
|
||||||
const toRemove = timeoutCandidates[0]
|
|
||||||
if (toRemove && !toRemove.exiting) {
|
|
||||||
toRemove.notificationData.removedByLimit = true
|
|
||||||
toRemove.notificationData.popup = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onPopupEntered(p) {
|
function _onPopupEntered(p) {
|
||||||
if (_isValidWindow(p))
|
|
||||||
_maybeStartOverflow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onPopupExitFinished(p) {
|
function _onPopupExitFinished(p) {
|
||||||
@@ -263,7 +257,6 @@ QtObject {
|
|||||||
for (var k = 0; k < survivors.length; ++k) {
|
for (var k = 0; k < survivors.length; ++k) {
|
||||||
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
survivors[k].screenY = topMargin + k * baseNotificationHeight
|
||||||
}
|
}
|
||||||
_maybeStartOverflow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupAllWindows() {
|
function cleanupAllWindows() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Singleton {
|
|||||||
readonly property list<NotifWrapper> notifications: []
|
readonly property list<NotifWrapper> notifications: []
|
||||||
readonly property list<NotifWrapper> allWrappers: []
|
readonly property list<NotifWrapper> allWrappers: []
|
||||||
readonly property list<NotifWrapper> popups: allWrappers.filter(
|
readonly property list<NotifWrapper> popups: allWrappers.filter(
|
||||||
n => n.popup)
|
n => n && n.popup)
|
||||||
|
|
||||||
property list<NotifWrapper> notificationQueue: []
|
property list<NotifWrapper> notificationQueue: []
|
||||||
property list<NotifWrapper> visibleNotifications: []
|
property list<NotifWrapper> visibleNotifications: []
|
||||||
@@ -25,6 +25,12 @@ Singleton {
|
|||||||
property int seqCounter: 0
|
property int seqCounter: 0
|
||||||
property bool bulkDismissing: false
|
property bool bulkDismissing: false
|
||||||
|
|
||||||
|
property int maxQueueSize: 32
|
||||||
|
property int maxIngressPerSecond: 20
|
||||||
|
property double _lastIngressSec: 0
|
||||||
|
property int _ingressCountThisSec: 0
|
||||||
|
property int maxStoredNotifications: 300
|
||||||
|
|
||||||
property var _dismissQueue: []
|
property var _dismissQueue: []
|
||||||
property int _dismissBatchSize: 8
|
property int _dismissBatchSize: 8
|
||||||
property int _dismissTickMs: 8
|
property int _dismissTickMs: 8
|
||||||
@@ -36,6 +42,80 @@ Singleton {
|
|||||||
_recomputeGroups()
|
_recomputeGroups()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _nowSec() { return Date.now() / 1000.0 }
|
||||||
|
|
||||||
|
function _ingressAllowed(notif) {
|
||||||
|
const t = _nowSec()
|
||||||
|
if (t - _lastIngressSec >= 1.0) {
|
||||||
|
_lastIngressSec = t
|
||||||
|
_ingressCountThisSec = 0
|
||||||
|
}
|
||||||
|
_ingressCountThisSec += 1
|
||||||
|
if (notif.urgency === NotificationUrgency.Critical)
|
||||||
|
return true
|
||||||
|
return _ingressCountThisSec <= maxIngressPerSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
function _enqueuePopup(wrapper) {
|
||||||
|
if (notificationQueue.length >= maxQueueSize) {
|
||||||
|
const gk = getGroupKey(wrapper)
|
||||||
|
let idx = notificationQueue.findIndex(w =>
|
||||||
|
w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical)
|
||||||
|
if (idx === -1) {
|
||||||
|
idx = notificationQueue.findIndex(w => w && w.urgency !== NotificationUrgency.Critical)
|
||||||
|
}
|
||||||
|
if (idx === -1) idx = 0
|
||||||
|
const victim = notificationQueue[idx]
|
||||||
|
if (victim) victim.popup = false
|
||||||
|
notificationQueue.splice(idx, 1)
|
||||||
|
}
|
||||||
|
notificationQueue = [...notificationQueue, wrapper]
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initWrapperPersistence(wrapper) {
|
||||||
|
const timeoutMs = wrapper.timer ? wrapper.timer.interval : 5000
|
||||||
|
const isCritical = wrapper.notification && wrapper.notification.urgency === NotificationUrgency.Critical
|
||||||
|
wrapper.isPersistent = isCritical || (timeoutMs === 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _trimStored() {
|
||||||
|
if (notifications.length > maxStoredNotifications) {
|
||||||
|
const overflow = notifications.length - maxStoredNotifications
|
||||||
|
let toDrop = []
|
||||||
|
for (let i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
|
||||||
|
const w = notifications[i]
|
||||||
|
if (w && w.notification && w.urgency !== NotificationUrgency.Critical)
|
||||||
|
toDrop.push(w)
|
||||||
|
}
|
||||||
|
for (let i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
|
||||||
|
const w = notifications[i]
|
||||||
|
if (w && w.notification && toDrop.indexOf(w) === -1)
|
||||||
|
toDrop.push(w)
|
||||||
|
}
|
||||||
|
for (const w of toDrop) { try { w.notification.dismiss() } catch(e) {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOverlayOpen() {
|
||||||
|
popupsDisabled = true
|
||||||
|
addGate.stop()
|
||||||
|
addGateBusy = false
|
||||||
|
|
||||||
|
notificationQueue = []
|
||||||
|
for (const w of visibleNotifications) {
|
||||||
|
if (w) {
|
||||||
|
w.popup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visibleNotifications = []
|
||||||
|
_recomputeGroupsLater()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOverlayClose() {
|
||||||
|
popupsDisabled = false
|
||||||
|
processQueue()
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: addGate
|
id: addGate
|
||||||
interval: enterAnimMs + 50
|
interval: enterAnimMs + 50
|
||||||
@@ -51,7 +131,7 @@ Singleton {
|
|||||||
id: timeUpdateTimer
|
id: timeUpdateTimer
|
||||||
interval: 30000
|
interval: 30000
|
||||||
repeat: true
|
repeat: true
|
||||||
running: root.allWrappers.length > 0
|
running: root.allWrappers.length > 0 || visibleNotifications.length > 0
|
||||||
triggeredOnStart: false
|
triggeredOnStart: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.timeUpdateTick = !root.timeUpdateTick
|
root.timeUpdateTick = !root.timeUpdateTick
|
||||||
@@ -114,8 +194,14 @@ Singleton {
|
|||||||
onNotification: notif => {
|
onNotification: notif => {
|
||||||
notif.tracked = true
|
notif.tracked = true
|
||||||
|
|
||||||
const shouldShowPopup = !root.popupsDisabled
|
if (!_ingressAllowed(notif)) {
|
||||||
&& !SessionData.doNotDisturb
|
if (notif.urgency !== NotificationUrgency.Critical) {
|
||||||
|
try { notif.dismiss() } catch(e) {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb
|
||||||
const wrapper = notifComponent.createObject(root, {
|
const wrapper = notifComponent.createObject(root, {
|
||||||
"popup": shouldShowPopup,
|
"popup": shouldShowPopup,
|
||||||
"notification": notif
|
"notification": notif
|
||||||
@@ -124,9 +210,14 @@ Singleton {
|
|||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
root.allWrappers.push(wrapper)
|
root.allWrappers.push(wrapper)
|
||||||
root.notifications.push(wrapper)
|
root.notifications.push(wrapper)
|
||||||
|
_trimStored()
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
_initWrapperPersistence(wrapper)
|
||||||
|
})
|
||||||
|
|
||||||
if (shouldShowPopup) {
|
if (shouldShowPopup) {
|
||||||
notificationQueue = [...notificationQueue, wrapper]
|
_enqueuePopup(wrapper)
|
||||||
processQueue()
|
processQueue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,6 +319,7 @@ Singleton {
|
|||||||
readonly property string summary: notification.summary
|
readonly property string summary: notification.summary
|
||||||
readonly property string body: notification.body
|
readonly property string body: notification.body
|
||||||
readonly property string htmlBody: {
|
readonly property string htmlBody: {
|
||||||
|
if (!popup && !root.popupsDisabled) return ""
|
||||||
if (body && (body.includes('<') && body.includes('>'))) {
|
if (body && (body.includes('<') && body.includes('>'))) {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
@@ -340,6 +432,11 @@ Singleton {
|
|||||||
if (notificationQueue.length === 0)
|
if (notificationQueue.length === 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
const activePopupCount = visibleNotifications.filter(n => n && n.popup).length
|
||||||
|
if (activePopupCount >= 4) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const next = notificationQueue.shift()
|
const next = notificationQueue.shift()
|
||||||
|
|
||||||
next.seq = ++seqCounter
|
next.seq = ++seqCounter
|
||||||
@@ -363,7 +460,7 @@ Singleton {
|
|||||||
visibleNotifications = visibleNotifications.filter(n => n !== w)
|
visibleNotifications = visibleNotifications.filter(n => n !== w)
|
||||||
notificationQueue = notificationQueue.filter(n => n !== w)
|
notificationQueue = notificationQueue.filter(n => n !== w)
|
||||||
|
|
||||||
if (w && w.destroy && !w.isPersistent) {
|
if (w && w.destroy && !w.isPersistent && notifications.indexOf(w) === -1) {
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
try {
|
try {
|
||||||
w.destroy()
|
w.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user