Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ debug/
release/
tests/build/
tests/Testing/
build-test/

# Compiled translations
*.qm

# Deepwork session state
.slim/deepwork/

# IDE
.idea/
.vscode/
Expand Down
2 changes: 2 additions & 0 deletions .ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
!.slim/deepwork/
!.slim/deepwork/**
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@

Unofficial Pushover client for SailfishOS. Real-time notifications via WebSocket, background daemon with systemd integration.

## Deep Sleep
## Background Delivery & Battery

SailfishOS has no system-level push service. WebSocket connections break in deep sleep. For reliable notifications:
- **Screen on:** persistent WebSocket for instant delivery.
- **Screen off:** polls Pushover on the configured interval (1–30 min, default 5). Messages are queued server-side, so nothing is lost.

```bash
mcetool --set-suspend-policy=early
```
Want real-time delivery while the screen is off? Enable **Keep Connection Alive When Screen Off** in Settings — it uses a per-app keepalive (higher battery use, but scoped to SailPush rather than the whole device).

This keeps the CPU and network alive when the screen is off. Without it, notifications arrive when the device wakes up (delayed up to the polling interval).
> Note: while the screen is off, polling only fires while the device is awake; wake-from-deep-sleep polling is not yet supported. After a daemon restart, open the app to catch any messages that didn't notify.

## Install

Expand All @@ -37,6 +36,13 @@ qmake5 && make # local build
mb2 build # RPM build
```

The unit tests under `tests/` build and run on a regular Linux box with Qt5
(no Sailfish SDK needed for the pure-Qt suites):

```bash
cmake -S tests -B tests/build && cmake --build tests/build && ctest --test-dir tests/build
```

## Architecture

Two-process model: **UI** (QML) communicates with **daemon** (background, systemd) over D-Bus (`com.zackslash.sailpush`). The daemon maintains a persistent WebSocket to Pushover servers and handles notifications.
Expand Down
17 changes: 3 additions & 14 deletions qml/pages/SettingsPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ Page {
text: qsTr("Status: %1").arg(rootDaemon.connectionState)
color: {
if (rootDaemon.connectionState === "ready" || rootDaemon.connectionState === "connected") return Theme.highlightColor
if (rootDaemon.connectionState === "error") return "#CC0000"
return Theme.secondaryColor
}
}
Expand Down Expand Up @@ -115,13 +114,13 @@ Page {
}

SectionHeader {
text: qsTr("Deep Sleep")
text: qsTr("Power")
}

TextSwitch {
x: Theme.horizontalPageMargin
width: page.width - 2 * Theme.horizontalPageMargin
text: qsTr("Prevent Deep Sleep During Sync")
text: qsTr("Keep Connection Alive When Screen Off")
checked: page.preventDeepSleep
onCheckedChanged: {
page.preventDeepSleep = checked
Expand All @@ -133,16 +132,7 @@ Page {
Label {
x: Theme.horizontalPageMargin
width: page.width - 2 * Theme.horizontalPageMargin
text: qsTr("Keeps CPU awake during message sync to ensure reliable delivery. Uses MCE keepalive API.")
color: Theme.secondaryColor
wrapMode: Text.WordWrap
font.pixelSize: Theme.fontSizeSmall
}

Label {
x: Theme.horizontalPageMargin
width: page.width - 2 * Theme.horizontalPageMargin
text: qsTr("Note: For persistent WebSocket in deep sleep, run:\nmcetool --set-suspend-policy=early")
text: qsTr("Keeps the WebSocket alive while the screen is off for real-time delivery, using the per-app MCE keepalive API. Higher battery use.")
color: Theme.secondaryColor
wrapMode: Text.WordWrap
font.pixelSize: Theme.fontSizeSmall
Expand All @@ -169,7 +159,6 @@ Page {
}

TextSwitch {
id: autoStartSwitch
x: Theme.horizontalPageMargin
width: page.width - 2 * Theme.horizontalPageMargin
text: qsTr("Start on boot")
Expand Down
26 changes: 19 additions & 7 deletions qml/sailpush.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ApplicationWindow {
cover: Qt.resolvedUrl("cover/CoverPage.qml")

property bool hasCredentials: loginHelper ? loginHelper.hasCredentials : false
property bool credentialsLoading: loginHelper ? loginHelper.credentialsLoading : false
property string pendingMessageId: ""
property string lastNavigatedMessageId: ""

Expand All @@ -35,7 +36,9 @@ ApplicationWindow {
}

onHasCredentialsChanged: {
if (hasCredentials && pageStack.currentPage != null) {
// Skip during initial async credential load — the initial page is
// pushed by onCredentialsLoadingChanged once the load finishes.
if (hasCredentials && !credentialsLoading && pageStack.currentPage != null) {
daemonConnector.reloadCredentials()
pageStack.replace(Qt.resolvedUrl("pages/MainPage.qml"), {rootDaemon: daemonConnector})
}
Expand Down Expand Up @@ -70,6 +73,21 @@ ApplicationWindow {
}

Component.onCompleted: {
// If credentials are still loading asynchronously, defer the initial
// page push until onCredentialsLoadingChanged fires. This avoids a
// LoginPage → MainPage flicker when stored credentials are found.
if (!credentialsLoading) {
pushInitialPage()
}
}

onCredentialsLoadingChanged: {
if (!credentialsLoading) {
pushInitialPage()
}
}

function pushInitialPage() {
if (hasCredentials) {
pageStack.push(Qt.resolvedUrl("pages/MainPage.qml"), {rootDaemon: daemonConnector})
checkPendingOpenMessage()
Expand Down Expand Up @@ -149,7 +167,6 @@ ApplicationWindow {
}
}

// When messages are refreshed (e.g., after daemon sync), check for pending message
Connections {
target: daemonConnector
onMessagesChanged: {
Expand All @@ -162,11 +179,6 @@ ApplicationWindow {
}
}

function navigateToMainPage() {
pageStack.clear()
pageStack.push(Qt.resolvedUrl("pages/MainPage.qml"), {rootDaemon: daemonConnector})
}

function navigateToLoginPage() {
pageStack.clear()
pageStack.push(Qt.resolvedUrl("pages/LoginPage.qml"))
Expand Down
22 changes: 16 additions & 6 deletions sailpush.pro
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
TARGET = sailpush

CONFIG += sailfishapp
QT += core gui quick qml network websockets dbus multimedia
QT += core gui quick qml network websockets dbus multimedia concurrent
PKGCONFIG += sailfishsecrets

# Auto-detect version from git. CI builds (tarball, no .git) use sed-replaced
Expand All @@ -20,6 +20,7 @@ SOURCES += src/main.cpp \
src/notificationmanager.cpp \
src/networkmonitor.cpp \
src/cpukeepalive.cpp \
src/displaymonitor.cpp \
src/loginhelper.cpp \
src/soundplayer.cpp

Expand All @@ -31,11 +32,12 @@ HEADERS += src/message.h \
src/messagestore.h \
src/dbusinterface.h \
src/notificationmanager.h \
src/networkmonitor.h \
src/cpukeepalive.h \
src/loginhelper.h \
src/soundplayer.h \
src/daemon.h
src/networkmonitor.h \
src/cpukeepalive.h \
src/displaymonitor.h \
src/loginhelper.h \
src/soundplayer.h \
src/daemon.h

DISTFILES += qml/sailpush.qml \
qml/cover/CoverPage.qml \
Expand All @@ -53,6 +55,14 @@ DISTFILES += qml/sailpush.qml \
SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172

# Translations — European languages
# lupdate_only is read by lupdate but ignored by qmake/make, so QML sources
# are scanned for qsTr() without affecting the build.
lupdate_only {
SOURCES += qml/sailpush.qml \
qml/cover/CoverPage.qml \
qml/pages/*.qml \
qml/components/*.qml
}
TRANSLATIONS += \
translations/sailpush_de.ts \
translations/sailpush_fr.ts \
Expand Down
Loading