diff --git a/Common/Theme.qml b/Common/Theme.qml index dabd25ff..392848a6 100644 --- a/Common/Theme.qml +++ b/Common/Theme.qml @@ -95,6 +95,8 @@ Singleton { property color error: currentThemeData.error || "#F2B8B5" property color warning: currentThemeData.warning || "#FF9800" property color info: currentThemeData.info || "#2196F3" + property color tempWarning: "#ff9933" + property color tempDanger: "#ff5555" property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12) property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08) diff --git a/Modules/Notifications/Center/NotificationKeyboardController.qml b/Modules/Notifications/Center/NotificationKeyboardController.qml index a89f4c78..1ffb17ed 100644 --- a/Modules/Notifications/Center/NotificationKeyboardController.qml +++ b/Modules/Notifications/Center/NotificationKeyboardController.qml @@ -308,109 +308,25 @@ QtObject { if (!group) return - // Save current state for smart navigation - const currentGroupKey = group.key - const isNotification = currentItem.type === "notification" - const notificationIndex = currentItem.notificationIndex - const totalNotificationsInGroup = group.notifications ? group.notifications.length : 0 - const isLastNotificationInGroup = isNotification - && totalNotificationsInGroup === 1 - const isLastNotificationInList = isNotification - && notificationIndex === totalNotificationsInGroup - 1 - - // Store what to select next BEFORE clearing - let nextTargetType = "" - let nextTargetGroupKey = "" - let nextTargetNotificationIndex = -1 - if (currentItem.type === "group") { NotificationService.dismissGroup(group.key) - - // Look for next group - for (var i = currentItem.groupIndex + 1; i < groups.length; i++) { - nextTargetType = "group" - nextTargetGroupKey = groups[i].key - break - } - - if (!nextTargetGroupKey && currentItem.groupIndex > 0) { - nextTargetType = "group" - nextTargetGroupKey = groups[currentItem.groupIndex - 1].key - } - } else if (isNotification) { - const notification = group.notifications[notificationIndex] + } else if (currentItem.type === "notification") { + const notification = group.notifications[currentItem.notificationIndex] NotificationService.dismissNotification(notification) - - if (isLastNotificationInGroup) { - for (var i = currentItem.groupIndex + 1; i < groups.length; i++) { - nextTargetType = "group" - nextTargetGroupKey = groups[i].key - break - } - - if (!nextTargetGroupKey && currentItem.groupIndex > 0) { - nextTargetType = "group" - nextTargetGroupKey = groups[currentItem.groupIndex - 1].key - } - } else if (isLastNotificationInList) { - // If group still has notifications after this one is removed, select the new last one - if (group.count > 1) { - nextTargetType = "notification" - nextTargetGroupKey = currentGroupKey - // After removing current notification, the new last index will be count-2 - nextTargetNotificationIndex = group.count - 2 - } else { - // Group will be empty or collapsed, select the group header - nextTargetType = "group" - nextTargetGroupKey = currentGroupKey - nextTargetNotificationIndex = -1 - } - } else { - nextTargetType = "notification" - nextTargetGroupKey = currentGroupKey - nextTargetNotificationIndex = notificationIndex - } } rebuildFlatNavigation() - // Find and select the target we identified if (flatNavigation.length === 0) { - selectedFlatIndex = 0 - updateSelectedIdFromIndex() - } else if (nextTargetGroupKey) { - let found = false - for (var i = 0; i < flatNavigation.length; i++) { - const item = flatNavigation[i] - - if (nextTargetType === "group" && item.type === "group" - && item.groupKey === nextTargetGroupKey) { - selectedFlatIndex = i - found = true - break - } else if (nextTargetType === "notification" - && item.type === "notification" - && item.groupKey === nextTargetGroupKey - && item.notificationIndex === nextTargetNotificationIndex) { - selectedFlatIndex = i - found = true - break - } + keyboardNavigationActive = false + if (listView) { + listView.keyboardActive = false } - - if (!found) { - selectedFlatIndex = Math.min(selectedFlatIndex, - flatNavigation.length - 1) - } - - updateSelectedIdFromIndex() } else { - selectedFlatIndex = Math.min(selectedFlatIndex, - flatNavigation.length - 1) + selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) updateSelectedIdFromIndex() + ensureVisible() } - - ensureVisible() } function ensureVisible() { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 37199924..b9900ff9 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -25,6 +25,17 @@ Singleton { property int seqCounter: 0 property bool bulkDismissing: false + property var _dismissQueue: [] + property int _dismissBatchSize: 8 + property int _dismissTickMs: 8 + property bool _suspendGrouping: false + property var _groupCache: ({"notifications": [], "popups": []}) + property bool _groupsDirty: false + + Component.onCompleted: { + _recomputeGroups() + } + Timer { id: addGate interval: enterAnimMs + 50 @@ -47,11 +58,41 @@ Singleton { } } + Timer { + id: dismissPump + interval: _dismissTickMs + repeat: true + running: false + onTriggered: { + let n = Math.min(_dismissBatchSize, _dismissQueue.length) + for (let i = 0; i < n; ++i) { + const w = _dismissQueue.pop() + try { + if (w && w.notification) w.notification.dismiss() + } catch (e) {} + } + if (_dismissQueue.length === 0) { + dismissPump.stop() + _suspendGrouping = false + bulkDismissing = false + popupsDisabled = false + _recomputeGroupsLater() + } + } + } + + Timer { + id: groupsDebounce + interval: 16 + repeat: false + onTriggered: _recomputeGroups() + } + property bool timeUpdateTick: false property bool clockFormatChanged: false - readonly property var groupedNotifications: getGroupedNotifications() - readonly property var groupedPopups: getGroupedPopups() + readonly property var groupedNotifications: _groupCache.notifications + readonly property var groupedPopups: _groupCache.popups property var expandedGroups: ({}) property var expandedMessages: ({}) @@ -89,6 +130,8 @@ Singleton { processQueue() } } + + _recomputeGroupsLater() } } @@ -217,12 +260,8 @@ Singleton { target: wrapper.notification.Retainable function onDropped(): void { - const notifIndex = root.notifications.indexOf(wrapper) - const allIndex = root.allWrappers.indexOf(wrapper) - if (allIndex !== -1) - root.allWrappers.splice(allIndex, 1) - if (notifIndex !== -1) - root.notifications.splice(notifIndex, 1) + root.allWrappers = root.allWrappers.filter(w => w !== wrapper) + root.notifications = root.notifications.filter(w => w !== wrapper) if (root.bulkDismissing) return @@ -236,6 +275,7 @@ Singleton { } cleanupExpansionStates() + root._recomputeGroupsLater() } function onAboutToDestroy(): void { @@ -256,30 +296,20 @@ Singleton { addGateBusy = false notificationQueue = [] - for (const w of visibleNotifications) + for (const w of allWrappers) w.popup = false visibleNotifications = [] - const toDismiss = notifications.slice() - + _dismissQueue = notifications.slice() if (notifications.length) - notifications.splice(0, notifications.length) + notifications = [] expandedGroups = {} expandedMessages = {} - for (var i = 0; i < toDismiss.length; ++i) { - const w = toDismiss[i] - if (w && w.notification) { - try { - w.notification.dismiss() - } catch (e) { + _suspendGrouping = true - } - } - } - - bulkDismissing = false - popupsDisabled = false + if (!dismissPump.running && _dismissQueue.length) + dismissPump.start() } function dismissNotification(wrapper) { @@ -293,10 +323,10 @@ Singleton { popupsDisabled = disable if (disable) { notificationQueue = [] - visibleNotifications = [] - for (const notif of root.allWrappers) { + for (const notif of visibleNotifications) { notif.popup = false } + visibleNotifications = [] } } @@ -325,32 +355,20 @@ Singleton { } function removeFromVisibleNotifications(wrapper) { - const i = visibleNotifications.findIndex(n => n === wrapper) - if (i !== -1) { - const v = [...visibleNotifications] - v.splice(i, 1) - visibleNotifications = v - processQueue() - } + visibleNotifications = visibleNotifications.filter(n => n !== wrapper) + processQueue() } function releaseWrapper(w) { - let v = visibleNotifications.slice() - const vi = v.indexOf(w) - if (vi !== -1) { - v.splice(vi, 1) - visibleNotifications = v - } - - let q = notificationQueue.slice() - const qi = q.indexOf(w) - if (qi !== -1) { - q.splice(qi, 1) - notificationQueue = q - } + visibleNotifications = visibleNotifications.filter(n => n !== w) + notificationQueue = notificationQueue.filter(n => n !== w) if (w && w.destroy && !w.isPersistent) { - w.destroy() + Qt.callLater(() => { + try { + w.destroy() + } catch (e) {} + }) } } @@ -362,7 +380,25 @@ Singleton { return wrapper.appName.toLowerCase() } - function getGroupedNotifications() { + function _recomputeGroups() { + if (_suspendGrouping) { + _groupsDirty = true + return + } + _groupCache = { + "notifications": _calcGroupedNotifications(), + "popups": _calcGroupedPopups() + } + _groupsDirty = false + } + + function _recomputeGroupsLater() { + _groupsDirty = true + if (!groupsDebounce.running) + groupsDebounce.start() + } + + function _calcGroupedNotifications() { const groups = {} for (const notif of notifications) { @@ -400,7 +436,7 @@ Singleton { }) } - function getGroupedPopups() { + function _calcGroupedPopups() { const groups = {} for (const notif of popups) { diff --git a/spam-notifications.sh b/spam-notifications.sh new file mode 100755 index 00000000..fcd9f6db --- /dev/null +++ b/spam-notifications.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +# Notification Spam Test Script - Sends 100 rapid notifications from fake apps + +echo "NOTIFICATION SPAM TEST - 100 RAPID NOTIFICATIONS" +echo "=============================================================" +echo "WARNING: This will send 100 notifications very quickly!" +echo "Press Ctrl+C to cancel, or wait 3 seconds to continue..." +sleep 3 + +# Arrays of fake app names and icons +APPS=( + "slack:mail-message-new" + "discord:internet-chat" + "teams:call-start" + "zoom:camera-video" + "spotify:audio-x-generic" + "chrome:web-browser" + "firefox:web-browser" + "vscode:text-editor" + "terminal:utilities-terminal" + "steam:applications-games" + "telegram:internet-chat" + "whatsapp:phone" + "signal:security-high" + "thunderbird:mail-client" + "calendar:office-calendar" + "notes:text-editor" + "todo:emblem-default" + "weather:weather-few-clouds" + "news:rss" + "reddit:web-browser" + "twitter:internet-web-browser" + "instagram:camera-photo" + "youtube:video-x-generic" + "netflix:media-playback-start" + "github:folder-development" + "gitlab:folder-development" + "jira:applications-office" + "notion:text-editor" + "obsidian:accessories-text-editor" + "dropbox:folder-remote" + "gdrive:folder-google-drive" + "onedrive:folder-cloud" + "backup:drive-harddisk" + "antivirus:security-high" + "vpn:network-vpn" + "torrent:network-server" + "docker:application-x-executable" + "kubernetes:applications-system" + "postgres:database" + "mongodb:database" + "redis:database" + "nginx:network-server" + "apache:network-server" + "jenkins:applications-development" + "gradle:applications-development" + "maven:applications-development" + "npm:package-x-generic" + "yarn:package-x-generic" + "pip:package-x-generic" + "apt:system-software-install" +) + +# Arrays of message types +TITLES=( + "New message" + "Update available" + "Download complete" + "Task finished" + "Build successful" + "Deployment complete" + "Sync complete" + "Backup finished" + "Security alert" + "New notification" + "Process complete" + "Upload finished" + "Connection established" + "Meeting starting" + "Reminder" + "Warning" + "Error occurred" + "Success" + "Failed" + "Pending" + "In progress" + "Scheduled" + "New activity" + "Status update" + "Alert" + "Information" + "Breaking news" + "Hot update" + "Trending" + "New release" +) + +MESSAGES=( + "Your request has been processed successfully" + "New content is available for download" + "Operation completed without errors" + "Check your inbox for updates" + "3 new items require your attention" + "Background task finished executing" + "All systems operational" + "Performance metrics updated" + "Configuration saved successfully" + "Database connection established" + "Cache cleared and rebuilt" + "Service restarted automatically" + "Logs have been rotated" + "Memory usage optimized" + "Network latency improved" + "Security scan completed - no threats" + "Automatic backup created" + "Files synchronized across devices" + "Updates installed successfully" + "New features are now available" + "Your subscription has been renewed" + "Report generated and ready" + "Analysis complete - view results" + "Queue processed: 42 items" + "Rate limit will reset in 5 minutes" + "API call successful (200 OK)" + "Webhook delivered successfully" + "Container started on port 8080" + "Build artifact uploaded" + "Test suite passed: 100/100" + "Coverage report: 95%" + "Dependencies updated to latest" + "Migration completed successfully" + "Index rebuilt for faster queries" + "SSL certificate renewed" + "Firewall rules updated" + "DNS propagation complete" + "CDN cache purged globally" + "Load balancer health check: OK" + "Cluster scaled to 5 nodes" +) + +# Urgency levels +URGENCY=("low" "normal") + +# Counter +COUNT=0 +TOTAL=100 + +echo "" +echo "Starting notification spam..." +echo "------------------------------" + +# Send notifications rapidly +for i in $(seq 1 $TOTAL); do + # Pick random app, title, message, and urgency + APP=${APPS[$RANDOM % ${#APPS[@]}]} + APP_NAME=${APP%%:*} + APP_ICON=${APP#*:} + TITLE=${TITLES[$RANDOM % ${#TITLES[@]}]} + MESSAGE=${MESSAGES[$RANDOM % ${#MESSAGES[@]}]} + URG=${URGENCY[$RANDOM % ${#URGENCY[@]}]} + + # Add some variety with random numbers and timestamps + RAND_NUM=$((RANDOM % 1000)) + TIMESTAMP=$(date +"%H:%M:%S") + + # Randomly add extra details to some messages + if [ $((RANDOM % 3)) -eq 0 ]; then + MESSAGE="[$TIMESTAMP] $MESSAGE (#$RAND_NUM)" + fi + + # Send notification with very short delay + notify-send \ + -h string:desktop-entry:$APP_NAME \ + -i $APP_ICON \ + -u $URG \ + "$APP_NAME: $TITLE" \ + "$MESSAGE" & + + # Increment counter + COUNT=$((COUNT + 1)) + + # Show progress every 10 notifications + if [ $((COUNT % 10)) -eq 0 ]; then + echo " Sent $COUNT/$TOTAL notifications..." + fi + + # Tiny delay to prevent complete system freeze + # Adjust this value: smaller = faster spam, larger = slower spam + sleep 0.01 +done + +# Wait for all background notifications to complete +wait + +echo "" +echo "Spam test complete!" +echo "=============================================================" +echo "Statistics:" +echo " Total notifications sent: $TOTAL" +echo " Apps simulated: ${#APPS[@]}" +echo " Message variations: ${#MESSAGES[@]}" +echo " Time taken: ~$(($TOTAL / 100)) seconds" +echo "" +echo "Check your notification center - it should be FULL!" +echo "Tip: You may want to clear all notifications after this test" +echo "" \ No newline at end of file