openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessionSnooze.swift
Peter Steinberger 1128a4ae92
feat(ios): snooze sessions from the session menu (#162399)
* feat(ios): snooze sessions from the session menu

Add shared calendar presets and identity-bound snooze/wake patches while preserving existing Swift transport call sites. iOS gains Active, Snoozed, and Archived scopes, wake labels, and deadline-driven updates for Sessions, the sidebar, and Overview. Snoozing keeps ongoing work and the open conversation intact.

* test(ios): capture session snooze proof states

Extend the synthetic Gateway with awake and snoozed sessions. Capture Active and Snoozed lists, snooze presets, and Wake through the real iOS session menu, using All Sessions navigation and a row selector that excludes the retained sidebar.

* fix(ios): show the weekday for the next-week snooze preset

* chore(ios): record snooze strings in the native i18n source inventory

* fix(ios): format snooze wake labels through the localized catalog

Apple i18n verification rejects runtime-interpolated localized strings
because they bypass the generated catalog, so the "Wake session" menu item
and the "tomorrow" wake description now use the repository's
String(format: String(localized: "… %@")) pattern, and the native string
inventory records the format sources.

* refactor(ios): drop the superseded mutation lease initializer

* fix(ios): keep snoozed sessions browsable offline

Separate cached browsing scopes from connected mutation controls. Keep Archived connected-only and reset an archived selection to Active on disconnect. Cover scope availability and the offline cached roster, including snoozed rows and wake labels.
2026-10-01 10:22:22 +00:00

82 lines
3.3 KiB
Swift

import Foundation
public enum OpenClawChatSnoozePatch: Sendable {
case until(Date)
case wake
}
public enum OpenClawChatSessionSnooze: Sendable {
public struct Preset: Sendable {
public let id: String
public let title: String
public let wakeAt: Date
public init(id: String, title: String, wakeAt: Date) {
self.id = id
self.title = title
self.wakeAt = wakeAt
}
}
public static func presets(now: Date = .now, calendar: Calendar = .current) -> [Preset] {
guard let oneHour = calendar.date(byAdding: .hour, value: 1, to: now),
let threeHours = calendar.date(byAdding: .hour, value: 3, to: now)
else { return [] }
var presets = [
Preset(id: "hour", title: String(localized: "In 1 hour"), wakeAt: oneHour),
Preset(id: "three-hours", title: String(localized: "In 3 hours"), wakeAt: threeHours),
]
let today = calendar.startOfDay(for: now)
if let evening = calendar.date(bySettingHour: 18, minute: 0, second: 0, of: today), evening > oneHour {
presets.append(Preset(id: "evening", title: String(localized: "This evening"), wakeAt: evening))
}
guard let tomorrow = calendar.date(byAdding: .day, value: 1, to: today),
let tomorrowMorning = calendar.date(bySettingHour: 9, minute: 0, second: 0, of: tomorrow)
else { return presets }
presets.append(Preset(id: "tomorrow", title: String(localized: "Tomorrow"), wakeAt: tomorrowMorning))
let daysUntilMonday = (9 - calendar.component(.weekday, from: today)) % 7
if let monday = calendar.date(byAdding: .day, value: daysUntilMonday == 0 ? 7 : daysUntilMonday, to: today),
let mondayMorning = calendar.date(bySettingHour: 9, minute: 0, second: 0, of: monday),
mondayMorning != tomorrowMorning
{
presets.append(Preset(id: "next-week", title: String(localized: "Next week"), wakeAt: mondayMorning))
}
return presets
}
public static func wakeDescription(
_ wakeAt: Date,
now: Date = .now,
calendar: Calendar = .current) -> String
{
var style = Date.FormatStyle(
locale: calendar.locale ?? .current,
calendar: calendar,
timeZone: calendar.timeZone)
.hour().minute()
let time = wakeAt.formatted(style)
if calendar.isDate(wakeAt, inSameDayAs: now) {
return time
}
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now),
calendar.isDate(wakeAt, inSameDayAs: tomorrow)
{
return String(format: String(localized: "tomorrow %@"), time)
}
if let nextWeek = calendar.date(byAdding: .day, value: 7, to: now), wakeAt > now, wakeAt <= nextWeek {
style = style.weekday(.abbreviated)
} else {
style = style.month(.abbreviated).day()
}
return wakeAt.formatted(style)
}
public static func nextWake(in entries: [OpenClawChatSessionEntry], now: Date = .now) -> Date? {
entries.lazy
.filter { $0.isSnoozed(at: now) }
.compactMap(\.snoozedUntil)
.min()
.map { Date(timeIntervalSince1970: $0 / 1000) }
}
}