-
Notifications
You must be signed in to change notification settings - Fork 8
feat: update push notification URL and add call notifications #589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
826e8ad
e4d4bb7
214dcf2
e20e40f
dc75f1b
72c40b2
4163e6c
e8ce1a1
1123405
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| <template> | ||
| <j-popover ref="popover" placement="top"> | ||
| <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"> | ||
| <j-flex direction="column" gap="400"> | ||
| <j-text nomargin variant="heading-sm" size="400">Invite to call</j-text> | ||
|
|
||
| <j-input | ||
| size="sm" | ||
| placeholder="Search members..." | ||
| type="search" | ||
| :value="searchInput" | ||
| @input="(e: any) => (searchInput = e.target.value)" | ||
| > | ||
| <j-icon name="search" size="xs" slot="start" /> | ||
| </j-input> | ||
|
|
||
| <div class="member-list"> | ||
| <label | ||
| v-for="member in filteredMembers" | ||
| :key="member.did" | ||
| class="member-row" | ||
| :class="{ selected: selectedDids.has(member.did!) }" | ||
| @click="() => member.did && toggleMember(member.did)" | ||
| > | ||
| <j-avatar size="sm" :did="member.did" :hash="member.did" :src="member.profileThumbnailPicture" /> | ||
| <j-text nomargin size="400" style="flex: 1"> | ||
| {{ member.username || 'Loading...' }} | ||
| </j-text> | ||
| <j-icon | ||
| v-if="selectedDids.has(member.did!)" | ||
| name="check-circle-fill" | ||
| color="primary-500" | ||
| size="sm" | ||
| /> | ||
| </label> | ||
|
|
||
| <j-text v-if="!filteredMembers.length" nomargin size="400" color="ui-400" style="text-align: center; padding: 8px"> | ||
| {{ searchInput ? 'No members found' : 'No members to invite' }} | ||
| </j-text> | ||
| </div> | ||
|
|
||
| <j-flex gap="300"> | ||
| <j-button size="sm" variant="primary" :disabled="!selectedDids.size" @click="sendInvites"> | ||
| <j-icon name="telephone-plus" size="xs" style="margin-right: 4px" /> | ||
| Invite {{ selectedDids.size ? `(${selectedDids.size})` : '' }} | ||
| </j-button> | ||
| <j-button size="sm" @click="inviteAll" :disabled="!invitableMembers.length"> | ||
| Invite all | ||
| </j-button> | ||
| </j-flex> | ||
| </j-flex> | ||
| </div> | ||
| </j-popover> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { useAppStore, useWebrtcStore } from '@/stores'; | ||
| import { useUiStore } from '@/stores'; | ||
| import { storeToRefs } from 'pinia'; | ||
| import { computed, ref, unref, watch } from 'vue'; | ||
|
|
||
| const appStore = useAppStore(); | ||
| const webrtcStore = useWebrtcStore(); | ||
| const uiStore = useUiStore(); | ||
|
|
||
| const { me } = storeToRefs(appStore); | ||
| const { agentsInCall, communityService } = storeToRefs(webrtcStore); | ||
| const { isMobile } = storeToRefs(uiStore); | ||
|
|
||
| // Resolve members via the community of the active call, not the current route. | ||
| // InviteToCallPopover renders inside CallContainer (sibling of RouterView in MainView), | ||
| // so Vue inject() cannot reach CommunityView's provider. unref() handles the case | ||
| // where Pinia's reactive proxy auto-unwraps the nested members ref. | ||
| const members = computed(() => unref(communityService.value?.members) ?? []); | ||
| function getMembers() { | ||
| communityService.value?.getMembers(); | ||
| } | ||
|
|
||
| const popover = ref<HTMLElement | null>(null); | ||
| const searchInput = ref(''); | ||
| const selectedDids = ref<Set<string>>(new Set()); | ||
|
|
||
| // Members who are not already in the call and not the current user | ||
| const invitableMembers = computed(() => | ||
| members.value.filter((m) => { | ||
| if (!m.did || m.did === me.value.did) return false; | ||
| return !agentsInCall.value.some((a) => a.did === m.did); | ||
| }), | ||
| ); | ||
|
|
||
| const filteredMembers = computed(() => { | ||
| if (!searchInput.value) return invitableMembers.value; | ||
| const q = searchInput.value.toLowerCase(); | ||
| return invitableMembers.value.filter((m) => { | ||
| const fields = [m.username, m.givenName, m.familyName].filter(Boolean); | ||
| return fields.some((f) => f?.toLowerCase().includes(q)); | ||
| }); | ||
| }); | ||
|
|
||
| function toggleMember(did: string) { | ||
| const next = new Set(selectedDids.value); | ||
| if (next.has(did)) next.delete(did); | ||
| else next.add(did); | ||
| selectedDids.value = next; | ||
| } | ||
|
|
||
| function sendInvites() { | ||
| if (!selectedDids.value.size) return; | ||
| webrtcStore.sendCallInvite([...selectedDids.value]); | ||
| selectedDids.value = new Set(); | ||
| popover.value?.removeAttribute('open'); | ||
| } | ||
|
|
||
| function inviteAll() { | ||
| const dids = invitableMembers.value.map((m) => m.did!).filter(Boolean); | ||
| if (!dids.length) return; | ||
| webrtcStore.sendCallInvite(dids); | ||
| popover.value?.removeAttribute('open'); | ||
| } | ||
|
|
||
| // Refresh members when popover is opened | ||
| watch( | ||
| () => popover.value?.hasAttribute('open'), | ||
| (isOpen) => { | ||
| if (isOpen) { | ||
| getMembers(); | ||
| selectedDids.value = new Set(); | ||
| searchInput.value = ''; | ||
| } | ||
| }, | ||
| ); | ||
| </script> | ||
|
|
||
| <style scoped lang="scss"> | ||
| .invite-popover { | ||
| padding: var(--j-space-400); | ||
| min-width: 240px; | ||
| max-width: 300px; | ||
| } | ||
|
|
||
| .member-list { | ||
| max-height: 200px; | ||
| overflow-y: auto; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: var(--j-space-200); | ||
| } | ||
|
|
||
| .member-row { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: var(--j-space-300); | ||
| padding: var(--j-space-200) var(--j-space-300); | ||
| border-radius: var(--j-border-radius); | ||
| cursor: pointer; | ||
| transition: background-color 0.15s; | ||
|
|
||
| &:hover { | ||
| background-color: var(--j-color-ui-100); | ||
| } | ||
|
|
||
| &.selected { | ||
| background-color: var(--j-color-primary-50); | ||
| } | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,27 +5,29 @@ import popWav from '@/assets/audio/pop.wav'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { HEARTBEAT_INTERVAL } from '@/composables/useSignallingService'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useTabCoordinator } from '@/composables/useTabCoordinator'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getCachedAgentProfile } from '@/utils/userProfileCache'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PerspectiveExpression } from '@coasys/ad4m'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Link, PerspectiveExpression } from '@coasys/ad4m'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { AgentState, AgentStatus, CallHealth, Profile, RouteParams } from '@coasys/flux-types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Howl } from 'howler'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { defineStore, storeToRefs } from 'pinia'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Instance } from 'simple-peer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { computed, ref, watch } from 'vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { computed, ref, toRaw, watch } from 'vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRoute } from 'vue-router'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useAppStore } from './appStore'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCommunityServiceStore } from './communityServiceStore'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMediaDevicesStore } from './mediaDevicesStore'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useUiStore } from './uiStore'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import SimplePeer from 'simple-peer/simplepeer.min.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { restoreNeighbourhoodPrefix } from '@/utils/routeUtils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { restoreChannelPrefix, restoreNeighbourhoodPrefix } from '@/utils/routeUtils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const CALL_HEALTH_CHECK_INTERVAL = 6000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const WEBRTC_SIGNAL = 'webrtc/signal'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const WEBRTC_STREAM_REQUEST = 'webrtc/stream-request'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const WEBRTC_EMOJI = 'webrtc/emoji'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const WEBRTC_MEDIA_SETTINGS_CHANGED = 'webrtc/media-settings-changed'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const WEBRTC_LEAVING_CALL = 'webrtc/leaving-call'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const CALL_INVITE = 'flux://call_invite'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const CALL_STARTED = 'flux://call_started'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_RECONNECTION_ATTEMPTS = 3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const defaultIceServers = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -628,6 +630,23 @@ export const useWebrtcStore = defineStore( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uiStore.setVideoLayout({ label: 'Focused', class: 'focused', icon: 'person-video2' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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 = toRaw(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)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+633
to
+648
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race between The tracking refs are only populated in the For Consider pre-recording the intended link before 🛡️ 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inCall.value = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Error joining call:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -642,6 +661,10 @@ export const useWebrtcStore = defineStore( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Signal all peers that we're leaving the call | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signalPeers(WEBRTC_LEAVING_CALL); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up persisted call invite links to prevent stale notifications | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanupInviteLinks(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanupCallStartedLink(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+664
to
+666
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Await removals before dropping cleanup state.
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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Close all peer connections | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peerConnections.value.forEach((_, did) => cleanupPeerConnection(did)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -689,6 +712,61 @@ export const useWebrtcStore = defineStore( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Track persisted invite links so we can clean them up when leaving the call | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const persistedInviteLinks = ref<Array<{ source: string; target: string }>>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const persistedCallStartedLink = ref<{ source: string; target: string } | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function sendCallInvite(dids: string[]): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!signallingService.value || !callRoute.value.channelId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const channelUrl = restoreChannelPrefix(callRoute.value.channelId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const perspective = toRaw(communityService.value?.perspective); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const did of dids) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Broadcast signal for real-time notification (when recipient is online) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signallingService.value.sendSignal({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| source: channelUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| predicate: CALL_INVITE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target: did, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Persist link for AD4M push notification trigger (when recipient is offline) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (perspective) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| perspective | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .add(new Link({ source: channelUrl, predicate: CALL_INVITE, target: did })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(() => persistedInviteLinks.value.push({ source: channelUrl, target: did })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((error) => console.error('Failed to persist call invite link:', error)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const count = dids.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| appStore.showSuccessToast({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: `Call invite sent to ${count} member${count > 1 ? 's' : ''}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function cleanupInviteLinks(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const perspective = toRaw(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 = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function cleanupCallStartedLink(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const perspective = toRaw(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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Close the call window on route param changes if not in a call or a channel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| watch( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () => route.params, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -812,6 +890,7 @@ export const useWebrtcStore = defineStore( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signalAgentsInCall, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayEmoji, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| copyCallLink, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendCallInvite, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { persist: false }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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:@openand@closeevents (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:
Repository: coasys/flux
Length of output: 113
🏁 Script executed:
Repository: coasys/flux
Length of output: 7236
🏁 Script executed:
Repository: coasys/flux
Length of output: 157
🏁 Script executed:
Repository: coasys/flux
Length of output: 6211
🏁 Script executed:
Repository: coasys/flux
Length of output: 6233
🏁 Script executed:
Repository: coasys/flux
Length of output: 129
🏁 Script executed:
Repository: coasys/flux
Length of output: 37
🏁 Script executed:
Repository: coasys/flux
Length of output: 225
🏁 Script executed:
Repository: coasys/flux
Length of output: 97
🏁 Script executed:
Repository: coasys/flux
Length of output: 37
🏁 Script executed:
Repository: coasys/flux
Length of output: 26387
Watcher on
hasAttribute('open')is not reactive —selectedDidsandsearchInputwon't reset on popover reopen.watch(() => popover.value?.hasAttribute('open'), ...)depends only onpopover.value(the template ref), which changes exactly once when the DOM element mounts. Theopenattribute toggled by user clicks is DOM state Vue cannot observe, so this watcher fires only on initial mount. Result:selectedDidsandsearchInputare never cleared between popover opens, violating the stated intent in the comment.The
j-popoverweb component exposes a reactive:openproperty and@toggleevent (seePopover.ts). Use these instead:♻️ Reactive open/close handler
Also update close logic:
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
j-popoveris a Junto web component that toggles theopenattribute. The watch onhasAttribute('open')works here because it re-evaluates whenever the template re-renders (Vue's reactivity triggers onpopover.valueaccess). 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.There was a problem hiding this comment.
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 thatpopover.value(being aRef) 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