CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
Web client (also what the desktop app runs): - Simplified Sliding Sync (MSC4186) when the homeserver advertises org.matrix.simplified_msc3575. src/matrix/sliding.ts answers the SDK's GET /sync with a sliding sync request and rewrites the response into the /sync shape, as the Android app does; the SDK's own sliding sync mode doesn't sort thread replies into threads. 20 rooms first, then every room (up to 1000); to_device, e2ee, account data, receipts and typing extensions; M_UNKNOWN_POS starts a new connection. The SDK's /members?at=<sync token> drops the token, which here is ours. Settings can switch back to /sync. - Notifications for new messages and incoming calls while Warp isn't in front, by the account's push rules, evaluated after decryption. The browser's Notification API once turned on in Settings, or the desktop app's bridge (window.warpDesktop). One per room; a click opens it. - Voice messages: record from the composer, sent as m.audio with MSC3245's voice marker and MSC1767's duration and waveform (encrypted in encrypted rooms); a waveform player in the timeline. - @warp/ui: a slot for extra composer buttons; bell-off, play and pause icons. Desktop app: - System notifications through Wails' notifications service, fed by /_desktop/bridge.js (/_desktop/notify, JSON only); clicking one shows the window and opens the room. - Tray icon with Open and Quit; closing the window hides it (a one-time notification says so), File > Quit ends it. The page title goes to the window and the tray tooltip, the unread count to the taskbar or Dock badge. - Auto-update from a Wails update manifest at DESKTOP_UPDATE_URL: a quiet check a minute after start and every six hours, a notification when a newer version exists, Help > Check for updates opens the update window. Optional signature key (DESKTOP_UPDATE_KEY). Linux package installs leave updates to the package manager. build/manifest writes the manifest. - Per-user NSIS installer (make desktop-windows-installer), the macOS bundle zipped for the updater, a desktop CI job, and a Desktop release workflow that builds all three platforms and the manifest. Tested on Windows against a local homeserver: sliding sync with E2EE, threads and a server restart; notifications (toast history), close to tray, relaunch restoring the window; voice messages recorded, sent and played, encrypted too; the update check finding a newer version. Not tested: installing an update, the NSIS installer, CI, macOS and Linux. Code signing needs certificates and isn't done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
5.8 KiB
Go
212 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"regexp"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
"github.com/wailsapp/wails/v3/pkg/events"
|
|
"github.com/wailsapp/wails/v3/pkg/services/dock"
|
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
|
|
|
"github.com/dariuszjaniak/warp/apps/desktop/internal/shell"
|
|
)
|
|
|
|
// desktop is what the app adds around the window: system notifications,
|
|
// the tray icon, the unread count on the taskbar or Dock, and closing to
|
|
// the tray. A chat app that quits when its window closes misses the
|
|
// messages it was meant to tell you about.
|
|
type desktop struct {
|
|
app *application.App
|
|
window *application.WebviewWindow
|
|
tray *application.SystemTray
|
|
notes *notifications.NotificationService
|
|
dock *dock.DockService
|
|
store *shell.Store
|
|
logger *slog.Logger
|
|
|
|
quitting atomic.Bool
|
|
// onUpdate installs an update: an update notification was clicked.
|
|
onUpdate func()
|
|
|
|
mu sync.Mutex
|
|
title string
|
|
unread int
|
|
}
|
|
|
|
func newDesktop(store *shell.Store, logger *slog.Logger) *desktop {
|
|
return &desktop{
|
|
notes: notifications.New(),
|
|
dock: dock.New(),
|
|
store: store,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// services are registered with the app; they start and stop with it.
|
|
func (d *desktop) services() []application.Service {
|
|
return []application.Service{application.NewService(d.notes), application.NewService(d.dock)}
|
|
}
|
|
|
|
// shouldQuit is the app's ShouldQuit: every way of quitting (the menus,
|
|
// Cmd+Q, signing out of the OS) passes here, and from then on closing the
|
|
// window closes it.
|
|
func (d *desktop) shouldQuit() bool {
|
|
d.quitting.Store(true)
|
|
return true
|
|
}
|
|
|
|
func (d *desktop) quit() {
|
|
d.quitting.Store(true)
|
|
d.app.Quit()
|
|
}
|
|
|
|
// attach sets up the tray and window behaviour, once both exist.
|
|
func (d *desktop) attach(app *application.App, window *application.WebviewWindow, icon []byte) {
|
|
d.app = app
|
|
d.window = window
|
|
|
|
menu := app.NewMenu()
|
|
menu.Add("Open Warp").OnClick(func(*application.Context) { d.show() })
|
|
menu.AddSeparator()
|
|
menu.Add("Quit Warp").OnClick(func(*application.Context) { d.quit() })
|
|
d.tray = app.SystemTray.New().SetIcon(icon).SetMenu(menu).OnClick(d.show)
|
|
d.tray.SetTooltip("Warp")
|
|
|
|
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
|
if d.quitting.Load() {
|
|
return
|
|
}
|
|
e.Cancel()
|
|
window.Hide()
|
|
d.hintOnce()
|
|
})
|
|
// The Dock icon of a running app whose window is hidden.
|
|
app.Event.OnApplicationEvent(events.Mac.ApplicationShouldHandleReopen, func(*application.ApplicationEvent) { d.show() })
|
|
|
|
d.notes.OnNotificationResponse(d.onNotificationResponse)
|
|
}
|
|
|
|
func (d *desktop) show() {
|
|
d.window.Show()
|
|
d.window.Restore()
|
|
d.window.Focus()
|
|
}
|
|
|
|
// hintOnce says, the first time the window is closed, where the app went.
|
|
func (d *desktop) hintOnce() {
|
|
st, err := d.store.Load()
|
|
if err != nil || st.TrayHintShown {
|
|
return
|
|
}
|
|
st.TrayHintShown = true
|
|
if err := d.store.Save(st); err != nil {
|
|
d.logger.Warn("can't save the settings", "err", err)
|
|
}
|
|
err = d.notes.SendNotification(notifications.NotificationOptions{
|
|
ID: "warp-tray-hint",
|
|
Title: "Warp is still running",
|
|
Body: "It keeps checking for messages. Open it or quit from the tray icon.",
|
|
Sound: ¬ifications.NotificationSound{Silent: true},
|
|
})
|
|
if err != nil {
|
|
d.logger.Warn("can't show a notification", "err", err)
|
|
}
|
|
}
|
|
|
|
// notify is the shell's Notify: one notification per room, replacing the
|
|
// room's last one where the system can. No ThreadID: Windows shows it as a
|
|
// header, and a room ID is no title.
|
|
func (d *desktop) notify(n shell.Notification) error {
|
|
return d.notes.SendNotification(notifications.NotificationOptions{
|
|
ID: n.ID,
|
|
Title: n.Title,
|
|
Body: n.Body,
|
|
Data: map[string]any{"roomId": n.RoomID},
|
|
})
|
|
}
|
|
|
|
// onNotificationResponse brings the window forward and opens the room of
|
|
// the notification that was clicked.
|
|
func (d *desktop) onNotificationResponse(r notifications.NotificationResult) {
|
|
if r.Error != nil {
|
|
d.logger.Warn("notification response", "err", r.Error)
|
|
return
|
|
}
|
|
if action, _ := r.Response.UserInfo["action"].(string); action == "update" && d.onUpdate != nil {
|
|
d.onUpdate()
|
|
return
|
|
}
|
|
d.show()
|
|
roomID, _ := r.Response.UserInfo["roomId"].(string)
|
|
if roomID == "" {
|
|
return
|
|
}
|
|
detail, _ := json.Marshal(roomID)
|
|
d.window.ExecJS(fmt.Sprintf(`window.dispatchEvent(new CustomEvent('warp-desktop:open-room', { detail: %s }))`, detail))
|
|
}
|
|
|
|
// notifyUpdate says that a newer version is ready to install.
|
|
func (d *desktop) notifyUpdate(version string) {
|
|
err := d.notes.SendNotification(notifications.NotificationOptions{
|
|
ID: "warp-update",
|
|
Title: "Warp " + version + " is available",
|
|
Body: "Click to install it, or use Help → Check for updates.",
|
|
Data: map[string]any{"action": "update"},
|
|
})
|
|
if err != nil {
|
|
d.logger.Warn("can't show a notification", "err", err)
|
|
}
|
|
}
|
|
|
|
// unreadPrefix is how the client's page title counts unread rooms: "(3) Room · Warp".
|
|
var unreadPrefix = regexp.MustCompile(`^\((\d+)\) `)
|
|
|
|
// unreadIn returns the unread count in a page title, 0 when it has none.
|
|
func unreadIn(title string) int {
|
|
m := unreadPrefix.FindStringSubmatch(title)
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
n, _ := strconv.Atoi(m[1])
|
|
return n
|
|
}
|
|
|
|
// setTitle is the shell's SetTitle: the page's title goes to the window
|
|
// and the tray, its unread count to the taskbar or Dock.
|
|
func (d *desktop) setTitle(title string) {
|
|
if title == "" {
|
|
title = "Warp"
|
|
}
|
|
n := unreadIn(title)
|
|
d.mu.Lock()
|
|
changed, countChanged := title != d.title, n != d.unread
|
|
d.title, d.unread = title, n
|
|
d.mu.Unlock()
|
|
if changed {
|
|
d.window.SetTitle(title)
|
|
d.tray.SetTooltip(title)
|
|
}
|
|
if !countChanged {
|
|
return
|
|
}
|
|
var err error
|
|
if n == 0 {
|
|
err = d.dock.RemoveBadge()
|
|
} else {
|
|
label := strconv.Itoa(n)
|
|
if n > 99 {
|
|
label = "99+"
|
|
}
|
|
err = d.dock.SetBadge(label)
|
|
}
|
|
if err != nil {
|
|
d.logger.Debug("can't set the badge", "err", err)
|
|
}
|
|
}
|