v0.12.0 codes for FCM and rebranded jama to RosterChirp
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/icons/jama.png" />
|
||||
<link rel="icon" type="image/png" href="/icons/rosterchirp.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<meta name="theme-color" content="#1a73e8" />
|
||||
<meta name="description" content="jama - just another messaging app" />
|
||||
<meta name="description" content="RosterChirp - team messaging" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
||||
<title>jama</title>
|
||||
<title>RosterChirp</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "jama-frontend",
|
||||
"name": "rosterchirp-frontend",
|
||||
"version": "0.11.25",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -17,7 +17,8 @@
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"date-fns": "^3.3.1",
|
||||
"marked": "^12.0.0"
|
||||
"marked": "^12.0.0",
|
||||
"firebase": "^10.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama",
|
||||
"short_name": "jama",
|
||||
"name": "RosterChirp",
|
||||
"short_name": "RosterChirp",
|
||||
"description": "Modern team messaging application",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
const CACHE_NAME = 'jama-v1';
|
||||
// ── Firebase Messaging (background push for Android PWA) ──────────────────────
|
||||
// Fill in the values below from Firebase Console → Project Settings → General → Your apps
|
||||
// Leave apiKey as '__FIREBASE_API_KEY__' if not using FCM (push will be disabled).
|
||||
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js');
|
||||
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js');
|
||||
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDx191unzXFT4WA1OvkdbrIY_c57kgruAU",
|
||||
authDomain: "rosterchirp-push.firebaseapp.com",
|
||||
projectId: "rosterchirp-push",
|
||||
storageBucket: "rosterchirp-push.firebasestorage.app",
|
||||
messagingSenderId: "126479377334",
|
||||
appId: "1:126479377334:web:280abdd135cf7e0c50d717"
|
||||
};
|
||||
|
||||
// Only initialise Firebase if the config has been filled in
|
||||
let messaging = null;
|
||||
if (FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') {
|
||||
firebase.initializeApp(FIREBASE_CONFIG);
|
||||
messaging = firebase.messaging();
|
||||
}
|
||||
|
||||
// ── Cache ─────────────────────────────────────────────────────────────────────
|
||||
const CACHE_NAME = 'rosterchirp-v1';
|
||||
const STATIC_ASSETS = ['/'];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
@@ -27,53 +50,35 @@ self.addEventListener('fetch', (event) => {
|
||||
);
|
||||
});
|
||||
|
||||
// Track badge count in SW scope
|
||||
// ── Badge counter ─────────────────────────────────────────────────────────────
|
||||
let badgeCount = 0;
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
if (!event.data) return;
|
||||
|
||||
let data = {};
|
||||
try { data = event.data.json(); } catch (e) { return; }
|
||||
|
||||
function showRosterChirpNotification(data) {
|
||||
badgeCount++;
|
||||
if (self.navigator?.setAppBadge) self.navigator.setAppBadge(badgeCount).catch(() => {});
|
||||
|
||||
// Update app badge
|
||||
if (self.navigator && self.navigator.setAppBadge) {
|
||||
self.navigator.setAppBadge(badgeCount).catch(() => {});
|
||||
}
|
||||
|
||||
// Check if app is currently visible — if so, skip the notification
|
||||
const showNotification = clients.matchAll({
|
||||
type: 'window',
|
||||
includeUncontrolled: true,
|
||||
}).then((clientList) => {
|
||||
const appVisible = clientList.some(
|
||||
(c) => c.visibilityState === 'visible'
|
||||
);
|
||||
// Still show if app is open but hidden (minimized), skip only if truly visible
|
||||
if (appVisible) return;
|
||||
|
||||
return self.registration.showNotification(data.title || 'New Message', {
|
||||
body: data.body || '',
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/icon-192-maskable.png',
|
||||
data: { url: data.url || '/' },
|
||||
// Use unique tag per group so notifications group by conversation
|
||||
tag: data.groupId ? `jama-group-${data.groupId}` : 'jama-message',
|
||||
renotify: true,
|
||||
});
|
||||
return self.registration.showNotification(data.title || 'New Message', {
|
||||
body: data.body || '',
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/icon-192-maskable.png',
|
||||
data: { url: data.url || '/' },
|
||||
tag: data.groupId ? `rosterchirp-group-${data.groupId}` : 'rosterchirp-message',
|
||||
renotify: true,
|
||||
});
|
||||
}
|
||||
|
||||
event.waitUntil(showNotification);
|
||||
});
|
||||
// ── FCM background messages ───────────────────────────────────────────────────
|
||||
if (messaging) {
|
||||
messaging.onBackgroundMessage((payload) => {
|
||||
return showRosterChirpNotification(payload.data || {});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Notification click ────────────────────────────────────────────────────────
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
badgeCount = 0;
|
||||
if (self.navigator && self.navigator.clearAppBadge) {
|
||||
self.navigator.clearAppBadge().catch(() => {});
|
||||
}
|
||||
if (self.navigator?.clearAppBadge) self.navigator.clearAppBadge().catch(() => {});
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
||||
const url = event.notification.data?.url || '/';
|
||||
@@ -88,17 +93,15 @@ self.addEventListener('notificationclick', (event) => {
|
||||
);
|
||||
});
|
||||
|
||||
// Clear badge when app signals it
|
||||
// ── Badge control messages from main thread ───────────────────────────────────
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'CLEAR_BADGE') {
|
||||
badgeCount = 0;
|
||||
if (self.navigator && self.navigator.clearAppBadge) {
|
||||
self.navigator.clearAppBadge().catch(() => {});
|
||||
}
|
||||
if (self.navigator?.clearAppBadge) self.navigator.clearAppBadge().catch(() => {});
|
||||
}
|
||||
if (event.data?.type === 'SET_BADGE') {
|
||||
badgeCount = event.data.count || 0;
|
||||
if (self.navigator && self.navigator.setAppBadge) {
|
||||
if (self.navigator?.setAppBadge) {
|
||||
if (badgeCount > 0) {
|
||||
self.navigator.setAppBadge(badgeCount).catch(() => {});
|
||||
} else {
|
||||
|
||||
@@ -27,7 +27,7 @@ function AuthRoute({ children }) {
|
||||
}
|
||||
|
||||
function RestoreTheme() {
|
||||
const saved = localStorage.getItem('jama-theme') || 'light';
|
||||
const saved = localStorage.getItem('rosterchirp-theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ export default function AboutModal({ onClose }) {
|
||||
}, []);
|
||||
|
||||
// Always use the original app identity — not the user-customised settings name/logo
|
||||
const appName = about?.default_app_name || 'jama';
|
||||
const logoSrc = about?.default_logo || '/icons/jama.png';
|
||||
const appName = about?.default_app_name || 'rosterchirp';
|
||||
const logoSrc = about?.default_logo || '/icons/rosterchirp.png';
|
||||
const version = about?.version || '';
|
||||
const a = about || {};
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ export default function BrandingModal({ onClose }) {
|
||||
useEffect(() => {
|
||||
api.getSettings().then(({ settings }) => {
|
||||
setSettings(settings);
|
||||
setAppName(settings.app_name || 'jama');
|
||||
setAppName(settings.app_name || 'rosterchirp');
|
||||
setColourTitle(settings.color_title || DEFAULT_TITLE_COLOR);
|
||||
setColourTitleDark(settings.color_title_dark || DEFAULT_TITLE_DARK_COLOR);
|
||||
setColourPublic(settings.color_avatar_public || DEFAULT_PUBLIC_COLOR);
|
||||
@@ -329,7 +329,7 @@ export default function BrandingModal({ onClose }) {
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const notifySidebarRefresh = () => window.dispatchEvent(new Event('jama:settings-changed'));
|
||||
const notifySidebarRefresh = () => window.dispatchEvent(new Event('rosterchirp:settings-changed'));
|
||||
|
||||
const handleSaveName = async () => {
|
||||
if (!appName.trim()) return;
|
||||
@@ -391,7 +391,7 @@ export default function BrandingModal({ onClose }) {
|
||||
await api.resetSettings();
|
||||
const { settings: fresh } = await api.getSettings();
|
||||
setSettings(fresh);
|
||||
setAppName(fresh.app_name || 'jama');
|
||||
setAppName(fresh.app_name || 'rosterchirp');
|
||||
setColourTitle(DEFAULT_TITLE_COLOR);
|
||||
setColourTitleDark(DEFAULT_TITLE_DARK_COLOR);
|
||||
setColourPublic(DEFAULT_PUBLIC_COLOR);
|
||||
@@ -433,7 +433,7 @@ export default function BrandingModal({ onClose }) {
|
||||
border: '1px solid var(--border)', overflow: 'hidden', display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', flexShrink: 0
|
||||
}}>
|
||||
<img src={settings.logo_url || '/icons/jama.png'} alt="logo" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
<img src={settings.logo_url || '/icons/rosterchirp.png'} alt="logo" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="btn btn-secondary btn-sm" style={{ cursor: 'pointer', display: 'inline-block' }}>
|
||||
|
||||
@@ -48,11 +48,11 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
|
||||
setIconGroupInfo(settings.icon_groupinfo || '');
|
||||
setAvatarColors({ public: settings.color_avatar_public || '#1a73e8', dm: settings.color_avatar_dm || '#a142f4' });
|
||||
}).catch(() => {});
|
||||
window.addEventListener('jama:settings-updated', handler);
|
||||
window.addEventListener('jama:settings-changed', handler);
|
||||
window.addEventListener('rosterchirp:settings-updated', handler);
|
||||
window.addEventListener('rosterchirp:settings-changed', handler);
|
||||
return () => {
|
||||
window.removeEventListener('jama:settings-updated', handler);
|
||||
window.removeEventListener('jama:settings-changed', handler);
|
||||
window.removeEventListener('rosterchirp:settings-updated', handler);
|
||||
window.removeEventListener('rosterchirp:settings-changed', handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -4,24 +4,24 @@ import { api } from '../utils/api.js';
|
||||
|
||||
export default function GlobalBar({ isMobile, showSidebar, onBurger }) {
|
||||
const { connected } = useSocket();
|
||||
const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '' });
|
||||
const [settings, setSettings] = useState({ app_name: 'rosterchirp', logo_url: '' });
|
||||
const [isDark, setIsDark] = useState(() => document.documentElement.getAttribute('data-theme') === 'dark');
|
||||
|
||||
useEffect(() => {
|
||||
api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {});
|
||||
const handler = () => api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {});
|
||||
window.addEventListener('jama:settings-changed', handler);
|
||||
window.addEventListener('rosterchirp:settings-changed', handler);
|
||||
const themeObserver = new MutationObserver(() => {
|
||||
setIsDark(document.documentElement.getAttribute('data-theme') === 'dark');
|
||||
});
|
||||
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
return () => {
|
||||
window.removeEventListener('jama:settings-changed', handler);
|
||||
window.removeEventListener('rosterchirp:settings-changed', handler);
|
||||
themeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const appName = settings.app_name || 'jama';
|
||||
const appName = settings.app_name || 'rosterchirp';
|
||||
const logoUrl = settings.logo_url;
|
||||
const titleColor = (isDark ? settings.color_title_dark : settings.color_title) || null;
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function GlobalBar({ isMobile, showSidebar, onBurger }) {
|
||||
</svg>
|
||||
</button>
|
||||
<div className="global-bar-brand">
|
||||
<img src={logoUrl || '/icons/jama.png'} alt={appName} className="global-bar-logo" />
|
||||
<img src={logoUrl || '/icons/rosterchirp.png'} alt={appName} className="global-bar-logo" />
|
||||
<span className="global-bar-title" style={titleColor ? { color: titleColor } : {}}>{appName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* HostPanel.jsx — JAMA-HOST Control Panel
|
||||
* HostPanel.jsx — RosterChirp-Host Control Panel
|
||||
*
|
||||
* Renders inside the main JAMA right-panel area (not a separate page/route).
|
||||
* Renders inside the main RosterChirp right-panel area (not a separate page/route).
|
||||
* Protected by:
|
||||
* 1. Only shown when is_host_domain === true (server-computed from HOST_DOMAIN)
|
||||
* 2. Only accessible to admin role users
|
||||
@@ -15,9 +15,9 @@ import UserFooter from './UserFooter.jsx';
|
||||
// ── 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_COLOURS = {
|
||||
@@ -307,7 +307,7 @@ function KeyEntry({ onSubmit }) {
|
||||
setChecking(true); setError('');
|
||||
try {
|
||||
const res = await fetch('/api/host/status', { headers: { 'X-Host-Admin-Key': key.trim() } });
|
||||
if (res.ok) { sessionStorage.setItem('jama-host-key', key.trim()); onSubmit(key.trim()); }
|
||||
if (res.ok) { sessionStorage.setItem('rosterchirp-host-key', key.trim()); onSubmit(key.trim()); }
|
||||
else setError('Invalid admin key');
|
||||
} catch { setError('Connection error'); }
|
||||
finally { setChecking(false); }
|
||||
@@ -336,7 +336,7 @@ function KeyEntry({ onSubmit }) {
|
||||
|
||||
export default function HostPanel({ onProfile, onHelp, onAbout }) {
|
||||
const { user } = useAuth();
|
||||
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);
|
||||
@@ -363,7 +363,7 @@ export default function HostPanel({ onProfile, onHelp, onAbout }) {
|
||||
toast(e.message, 'error');
|
||||
// Key is invalid — clear it so the prompt shows again
|
||||
if (e.message.includes('401') || e.message.includes('Invalid') || e.message.includes('401')) {
|
||||
sessionStorage.removeItem('jama-host-key');
|
||||
sessionStorage.removeItem('rosterchirp-host-key');
|
||||
setAdminKey('');
|
||||
}
|
||||
} finally { setLoading(false); }
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useToast } from '../contexts/ToastContext.jsx';
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const APP_TYPES = {
|
||||
'JAMA-Chat': { label: 'JAMA-Chat', desc: 'Chat only. No Branding, Group Manager or Schedule Manager.' },
|
||||
'JAMA-Brand': { label: 'JAMA-Brand', desc: 'Chat and Branding.' },
|
||||
'JAMA-Team': { label: 'JAMA-Team', desc: 'Chat, Branding, Group Manager and Schedule Manager.' },
|
||||
'RosterChirp-Chat': { label: 'RosterChirp-Chat', desc: 'Chat only. No Branding, Group Manager or Schedule Manager.' },
|
||||
'RosterChirp-Brand': { label: 'RosterChirp-Brand', desc: 'Chat and Branding.' },
|
||||
'RosterChirp-Team': { label: 'RosterChirp-Team', desc: 'Chat, Branding, Group Manager and Schedule Manager.' },
|
||||
};
|
||||
|
||||
// ── Team Management Tab ───────────────────────────────────────────────────────
|
||||
@@ -34,7 +34,7 @@ function TeamManagementTab() {
|
||||
try {
|
||||
await api.updateTeamSettings({ toolManagers });
|
||||
toast('Team settings saved', 'success');
|
||||
window.dispatchEvent(new Event('jama:settings-changed'));
|
||||
window.dispatchEvent(new Event('rosterchirp:settings-changed'));
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
@@ -82,7 +82,7 @@ function RegistrationTab({ onFeaturesChanged }) {
|
||||
api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const appType = settings.app_type || 'JAMA-Chat';
|
||||
const appType = settings.app_type || 'RosterChirp-Chat';
|
||||
const activeCode = settings.registration_code || '';
|
||||
const adminEmail = settings.admin_email || '—';
|
||||
|
||||
@@ -104,7 +104,7 @@ function RegistrationTab({ onFeaturesChanged }) {
|
||||
const fresh = await api.getSettings();
|
||||
setSettings(fresh.settings);
|
||||
toast('Registration applied successfully.', 'success');
|
||||
window.dispatchEvent(new Event('jama:settings-changed'));
|
||||
window.dispatchEvent(new Event('rosterchirp:settings-changed'));
|
||||
onFeaturesChanged && onFeaturesChanged(f);
|
||||
} catch (e) { toast(e.message || 'Invalid registration code', 'error'); }
|
||||
finally { setRegLoading(false); }
|
||||
@@ -116,12 +116,12 @@ function RegistrationTab({ onFeaturesChanged }) {
|
||||
const fresh = await api.getSettings();
|
||||
setSettings(fresh.settings);
|
||||
toast('Registration cleared.', 'success');
|
||||
window.dispatchEvent(new Event('jama:settings-changed'));
|
||||
window.dispatchEvent(new Event('rosterchirp:settings-changed'));
|
||||
onFeaturesChanged && onFeaturesChanged(f);
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
};
|
||||
|
||||
const typeInfo = APP_TYPES[appType] || APP_TYPES['JAMA-Chat'];
|
||||
const typeInfo = APP_TYPES[appType] || APP_TYPES['RosterChirp-Chat'];
|
||||
const siteUrl = window.location.origin;
|
||||
|
||||
return (
|
||||
@@ -132,7 +132,7 @@ function RegistrationTab({ onFeaturesChanged }) {
|
||||
Registration {activeCode ? 'is' : 'required:'}
|
||||
</p>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6 }}>
|
||||
JAMA {activeCode ? 'is' : 'will be'} registered to:<br />
|
||||
RosterChirp {activeCode ? 'is' : 'will be'} registered to:<br />
|
||||
<strong>Type:</strong> {typeInfo.label}<br />
|
||||
<strong>URL:</strong> {siteUrl}
|
||||
</p>
|
||||
@@ -185,9 +185,9 @@ function RegistrationTab({ onFeaturesChanged }) {
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 16, lineHeight: 1.5 }}>
|
||||
Registration codes unlock application features. Contact your JAMA provider for a code.<br />
|
||||
<strong>JAMA-Brand</strong> — unlocks Branding.
|
||||
<strong>JAMA-Team</strong> — unlocks Branding, Group Manager and Schedule Manager.
|
||||
Registration codes unlock application features. Contact your RosterChirp provider for a code.<br />
|
||||
<strong>RosterChirp-Brand</strong> — unlocks Branding.
|
||||
<strong>RosterChirp-Team</strong> — unlocks Branding, Group Manager and Schedule Manager.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -270,18 +270,18 @@ function WebPushTab() {
|
||||
// ── Main modal ────────────────────────────────────────────────────────────────
|
||||
export default function SettingsModal({ onClose, onFeaturesChanged }) {
|
||||
const [tab, setTab] = useState('registration');
|
||||
const [appType, setAppType] = useState('JAMA-Chat');
|
||||
const [appType, setAppType] = useState('RosterChirp-Chat');
|
||||
|
||||
useEffect(() => {
|
||||
api.getSettings().then(({ settings }) => {
|
||||
setAppType(settings.app_type || 'JAMA-Chat');
|
||||
setAppType(settings.app_type || 'RosterChirp-Chat');
|
||||
}).catch(() => {});
|
||||
const handler = () => api.getSettings().then(({ settings }) => setAppType(settings.app_type || 'JAMA-Chat')).catch(() => {});
|
||||
window.addEventListener('jama:settings-changed', handler);
|
||||
return () => window.removeEventListener('jama:settings-changed', handler);
|
||||
const handler = () => api.getSettings().then(({ settings }) => setAppType(settings.app_type || 'RosterChirp-Chat')).catch(() => {});
|
||||
window.addEventListener('rosterchirp:settings-changed', handler);
|
||||
return () => window.removeEventListener('rosterchirp:settings-changed', handler);
|
||||
}, []);
|
||||
|
||||
const isTeam = appType === 'JAMA-Team';
|
||||
const isTeam = appType === 'RosterChirp-Team';
|
||||
|
||||
const tabs = [
|
||||
isTeam && { id: 'team', label: 'Team Management' },
|
||||
|
||||
@@ -14,20 +14,20 @@ function nameToColor(name) {
|
||||
}
|
||||
|
||||
function useAppSettings() {
|
||||
const [settings, setSettings] = useState({ app_name: 'jama', logo_url: '', color_avatar_public: '', color_avatar_dm: '' });
|
||||
const [settings, setSettings] = useState({ app_name: 'rosterchirp', logo_url: '', color_avatar_public: '', color_avatar_dm: '' });
|
||||
const fetchSettings = () => {
|
||||
api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {});
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
window.addEventListener('jama:settings-changed', fetchSettings);
|
||||
return () => window.removeEventListener('jama:settings-changed', fetchSettings);
|
||||
window.addEventListener('rosterchirp:settings-changed', fetchSettings);
|
||||
return () => window.removeEventListener('rosterchirp:settings-changed', fetchSettings);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const name = settings.app_name || 'jama';
|
||||
const name = settings.app_name || 'rosterchirp';
|
||||
const prefix = document.title.match(/^(\(\d+\)\s*)/)?.[1] || '';
|
||||
document.title = prefix + name;
|
||||
const faviconUrl = settings.logo_url || '/icons/jama.png';
|
||||
const faviconUrl = settings.logo_url || '/icons/rosterchirp.png';
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); }
|
||||
link.href = faviconUrl;
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useAuth } from '../contexts/AuthContext.jsx';
|
||||
import Avatar from './Avatar.jsx';
|
||||
|
||||
function useTheme() {
|
||||
const [dark, setDark] = useState(() => localStorage.getItem('jama-theme') === 'dark');
|
||||
const [dark, setDark] = useState(() => localStorage.getItem('rosterchirp-theme') === 'dark');
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('jama-theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('rosterchirp-theme', dark ? 'dark' : 'light');
|
||||
}, [dark]);
|
||||
return [dark, setDark];
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export function AuthProvider({ children }) {
|
||||
setUser(null);
|
||||
setMustChangePassword(false);
|
||||
};
|
||||
window.addEventListener('jama:session-displaced', handler);
|
||||
return () => window.removeEventListener('jama:session-displaced', handler);
|
||||
window.addEventListener('rosterchirp:session-displaced', handler);
|
||||
return () => window.removeEventListener('rosterchirp:session-displaced', handler);
|
||||
}, []);
|
||||
|
||||
const updateUser = (updates) => setUser(prev => ({ ...prev, ...updates }));
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SocketProvider({ children }) {
|
||||
|
||||
// Session displaced: another login on the same device type has kicked this session
|
||||
socket.on('session:displaced', () => {
|
||||
window.dispatchEvent(new CustomEvent('jama:session-displaced'));
|
||||
window.dispatchEvent(new CustomEvent('rosterchirp:session-displaced'));
|
||||
});
|
||||
|
||||
// Bug B fix: when app returns to foreground, force socket reconnect if disconnected
|
||||
|
||||
@@ -26,7 +26,7 @@ if ('serviceWorker' in navigator) {
|
||||
//
|
||||
// 2. PULL-TO-REFRESH → blocked in PWA standalone mode only.
|
||||
(function () {
|
||||
const LS_KEY = 'jama_font_scale';
|
||||
const LS_KEY = 'rosterchirp_font_scale';
|
||||
const MIN_SCALE = 0.8;
|
||||
const MAX_SCALE = 2.0;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -36,7 +36,7 @@ async function req(method, path, body, opts = {}) {
|
||||
if (res.status === 401 && data.error?.includes('Session expired')) {
|
||||
localStorage.removeItem('tc_token');
|
||||
sessionStorage.removeItem('tc_token');
|
||||
window.dispatchEvent(new CustomEvent('jama:session-displaced'));
|
||||
window.dispatchEvent(new CustomEvent('rosterchirp:session-displaced'));
|
||||
}
|
||||
throw new Error(data.error || 'Request failed');
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export const api = {
|
||||
bulkAvailability: (responses) => req('POST', '/schedule/me/bulk-availability', { responses }),
|
||||
importPreview: (file) => {
|
||||
const fd = new FormData(); fd.append('file', file);
|
||||
return fetch('/api/schedule/import/preview', { method: 'POST', headers: { Authorization: 'Bearer ' + localStorage.getItem('jama-token') }, body: fd }).then(r => r.json());
|
||||
return fetch('/api/schedule/import/preview', { method: 'POST', headers: { Authorization: 'Bearer ' + localStorage.getItem('tc_token') }, body: fd }).then(r => r.json());
|
||||
},
|
||||
importConfirm: (rows) => req('POST', '/schedule/import/confirm', { rows }),
|
||||
|
||||
@@ -166,16 +166,11 @@ export const api = {
|
||||
},
|
||||
resetSettings: () => req('POST', '/settings/reset'),
|
||||
|
||||
// Push notifications
|
||||
getPushKey: () => req('GET', '/push/vapid-public'),
|
||||
subscribePush: (sub) => req('POST', '/push/subscribe', sub),
|
||||
unsubscribePush: (endpoint) => req('POST', '/push/unsubscribe', { endpoint }),
|
||||
// Push notifications (FCM)
|
||||
getFirebaseConfig: () => req('GET', '/push/firebase-config'),
|
||||
subscribePush: (fcmToken) => req('POST', '/push/subscribe', { fcmToken }),
|
||||
unsubscribePush: () => req('POST', '/push/unsubscribe'),
|
||||
|
||||
// Link preview
|
||||
getLinkPreview: (url) => req('GET', `/link-preview?url=${encodeURIComponent(url)}`),
|
||||
|
||||
|
||||
|
||||
// VAPID key management (admin only)
|
||||
generateVapidKeys: () => req('POST', '/push/generate-vapid'),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user