Only raise notifications for new messages (#663)

This commit is contained in:
Coracle-Bot 2026-09-26 03:45:58 +00:00 committed by hodlbod
parent 04360b212f
commit 173d996de3
9 changed files with 86 additions and 22 deletions

View file

@ -200,12 +200,10 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val body = "New activity"
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_chat)
.setContentTitle("Flotilla")
.setContentText(body)
.setContentText("New message")
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)

View file

@ -156,7 +156,7 @@ the story catalog in <code>e2e/USER_STORIES.md</code>.</p>
<tr>
<td>Push notifications</td>
<td><span class="status s-ok">Implemented</span></td>
<td><code>app/push/</code>, <code>settings/alerts</code>; web, FCM/APNs and Android fallback. US-086, US-120</td>
<td><code>app/push/</code>, <code>settings/alerts</code>; web, FCM/APNs and Android fallback. US-086, US-120, US-136</td>
</tr>
<tr>
<td>Threads</td>

View file

@ -1675,6 +1675,18 @@ Acceptance:
- Its body is the words bob wrote rather than the quote his reply is prepended
with, and a url in it is named by its host instead of spelled out.
### US-136 — Be notified about messages and nothing else
As alice, I want notifications to be about what people say to me, so that a
space with busy boards does not buzz my phone all day.
Acceptance:
- With push notifications on and the tab in the background, a message from bob
in a room alice is in raises one.
- A thread bob starts in the same room raises none, however much of it reaches
her client.
### US-105 — Land on the home page
As a new user, I want the home page to route me somewhere useful, so that I'm

View file

@ -9,6 +9,8 @@ import {
expect,
message,
messageActions,
modalForm,
noteEditor,
openRoomDetail,
pageBar,
pathPattern,
@ -867,3 +869,59 @@ test("US-120 read what a notification says", async ({seed, as}) => {
body: "sunday, the notice is at a link to harbor.example",
})
})
test("US-136 hear about a new message and not about a new thread", async ({seed, as}) => {
const scenario = await seed(({relay, user}) => {
const space = relay("space")
space.room("general", {name: "General"})
space.join(user.alice, "general")
space.join(user.bob, "general")
space.profile(user.alice, {name: "Alice Anchor"})
space.profile(user.bob, {name: "Bob Barnacle"})
})
const space = scenario.space("space")
// The tab has to be in the background before a notification is raised at all.
const alice = await as(users.alice, "/settings/alerts", {
context: {permissions: ["notifications"]},
})
await settingRow(alice, "Enable push notifications").getByRole("checkbox").check()
await alice.getByRole("button", {name: "Save Changes"}).click()
await expect(alice.getByRole("alert")).toContainText("Your settings have been saved!")
await spaceNavItem(alice, space.name).click()
await roomLink(alice, "General").click()
await expect(alice).toHaveURL(pathPattern(roomPath(space.url, "general")))
const notifications = await captureNotifications(alice)
const bob = await as(users.bob, `${spacePath(space.url)}/threads`)
const board = bob
.locator("section")
.filter({has: bob.getByRole("heading", {name: "General", exact: true})})
await board.getByRole("button", {name: "Create", exact: true}).click()
const threadForm = modalForm(bob, "Create a Thread")
await threadForm.getByPlaceholder("What is this thread about?").fill("the dock is closing")
await noteEditor(threadForm).pressSequentially("sunday, apparently")
await threadForm.getByRole("button", {name: "Create Thread"}).click()
await expect(board.getByRole("row").filter({hasText: "the dock is closing"})).toBeVisible()
// Alice holds the thread before bob says anything, so a notification for it would be here by now.
await contentNavItem(alice, "Threads").click()
await expect(alice.getByRole("row").filter({hasText: "the dock is closing"})).toBeVisible()
await postTo(bob, "General", "when does the dock close?")
await expect
.poll(() => notifications)
.toEqual([{title: "New message", body: "when does the dock close?"}])
})

View file

@ -65,7 +65,7 @@ test("US-086 configure alerts", async ({seed, as}) => {
const page = await as(users.alice, "/settings/alerts")
const sound = settingToggle(page, "Play sound for new activity")
const sound = settingToggle(page, "Play sound for new messages")
const alertTypes = page.locator("div.card").filter({has: page.getByText("Alert Types")})
await expect(sound).toBeChecked()
@ -98,7 +98,7 @@ test("US-086 configure alerts", async ({seed, as}) => {
await waitForToastToClear(page)
await page.reload()
await expect(settingToggle(page, "Play sound for new activity")).not.toBeChecked()
await expect(settingToggle(page, "Play sound for new messages")).not.toBeChecked()
await expect(reminders).toHaveCount(2)
await expect(reminders.nth(1).locator("input")).toHaveValue("1440")
})

View file

@ -11,7 +11,7 @@
<div class="flex flex-col gap-1">
<strong>Enable notifications for this space</strong>
<p class="text-xs opacity-75">
Get notified about new activity in this space. You can change this later in settings.
Get notified about new messages in this space. You can change this later in settings.
</p>
</div>
<input type="checkbox" class="toggle" bind:checked={notifications} />

View file

@ -1,7 +1,7 @@
import {writable} from "svelte/store"
import type {Subscriber, Unsubscriber} from "svelte/store"
import {Capacitor} from "@capacitor/core"
import {assoc, call, ms, now, on, parseJson, poll, spec, throttle, uniq} from "@welshman/lib"
import {call, ms, now, on, parseJson, poll, spec, throttle, uniq} from "@welshman/lib"
import {LOCAL_RELAY_URL} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {
@ -19,7 +19,7 @@ import {merged, withGetter} from "@welshman/store"
import {User} from "@welshman/app"
import {navigate} from "@app/modal"
import {app, messagingRelayLists, network, roomLists} from "@app/core"
import {DM_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
import {DM_KINDS} from "@app/content"
import {getMutedRooms, notificationSettings, shouldNotify, userSettingsValues} from "@app/settings"
import {makeEventPath, goToChat, goToSpace} from "@app/routes"
@ -47,10 +47,6 @@ export interface IPushAdapter {
}
export const onNotification = call(() => {
const allFilters = [
{kinds: [MESSAGE, ...CONTENT_KINDS, ...DM_KINDS]},
makeCommentFilter(CONTENT_KINDS),
]
const subscribers: Subscriber<TrustedEvent>[] = []
let unsubscribe: Unsubscriber | undefined
@ -62,7 +58,7 @@ export const onNotification = call(() => {
let unsubscribeRepository: Unsubscriber | undefined
const unsubscribeApp = app.subscribe($app => {
unsubscribeRepository?.()
const filters = allFilters.map(assoc("since", now()))
const filters = [{kinds: [MESSAGE, ...DM_KINDS], since: now()}]
unsubscribeRepository = on($app.repository, "update", ({added}: RepositoryUpdate) => {
const $pubkey = $app.user?.pubkey
@ -163,7 +159,7 @@ export const syncRelaySubscriptions = (
userSettingsValues,
]).subscribe(
throttle(3000, ([$spaceUrls, {spaces, mentions}, $settings]) => {
const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)]
const baseFilter = {kinds: [MESSAGE]}
for (const url of $spaceUrls) {
const {notify = true, exceptions = []} = $settings.alerts.find(spec({url})) || {}
@ -178,18 +174,18 @@ export const syncRelaySubscriptions = (
if (skipped.length > 0) {
ignore.push({"#h": skipped})
}
filters.push(...baseFilters)
filters.push(baseFilter)
} else {
const included = exceptions.filter(h => !muted.includes(h))
if (included.length > 0) {
filters.push(...baseFilters.map(f => ({...f, "#h": included})))
filters.push({...baseFilter, "#h": included})
}
}
}
if (mentions) {
filters.push(...baseFilters.map(f => ({...f, "#p": [$pubkey]})))
filters.push({...baseFilter, "#p": [$pubkey]})
}
sync(url, "spaces", filters, ignore)

View file

@ -68,7 +68,7 @@ export class WebNotifications implements IPushAdapter {
preview || "Someone tagged you in a message.",
)
} else if (spaces) {
this._notify(event, "New activity", preview || "Someone posted a new message.")
this._notify(event, "New message", preview || "Someone posted a new message.")
}
}
})

View file

@ -108,7 +108,7 @@
{#if !Capacitor.isNativePlatform()}
<FieldInline>
{#snippet label()}
<p>Play sound for new activity</p>
<p>Play sound for new messages</p>
{/snippet}
{#snippet input()}
<ToggleInput bind:checked={settings.sound} />
@ -131,7 +131,7 @@
<strong class="text-lg">Alert Types</strong>
<FieldInline>
{#snippet label()}
<p>Notify me about new activity</p>
<p>Notify me about new messages in my spaces</p>
{/snippet}
{#snippet input()}
<ToggleInput bind:checked={settings.spaces} />
@ -147,7 +147,7 @@
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Notify me about new messages</p>
<p>Notify me about new direct messages</p>
{/snippet}
{#snippet input()}
<ToggleInput bind:checked={settings.messages} />