feat(toast): message toast opens the related chat + has a close icon (#33)
- Add a close (X) button to the message toast (closeToast, @click.stop) like the system notifications. - Carry the sender pubkey on the toast; clicking now deep-links to that conversation (/dashboard/mesh?peer=<pubkey>) instead of the generic mesh page. - Mesh.vue reads ?peer= on mount and opens the matching peer (by pubkey_hex/did), gracefully falling back to the mesh list when no match (B1/B2 identity). type-check clean; useMessageToast tests 11/11. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4576964be4
commit
b602a9cea5
@ -59,6 +59,15 @@
|
|||||||
<p class="mt-0.5 text-sm text-white/70 line-clamp-2">{{ toastMessage.text }}</p>
|
<p class="mt-0.5 text-sm text-white/70 line-clamp-2">{{ toastMessage.text }}</p>
|
||||||
<p class="mt-1 text-xs text-orange-400">Click to view</p>
|
<p class="mt-1 text-xs text-orange-400">Click to view</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
@click.stop="messageToast.closeToast"
|
||||||
|
aria-label="Dismiss notification"
|
||||||
|
class="-mt-1 -mr-1 shrink-0 rounded-full p-1 text-white/40 transition-colors hover:bg-white/10 hover:text-white/80"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const MESSAGE_POLL_INTERVAL = 30000 // 30s
|
|||||||
const receivedMessages = ref<ReceivedMessage[]>([])
|
const receivedMessages = ref<ReceivedMessage[]>([])
|
||||||
const lastMessageCount = ref(0)
|
const lastMessageCount = ref(0)
|
||||||
const loadingMessages = ref(false)
|
const loadingMessages = ref(false)
|
||||||
const toastMessage = ref<{ show: boolean; text: string }>({ show: false, text: '' })
|
const toastMessage = ref<{ show: boolean; text: string; fromPubkey: string }>({ show: false, text: '', fromPubkey: '' })
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
export function useMessageToast() {
|
export function useMessageToast() {
|
||||||
@ -37,6 +37,9 @@ export function useMessageToast() {
|
|||||||
toastMessage.value = {
|
toastMessage.value = {
|
||||||
show: true,
|
show: true,
|
||||||
text: (newCount === 1 ? latest?.message : null) ?? `${newCount} new messages`,
|
text: (newCount === 1 ? latest?.message : null) ?? `${newCount} new messages`,
|
||||||
|
// Only deep-link to a specific chat when it's a single new message
|
||||||
|
// from one sender; otherwise open the mesh list.
|
||||||
|
fromPubkey: newCount === 1 ? (latest?.from_pubkey ?? '') : '',
|
||||||
}
|
}
|
||||||
lastMessageCount.value = msgs.length
|
lastMessageCount.value = msgs.length
|
||||||
} else {
|
} else {
|
||||||
@ -83,9 +86,16 @@ export function useMessageToast() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dismissToastAndOpenMessages() {
|
function dismissToastAndOpenMessages() {
|
||||||
toastMessage.value = { show: false, text: '' }
|
const peer = toastMessage.value.fromPubkey
|
||||||
|
toastMessage.value = { show: false, text: '', fromPubkey: '' }
|
||||||
markAsRead()
|
markAsRead()
|
||||||
router.push('/dashboard/mesh')
|
// Open the specific conversation when we know the sender; else the mesh list.
|
||||||
|
router.push(peer ? { path: '/dashboard/mesh', query: { peer } } : '/dashboard/mesh')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dismiss the toast without navigating (the close icon).
|
||||||
|
function closeToast() {
|
||||||
|
toastMessage.value = { show: false, text: '', fromPubkey: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -99,5 +109,6 @@ export function useMessageToast() {
|
|||||||
stopPolling,
|
stopPolling,
|
||||||
markAsRead,
|
markAsRead,
|
||||||
dismissToastAndOpenMessages,
|
dismissToastAndOpenMessages,
|
||||||
|
closeToast,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useMeshStore } from '@/stores/mesh'
|
import { useMeshStore } from '@/stores/mesh'
|
||||||
import { useTransportStore } from '@/stores/transport'
|
import { useTransportStore } from '@/stores/transport'
|
||||||
import type { MeshMessage, MeshPeer, SessionStatus } from '@/stores/mesh'
|
import type { MeshMessage, MeshPeer, SessionStatus } from '@/stores/mesh'
|
||||||
@ -12,6 +13,7 @@ import '@/views/mesh/mesh-styles.css'
|
|||||||
|
|
||||||
const mesh = useMeshStore()
|
const mesh = useMeshStore()
|
||||||
const transport = useTransportStore()
|
const transport = useTransportStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
// Responsive layout breakpoints
|
// Responsive layout breakpoints
|
||||||
const isWideDesktop = ref(window.innerWidth >= 1536)
|
const isWideDesktop = ref(window.innerWidth >= 1536)
|
||||||
@ -301,6 +303,15 @@ onMounted(async () => {
|
|||||||
loadPendingFromSession()
|
loadPendingFromSession()
|
||||||
await Promise.all([mesh.refreshAll(), transport.fetchStatus(), refreshFederationNodes(), refreshSelfOnion(), refreshSelfDid(), refreshContacts()])
|
await Promise.all([mesh.refreshAll(), transport.fetchStatus(), refreshFederationNodes(), refreshSelfOnion(), refreshSelfDid(), refreshContacts()])
|
||||||
refreshOutboxCount()
|
refreshOutboxCount()
|
||||||
|
// Deep-link from a message toast: open the sender's conversation if we can
|
||||||
|
// match it; otherwise just land on the mesh page (graceful fallback).
|
||||||
|
const targetPeer = typeof route.query.peer === 'string' ? route.query.peer : ''
|
||||||
|
if (targetPeer) {
|
||||||
|
const match = mesh.peers.find(
|
||||||
|
(p) => p.pubkey_hex === targetPeer || p.did === targetPeer
|
||||||
|
)
|
||||||
|
if (match) openChat(match)
|
||||||
|
}
|
||||||
// Start background polling for Archipelago (Tor) messages so unread count works
|
// Start background polling for Archipelago (Tor) messages so unread count works
|
||||||
loadArchMessages()
|
loadArchMessages()
|
||||||
if (!archPollInterval) {
|
if (!archPollInterval) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user