Show new-messages badge in tab title when re-foregrounding (#332)

Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social>
This commit is contained in:
Aditya Chaudhary 2026-08-10 16:16:30 +00:00 committed by hodlbod
parent b2f58b11c0
commit 65c9eac5e5
3 changed files with 40 additions and 6 deletions

View file

@ -15,6 +15,7 @@
import Login2 from "@assets/icons/login-3.svg?dataurl"
import {fade, fly} from "@lib/transition"
import {popKey} from "@lib/implicit"
import {documentActive} from "@lib/html"
import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.svelte"
import Icon from "@lib/components/Icon.svelte"
@ -301,8 +302,10 @@
}
}
const onVisibilityChange = () => {
if (document.hidden) {
// A tab can be `visible` but unfocused (user alt-tabbed to another app), so we
// can't rely on document.hidden alone to know the room is actually being watched.
const onActiveChange = (active: boolean) => {
if (!active) {
lastVisibleAt = now()
} else if ($events.some(e => e.pubkey !== $user.pubkey && e.created_at > lastVisibleAt)) {
newMessagesAfter = lastVisibleAt
@ -450,12 +453,12 @@
onMount(() => {
start()
document.addEventListener("visibilitychange", onVisibilityChange)
const unsubscribeActive = documentActive.subscribe(onActiveChange)
return () => {
// Wrap in a closure to avoid calling a stale cleanup function
cleanup?.()
document.removeEventListener("visibilitychange", onVisibilityChange)
unsubscribeActive()
}
})
</script>

View file

@ -1,7 +1,32 @@
import {readable} from "svelte/store"
import {sleep, randomId} from "@welshman/lib"
import {Capacitor} from "@capacitor/core"
export {preventDefault, stopPropagation} from "svelte/legacy"
/** Whether the user is actually looking at this tab right now. Display-only concern,
* kept separate from any data store — consult it wherever "is someone watching" should
* affect what's rendered. `document.hidden` alone misses window blur: switching to
* another app without switching tabs leaves visibilityState "visible", so we also track
* focus and require both. */
export const documentActive = readable(
typeof document === "undefined" ? true : !document.hidden && document.hasFocus(),
set => {
if (typeof document === "undefined") return
const update = () => set(!document.hidden && document.hasFocus())
document.addEventListener("visibilitychange", update)
window.addEventListener("blur", update)
window.addEventListener("focus", update)
return () => {
document.removeEventListener("visibilitychange", update)
window.removeEventListener("blur", update)
window.removeEventListener("focus", update)
}
},
)
// Anchors an @svelte-plugins/datepicker popup with fixed positioning so it
// escapes scroll-container clipping (e.g. inside modals). Call when the picker
// opens; returns a cleanup function that removes the listeners.

View file

@ -15,7 +15,7 @@
import * as util from "@welshman/util"
import * as lib from "@welshman/lib"
import {Logger} from "@welshman/app"
import {isMobile} from "@lib/html"
import {isMobile, documentActive} from "@lib/html"
import AppContainer from "@app/components/AppContainer.svelte"
import ModalContainer from "@app/components/ModalContainer.svelte"
import {app} from "@app/core"
@ -33,6 +33,7 @@
import {theme} from "@app/theme"
import {toast, pushToast} from "@app/toast"
import * as notifications from "@app/notifications"
import {notifications as notificationPaths, allNotifications} from "@app/notifications"
import {Push} from "@app/push"
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
import {syncKeyboard} from "@app/keyboard"
@ -239,7 +240,12 @@
})
$effect(() => {
document.title = getPageTitle({page: $page, pubkey: $app.user?.pubkey})
const title = getPageTitle({page: $page, pubkey: $app.user?.pubkey})
// While the tab isn't actively focused the user isn't actually looking at the
// active page, so count notifications for it too rather than treating it as read.
const unreadCount = $documentActive ? $notificationPaths.size : $allNotifications.size
document.title = unreadCount > 0 ? `(${unreadCount}) ${title}` : title
})
</script>