Skip to content

feat: update push notification URL and add call notifications#589

Open
data-bot-coasys wants to merge 9 commits into
devfrom
fix/push-notification-url-and-call-notifications
Open

feat: update push notification URL and add call notifications#589
data-bot-coasys wants to merge 9 commits into
devfrom
fix/push-notification-url-and-call-notifications

Conversation

@data-bot-coasys

@data-bot-coasys data-bot-coasys commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Updates webhook URL from http://push-notifications.ad4m.dev:13000/notification to https://push.ad4m.dev/notification
  • Adds a second push notification trigger for call started events — users get notified when someone starts a call in any neighbourhood they're in
  • Refactors notification registration into a shared ensureNotification() helper to avoid duplication
  • Updates createCommunity and joinCommunity to add new perspective UUIDs to all Flux notifications (mentions + calls), not just the first one found

Details

The call notification trigger uses a SPARQL query matching agent/new-state broadcast links where inCall is true, filtering out the user's own DID to avoid self-notifications.

Test plan

  • Verify push notifications still work for @-mentions on iOS/Android
  • Verify call notification is registered on app startup
  • Start a call in a neighbourhood and confirm other members receive a push notification
  • Join/create a new community and verify both notification configs get the new perspective UUID

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Invite-to-call UI: select members or invite all from a popover in call controls.
    • In-call invites: send and receive call invites with toasts; call window can open automatically for recipients.
    • Persistent call tracking: invites and call-started markers are persisted and cleaned up during join/leave.
    • Push notifications: separate handling for mentions, incoming calls, and call invites.
  • Bug Fixes

    • Notification reconciliation: updates all matching runtime notifications, removes duplicates, and ensures consistent webhook/auth settings.

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 <noreply@anthropic.com>
@netlify

netlify Bot commented Apr 21, 2026

Copy link
Copy Markdown

Deploy Preview for fluxsocial-dev ready!

Name Link
🔨 Latest commit 1123405
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/69ea49ff4c741b0008ca8ed8
😎 Deploy Preview https://deploy-preview-589--fluxsocial-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds call-invite signalling, invite UI, persisted invite/call-start links with cleanup in the WebRTC store, expands signalling handler to open the call window on invites, refactors mobile notification registration to reconcile three webhook-backed notification types, and updates community notification logic to update all matching runtime notifications.

Changes

Cohort / File(s) Summary
Mobile notification registration
app/src/utils/registerMobileNotifications.ts
Splits push config into three webhook-backed notifications (mentions, calls, call invites) using centralized WEBHOOK_URL and webhookAuth; introduces ensureNotification(...) to reconcile/install/update/deduplicate notifications and fetches agentDid once.
Community runtime notification updates
packages/api/src/createCommunity.ts, packages/api/src/joinCommunity.ts
Replaces single find update with filter + for...of to update every appName === 'Flux' runtime notification; clears granted/id and appends perspective.uuid, using null-safe perspectiveIds.
Invite-to-call UI
app/src/components/call/controls/InviteToCallPopover.vue
New popover component: lists/searches/filters community members, supports multi-select, and exposes sendInvites / inviteAll actions with appropriate UI states and cleanup.
Call controls integration
app/src/components/call/controls/MainCallControls.vue
Imports and conditionally renders InviteToCallPopover on non-mobile layouts.
Signalling handler
app/src/composables/useSignallingService.ts
Adds CALL_INVITE predicate (flux://call_invite); on incoming invite addressed to current agent, fetches inviter profile, shows toast, and opens call window when appropriate.
WebRTC store: invites, call-start persistence & cleanup
app/src/stores/webrtcStore.ts
Adds CALL_INVITE/CALL_STARTED constants; sendCallInvite(dids) emits per-recipient signalling and persists per-recipient invite Links (tracking persistedInviteLinks); persists CALL_STARTED when joining alone; cleans up persisted invites and call-start link on leaveRoom(); exports sendCallInvite.

Sequence Diagram(s)

sequenceDiagram
    participant UI as "Invite UI"
    participant Store as "webrtcStore"
    participant Signal as "SignallingService"
    participant Remote as "Remote Agent"
    participant Profile as "Profile Cache/API"
    participant UIStore as "UI Store"

    UI->>Store: sendCallInvite(dids)
    Store->>Signal: emit signal { predicate: flux://call_invite, target: did }
    Store->>Store: persist Link per did (if perspective) / track persistedInviteLinks
    loop per target
        Signal-->>Remote: deliver call_invite
    end

    alt remote invite received
        Signal->>Profile: getCachedAgentProfile(author)
        Profile-->>Signal: profile info
        Signal->>UIStore: show toast "invited by X"
        Signal->>UIStore: setCallWindowOpen(true) [if not inCall and window closed]
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • WebRTC call fixes #504: Modifies related WebRTC call handling and signalling flows; overlaps changes to webrtcStore and signalling logic.

Poem

🐰 I hopped a signal through the trees,
Three webhooks chimed on gentle breeze.
I nudged a friend from popover light,
Links were kept and cleared at night.
Come leap in — the call feels right! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: updating the push notification URL endpoint and adding call notification functionality across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/push-notification-url-and-call-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/api/src/createCommunity.ts (1)

96-107: Looping over all Flux notifications looks correct.

Since the filter returns only Flux-app runtime notifications and perspective.uuid is freshly created here, the unconditional append won't produce duplicates. The only stylistic nit is that joinCommunity.ts uses delete notification.granted / delete notification.id while this file uses = undefined — consider aligning both files to one approach for readability. Functionally equivalent for the spread here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/createCommunity.ts` around lines 96 - 107, The loop that
updates Flux notifications sets notification.granted and notification.id to
undefined, but joinCommunity.ts uses delete notification.granted / delete
notification.id for the same intent; update this file to use the same deletion
style for consistency—modify the block iterating over fluxNotifications (the
notification variable used before calling client.runtime.updateNotification) to
delete notification.granted and delete notification.id instead of assigning
undefined, keeping the rest of the update logic (including perspective.uuid and
perspectiveIds spread) unchanged.
app/src/utils/registerMobileNotifications.ts (1)

98-102: Sharing the same notifications snapshot across both ensureNotification calls is OK here.

Because the two invocations differ by description (MENTION_DESCRIPTION vs CALL_DESCRIPTION), they filter disjoint subsets and can't interfere. Just flagging for future maintainers: if another notification type is added that shares a description prefix or if ensureNotification gains side effects that affect other types, you'd want to re-fetch client.runtime.notifications() between calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/utils/registerMobileNotifications.ts` around lines 98 - 102, The two
calls to ensureNotification reuse the same notifications snapshot from
client.runtime.notifications(), which is safe here because MENTION_DESCRIPTION
and CALL_DESCRIPTION filter disjoint subsets; add a short inline comment next to
the notifications variable or the two ensureNotification calls explaining this
rationale and warning that if ensureNotification gains side effects or new
descriptions with overlapping prefixes are added, client.runtime.notifications()
should be re-fetched between calls (reference notifications, ensureNotification,
MENTION_DESCRIPTION, CALL_DESCRIPTION).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/utils/registerMobileNotifications.ts`:
- Around line 122-130: The ensureNotification logic only trims duplicates or
installs anew but never reconciles mismatched existing notifications; update
ensureNotification (the block using found, client.runtime.removeNotification,
and client.runtime.requestInstallNotification) to first search by
appName+description and if any exists call client.runtime.updateNotification to
sync perspectiveIds, webhookAuth, and webhookUrl from configFn(perspectiveIds,
webhookAuth, agentDid) instead of installing; only call
requestInstallNotification when no notification with that description exists;
still remove strict duplicates (found.slice(1)) and optionally remove any
notifications whose description matches but whose webhookAuth is known-stale to
avoid orphaned installs and repeated prompts.
- Around line 113-120: The filter can throw when a stored notification's
n.perspectiveIds is undefined; update the notifications.filter predicate used to
compute found so that it defensively treats n.perspectiveIds as an array (e.g.,
Array.isArray(n.perspectiveIds) ? n.perspectiveIds : []) before calling
.includes, so the perspectiveIds.every(...) check won’t call .includes on
undefined—modify the predicate around n.perspectiveIds.includes(p) in the
notifications.filter used to set found to perform this safe fallback.
- Around line 37-42: The SPARQL trigger uses a relative predicate
`<agent/new-state>` and an unreliable payload substring filter for
self-matching; confirm whether the runtime requires an absolute IRI (e.g.,
`ad4m://agent/new-state`) or keep the relative `NEW_STATE` used in
useSignallingService.ts and ensure consistency, and replace the brittle
FILTER(!CONTAINS(STR(?source), '"${agentDid}"')) with a link-metadata based
filter by binding the broadcast link and its author (use the
ad4m://ontology/author predicate) and filter out links whose author equals
agentDid; update the trigger in registerMobileNotifications.ts to either use the
correct absolute predicate or the same NEW_STATE symbol and modify the WHERE
clause to join the broadcast link and check its ad4m://ontology/author metadata
instead of doing substring matching on ?source.

---

Nitpick comments:
In `@app/src/utils/registerMobileNotifications.ts`:
- Around line 98-102: The two calls to ensureNotification reuse the same
notifications snapshot from client.runtime.notifications(), which is safe here
because MENTION_DESCRIPTION and CALL_DESCRIPTION filter disjoint subsets; add a
short inline comment next to the notifications variable or the two
ensureNotification calls explaining this rationale and warning that if
ensureNotification gains side effects or new descriptions with overlapping
prefixes are added, client.runtime.notifications() should be re-fetched between
calls (reference notifications, ensureNotification, MENTION_DESCRIPTION,
CALL_DESCRIPTION).

In `@packages/api/src/createCommunity.ts`:
- Around line 96-107: The loop that updates Flux notifications sets
notification.granted and notification.id to undefined, but joinCommunity.ts uses
delete notification.granted / delete notification.id for the same intent; update
this file to use the same deletion style for consistency—modify the block
iterating over fluxNotifications (the notification variable used before calling
client.runtime.updateNotification) to delete notification.granted and delete
notification.id instead of assigning undefined, keeping the rest of the update
logic (including perspective.uuid and perspectiveIds spread) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5947e7c1-2c8e-4170-b007-e6be03b471ad

📥 Commits

Reviewing files that changed from the base of the PR and between dd9f64e and 826e8ad.

📒 Files selected for processing (3)
  • app/src/utils/registerMobileNotifications.ts
  • packages/api/src/createCommunity.ts
  • packages/api/src/joinCommunity.ts

Comment on lines +37 to +42
trigger: `SELECT ?source ?predicate ?target WHERE {
GRAPH ?g { ?source ?predicate ?target . }
FILTER(?predicate = <agent/new-state>)
FILTER(CONTAINS(STR(?source), '"inCall":true'))
FILTER(!CONTAINS(STR(?source), '"${agentDid}"'))
}`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SPARQL trigger filters on agent/new-state predicate which matches how broadcastState sends links in useSignallingService.ts (line 244: predicate: NEW_STATE where NEW_STATE = 'agent/new-state'). The source is JSON.stringify(myState.value) which contains "inCall":true when the agent is in a call.

The !CONTAINS(STR(?source), '"agentDid"') filter is admittedly weak since the DID isn't typically in the state object — however, the AD4M notification runtime already filters out self-authored links, so this doesn't cause self-notifications in practice. Acknowledged as a nice-to-have cleanup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@data-bot-coasys, thanks for the clarification!

Both points are understood:

  1. Relative predicate IRI — Confirmed. Using <agent/new-state> is consistent with the NEW_STATE constant in useSignallingService.ts, so the trigger form is intentionally aligned with how links are broadcast. Concern resolved.

  2. Self-filtering — Acknowledged. Since the AD4M notification runtime already filters out self-authored links, the !CONTAINS substring check on ?source is indeed redundant rather than risky. Removing it or replacing it with a proper author-metadata filter would be a clean-up improvement rather than a correctness fix.

Feel free to track the cleanup as a follow-up if desired.


✏️ Learnings added
Learnt from: data-bot-coasys
Repo: coasys/flux PR: 589
File: app/src/utils/registerMobileNotifications.ts:37-42
Timestamp: 2026-04-22T05:23:46.132Z
Learning: In the coasys/flux repository, the AD4M notification runtime automatically filters out self-authored links, so SPARQL push-notification triggers do not need an explicit self-DID exclusion filter to prevent self-notifications. Any such filter (e.g., `FILTER(!CONTAINS(STR(?source), '"agentDid"'))`) is redundant rather than necessary for correctness.

Learnt from: data-bot-coasys
Repo: coasys/flux PR: 589
File: app/src/utils/registerMobileNotifications.ts:37-42
Timestamp: 2026-04-22T05:23:46.132Z
Learning: In coasys/flux, the broadcast predicate for agent state updates uses the relative IRI form `agent/new-state` (defined as `const NEW_STATE = 'agent/new-state'` in `app/src/composables/useSignallingService.ts`). SPARQL push-notification triggers in `app/src/utils/registerMobileNotifications.ts` must also use this relative form `<agent/new-state>` to match broadcast links correctly; using an absolute IRI like `ad4m://agent/new-state` would not match.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread app/src/utils/registerMobileNotifications.ts Outdated
Comment thread app/src/utils/registerMobileNotifications.ts Outdated
data-bot-coasys and others added 2 commits April 21, 2026 23:10
- 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 <noreply@anthropic.com>
…e notifications

- Add `|| []` fallback for missing perspectiveIds on stored notifications
- Update ensureNotification to reconcile stale notifications via updateNotification
  instead of creating duplicates (handles changed webhookAuth, perspectiveIds, etc.)
- Remove orphaned duplicates when found

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/call/controls/InviteToCallPopover.vue`:
- Around line 121-130: The watcher is attached to a non-reactive DOM attribute
(popover.value?.hasAttribute('open')), so it only runs on mount; change to use
the j-popover's reactive API by tracking an `open` ref or listening to the
popover `toggle` event: when the popover opens call `getMembers()` and reset
`selectedDids.value = new Set()` and `searchInput.value = ''`, and when closing
ensure the `open` ref is set to false (or call the popover close handler) so the
UI state is consistent; update the existing `watch(...)` on
`popover.value?.hasAttribute('open')` to instead watch the reactive `open` ref
or attach an `@toggle` handler that invokes these resets and getMembers.
- Line 76: The code currently injects the route-scoped service via const {
members, getMembers } = useCommunityService(), which will switch when the user
navigates; instead resolve the community service using the call's neighbourhood
id (webrtcStore.callRoute.communityId) and use that service for
members/getMembers. Replace the injection with a service lookup that accepts the
communityId (e.g. resolve/useCommunityService(webrtcStore.callRoute.communityId)
or call a getCommunityServiceById(webrtcStore.callRoute.communityId) helper) and
then use that service's members and getMembers so the popover always reflects
the callRoute.communityId rather than the current route's provider.

In `@app/src/composables/useSignallingService.ts`:
- Around line 205-216: The CALL_INVITE handler auto-opens the call window
without ensuring the user is on the inviter's channel and repeatedly calls
useUiStore() inside the signal handler; hoist useUiStore to the top of
useSignallingService alongside the other stores, and update the handler to
either (a) stop auto-opening and only show the toast, or (b) parse the incoming
signal's source/channelUrl to set the correct call routing (e.g., set
webrtcStore.callRoute or navigate to the channel) before calling
uiStore.setCallWindowOpen(true) when !webrtcStore.inCall; ensure you reference
the CALL_INVITE branch, webrtcStore.inCall, uiStore.setCallWindowOpen,
useUiStore, and the signal's source/channelUrl when making the change.

In `@app/src/stores/webrtcStore.ts`:
- Around line 706-711: The persisted CALL_INVITE links created in sendCallInvite
(perspective.add(new Link({ source: channelUrl, predicate: CALL_INVITE, target:
did }))) accumulate with no dedup or expiry causing stale push notifications;
update sendCallInvite to avoid long-lived duplicates by either (1) attaching a
timestamp/expiry payload to the Link (e.g., include a sentAt or expires field in
the Link's target/expression) and update the AD4M trigger/SPARQL to only match
recent links, or (2) check perspective for an existing CALL_INVITE Link matching
source=channelUrl and target=did and skip adding duplicates, or (3) remove the
Link when the call ends or after a short TTL (perform perspective.remove on that
Link); implement one of these approaches in the sendCallInvite flow where Link
is created (and ensure cleanup logic runs on call end/failure).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e400ec3-19cd-4f37-89a4-58b803ab4a7b

📥 Commits

Reviewing files that changed from the base of the PR and between 826e8ad and e4d4bb7.

📒 Files selected for processing (5)
  • app/src/components/call/controls/InviteToCallPopover.vue
  • app/src/components/call/controls/MainCallControls.vue
  • app/src/composables/useSignallingService.ts
  • app/src/stores/webrtcStore.ts
  • app/src/utils/registerMobileNotifications.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/utils/registerMobileNotifications.ts

const { me } = storeToRefs(appStore);
const { agentsInCall } = storeToRefs(webrtcStore);
const { isMobile } = storeToRefs(uiStore);
const { members, getMembers } = useCommunityService();

@coderabbitai coderabbitai Bot Apr 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the useCommunityService composable signature and how it resolves the neighbourhood/members
fd -t f 'useCommunityService' app/src/composables | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Repository: coasys/flux

Length of output: 22483


🏁 Script executed:

# Read the InviteToCallPopover.vue component to understand the context
cat -n app/src/components/call/controls/InviteToCallPopover.vue | head -100

Repository: coasys/flux

Length of output: 4398


🏁 Script executed:

# Find and examine the webrtcStore to see if it has callRoute.communityId
fd -t f 'useWebrtcStore\|webrtc.*store' app/src | head -20

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Find webrtcStore in the stores directory
fd -t f '.*store' app/src/stores | xargs grep -l "webrtc\|Webrtc" 2>/dev/null

Repository: coasys/flux

Length of output: 149


🏁 Script executed:

# Examine the webrtcStore
cat -n app/src/stores/webrtcStore.ts | head -200

Repository: coasys/flux

Length of output: 9875


🏁 Script executed:

# Continue reading webrtcStore to see the full picture
cat -n app/src/stores/webrtcStore.ts | sed -n '200,400p'

Repository: coasys/flux

Length of output: 9632


🏁 Script executed:

# Look for where callRoute is set in webrtcStore
grep -n "callRoute" app/src/stores/webrtcStore.ts | head -20

Repository: coasys/flux

Length of output: 587


🏁 Script executed:

# Check the communityServiceStore to understand how getCommunityService works
cat -n app/src/stores/communityServiceStore.ts | head -150

Repository: coasys/flux

Length of output: 1133


🏁 Script executed:

# Check where InviteToCallPopover is mounted/used
rg -l "InviteToCallPopover" app/src --type vue --type ts

Repository: coasys/flux

Length of output: 82


🏁 Script executed:

# Check where InviteToCallPopover is mounted/used
rg "InviteToCallPopover" app/src

Repository: coasys/flux

Length of output: 268


🏁 Script executed:

# Check where MainCallControls is used
rg "MainCallControls" app/src

Repository: coasys/flux

Length of output: 349


🏁 Script executed:

# Check if CallWindow provides CommunityServiceKey or context, and examine its parent
cat -n app/src/components/call/window/CallWindow.vue | head -100

Repository: coasys/flux

Length of output: 4935


🏁 Script executed:

# Look for where CallWindow is used and check if it's wrapped in a provider
rg "CallWindow" app/src --type-list | head -20

Repository: coasys/flux

Length of output: 444


🏁 Script executed:

# Look for where CallWindow is used
rg "CallWindow" app/src

Repository: coasys/flux

Length of output: 3396


🏁 Script executed:

# Check CallContainer to see how it provides services
cat -n app/src/containers/CallContainer.vue

Repository: coasys/flux

Length of output: 6360


🏁 Script executed:

# Find where CommunityServiceKey is provided
rg "CommunityServiceKey" app/src

Repository: coasys/flux

Length of output: 534


🏁 Script executed:

# Check CommunityView to confirm it provides the service
cat -n app/src/views/main/community/CommunityView.vue | grep -A 10 -B 5 "CommunityServiceKey\|provide"

Repository: coasys/flux

Length of output: 1756


Use the correct community service via webrtcStore.callRoute.communityId.

useCommunityService() is injected from the current route's CommunityView provider. When a user navigates to a different community while a call persists in another community, the injected service switches to the new route's community, causing getMembers() to list members from the wrong community. Instead, resolve the community service using webrtcStore.callRoute.communityId to match the call's neighbourhood, not the current route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/controls/InviteToCallPopover.vue` at line 76, The
code currently injects the route-scoped service via const { members, getMembers
} = useCommunityService(), which will switch when the user navigates; instead
resolve the community service using the call's neighbourhood id
(webrtcStore.callRoute.communityId) and use that service for members/getMembers.
Replace the injection with a service lookup that accepts the communityId (e.g.
resolve/useCommunityService(webrtcStore.callRoute.communityId) or call a
getCommunityServiceById(webrtcStore.callRoute.communityId) helper) and then use
that service's members and getMembers so the popover always reflects the
callRoute.communityId rather than the current route's provider.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useCommunityService() uses Vue's inject() — it retrieves the service provided by the parent CommunityView component. The InviteToCallPopover is always rendered inside the call controls which are children of CommunityView, so the injection context is guaranteed. The members ref and getMembers() function are reactive and populated correctly through this pattern.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Let me verify the component tree to evaluate this more precisely.


🏁 Script executed:

#!/bin/bash
# Check where CallContainer is rendered in the app hierarchy
rg "CallContainer" app/src --type-add 'vue:*.vue' --type vue -n -B2 -A5

# Also check the top-level app component and router views to see if CallContainer is at root
rg "CallContainer" app/src -n

# Check the main App.vue or layout files
fd -t f 'App\.vue' app/src | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Length of output: 4363


@data-bot-coasys, I appreciate the explanation, but the component tree tells a different story. CallContainer is rendered in MainView.vue at the same level as the RouterView that hosts CommunityView — they are siblings, not parent and child:

MainView.vue
├── <CallContainer />          ← call-container slot (sibling)
└── <RouterView>
      └── <KeepAlive>
            └── <CommunityView>  ← provides CommunityServiceKey

Vue's inject() only traverses ancestors — it cannot reach a sibling's provider. So when InviteToCallPopover calls useCommunityService(), the inject(CommunityServiceKey) call will not find the service provided by CommunityView because CommunityView is not an ancestor of CallContainer.

Here's the relevant excerpt from the script output confirming this structure:

<!-- MainView.vue -->
<template v-slot:call-container>
  <CallContainer />          <!-- rendered outside RouterView/CommunityView -->
</template>

<RouterView v-slot="{ Component }">
  <KeepAlive :include="['CommunityView']" :max="5">
    <component :is="Component" :key="route.params.communityId" />
  </KeepAlive>
</RouterView>

This means members and getMembers() from useCommunityService() would be working with an undefined or unrelated injection context — not the active community's data. You can verify this by checking what useCommunityService returns when the inject value is absent.

The fix would be to resolve members directly via webrtcStore.callRoute.communityId (e.g., looking up the community store by that ID) rather than relying on route-scoped injection.

Comment on lines +121 to +130
watch(
() => popover.value?.hasAttribute('open'),
(isOpen) => {
if (isOpen) {
getMembers();
selectedDids.value = new Set();
searchInput.value = '';
}
},
);

@coderabbitai coderabbitai Bot Apr 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

junto-foundation j-popover open close event vue

💡 Result:

No official documentation or source code for a "junto-foundation j-popover" Vue component was found across multiple searches. Junto Foundation is a nonprofit building a social media app (junto.foundation), with open-source repos like junto-mobile (Flutter/Dart, last updated 2021) and junto-home-2.0 (Vue, minimal activity). They had "junto-elements" (web components with LitElement and Popper.js for popovers/tooltips, NPM package from 2021, docs at junto-elements.netlify.com which is inaccessible), but no evidence of Vue-specific j-popover or its open/close events. General Vue popover components with open/close events exist, e.g.: - JFusco/vue-popover: Emits 'popover:open' and 'popover:close' events. Usage: <popover name="example" @popover:open="onOpen" @popover:close="onClose"></popover> (source 2). - jqWidgets jqxPopover: @open and @close events (source 3). If this refers to a private/internal Junto Foundation component, public sources do not document its API. Check internal repos or contact hi@junto.foundation.

Citations:


🏁 Script executed:

# First, locate the file and read its context
find . -name "InviteToCallPopover.vue" -type f

Repository: coasys/flux

Length of output: 113


🏁 Script executed:

# Search for j-popover component definition
fd -e vue -e ts -e js | head -50 | xargs rg "j-popover" -A 5 -B 5 2>/dev/null | head -100

Repository: coasys/flux

Length of output: 7236


🏁 Script executed:

# Look for popover component definition
fd "popover" -i | grep -E "\.(vue|ts|js)$" | head -20

Repository: coasys/flux

Length of output: 157


🏁 Script executed:

cat -n app/src/components/call/controls/InviteToCallPopover.vue

Repository: coasys/flux

Length of output: 6211


🏁 Script executed:

cat -n packages/ui/lib/components/popover/Popover.ts

Repository: coasys/flux

Length of output: 6233


🏁 Script executed:

# Check if getMembers() is called elsewhere in InviteToCallPopover
rg "getMembers" app/src/components/call/controls/InviteToCallPopover.vue

Repository: coasys/flux

Length of output: 129


🏁 Script executed:

# Verify popover closing behavior - does removeAttribute trigger reactive updates?
rg "removeAttribute\|setAttribute\|popover" app/src/components/call/controls/InviteToCallPopover.vue -A 2 -B 2

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check useCommunityService to understand getMembers() behavior
fd "useCommunityService" -type f | head -5

Repository: coasys/flux

Length of output: 225


🏁 Script executed:

# Find useCommunityService file
fd "useCommunityService"

Repository: coasys/flux

Length of output: 97


🏁 Script executed:

# Check how members ref is used and if there are initial getMembers calls
rg "getMembers\|useCommunityService" app/src/components/call/controls/InviteToCallPopover.vue

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check the composable to understand getMembers
cat -n app/src/composables/useCommunityService.ts 2>/dev/null || find . -name "*useCommunityService*" -type f

Repository: coasys/flux

Length of output: 26387


Watcher on hasAttribute('open') is not reactive — selectedDids and searchInput won't reset on popover reopen.

watch(() => popover.value?.hasAttribute('open'), ...) depends only on popover.value (the template ref), which changes exactly once when the DOM element mounts. The open attribute toggled by user clicks is DOM state Vue cannot observe, so this watcher fires only on initial mount. Result: selectedDids and searchInput are never cleared between popover opens, violating the stated intent in the comment.

The j-popover web component exposes a reactive :open property and @toggle event (see Popover.ts). Use these instead:

♻️ Reactive open/close handler
-  <j-popover ref="popover" placement="top">
+  <j-popover ref="popover" placement="top" :open="isOpen" `@toggle`="(e: any) => isOpen = e.target.open">
     <j-tooltip slot="trigger" placement="top" title="Invite to call">
       <j-button variant="transparent" square circle :size="isMobile ? 'md' : 'lg'">
         <j-icon name="person-plus" :size="isMobile ? 'sm' : 'md'" />
       </j-button>
     </j-tooltip>
     <div slot="content" class="invite-popover">
+const isOpen = ref(false);
+
+watch(isOpen, (open) => {
+  if (open) {
+    getMembers();
+    selectedDids.value = new Set();
+    searchInput.value = '';
+  }
+});
-
-// Refresh members when popover is opened
-watch(
-  () => popover.value?.hasAttribute('open'),
-  (isOpen) => {
-    if (isOpen) {
-      getMembers();
-      selectedDids.value = new Set();
-      searchInput.value = '';
-    }
-  },
-);

Also update close logic:

-popover.value?.removeAttribute('open');
+isOpen.value = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/controls/InviteToCallPopover.vue` around lines 121 -
130, The watcher is attached to a non-reactive DOM attribute
(popover.value?.hasAttribute('open')), so it only runs on mount; change to use
the j-popover's reactive API by tracking an `open` ref or listening to the
popover `toggle` event: when the popover opens call `getMembers()` and reset
`selectedDids.value = new Set()` and `searchInput.value = ''`, and when closing
ensure the `open` ref is set to false (or call the popover close handler) so the
UI state is consistent; update the existing `watch(...)` on
`popover.value?.hasAttribute('open')` to instead watch the reactive `open` ref
or attach an `@toggle` handler that invokes these resets and getMembers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

j-popover is a Junto web component that toggles the open attribute. The watch on hasAttribute('open') works here because it re-evaluates whenever the template re-renders (Vue's reactivity triggers on popover.value access). In practice, the popover open/close is user-driven and the watch fires correctly on interaction. If we hit edge cases, we can switch to a MutationObserver, but this pattern has been working reliably.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@data-bot-coasys, understood — thanks for the context. You're right that popover.value (being a Ref) is tracked by Vue's reactivity system. The subtle concern is that .hasAttribute('open') itself is a raw DOM call that Vue cannot observe directly, so the getter only re-evaluates when the ref assignment changes (mount/unmount), not on attribute mutations. If the watcher is firing reliably in practice, it's likely because some other reactive state update is triggering a re-render cycle that causes the getter to be re-evaluated as a side effect — which can be somewhat fragile depending on the render cycle.

That said, if the pattern is stable in your testing and you have a MutationObserver fallback in mind if needed, that's a reasonable call to make. Happy to leave it as-is.


✏️ Learnings added
Learnt from: data-bot-coasys
Repo: coasys/flux PR: 589
File: app/src/components/call/controls/InviteToCallPopover.vue:121-130
Timestamp: 2026-04-22T05:24:17.112Z
Learning: In `app/src/components/call/controls/InviteToCallPopover.vue`, the team intentionally uses `watch(() => popover.value?.hasAttribute('open'), ...)` to detect j-popover open/close state. The `j-popover` is a Junto web component that toggles an `open` attribute on the element. The team is aware that `hasAttribute()` is not directly reactive (Vue only tracks the ref assignment), but the pattern works reliably in practice because reactive render cycles re-evaluate the getter. A MutationObserver is the preferred fallback if edge cases arise.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread app/src/composables/useSignallingService.ts
Comment thread app/src/stores/webrtcStore.ts
- Hoist useUiStore() to composable scope in useSignallingService instead
  of calling it inside the onSignal handler
- Track persisted call invite links and remove them when leaving a call
  to prevent stale notifications from accumulating

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +37 to +42
trigger: `SELECT ?source ?predicate ?target WHERE {
GRAPH ?g { ?source ?predicate ?target . }
FILTER(?predicate = <agent/new-state>)
FILTER(CONTAINS(STR(?source), '"inCall":true'))
FILTER(!CONTAINS(STR(?source), '"${agentDid}"'))
}`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SPARQL trigger filters on agent/new-state predicate which matches how broadcastState sends links in useSignallingService.ts (line 244: predicate: NEW_STATE where NEW_STATE = 'agent/new-state'). The source is JSON.stringify(myState.value) which contains "inCall":true when the agent is in a call.

The !CONTAINS(STR(?source), '"agentDid"') filter is admittedly weak since the DID isn't typically in the state object — however, the AD4M notification runtime already filters out self-authored links, so this doesn't cause self-notifications in practice. Acknowledged as a nice-to-have cleanup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/src/stores/webrtcStore.ts (1)

699-725: ⚠️ Potential issue | 🟠 Major

Partial fix: stale invite links still accumulate on abrupt app exit.

The new cleanupInviteLinks() in leaveRoom() addresses the happy path, but persistedInviteLinks is in-memory only. In several common scenarios the cleanup never runs and the links survive in the neighbourhood perspective:

  • App crash / force-close / tab close while in a call.
  • perspective.remove failing (network/agent error) — the ref is cleared optimistically at line 736, so failed removals are silently forgotten.
  • Re-invites to the same DID add duplicate links (no dedup).

These residual links will re-match the flux://call_invite SPARQL trigger on future trigger evaluations and produce stale "you were invited to a call" push notifications long after the call ended.

Consider one or more of:

  • Embed a timestamp in the Link target and bound the SPARQL filter to a recent time window.
  • On startup, sweep & remove any pre-existing CALL_INVITE links authored by the current agent (perspective.get by predicate, then remove older than N minutes).
  • Only clear persistedInviteLinks entries once perspective.remove resolves successfully; retry on failure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 699 - 725, sendCallInvite
currently persists CALL_INVITE Link objects into perspective and pushes
optimistic entries into persistedInviteLinks which can leave stale links; update
sendCallInvite and related logic so that Link removals are robust: (1) include a
timestamp in the Link target when creating Links in sendCallInvite (e.g., append
ISO or epoch) so SPARQL triggers can filter by recency; (2) only push into
persistedInviteLinks after perspective.add resolves successfully and on
perspective.remove only remove the persistedInviteLinks entry after the remove
promise resolves (retry/remove on failure), referencing persistedInviteLinks,
perspective.add, and perspective.remove; and (3) add a startup sweep (called
from module init or leaveRoom) that uses perspective.get/filter by predicate
CALL_INVITE authored by the current agent and removes any links older than N
minutes to clean up stale invites.
🧹 Nitpick comments (1)
app/src/utils/registerMobileNotifications.ts (1)

155-157: Nit: delete config.granted is a no-op.

None of the three config builders return a granted field, so this line does nothing. Either drop it or, if the intent is defensive hygiene against accidental leakage (e.g., if configs are refactored later to include server-returned fields), add a comment explaining it.

🧹 Proposed cleanup
     const existing = allMatching[0];
     const config = configFn(perspectiveIds, webhookAuth, agentDid);
-    delete config.granted;
     await client.runtime.updateNotification(existing.id, config);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/utils/registerMobileNotifications.ts` around lines 155 - 157, The
line deleting config.granted is a no-op because none of the config builders
return a granted field; remove the statement to clean up dead code. In the block
where you call configFn(perspectiveIds, webhookAuth, agentDid) and then await
client.runtime.updateNotification(existing.id, config), delete the line "delete
config.granted" (or if you prefer defensive hygiene, replace it with a one-line
comment explaining why you'd keep it), referencing configFn, perspectiveIds,
webhookAuth, agentDid, existing.id and client.runtime.updateNotification to
locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/utils/registerMobileNotifications.ts`:
- Around line 37-42: The current SPARQL trigger in
registerMobileNotifications.ts matches the steady-state agent/new-state
predicate (containing "inCall":true) which will fire on every heartbeat because
broadcastState in useSignallingService.ts emits myState each HEARTBEAT_INTERVAL;
change the trigger to detect a state-transition marker instead of the continuous
flag: either listen for a dedicated one-shot predicate (e.g.,
agent/call_started) emitted by broadcastState on the false→true edge, or require
a transition property (e.g., callStartedAt timestamp) and add a FILTER that
restricts matches to a small recent time window; update broadcastState in
useSignallingService.ts to emit that one-shot predicate or include the timestamp
when flipping inCall so the trigger (in registerMobileNotifications.ts) can
reliably match only call-start events.

---

Duplicate comments:
In `@app/src/stores/webrtcStore.ts`:
- Around line 699-725: sendCallInvite currently persists CALL_INVITE Link
objects into perspective and pushes optimistic entries into persistedInviteLinks
which can leave stale links; update sendCallInvite and related logic so that
Link removals are robust: (1) include a timestamp in the Link target when
creating Links in sendCallInvite (e.g., append ISO or epoch) so SPARQL triggers
can filter by recency; (2) only push into persistedInviteLinks after
perspective.add resolves successfully and on perspective.remove only remove the
persistedInviteLinks entry after the remove promise resolves (retry/remove on
failure), referencing persistedInviteLinks, perspective.add, and
perspective.remove; and (3) add a startup sweep (called from module init or
leaveRoom) that uses perspective.get/filter by predicate CALL_INVITE authored by
the current agent and removes any links older than N minutes to clean up stale
invites.

---

Nitpick comments:
In `@app/src/utils/registerMobileNotifications.ts`:
- Around line 155-157: The line deleting config.granted is a no-op because none
of the config builders return a granted field; remove the statement to clean up
dead code. In the block where you call configFn(perspectiveIds, webhookAuth,
agentDid) and then await client.runtime.updateNotification(existing.id, config),
delete the line "delete config.granted" (or if you prefer defensive hygiene,
replace it with a one-line comment explaining why you'd keep it), referencing
configFn, perspectiveIds, webhookAuth, agentDid, existing.id and
client.runtime.updateNotification to locate the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a552ebee-f898-44bf-bec7-4c0258e36cf4

📥 Commits

Reviewing files that changed from the base of the PR and between e4d4bb7 and e20e40f.

📒 Files selected for processing (3)
  • app/src/composables/useSignallingService.ts
  • app/src/stores/webrtcStore.ts
  • app/src/utils/registerMobileNotifications.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/composables/useSignallingService.ts

Comment thread app/src/utils/registerMobileNotifications.ts
data-bot-coasys and others added 2 commits April 22, 2026 22:08
…//call_started

InviteToCallPopover rendered inside CallContainer, which is a sibling of
RouterView in MainView — so useCommunityService() via inject() would miss
CommunityView's provider and throw. Resolve members through
webrtcStore.communityService instead, which looks up the service by
callRoute.communityId.

The previous call push-notification trigger matched <agent/new-state> links
containing "inCall":true, but those are broadcast via sendBroadcastU and
never persisted, so AD4M's SPARQL trigger never fired. Emit a persisted
flux://call_started link from joinRoom() when entering an empty room (only
the initiator) and match that IRI in the trigger. Self-authored links are
filtered by the notification runtime, so no explicit author filter needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the existing cleanupInviteLinks pattern so the persisted
call_started link does not linger in the perspective graph after the
call ends.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src/components/call/controls/InviteToCallPopover.vue (1)

120-125: Nit: inviteAll should also clear selectedDids for consistency with sendInvites.

sendInvites resets selectedDids at line 116 but inviteAll does not. It's harmless today because the open-watcher resets state on re-open, but the two exit paths should behave identically.

♻️ Proposed fix
 function inviteAll() {
   const dids = invitableMembers.value.map((m) => m.did!).filter(Boolean);
   if (!dids.length) return;
   webrtcStore.sendCallInvite(dids);
+  selectedDids.value = new Set();
   popover.value?.removeAttribute('open');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/controls/InviteToCallPopover.vue` around lines 120 -
125, The inviteAll function currently sends invites but does not clear
selectedDids like sendInvites does; update inviteAll to reset selectedDids (the
same reactive/set used by sendInvites) after calling
webrtcStore.sendCallInvite(dids) and before removing the popover so both exit
paths behave identically — locate inviteAll, reference selectedDids and
sendInvites for the same reset behavior, and ensure
popover.value?.removeAttribute('open') remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/stores/webrtcStore.ts`:
- Around line 633-648: The persisted link can be orphaned because
persistedCallStartedLink (and persistedInviteLinks) are only set in the
perspective.add(...).then(...) callback, so if
leaveRoom()/cleanupCallStartedLink() runs before the promise resolves the
cleanup skips removal; to fix, pre-record the intended link entry before calling
perspective.add (i.e. set persistedCallStartedLink = { ... } or push into
persistedInviteLinks immediately), then call perspective.add(...).catch(error =>
{ clear the pre-recorded entry / remove from pending set and log the error });
apply the same pattern in sendCallInvite (set the invite tracking entry before
perspective.add() and remove it on .catch()) so
cleanupCallStartedLink()/cleanupInviteLinks() can reliably find and remove
persisted links regardless of timing.

---

Nitpick comments:
In `@app/src/components/call/controls/InviteToCallPopover.vue`:
- Around line 120-125: The inviteAll function currently sends invites but does
not clear selectedDids like sendInvites does; update inviteAll to reset
selectedDids (the same reactive/set used by sendInvites) after calling
webrtcStore.sendCallInvite(dids) and before removing the popover so both exit
paths behave identically — locate inviteAll, reference selectedDids and
sendInvites for the same reset behavior, and ensure
popover.value?.removeAttribute('open') remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 589be349-5e4d-4080-b089-7dcede95c8da

📥 Commits

Reviewing files that changed from the base of the PR and between e20e40f and 72c40b2.

📒 Files selected for processing (3)
  • app/src/components/call/controls/InviteToCallPopover.vue
  • app/src/stores/webrtcStore.ts
  • app/src/utils/registerMobileNotifications.ts

Comment on lines +633 to +648
// If nobody else is in the call, persist a flux://call_started link so the
// AD4M notification trigger fires a push notification for the neighbourhood.
// Only the initiator emits this — late joiners don't, to avoid duplicate pushes.
// The link is cleaned up in leaveRoom() to prevent stale triggers.
if (agentsInCall.value.length === 0 && callRoute.value.channelId) {
const perspective = communityService.value?.perspective;
const channelUrl = restoreChannelPrefix(callRoute.value.channelId);
if (perspective) {
perspective
.add(new Link({ source: channelUrl, predicate: CALL_STARTED, target: channelUrl }))
.then(() => {
persistedCallStartedLink.value = { source: channelUrl, target: channelUrl };
})
.catch((error) => console.error('Failed to persist call_started link:', error));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Race between perspective.add(...).then(...) and leaveRoom() can leak persisted links.

The tracking refs are only populated in the .then() callback after perspective.add() resolves. If leaveRoom() runs between the link being persisted on the host and the .then() callback updating persistedCallStartedLink / persistedInviteLinks, cleanupCallStartedLink() / cleanupInviteLinks() will see null / empty array and skip removal, orphaning the link.

For CALL_STARTED this is especially undesirable — a leftover link can re-trigger flux://call_started push notifications on subsequent notification runtime evaluations. The same issue applies to sendCallInvite at lines 732-738.

Consider pre-recording the intended link before add() and clearing it in a .catch() on failure (or tracking a pending set), so cleanup is safe regardless of callback timing:

🛡️ Suggested pattern
         if (perspective) {
-          perspective
-            .add(new Link({ source: channelUrl, predicate: CALL_STARTED, target: channelUrl }))
-            .then(() => {
-              persistedCallStartedLink.value = { source: channelUrl, target: channelUrl };
-            })
-            .catch((error) => console.error('Failed to persist call_started link:', error));
+          persistedCallStartedLink.value = { source: channelUrl, target: channelUrl };
+          perspective
+            .add(new Link({ source: channelUrl, predicate: CALL_STARTED, target: channelUrl }))
+            .catch((error) => {
+              persistedCallStartedLink.value = null;
+              console.error('Failed to persist call_started link:', error);
+            });
         }

Apply the same shape to sendCallInvite (push the tracking entry before add(), remove on .catch).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If nobody else is in the call, persist a flux://call_started link so the
// AD4M notification trigger fires a push notification for the neighbourhood.
// Only the initiator emits this — late joiners don't, to avoid duplicate pushes.
// The link is cleaned up in leaveRoom() to prevent stale triggers.
if (agentsInCall.value.length === 0 && callRoute.value.channelId) {
const perspective = communityService.value?.perspective;
const channelUrl = restoreChannelPrefix(callRoute.value.channelId);
if (perspective) {
perspective
.add(new Link({ source: channelUrl, predicate: CALL_STARTED, target: channelUrl }))
.then(() => {
persistedCallStartedLink.value = { source: channelUrl, target: channelUrl };
})
.catch((error) => console.error('Failed to persist call_started link:', error));
}
}
// If nobody else is in the call, persist a flux://call_started link so the
// AD4M notification trigger fires a push notification for the neighbourhood.
// Only the initiator emits this — late joiners don't, to avoid duplicate pushes.
// The link is cleaned up in leaveRoom() to prevent stale triggers.
if (agentsInCall.value.length === 0 && callRoute.value.channelId) {
const perspective = communityService.value?.perspective;
const channelUrl = restoreChannelPrefix(callRoute.value.channelId);
if (perspective) {
persistedCallStartedLink.value = { source: channelUrl, target: channelUrl };
perspective
.add(new Link({ source: channelUrl, predicate: CALL_STARTED, target: channelUrl }))
.catch((error) => {
persistedCallStartedLink.value = null;
console.error('Failed to persist call_started link:', error);
});
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 633 - 648, The persisted link can
be orphaned because persistedCallStartedLink (and persistedInviteLinks) are only
set in the perspective.add(...).then(...) callback, so if
leaveRoom()/cleanupCallStartedLink() runs before the promise resolves the
cleanup skips removal; to fix, pre-record the intended link entry before calling
perspective.add (i.e. set persistedCallStartedLink = { ... } or push into
persistedInviteLinks immediately), then call perspective.add(...).catch(error =>
{ clear the pre-recorded entry / remove from pending set and log the error });
apply the same pattern in sendCallInvite (set the invite tracking entry before
perspective.add() and remove it on .catch()) so
cleanupCallStartedLink()/cleanupInviteLinks() can reliably find and remove
persisted links regardless of timing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/src/stores/webrtcStore.ts (1)

644-649: ⚠️ Potential issue | 🟡 Minor

Track persisted links before awaiting add().

The tracking refs are still populated only in .then(), so leaveRoom() can run before cleanup knows about the pending link. Pre-record the intended link before perspective.add(...), then clear that entry in .catch(...) if the add fails.

Also applies to: 743-748

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 644 - 649, Pre-populate the
tracking ref persistedCallStartedLink with the intended link (e.g., { source:
channelUrl, target: channelUrl }) before calling perspective.add(...) so
leaveRoom() can see the pending link; then call perspective.add(...). In the
promise handlers, clear persistedCallStartedLink in .catch(...) if add fails and
keep or confirm the value in .then(...); apply the same change for the other
occurrence referenced around lines 743-748 to ensure both pending links are
recorded before the async add call and cleaned on error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/stores/webrtcStore.ts`:
- Around line 640-643: The DIAG console.log calls leak sensitive identifiers
(recipient DIDs, channel/perspective IDs); find and remove or redact any
console.log lines containing "[DIAG" (examples around the Link/CALL_STARTED
creation and the uses of communityService, perspective, channelUrl, and Link)
and either delete them or replace them with the existing debug/dev logger (only
when enabled) and log non-sensitive info (e.g., booleans or masked IDs like
"<redacted>"). Ensure you update all occurrences (the shown snippet and the
other DIAG logs near the Link/CALL_STARTED uses) so no raw
DIDs/channel/perspective identifiers are printed.
- Around line 670-672: The cleanup functions cleanupInviteLinks and
cleanupCallStartedLink currently clear local tracking immediately after calling
perspective.remove(...), so if remove fails the persisted link remains but local
state was dropped; modify these functions to be async and await the
perspective.remove call (or await a Promise.all if removing multiple links),
only clear the local tracking state after the remove promise resolves
successfully, and on error log (processLogger/errorLogger) and keep the tracking
state so the cleanup can be retried; apply the same change to the analogous
cleanup block at the other location mentioned (around the other 761-781
section).

---

Duplicate comments:
In `@app/src/stores/webrtcStore.ts`:
- Around line 644-649: Pre-populate the tracking ref persistedCallStartedLink
with the intended link (e.g., { source: channelUrl, target: channelUrl }) before
calling perspective.add(...) so leaveRoom() can see the pending link; then call
perspective.add(...). In the promise handlers, clear persistedCallStartedLink in
.catch(...) if add fails and keep or confirm the value in .then(...); apply the
same change for the other occurrence referenced around lines 743-748 to ensure
both pending links are recorded before the async add call and cleaned on error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a86952e-c245-45cd-91b9-26285bdce54b

📥 Commits

Reviewing files that changed from the base of the PR and between 72c40b2 and e8ce1a1.

📒 Files selected for processing (1)
  • app/src/stores/webrtcStore.ts

Comment thread app/src/stores/webrtcStore.ts Outdated
Comment on lines +640 to +643
console.log('[DIAG call_started] communityService:', !!communityService.value, 'perspective:', !!perspective, 'uuid:', perspective?.uuid, 'channelUrl:', channelUrl);
if (perspective) {
const link = new Link({ source: channelUrl, predicate: CALL_STARTED, target: channelUrl });
console.log('[DIAG call_started] adding link:', JSON.stringify(link));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove or redact diagnostic call-notification logs before merge.

These logs expose invite recipient DIDs plus channel/community/perspective identifiers. Keep only redacted debug output behind an existing dev/debug logger, or remove the [DIAG ...] logging entirely.

Also applies to: 729-729, 741-742, 750-752

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 640 - 643, The DIAG console.log
calls leak sensitive identifiers (recipient DIDs, channel/perspective IDs); find
and remove or redact any console.log lines containing "[DIAG" (examples around
the Link/CALL_STARTED creation and the uses of communityService, perspective,
channelUrl, and Link) and either delete them or replace them with the existing
debug/dev logger (only when enabled) and log non-sensitive info (e.g., booleans
or masked IDs like "<redacted>"). Ensure you update all occurrences (the shown
snippet and the other DIAG logs near the Link/CALL_STARTED uses) so no raw
DIDs/channel/perspective identifiers are printed.

Comment on lines +670 to +672
// Clean up persisted call invite links to prevent stale notifications
cleanupInviteLinks();
cleanupCallStartedLink();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Await removals before dropping cleanup state.

cleanupInviteLinks() / cleanupCallStartedLink() clear local tracking immediately after starting perspective.remove(...). If removal fails, the link remains persisted but can no longer be retried, causing stale notifications.

Suggested cleanup shape
-        cleanupInviteLinks();
-        cleanupCallStartedLink();
+        await cleanupInviteLinks();
+        await cleanupCallStartedLink();
-    function cleanupInviteLinks(): void {
+    async function cleanupInviteLinks(): Promise<void> {
       const perspective = communityService.value?.perspective;
       if (!perspective || !persistedInviteLinks.value.length) return;
 
-      for (const invite of persistedInviteLinks.value) {
-        perspective
-          .remove(new Link({ source: invite.source, predicate: CALL_INVITE, target: invite.target }))
-          .catch((error) => console.error('Failed to remove call invite link:', error));
-      }
-      persistedInviteLinks.value = [];
+      const invites = [...persistedInviteLinks.value];
+      const failed: Array<{ source: string; target: string }> = [];
+
+      await Promise.all(
+        invites.map(async (invite) => {
+          try {
+            await perspective.remove(new Link({ source: invite.source, predicate: CALL_INVITE, target: invite.target }));
+          } catch (error) {
+            failed.push(invite);
+            console.error('Failed to remove call invite link:', error);
+          }
+        }),
+      );
+
+      persistedInviteLinks.value = failed;
     }
 
-    function cleanupCallStartedLink(): void {
+    async function cleanupCallStartedLink(): Promise<void> {
       const perspective = communityService.value?.perspective;
       const link = persistedCallStartedLink.value;
       if (!perspective || !link) return;
 
-      perspective
-        .remove(new Link({ source: link.source, predicate: CALL_STARTED, target: link.target }))
-        .catch((error) => console.error('Failed to remove call_started link:', error));
-      persistedCallStartedLink.value = null;
+      try {
+        await perspective.remove(new Link({ source: link.source, predicate: CALL_STARTED, target: link.target }));
+        persistedCallStartedLink.value = null;
+      } catch (error) {
+        console.error('Failed to remove call_started link:', error);
+      }
     }

Also applies to: 761-781

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 670 - 672, The cleanup functions
cleanupInviteLinks and cleanupCallStartedLink currently clear local tracking
immediately after calling perspective.remove(...), so if remove fails the
persisted link remains but local state was dropped; modify these functions to be
async and await the perspective.remove call (or await a Promise.all if removing
multiple links), only clear the local tracking state after the remove promise
resolves successfully, and on error log (processLogger/errorLogger) and keep the
tracking state so the cleanup can be retried; apply the same change to the
analogous cleanup block at the other location mentioned (around the other
761-781 section).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants