v0.12.0 codes for FCM and rebranded jama to RosterChirp

This commit is contained in:
2026-03-22 20:15:57 -04:00
parent 21dc788cd3
commit 819d60d693
40 changed files with 426 additions and 363 deletions

View File

@@ -42,7 +42,7 @@ export default function Chat() {
const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat' | 'help' | 'groupmanager'
const [page, setPage] = useState('chat'); // 'chat' | 'schedule' | 'groupmessages'
const [drawerOpen, setDrawerOpen] = useState(false);
const [features, setFeatures] = useState({ branding: false, groupManager: false, scheduleManager: false, appType: 'JAMA-Chat', teamToolManagers: [], isHostDomain: false });
const [features, setFeatures] = useState({ branding: false, groupManager: false, scheduleManager: false, appType: 'RosterChirp-Chat', teamToolManagers: [], isHostDomain: false });
const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [showSidebar, setShowSidebar] = useState(true);
@@ -81,7 +81,7 @@ export default function Chat() {
branding: settings.feature_branding === 'true',
groupManager: settings.feature_group_manager === 'true',
scheduleManager: settings.feature_schedule_manager === 'true',
appType: settings.app_type || 'JAMA-Chat',
appType: settings.app_type || 'RosterChirp-Chat',
teamToolManagers: JSON.parse(settings.team_tool_managers || settings.team_group_managers || '[]'),
isHostDomain: settings.is_host_domain === 'true',
}));
@@ -93,53 +93,59 @@ export default function Chat() {
useEffect(() => {
loadFeatures();
window.addEventListener('jama:settings-changed', loadFeatures);
return () => window.removeEventListener('jama:settings-changed', loadFeatures);
window.addEventListener('rosterchirp:settings-changed', loadFeatures);
return () => window.removeEventListener('rosterchirp:settings-changed', loadFeatures);
}, [loadFeatures]);
// Register / refresh push subscription
// Register / refresh FCM push subscription
useEffect(() => {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
if (!('serviceWorker' in navigator)) return;
const registerPush = async () => {
try {
const permission = Notification.permission;
if (permission === 'denied') return;
if (Notification.permission === 'denied') return;
// Fetch Firebase config from backend (returns 503 if FCM not configured)
const configRes = await fetch('/api/push/firebase-config');
if (!configRes.ok) return;
const { apiKey, projectId, messagingSenderId, appId, vapidKey } = await configRes.json();
// Dynamically import the Firebase SDK (tree-shaken, only loaded when needed)
const { initializeApp, getApps } = await import('firebase/app');
const { getMessaging, getToken } = await import('firebase/messaging');
const firebaseApp = getApps().length
? getApps()[0]
: initializeApp({ apiKey, projectId, messagingSenderId, appId });
const firebaseMessaging = getMessaging(firebaseApp);
const reg = await navigator.serviceWorker.ready;
const { publicKey } = await fetch('/api/push/vapid-public').then(r => r.json());
const token = localStorage.getItem('tc_token') || sessionStorage.getItem('tc_token');
let sub = await reg.pushManager.getSubscription();
if (!sub) {
// First time or subscription was lost — request permission then subscribe
const granted = permission === 'granted'
? 'granted'
: await Notification.requestPermission();
if (Notification.permission !== 'granted') {
const granted = await Notification.requestPermission();
if (granted !== 'granted') return;
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
}
// Always re-register subscription with the server (keeps it fresh on mobile)
const fcmToken = await getToken(firebaseMessaging, {
vapidKey,
serviceWorkerRegistration: reg,
});
if (!fcmToken) return;
const token = localStorage.getItem('tc_token') || sessionStorage.getItem('tc_token');
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(sub.toJSON()),
body: JSON.stringify({ fcmToken }),
});
console.log('[Push] Subscription registered');
console.log('[Push] FCM subscription registered');
} catch (e) {
console.warn('[Push] Subscription failed:', e.message);
console.warn('[Push] FCM subscription failed:', e.message);
}
};
registerPush();
// Bug A fix: re-register push subscription when app returns to foreground
// Mobile browsers can drop push subscriptions when the app is backgrounded
const handleVisibility = () => {
if (document.visibilityState === 'visible') registerPush();
};
@@ -263,7 +269,7 @@ export default function Chat() {
// so we force logout unconditionally — the new session will reconnect cleanly)
localStorage.removeItem('tc_token');
sessionStorage.removeItem('tc_token');
window.dispatchEvent(new CustomEvent('jama:session-displaced'));
window.dispatchEvent(new CustomEvent('rosterchirp:session-displaced'));
};
// Online presence
@@ -320,7 +326,7 @@ export default function Chat() {
if (isMobile) {
setShowSidebar(false);
// Push a history entry so swipe-back returns to sidebar instead of exiting the app
window.history.pushState({ jamaChatOpen: true }, '');
window.history.pushState({ rosterchirpChatOpen: true }, '');
}
// Clear notifications and unread count for this group
setNotifications(prev => prev.filter(n => n.groupId !== id));
@@ -334,7 +340,7 @@ export default function Chat() {
setShowSidebar(true);
setActiveGroupId(null);
// Push another entry so subsequent back gestures are also intercepted
window.history.pushState({ jamaChatOpen: true }, '');
window.history.pushState({ rosterchirpChatOpen: true }, '');
}
};
window.addEventListener('popstate', handlePopState);