v0.12.0 codes for FCM and rebranded jama to RosterChirp
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const PLANS = [
|
||||
{ value: 'chat', label: 'JAMA-Chat', desc: 'Chat only' },
|
||||
{ value: 'brand', label: 'JAMA-Brand', desc: 'Chat + Branding' },
|
||||
{ value: 'team', label: 'JAMA-Team', desc: 'Chat + Branding + Groups + Schedule' },
|
||||
{ value: 'chat', label: 'RosterChirp-Chat', desc: 'Chat only' },
|
||||
{ value: 'brand', label: 'RosterChirp-Brand', desc: 'Chat + Branding' },
|
||||
{ value: 'team', label: 'RosterChirp-Team', desc: 'Chat + Branding + Groups + Schedule' },
|
||||
];
|
||||
|
||||
const PLAN_BADGE = {
|
||||
@@ -393,7 +393,7 @@ function KeyEntry({ onSubmit }) {
|
||||
headers: { 'X-Host-Admin-Key': key.trim() },
|
||||
});
|
||||
if (res.ok) {
|
||||
sessionStorage.setItem('jama-host-key', key.trim());
|
||||
sessionStorage.setItem('rosterchirp-host-key', key.trim());
|
||||
onSubmit(key.trim());
|
||||
} else {
|
||||
setError('Invalid admin key');
|
||||
@@ -406,7 +406,7 @@ function KeyEntry({ onSubmit }) {
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: 40, width: '100%', maxWidth: 380,
|
||||
boxShadow: '0 2px 16px rgba(0,0,0,0.12)', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 8 }}>🏠</div>
|
||||
<h1 style={{ fontSize: 20, fontWeight: 700, margin: '0 0 4px' }}>JAMA-HOST</h1>
|
||||
<h1 style={{ fontSize: 20, fontWeight: 700, margin: '0 0 4px' }}>RosterChirp-Host</h1>
|
||||
<p style={{ color: '#5f6368', fontSize: 13, margin: '0 0 24px' }}>Host Administration Panel</p>
|
||||
{error && <div style={{ padding: '8px 12px', background: '#fce8e6', color: '#d93025',
|
||||
borderRadius: 6, fontSize: 13, marginBottom: 16 }}>{error}</div>}
|
||||
@@ -428,7 +428,7 @@ function KeyEntry({ onSubmit }) {
|
||||
// ── Main host admin panel ─────────────────────────────────────────────────────
|
||||
|
||||
export default function HostAdmin() {
|
||||
const [adminKey, setAdminKey] = useState(() => sessionStorage.getItem('jama-host-key') || '');
|
||||
const [adminKey, setAdminKey] = useState(() => sessionStorage.getItem('rosterchirp-host-key') || '');
|
||||
const [status, setStatus] = useState(null);
|
||||
const [tenants, setTenants] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -454,7 +454,7 @@ export default function HostAdmin() {
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
if (e.message.includes('Invalid') || e.message.includes('401')) {
|
||||
sessionStorage.removeItem('jama-host-key');
|
||||
sessionStorage.removeItem('rosterchirp-host-key');
|
||||
setAdminKey('');
|
||||
}
|
||||
} finally { setLoading(false); }
|
||||
@@ -479,7 +479,7 @@ export default function HostAdmin() {
|
||||
!search || t.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
t.slug.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
const baseDomain = status?.baseDomain || 'jamachat.com';
|
||||
const baseDomain = status?.baseDomain || 'rosterchirp.com';
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#f1f3f4', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' }}>
|
||||
@@ -489,7 +489,7 @@ export default function HostAdmin() {
|
||||
justifyContent: 'space-between', height: 56 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 20 }}>🏠</span>
|
||||
<span style={{ fontWeight: 700, fontSize: 16 }}>JAMA-HOST</span>
|
||||
<span style={{ fontWeight: 700, fontSize: 16 }}>RosterChirp-Host</span>
|
||||
<span style={{ opacity: 0.7, fontSize: 13 }}>/ {baseDomain}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
@@ -498,7 +498,7 @@ export default function HostAdmin() {
|
||||
{status.tenants.active} active · {status.tenants.total} total
|
||||
</span>
|
||||
)}
|
||||
<Btn size="sm" variant="secondary" onClick={() => { sessionStorage.removeItem('jama-host-key'); setAdminKey(''); }}>
|
||||
<Btn size="sm" variant="secondary" onClick={() => { sessionStorage.removeItem('rosterchirp-host-key'); setAdminKey(''); }}>
|
||||
Sign Out
|
||||
</Btn>
|
||||
</div>
|
||||
@@ -581,7 +581,7 @@ export default function HostAdmin() {
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{ textAlign: 'center', marginTop: 24, fontSize: 12, color: '#9aa0a6' }}>
|
||||
JAMA-HOST Control Plane · {baseDomain}
|
||||
RosterChirp-Host Control Plane · {baseDomain}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
const appName = settings.app_name || 'jama';
|
||||
const appName = settings.app_name || 'rosterchirp';
|
||||
const logoUrl = settings.logo_url;
|
||||
|
||||
return (
|
||||
@@ -77,7 +77,7 @@ export default function Login() {
|
||||
{logoUrl ? (
|
||||
<img src={logoUrl} alt={appName} className="logo-img" />
|
||||
) : (
|
||||
<img src="/icons/jama.png" alt="jama" className="logo-img" />
|
||||
<img src="/icons/rosterchirp.png" alt="rosterchirp" className="logo-img" />
|
||||
)}
|
||||
<h1>{appName}</h1>
|
||||
<p>Sign in to continue</p>
|
||||
|
||||
Reference in New Issue
Block a user