publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed from the internal source named below under a manifest that decides which paths may leave, then scanned as a whole tree rather than as a series of patches, and only then published. Public history starts here because the history before it was not admissible, and neither was the tree. What used to stand in this repository included a rescue copy of another machine, a directory of phone handoffs, deployment wired to one house, and a submodule pointing at a forge no stranger can reach. None of that was ever the product. It stays in the private forge, which is allowed to hold the whole working organism, and this is what was deliberately sent out instead. Three mechanisms produced this tree, in decreasing order of trust. A top-level path the manifest does not name never arrives at all, which is the one that catches directories nobody has thought of yet. Named internal files inside admitted roots are dropped. A short, reviewed table replaces deployment defaults that a public build must not carry -- an endpoint aimed at one LAN, a VPN profile belonging to one phone, packaging built from one checkout path. Everything after this commit is an ordinary publication with the same three trailers, so a force push stops being routine and starts meaning that something deliberate happened. The trailers bind the projection to its source without pretending the public SHA is the private one: same lineage, different tree, and the record says so. Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047 Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27 Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
commit
8f42fc953d
1476 changed files with 238455 additions and 0 deletions
305
surfaces/quickshell/ii-base/services/Notifications.qml
Normal file
305
surfaces/quickshell/ii-base/services/Notifications.qml
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
/**
|
||||
* Provides extra features not in Quickshell.Services.Notifications:
|
||||
* - Persistent storage
|
||||
* - Popup notifications, with timeout
|
||||
* - Notification groups by app
|
||||
*/
|
||||
Singleton {
|
||||
id: root
|
||||
component Notif: QtObject {
|
||||
id: wrapper
|
||||
required property int notificationId // Could just be `id` but it conflicts with the default prop in QtObject
|
||||
property Notification notification
|
||||
property list<var> actions: notification?.actions.map((action) => ({
|
||||
"identifier": action.identifier,
|
||||
"text": action.text,
|
||||
})) ?? []
|
||||
property bool popup: false
|
||||
property bool isTransient: notification?.hints.transient ?? false
|
||||
property string appIcon: notification?.appIcon ?? ""
|
||||
property string appName: notification?.appName ?? ""
|
||||
property string body: notification?.body ?? ""
|
||||
property string image: notification?.image ?? ""
|
||||
property string summary: notification?.summary ?? ""
|
||||
property double time
|
||||
property string urgency: notification?.urgency.toString() ?? "normal"
|
||||
property Timer timer
|
||||
|
||||
onNotificationChanged: {
|
||||
if (notification === null) {
|
||||
root.discardNotification(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifToJSON(notif) {
|
||||
return {
|
||||
"notificationId": notif.notificationId,
|
||||
"actions": notif.actions,
|
||||
"appIcon": notif.appIcon,
|
||||
"appName": notif.appName,
|
||||
"body": notif.body,
|
||||
"image": notif.image,
|
||||
"summary": notif.summary,
|
||||
"time": notif.time,
|
||||
"urgency": notif.urgency,
|
||||
}
|
||||
}
|
||||
function notifToString(notif) {
|
||||
return JSON.stringify(notifToJSON(notif), null, 2);
|
||||
}
|
||||
|
||||
component NotifTimer: Timer {
|
||||
required property int notificationId
|
||||
interval: 7000
|
||||
running: true
|
||||
onTriggered: () => {
|
||||
const index = root.list.findIndex((notif) => notif.notificationId === notificationId);
|
||||
const notifObject = root.list[index];
|
||||
print("[Notifications] Notification timer triggered for ID: " + notificationId + ", transient: " + notifObject?.isTransient);
|
||||
if (notifObject.isTransient) root.discardNotification(notificationId);
|
||||
else root.timeoutNotification(notificationId);
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
|
||||
property bool silent: false
|
||||
property int unread: 0
|
||||
property var filePath: Directories.notificationsPath
|
||||
property list<Notif> list: []
|
||||
property var popupList: list.filter((notif) => notif.popup);
|
||||
property bool popupInhibited: (GlobalStates?.sidebarRightOpen ?? false) || silent
|
||||
property var latestTimeForApp: ({})
|
||||
Component {
|
||||
id: notifComponent
|
||||
Notif {}
|
||||
}
|
||||
Component {
|
||||
id: notifTimerComponent
|
||||
NotifTimer {}
|
||||
}
|
||||
|
||||
function stringifyList(list) {
|
||||
return JSON.stringify(list.map((notif) => notifToJSON(notif)), null, 2);
|
||||
}
|
||||
|
||||
onListChanged: {
|
||||
// Update latest time for each app
|
||||
root.list.forEach((notif) => {
|
||||
if (!root.latestTimeForApp[notif.appName] || notif.time > root.latestTimeForApp[notif.appName]) {
|
||||
root.latestTimeForApp[notif.appName] = Math.max(root.latestTimeForApp[notif.appName] || 0, notif.time);
|
||||
}
|
||||
});
|
||||
// Remove apps that no longer have notifications
|
||||
Object.keys(root.latestTimeForApp).forEach((appName) => {
|
||||
if (!root.list.some((notif) => notif.appName === appName)) {
|
||||
delete root.latestTimeForApp[appName];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appNameListForGroups(groups) {
|
||||
return Object.keys(groups).sort((a, b) => {
|
||||
// Sort by time, descending
|
||||
return groups[b].time - groups[a].time;
|
||||
});
|
||||
}
|
||||
|
||||
function groupsForList(list) {
|
||||
const groups = {};
|
||||
list.forEach((notif) => {
|
||||
if (!groups[notif.appName]) {
|
||||
groups[notif.appName] = {
|
||||
appName: notif.appName,
|
||||
appIcon: notif.appIcon,
|
||||
notifications: [],
|
||||
time: 0
|
||||
};
|
||||
}
|
||||
groups[notif.appName].notifications.push(notif);
|
||||
// Always set to the latest time in the group
|
||||
groups[notif.appName].time = latestTimeForApp[notif.appName] || notif.time;
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
property var groupsByAppName: groupsForList(root.list)
|
||||
property var popupGroupsByAppName: groupsForList(root.popupList)
|
||||
property list<string> appNameList: appNameListForGroups(root.groupsByAppName)
|
||||
property list<string> popupAppNameList: appNameListForGroups(root.popupGroupsByAppName)
|
||||
|
||||
// Quickshell's notification IDs starts at 1 on each run, while saved notifications
|
||||
// can already contain higher IDs. This is for avoiding id collisions
|
||||
property int idOffset
|
||||
signal initDone();
|
||||
signal notify(notification: var);
|
||||
signal discard(id: int);
|
||||
signal discardAll();
|
||||
signal timeout(id: var);
|
||||
|
||||
NotificationServer {
|
||||
id: notifServer
|
||||
// actionIconsSupported: true
|
||||
actionsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
bodySupported: true
|
||||
imageSupported: true
|
||||
keepOnReload: false
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: (notification) => {
|
||||
notification.tracked = true
|
||||
const newNotifObject = notifComponent.createObject(root, {
|
||||
"notificationId": notification.id + root.idOffset,
|
||||
"notification": notification,
|
||||
"time": Date.now(),
|
||||
});
|
||||
root.list = [...root.list, newNotifObject];
|
||||
|
||||
// Popup
|
||||
if (!root.popupInhibited) {
|
||||
newNotifObject.popup = true;
|
||||
if (notification.expireTimeout != 0) {
|
||||
newNotifObject.timer = notifTimerComponent.createObject(root, {
|
||||
"notificationId": newNotifObject.notificationId,
|
||||
"interval": Config?.options.notifications.timeout ?? 7000,
|
||||
});
|
||||
}
|
||||
root.unread++;
|
||||
}
|
||||
root.notify(newNotifObject);
|
||||
// console.log(notifToString(newNotifObject));
|
||||
notifFileView.setText(stringifyList(root.list));
|
||||
}
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
root.unread = 0;
|
||||
}
|
||||
|
||||
function discardNotification(id) {
|
||||
console.log("[Notifications] Discarding notification with ID: " + id);
|
||||
const index = root.list.findIndex((notif) => notif.notificationId === id);
|
||||
const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id);
|
||||
if (index !== -1) {
|
||||
root.list.splice(index, 1);
|
||||
notifFileView.setText(stringifyList(root.list));
|
||||
triggerListChange()
|
||||
}
|
||||
if (notifServerIndex !== -1) {
|
||||
notifServer.trackedNotifications.values[notifServerIndex].dismiss()
|
||||
}
|
||||
root.discard(id); // Emit signal
|
||||
}
|
||||
|
||||
function discardAllNotifications() {
|
||||
root.list = []
|
||||
triggerListChange()
|
||||
notifFileView.setText(stringifyList(root.list));
|
||||
notifServer.trackedNotifications.values.forEach((notif) => {
|
||||
notif.dismiss()
|
||||
})
|
||||
root.discardAll();
|
||||
}
|
||||
|
||||
function cancelTimeout(id) {
|
||||
const index = root.list.findIndex((notif) => notif.notificationId === id);
|
||||
if (root.list[index] != null)
|
||||
root.list[index].timer.stop();
|
||||
}
|
||||
|
||||
function timeoutNotification(id) {
|
||||
const index = root.list.findIndex((notif) => notif.notificationId === id);
|
||||
if (root.list[index] != null)
|
||||
root.list[index].popup = false;
|
||||
root.timeout(id);
|
||||
}
|
||||
|
||||
function timeoutAll() {
|
||||
root.popupList.forEach((notif) => {
|
||||
root.timeout(notif.notificationId);
|
||||
})
|
||||
root.popupList.forEach((notif) => {
|
||||
notif.popup = false;
|
||||
});
|
||||
}
|
||||
|
||||
function attemptInvokeAction(id, notifIdentifier) {
|
||||
console.log("[Notifications] Attempting to invoke action with identifier: " + notifIdentifier + " for notification ID: " + id);
|
||||
const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id);
|
||||
console.log("Notification server index: " + notifServerIndex);
|
||||
if (notifServerIndex !== -1) {
|
||||
const notifServerNotif = notifServer.trackedNotifications.values[notifServerIndex];
|
||||
const action = notifServerNotif.actions.find((action) => action.identifier === notifIdentifier);
|
||||
// console.log("Action found: " + JSON.stringify(action));
|
||||
action.invoke()
|
||||
}
|
||||
else {
|
||||
console.log("Notification not found in server: " + id)
|
||||
}
|
||||
root.discardNotification(id);
|
||||
}
|
||||
|
||||
function triggerListChange() {
|
||||
root.list = root.list.slice(0)
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
notifFileView.reload()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
refresh()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: notifFileView
|
||||
path: Qt.resolvedUrl(filePath)
|
||||
onLoaded: {
|
||||
const fileContents = notifFileView.text()
|
||||
root.list = JSON.parse(fileContents).map((notif) => {
|
||||
return notifComponent.createObject(root, {
|
||||
"notificationId": notif.notificationId,
|
||||
"actions": [], // Notification actions are meaningless if they're not tracked by the server or the sender is dead
|
||||
"appIcon": notif.appIcon,
|
||||
"appName": notif.appName,
|
||||
"body": notif.body,
|
||||
"image": notif.image,
|
||||
"summary": notif.summary,
|
||||
"time": notif.time,
|
||||
"urgency": notif.urgency,
|
||||
});
|
||||
});
|
||||
// Find largest notificationId
|
||||
let maxId = 0
|
||||
root.list.forEach((notif) => {
|
||||
maxId = Math.max(maxId, notif.notificationId)
|
||||
})
|
||||
|
||||
console.log("[Notifications] File loaded")
|
||||
root.idOffset = maxId
|
||||
root.initDone()
|
||||
}
|
||||
onLoadFailed: (error) => {
|
||||
if(error == FileViewError.FileNotFound) {
|
||||
console.log("[Notifications] File not found, creating new file.")
|
||||
root.list = []
|
||||
notifFileView.setText(stringifyList(root.list));
|
||||
} else {
|
||||
console.log("[Notifications] Error loading file: " + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue