From 826e8ad8e6c6981b660a70e7c64f7474c992fe6a Mon Sep 17 00:00:00 2001 From: Data Date: Tue, 21 Apr 2026 22:09:33 +0200 Subject: [PATCH 1/8] feat: update push notification URL and add call notifications Update webhook URL from http://push-notifications.ad4m.dev:13000 to https://push.ad4m.dev. Add a second notification trigger for detecting when someone starts a call in any neighbourhood, so users get push notifications for calls in addition to @-mentions. Co-Authored-By: Claude Opus 4.6 --- app/src/utils/registerMobileNotifications.ts | 61 ++++++++++++++++---- packages/api/src/createCommunity.ts | 6 +- packages/api/src/joinCommunity.ts | 19 +++--- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/app/src/utils/registerMobileNotifications.ts b/app/src/utils/registerMobileNotifications.ts index e9c056ade..95767214e 100644 --- a/app/src/utils/registerMobileNotifications.ts +++ b/app/src/utils/registerMobileNotifications.ts @@ -4,11 +4,13 @@ import { ActionPerformed, PushNotificationSchema, PushNotifications, Token } fro import { Ad4mClient } from '@coasys/ad4m'; const APP_NAME = 'Flux'; -const DESCRIPTION = 'Mobile push notifications for @-mentions'; -function notificationConfig(perspectiveIds: string[], webhookAuth: string, agentDid: string) { +const WEBHOOK_URL = 'https://push.ad4m.dev/notification'; + +const MENTION_DESCRIPTION = 'Mobile push notifications for @-mentions'; +function mentionNotificationConfig(perspectiveIds: string[], webhookAuth: string, agentDid: string) { return { appName: APP_NAME, - description: DESCRIPTION, + description: MENTION_DESCRIPTION, appUrl: window.location.origin, appIconPath: window.location.origin + '/icon.png', trigger: `SELECT ?source ?predicate ?target WHERE { @@ -20,7 +22,26 @@ function notificationConfig(perspectiveIds: string[], webhookAuth: string, agent )) }`, perspectiveIds, - webhookUrl: 'http://push-notifications.ad4m.dev:13000/notification', + webhookUrl: WEBHOOK_URL, + webhookAuth, + }; +} + +const CALL_DESCRIPTION = 'Mobile push notifications for calls'; +function callNotificationConfig(perspectiveIds: string[], webhookAuth: string, agentDid: string) { + return { + appName: APP_NAME, + description: CALL_DESCRIPTION, + appUrl: window.location.origin, + appIconPath: window.location.origin + '/icon.png', + trigger: `SELECT ?source ?predicate ?target WHERE { + GRAPH ?g { ?source ?predicate ?target . } + FILTER(?predicate = ) + FILTER(CONTAINS(STR(?source), '"inCall":true')) + FILTER(!CONTAINS(STR(?source), '"${agentDid}"')) + }`, + perspectiveIds, + webhookUrl: WEBHOOK_URL, webhookAuth, }; } @@ -71,24 +92,40 @@ export async function registerNotification(client: Ad4mClient) { }); } + const agentStatus = await client.agent.status(); + const agentDid = agentStatus.did!; + let notifications = await client.runtime.notifications(); - let foundNotifications = notifications.filter( + + await ensureNotification(client, notifications, MENTION_DESCRIPTION, perspectiveIds, webhookAuth, agentDid, mentionNotificationConfig); + await ensureNotification(client, notifications, CALL_DESCRIPTION, perspectiveIds, webhookAuth, agentDid, callNotificationConfig); +} + +async function ensureNotification( + client: Ad4mClient, + notifications: any[], + description: string, + perspectiveIds: string[], + webhookAuth: string, + agentDid: string, + configFn: (perspectiveIds: string[], webhookAuth: string, agentDid: string) => any, +) { + let found = notifications.filter( (n) => n.appName == APP_NAME && - n.description == DESCRIPTION && + n.description == description && perspectiveIds.every((p) => n.perspectiveIds.includes(p)) && n.granted && n.webhookAuth == webhookAuth, ); - if (foundNotifications.length > 1) { - for (let i = 1; i < foundNotifications.length; i++) { - await client.runtime.removeNotification(foundNotifications[i].id); + if (found.length > 1) { + for (let i = 1; i < found.length; i++) { + await client.runtime.removeNotification(found[i].id); } } - if (foundNotifications.length == 0) { - const agentStatus = await client.agent.status(); - await client.runtime.requestInstallNotification(notificationConfig(perspectiveIds, webhookAuth, agentStatus.did!)); + if (found.length == 0) { + await client.runtime.requestInstallNotification(configFn(perspectiveIds, webhookAuth, agentDid)); } } diff --git a/packages/api/src/createCommunity.ts b/packages/api/src/createCommunity.ts index e4138dd0d..d2cb56799 100644 --- a/packages/api/src/createCommunity.ts +++ b/packages/api/src/createCommunity.ts @@ -93,10 +93,10 @@ export default async function createCommunity({ }), }); - // Update notifications to include the new community + // Update all Flux notifications to include the new community const notifications = await client.runtime.notifications(); - const notification = notifications.find((n) => n.appName === 'Flux'); - if (notification) { + const fluxNotifications = notifications.filter((n) => n.appName === 'Flux'); + for (const notification of fluxNotifications) { const notificationId = notification.id; notification.granted = undefined; notification.id = undefined; diff --git a/packages/api/src/joinCommunity.ts b/packages/api/src/joinCommunity.ts index a1605d839..c3c5cd490 100644 --- a/packages/api/src/joinCommunity.ts +++ b/packages/api/src/joinCommunity.ts @@ -27,17 +27,18 @@ export default async ({ joiningLink, client }: Payload): Promise => { await client.perspective.update(perspective.uuid, neighbourhoodMeta.name); const notifications = await client.runtime.notifications(); + const fluxNotifications = notifications.filter((n) => n.appName === 'Flux'); - const notification = notifications.find((notification) => notification.appName === 'Flux'); + for (const notification of fluxNotifications) { + const notificationId = notification.id; + delete notification.granted; + delete notification.id; - const notificationId = notification.id; - delete notification.granted; - delete notification.id; - - await client.runtime.updateNotification(notificationId, { - ...notification, - perspectiveIds: [...notification.perspectiveIds, perspective.uuid], - }); + await client.runtime.updateNotification(notificationId, { + ...notification, + perspectiveIds: [...(notification.perspectiveIds || []), perspective.uuid], + }); + } return { uuid: perspective!.uuid, From e4d4bb7128192601adad2033e3c4ee9cda18d5cd Mon Sep 17 00:00:00 2001 From: Data Date: Tue, 21 Apr 2026 23:10:38 +0200 Subject: [PATCH 2/8] feat: add call invite button with flux://call_invite links - Add InviteToCallPopover with "Invite All" + member dropdown - Send call invites as AD4M links: source=channel, predicate=flux://call_invite, target=DID - Broadcast signal for real-time notification + persist link for push notifications - Handle incoming invites: show toast + auto-open call window - Register push notification trigger for flux://call_invite Co-Authored-By: Claude Opus 4.6 --- .../call/controls/InviteToCallPopover.vue | 165 ++++++++++++++++++ .../call/controls/MainCallControls.vue | 3 + app/src/composables/useSignallingService.ts | 16 +- app/src/stores/webrtcStore.ts | 33 +++- app/src/utils/registerMobileNotifications.ts | 19 ++ 5 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 app/src/components/call/controls/InviteToCallPopover.vue diff --git a/app/src/components/call/controls/InviteToCallPopover.vue b/app/src/components/call/controls/InviteToCallPopover.vue new file mode 100644 index 000000000..67e81450f --- /dev/null +++ b/app/src/components/call/controls/InviteToCallPopover.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/app/src/components/call/controls/MainCallControls.vue b/app/src/components/call/controls/MainCallControls.vue index c4e332382..d7c464427 100644 --- a/app/src/components/call/controls/MainCallControls.vue +++ b/app/src/components/call/controls/MainCallControls.vue @@ -92,6 +92,8 @@ + +