Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions composables/useBranding.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import { computed, watch } from "vue";
import { computed, ref, watch } from "vue";
import { useApplicationSettingsStore } from "~/stores/ApplicationSettings";

// The browser falls back to this on its own when the page has no icon link.
const DEFAULT_FAVICON = "/favicon.ico";

// Something that has to own the tab's icon for a while (the tab flash's alert
// icon) sets this instead of writing the link itself, so a branding change in
// the meantime can't overwrite it and clearing it always lands on the current
// branded icon rather than a stale copy.
export const faviconOverride = ref<string | null>(null);

export const brandedFaviconHref = computed(() => {
const setting = useApplicationSettingsStore().settings.find(
(s: { name: string }) => s.name === "public.favicon_url",
);

if (!setting?.value) {
return DEFAULT_FAVICON;
}

// The asset path is stable, so the setting value is the cache-busting token.
return `https://${useRuntimeConfig().public.apiDomain}/branding/favicon?v=${encodeURIComponent(setting.value)}`;
});

// The app is dark-only, so these are the only branding colors applied.
const colorMap: Record<string, string> = {
// Core
Expand Down Expand Up @@ -103,6 +125,28 @@ export function useBranding() {
// Swap the favicon + iOS apple-touch-icon to the uploaded branding asset.
// The PWA manifest link is set once in app.vue (host-aware), not here, so
// Chrome never sees the static "5stack" manifest flip to the branded one.
watch(
[faviconOverride, brandedFaviconHref],
([override, branded]) => {
const href = override ?? branded;
let link = document.querySelector(
"link[rel='icon']",
) as HTMLLinkElement | null;

if (!link) {
if (href === DEFAULT_FAVICON) {
return;
}
link = document.createElement("link");
link.rel = "icon";
document.head.appendChild(link);
}

link.href = href;
},
{ immediate: true },
);

watch(
() => store.settings,
() => {
Expand All @@ -117,27 +161,14 @@ export function useBranding() {

// Asset paths are stable, so the setting values are used as cache-busting
// version tokens. Favicon and logo/PWA icon are independent uploads now.
const faviconVersion = encodeURIComponent(faviconSetting?.value || "");
const pwaVersion = encodeURIComponent(pwaIconSetting?.value || "");

if (faviconSetting?.value) {
let link = document.querySelector(
"link[rel='icon']",
) as HTMLLinkElement | null;
if (!link) {
link = document.createElement("link");
link.rel = "icon";
document.head.appendChild(link);
}
link.href = `https://${apiDomain}/branding/favicon?v=${faviconVersion}`;
}

// iOS "Add to Home Screen" uses apple-touch-icon, not the manifest icon.
// Prefer the 512px PWA icon (a tiny .ico favicon makes a poor home icon).
const appleHref = pwaIconSetting?.value
? `https://${apiDomain}/branding/pwa/512?v=${pwaVersion}`
: faviconSetting?.value
? `https://${apiDomain}/branding/favicon?v=${faviconVersion}`
? brandedFaviconHref.value
: null;

if (appleHref) {
Expand Down
6 changes: 6 additions & 0 deletions composables/useCameraTalkback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
hangupPlayerTalk,
negotiateWebRtc,
} from "~/composables/useCameraApi";
import { useTabFlash } from "~/composables/useTabFlash";
import { useIceServers } from "~/composables/useIceServers";

// The other direction: an organizer talking to the player whose camera this is.
Expand Down Expand Up @@ -75,6 +76,7 @@ export function useCameraTalkback(matchId: () => string) {

function end() {
talking.value = false;
useTabFlash().clear("admin_call");
muted.value = false;
talkPc?.close();
talkPc = null;
Expand All @@ -93,6 +95,10 @@ export function useCameraTalkback(matchId: () => string) {

if (ready && !talking.value) {
await join();

if (talking.value && !disposed) {
useTabFlash().signal("admin_call");
}
} else if (!ready && talking.value) {
end();
}
Expand Down
3 changes: 3 additions & 0 deletions composables/useChatTabSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export function useChatTabSetup() {
if (String(message?.from?.steam_id) === String(authStore.me?.steam_id)) {
return;
}
// Ahead of the on-screen check, which counts an open room as read even
// while the browser tab is hidden.
useTabFlash().signalChat(tab.type, message);

const isOnScreen =
activeTabId.value === tab.id &&
Expand Down
11 changes: 9 additions & 2 deletions composables/useIncomingDirectMessages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { watch } from "vue";
import socket from "~/web-sockets/Socket";
import socket, { type LobbyMessage } from "~/web-sockets/Socket";
import {
directTabId,
peerSteamId,
Expand Down Expand Up @@ -94,13 +94,20 @@ export function useIncomingDirectMessages() {

socket.listen(
"direct:incoming",
(data: { roomId: string; from: DirectMessagePeer }) => {
(data: {
roomId: string;
from: DirectMessagePeer;
message?: LobbyMessage;
}) => {
const steamId = authStore.me?.steam_id;

if (!steamId || !data?.roomId) {
return;
}

// The room's own `lobby:chat` carries the same id when its tab is open.
useTabFlash().signalChat("direct", data.message);

// Deliberately does not inject the message: opening the tab makes
// useChatTabSetup join the room, and the join's history snapshot delivers
// it (deduped by chatMessageKey either way).
Expand Down
Loading
Loading