Send notifications for events that are not announced when window is out of focus.

This commit is contained in:
Storm Dragon
2026-07-28 17:22:24 -04:00
parent 6a94f893b6
commit 9125e76d43
9 changed files with 161 additions and 19 deletions
+8 -8
View File
@@ -262,7 +262,7 @@ func scheduleHallUnlock(g *hall.Hall, issuer *webClient, deadline, now time.Time
}
entry.mu.Lock()
if entry.active {
broadcastHallInfo(g, message)
broadcastHallInfo(g, message, true)
}
entry.mu.Unlock()
return nil
@@ -280,7 +280,7 @@ func announceScheduledUnlock(entry *scheduledHallUnlock, remaining time.Duration
}
broadcastHallInfo(entry.hall, fmt.Sprintf(
"Hall will unlock in %s.", formatMinutesUntil(remaining),
))
), true)
}
func finishScheduledUnlock(entry *scheduledHallUnlock) {
@@ -296,17 +296,17 @@ func finishScheduledUnlock(entry *scheduledHallUnlock) {
result := entry.hall.UnlockIfOperatorPresent(entry.issuer)
switch result {
case hall.ConditionalUnlockSucceeded:
broadcastHallInfo(entry.hall, "Hall unlocked")
broadcastHallInfo(entry.hall, "Hall unlocked", true)
case hall.ConditionalUnlockClientAbsent:
broadcastHallInfo(entry.hall, fmt.Sprintf(
"Scheduled unlock cancelled because %s is no longer present in the hall.",
entry.issuerName,
))
), true)
case hall.ConditionalUnlockClientNotOperator:
broadcastHallInfo(entry.hall, fmt.Sprintf(
"Scheduled unlock cancelled because %s is no longer an operator.",
entry.issuerName,
))
), true)
}
entry.mu.Unlock()
hallUnlockSchedules.Unlock()
@@ -322,11 +322,11 @@ func setHallLockState(g *hall.Hall, locked bool, message string) bool {
g.SetLocked(locked, message)
if locked {
if entry != nil {
broadcastHallInfo(g, "Scheduled hall unlock cancelled because the hall was locked again.")
broadcastHallInfo(g, "Scheduled hall unlock cancelled because the hall was locked again.", true)
}
broadcastHallInfo(g, "Hall locked")
broadcastHallInfo(g, "Hall locked", false)
} else {
broadcastHallInfo(g, "Hall unlocked")
broadcastHallInfo(g, "Hall unlocked", false)
}
hallUnlockSchedules.Unlock()
return entry != nil
+50 -6
View File
@@ -191,13 +191,21 @@ func TestUnlockWarningTimes(t *testing.T) {
}
func latestHallInfo(messages []clientMessage) string {
message, ok := latestHallInfoMessage(messages)
if !ok {
return ""
}
text, _ := message.Value.(string)
return text
}
func latestHallInfoMessage(messages []clientMessage) (clientMessage, bool) {
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Type == "usermessage" && messages[i].Kind == "info" {
message, _ := messages[i].Value.(string)
return message
return messages[i], true
}
}
return ""
return clientMessage{}, false
}
func scheduledUnlockEntry(g *hall.Hall) *scheduledHallUnlock {
@@ -225,9 +233,28 @@ func TestScheduledUnlockRequiresIssuingOperator(t *testing.T) {
if got := len(entry.timers); got != 4 {
t.Fatalf("30-minute schedule has %d timers, want 4", got)
}
if got := latestHallInfo(drainMessages(participant)); !strings.Contains(got, "30 minutes") {
scheduleMessage, ok := latestHallInfoMessage(drainMessages(participant))
if !ok {
t.Fatal("schedule announcement was not sent")
}
if got, _ := scheduleMessage.Value.(string); !strings.Contains(got, "30 minutes") {
t.Fatalf("schedule announcement = %q", got)
}
if !scheduleMessage.Notify {
t.Fatal("schedule announcement did not request a browser notification")
}
announceScheduledUnlock(entry, 20*time.Minute)
warningMessage, ok := latestHallInfoMessage(drainMessages(participant))
if !ok {
t.Fatal("countdown announcement was not sent")
}
if got, _ := warningMessage.Value.(string); got != "Hall will unlock in 20 minutes." {
t.Fatalf("countdown announcement = %q", got)
}
if !warningMessage.Notify {
t.Fatal("countdown announcement did not request a browser notification")
}
hall.DelClient(operator)
drainMessages(participant)
@@ -257,9 +284,16 @@ func TestScheduledUnlockCompletesWhileIssuerIsOperator(t *testing.T) {
if locked, _ := g.Locked(); locked {
t.Fatal("hall remained locked while issuing operator was present")
}
if got := latestHallInfo(drainMessages(operator)); got != "Hall unlocked" {
unlockMessage, ok := latestHallInfoMessage(drainMessages(operator))
if !ok {
t.Fatal("unlock announcement was not sent")
}
if got, _ := unlockMessage.Value.(string); got != "Hall unlocked" {
t.Fatalf("unlock announcement = %q", got)
}
if !unlockMessage.Notify {
t.Fatal("scheduled unlock completion did not request a browser notification")
}
}
func TestScheduledUnlockRequiresOperatorPermissionAtDeadline(t *testing.T) {
@@ -386,9 +420,16 @@ func TestImmediateUnlockCommandCancelsScheduledUnlock(t *testing.T) {
if locked, _ := g.Locked(); locked {
t.Fatal("immediate unlock left hall locked")
}
if got := latestHallInfo(drainMessages(operator)); got != "Hall unlocked" {
unlockMessage, ok := latestHallInfoMessage(drainMessages(operator))
if !ok {
t.Fatal("immediate unlock announcement was not sent")
}
if got, _ := unlockMessage.Value.(string); got != "Hall unlocked" {
t.Fatalf("immediate unlock announcement = %q", got)
}
if unlockMessage.Notify {
t.Fatal("immediate unlock requested a scheduled-unlock browser notification")
}
}
func TestLockCommandCancelsScheduledUnlock(t *testing.T) {
@@ -426,6 +467,9 @@ func TestLockCommandCancelsScheduledUnlock(t *testing.T) {
for _, message := range messages {
if message.Kind == "info" && strings.Contains(message.Value.(string), "locked again") {
foundCancellation = true
if !message.Notify {
t.Fatal("scheduled-unlock cancellation did not request a browser notification")
}
}
}
if !foundCancellation {
+3 -1
View File
@@ -70,11 +70,12 @@ func broadcastRecordingStatus(g *hall.Hall, message string) {
}
}
func broadcastHallInfo(g *hall.Hall, message string) {
func broadcastHallInfo(g *hall.Hall, message string, notify bool) {
err := broadcast(g.GetClients(nil), clientMessage{
Type: "usermessage",
Kind: "info",
Privileged: true,
Notify: notify,
Value: message,
})
if err != nil {
@@ -188,6 +189,7 @@ type clientMessage struct {
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
Privileged bool `json:"privileged,omitempty"`
Notify bool `json:"notify,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Status *hall.Status `json:"status,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`