v0.12.33 text cleanup of the app and bug fixes.

This commit is contained in:
2026-03-28 12:55:53 -04:00
parent eb3e45d88f
commit fb9d4dc956
12 changed files with 124 additions and 49 deletions

View File

@@ -14,6 +14,55 @@ function nameToColor(name) {
return AVATAR_COLORS[(name || '').charCodeAt(0) % AVATAR_COLORS.length];
}
// Composite avatar layouts for the 40×40 chat header icon
const COMPOSITE_LAYOUTS_SM = {
1: [{ top: 4, left: 4, size: 32 }],
2: [
{ top: 10, left: 1, size: 19 },
{ top: 10, right: 1, size: 19 },
],
3: [
{ top: 2, left: 2, size: 17 },
{ top: 2, right: 2, size: 17 },
{ bottom: 2, left: 11, size: 17 },
],
4: [
{ top: 1, left: 1, size: 18 },
{ top: 1, right: 1, size: 18 },
{ bottom: 1, left: 1, size: 18 },
{ bottom: 1, right: 1, size: 18 },
],
};
function GroupAvatarCompositeSm({ memberPreviews }) {
const members = (memberPreviews || []).slice(0, 4);
const positions = COMPOSITE_LAYOUTS_SM[members.length];
if (!positions) return null;
return (
<div className="group-icon-sm" style={{ background: '#1a1a2e', position: 'relative', padding: 0, overflow: 'hidden' }}>
{members.map((m, i) => {
const pos = positions[i];
const base = {
position: 'absolute',
width: pos.size, height: pos.size,
borderRadius: '50%',
...(pos.top !== undefined ? { top: pos.top } : {}),
...(pos.bottom !== undefined ? { bottom: pos.bottom } : {}),
...(pos.left !== undefined ? { left: pos.left } : {}),
...(pos.right !== undefined ? { right: pos.right } : {}),
overflow: 'hidden', flexShrink: 0,
};
if (m.avatar) return <img key={m.id} src={m.avatar} alt={m.name} style={{ ...base, objectFit: 'cover' }} />;
return (
<div key={m.id} style={{ ...base, background: nameToColor(m.name), display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: Math.round(pos.size * 0.42), fontWeight: 700, color: 'white' }}>
{(m.name || '')[0]?.toUpperCase()}
</div>
);
})}
</div>
);
}
export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMessage, onMessageDeleted, onHasTextChange, onlineUserIds = new Set() }) {
const { user: currentUser } = useAuth();
const { socket } = useSocket();
@@ -267,6 +316,8 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
<div className="group-icon-sm" style={{ background: avatarColors.dm, borderRadius: 8, flexShrink: 0, fontSize: 11, fontWeight: 700 }}>
{group.is_multi_group ? 'MG' : 'UG'}
</div>
) : group.composite_members?.length > 0 ? (
<GroupAvatarCompositeSm memberPreviews={group.composite_members} />
) : (
<div className="group-icon-sm" style={{ background: group.type === 'public' ? avatarColors.public : avatarColors.dm, flexShrink: 0 }}>
{group.type === 'public' ? '#' : group.name[0]?.toUpperCase()}

View File

@@ -58,7 +58,7 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupMessages,
{/* Close X */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<div className="nav-drawer-section-label" style={{ margin: 0, padding: 0 }}>Menu</div>
<div className="nav-drawer-section-label" style={{ margin: 0, padding: 0 }}>User Menu</div>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-secondary)', padding: 4, borderRadius: 6, display: 'flex', alignItems: 'center' }} aria-label="Close menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>

View File

@@ -773,6 +773,7 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
useEffect(()=>{setMyResp(event.my_response);setAvail(event.availability||[]);},[event]);
const counts={going:0,maybe:0,not_going:0};
avail.forEach(r=>{if(counts[r.response]!==undefined)counts[r.response]++;});
const isPast = !event.all_day && event.end_at && new Date(event.end_at) < new Date();
const handleResp=async resp=>{
const prev=myResp;
@@ -801,7 +802,7 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
</div>
</div>
<div style={{display:'flex',gap:6,flexShrink:0}}>
{(isToolManager||(userId&&event.created_by===userId))&&<button className="btn btn-secondary btn-sm" onClick={()=>{onClose();onEdit();}}>Edit</button>}
{(isToolManager||(userId&&event.created_by===userId))&&!isPast&&<button className="btn btn-secondary btn-sm" onClick={()=>{onClose();onEdit();}}>Edit</button>}
<button className="btn-icon" onClick={onClose}><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
</div>
@@ -823,13 +824,17 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
{!!event.track_availability&&(
<div style={{borderTop:'1px solid var(--border)',paddingTop:16,marginTop:4}}>
<div style={{fontSize:12,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.6px',marginBottom:10}}>Your Availability</div>
<div style={{display:'flex',gap:8,marginBottom:16}}>
{Object.entries(RESP_LABEL).map(([key,label])=>(
<button key={key} onClick={()=>handleResp(key)} style={{flex:1,padding:'9px 4px',borderRadius:'var(--radius)',border:`2px solid ${RESP_COLOR[key]}`,background:myResp===key?RESP_COLOR[key]:'transparent',color:myResp===key?'white':RESP_COLOR[key],fontSize:13,fontWeight:600,cursor:'pointer',transition:'all 0.15s'}}>
{myResp===key?'✓ ':''}{label}
</button>
))}
</div>
{isPast ? (
<p style={{fontSize:13,color:'var(--text-tertiary)',marginBottom:16}}>Past event availability is read-only.</p>
) : (
<div style={{display:'flex',gap:8,marginBottom:16}}>
{Object.entries(RESP_LABEL).map(([key,label])=>(
<button key={key} onClick={()=>handleResp(key)} style={{flex:1,padding:'9px 4px',borderRadius:'var(--radius)',border:`2px solid ${RESP_COLOR[key]}`,background:myResp===key?RESP_COLOR[key]:'transparent',color:myResp===key?'white':RESP_COLOR[key],fontSize:13,fontWeight:600,cursor:'pointer',transition:'all 0.15s'}}>
{myResp===key?'✓ ':''}{label}
</button>
))}
</div>
)}
{isToolManager&&(
<>
<div style={{fontSize:12,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.6px',marginBottom:8}}>Responses</div>

View File

@@ -185,9 +185,7 @@ function RegistrationTab({ onFeaturesChanged }) {
)}
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 16, lineHeight: 1.5 }}>
Registration codes unlock application features. Contact your RosterChirp provider for a code.<br />
<strong>RosterChirp-Brand</strong> unlocks Branding.&nbsp;
<strong>RosterChirp-Team</strong> unlocks Branding, Group Manager and Schedule Manager.
Registration codes unlock application features. Contact your RosterChirp provider for a code.
</p>
</div>
);

View File

@@ -181,7 +181,7 @@
.footer-menu {
position: absolute;
bottom: 68px;
bottom: calc(68px + env(safe-area-inset-bottom, 0px));
left: 8px;
right: 8px;
background: white;

View File

@@ -55,6 +55,10 @@ function DebugRow({ label, value, ok, bad }) {
}
// ── Test Notifications Modal ──────────────────────────────────────────────────
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isAndroid = /Android/i.test(navigator.userAgent);
const isMobileDevice = isIOS || isAndroid;
function TestNotificationsModal({ onClose }) {
const toast = useToast();
const [debugData, setDebugData] = useState(null);
@@ -130,14 +134,20 @@ function TestNotificationsModal({ onClose }) {
<div style={sectionLabel}>This Device</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 10 }}>
<DebugRow label="Permission" value={permission} ok={permission === 'granted'} bad={permission === 'denied'} />
<DebugRow label="Cached FCM token" value={cachedToken ? cachedToken.slice(0, 36) + '…' : 'None'} ok={!!cachedToken} bad={!cachedToken} />
{debugData && <DebugRow label="FCM env vars" value={debugData.fcmConfigured ? 'Present' : 'Missing'} ok={debugData.fcmConfigured} bad={!debugData.fcmConfigured} />}
{debugData && <DebugRow label="Firebase Admin" value={debugData.firebaseAdminReady ? 'Ready' : 'Not ready'} ok={debugData.firebaseAdminReady} bad={!debugData.firebaseAdminReady} />}
{!isIOS && !isAndroid && <DebugRow label="FCM token" value={cachedToken ? cachedToken.slice(0, 36) + '…' : 'None'} ok={!!cachedToken} bad={!cachedToken} />}
{isAndroid && (
<div style={{ fontSize: 13 }}>
<span style={{ color: 'var(--text-secondary)' }}>FCM token</span>
<div style={{ color: cachedToken ? 'var(--success)' : 'var(--error)', fontFamily: 'monospace', fontSize: 11, marginTop: 3, wordBreak: 'break-all', lineHeight: 1.5 }}>{cachedToken || 'None'}</div>
</div>
)}
{!isIOS && debugData && <DebugRow label="FCM env vars" value={debugData.fcmConfigured ? 'Present' : 'Missing'} ok={debugData.fcmConfigured} bad={!debugData.fcmConfigured} />}
{!isIOS && debugData && <DebugRow label="Firebase Admin" value={debugData.firebaseAdminReady ? 'Ready' : 'Not ready'} ok={debugData.firebaseAdminReady} bad={!debugData.firebaseAdminReady} />}
{lastError && <DebugRow label="Last reg. error" value={lastError} bad={true} />}
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button className="btn btn-sm btn-primary" onClick={reregister}>Re-register</button>
<button className="btn btn-sm btn-secondary" onClick={clearToken}>Clear cached token</button>
{!isIOS && <button className="btn btn-sm btn-secondary" onClick={clearToken}>Clear token</button>}
</div>
</div>
@@ -158,30 +168,34 @@ function TestNotificationsModal({ onClose }) {
</div>
</div>
{/* Registered devices */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<div className="settings-section-label" style={{ margin: 0 }}>Registered Devices</div>
<button className="btn btn-sm btn-secondary" onClick={load} disabled={loading}>{loading ? 'Loading…' : 'Refresh'}</button>
</div>
{/* Registered devices — desktop only */}
{!isMobileDevice && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<div className="settings-section-label" style={{ margin: 0 }}>Registered Devices</div>
<button className="btn btn-sm btn-secondary" onClick={load} disabled={loading}>{loading ? 'Loading…' : 'Refresh'}</button>
</div>
{loading ? (
<p style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Loading</p>
) : !debugData?.subscriptions?.length ? (
<p style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>No FCM tokens registered.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{debugData.subscriptions.map(sub => (
<div key={sub.id} style={box}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<span style={{ fontSize: 13, fontWeight: 600 }}>{sub.name || sub.email}</span>
<span style={{ fontSize: 11, color: 'var(--text-tertiary)', background: 'var(--surface)', padding: '2px 7px', borderRadius: 4, border: '1px solid var(--border)' }}>{sub.device}</span>
</div>
<code style={{ fontSize: 10, color: 'var(--text-secondary)', wordBreak: 'break-all', lineHeight: 1.6, display: 'block' }}>
{sub.fcm_token}
</code>
{loading ? (
<p style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Loading</p>
) : !debugData?.subscriptions?.length ? (
<p style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>No FCM tokens registered.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{debugData.subscriptions.map(sub => (
<div key={sub.id} style={box}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<span style={{ fontSize: 13, fontWeight: 600 }}>{sub.name || sub.email}</span>
<span style={{ fontSize: 11, color: 'var(--text-tertiary)', background: 'var(--surface)', padding: '2px 7px', borderRadius: 4, border: '1px solid var(--border)' }}>{sub.device}</span>
</div>
<code style={{ fontSize: 10, color: 'var(--text-secondary)', wordBreak: 'break-all', lineHeight: 1.6, display: 'block' }}>
{sub.fcm_token}
</code>
</div>
))}
</div>
))}
</div>
)}
</>
)}
</div>
</div>