v0.12.9 bug fixes (FCM and list ordering)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rosterchirp-backend",
|
||||
"version": "0.12.8",
|
||||
"version": "0.12.9",
|
||||
"description": "RosterChirp backend server",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-0.12.8}"
|
||||
VERSION="${1:-0.12.9}"
|
||||
ACTION="${2:-}"
|
||||
REGISTRY="${REGISTRY:-}"
|
||||
IMAGE_NAME="rosterchirp"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rosterchirp-frontend",
|
||||
"version": "0.12.8",
|
||||
"version": "0.12.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
||||
Members ({members.length})
|
||||
</div>
|
||||
<div style={{ maxHeight: 180, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{members.map(m => (
|
||||
{[...members].sort((a, b) => a.name.localeCompare(b.name)).map(m => (
|
||||
<div key={m.id} className="flex items-center" style={{ gap: 10, padding: '6px 0' }}>
|
||||
<Avatar user={m} size="sm" />
|
||||
<span className="flex-1 text-sm">{m.name}</span>
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function NewChatModal({ onClose, onCreated }) {
|
||||
)}
|
||||
|
||||
<div style={{ maxHeight: 200, overflowY: 'auto', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
||||
{users.filter(u => u.id !== user.id && u.allow_dm !== 0).map(u => (
|
||||
{users.filter(u => u.id !== user.id && u.allow_dm !== 0).sort((a, b) => a.name.localeCompare(b.name)).map(u => (
|
||||
<label key={u.id} className="flex items-center gap-10 pointer" style={{ padding: '10px 14px', gap: 12, borderBottom: '1px solid var(--border)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={!!selected.find(s => s.id === u.id)} onChange={() => toggle(u)} />
|
||||
<Avatar user={u} size="sm" />
|
||||
|
||||
@@ -18,7 +18,7 @@ function TeamManagementTab() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.getUserGroups().then(({ groups }) => setUserGroups(groups || [])).catch(() => {});
|
||||
api.getUserGroups().then(({ groups }) => setUserGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {});
|
||||
api.getSettings().then(({ settings }) => {
|
||||
// Read from unified key, fall back to legacy key
|
||||
setToolManagers(JSON.parse(settings.team_tool_managers || settings.team_group_managers || '[]'));
|
||||
@@ -313,80 +313,6 @@ function PushDebugTab() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Web Push Tab ──────────────────────────────────────────────────────────────
|
||||
function WebPushTab() {
|
||||
const toast = useToast();
|
||||
const [vapidPublic, setVapidPublic] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [showRegenWarning, setShowRegenWarning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.getSettings().then(({ settings }) => {
|
||||
setVapidPublic(settings.vapid_public || '');
|
||||
setLoading(false);
|
||||
}).catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const doGenerate = async () => {
|
||||
setGenerating(true);
|
||||
setShowRegenWarning(false);
|
||||
try {
|
||||
const { publicKey } = await api.generateVapidKeys();
|
||||
setVapidPublic(publicKey);
|
||||
toast('VAPID keys generated. Push notifications are now active.', 'success');
|
||||
} catch (e) {
|
||||
toast(e.message || 'Failed to generate keys', 'error');
|
||||
} finally { setGenerating(false); }
|
||||
};
|
||||
|
||||
if (loading) return <p style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Loading…</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="settings-section-label" style={{ marginBottom: 12 }}>Web Push Notifications (VAPID)</div>
|
||||
|
||||
{vapidPublic ? (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ background: 'var(--surface-variant)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '10px 12px', marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.5px' }}>Public Key</div>
|
||||
<code style={{ fontSize: 11, color: 'var(--text-primary)', wordBreak: 'break-all', lineHeight: 1.5, display: 'block' }}>{vapidPublic}</code>
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: 'var(--success)', display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Push notifications active
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
|
||||
No VAPID keys found. Generate keys to enable Web Push notifications.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showRegenWarning && (
|
||||
<div style={{ background: '#fce8e6', border: '1px solid #f5c6c2', borderRadius: 'var(--radius)', padding: '14px 16px', marginBottom: 16 }}>
|
||||
<p style={{ fontSize: 13, fontWeight: 600, color: 'var(--error)', marginBottom: 8 }}>⚠️ Regenerate VAPID keys?</p>
|
||||
<p style={{ fontSize: 13, color: '#5c2c28', marginBottom: 12, lineHeight: 1.5 }}>
|
||||
Generating new keys will <strong>invalidate all existing push subscriptions</strong>. Users will need to re-enable notifications.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn btn-sm" style={{ background: 'var(--error)', color: 'white' }} onClick={doGenerate} disabled={generating}>{generating ? 'Generating…' : 'Yes, regenerate keys'}</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setShowRegenWarning(false)}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!showRegenWarning && (
|
||||
<button className="btn btn-primary btn-sm" onClick={() => vapidPublic ? setShowRegenWarning(true) : doGenerate()} disabled={generating}>
|
||||
{generating ? 'Generating…' : vapidPublic ? 'Regenerate Keys' : 'Generate Keys'}
|
||||
</button>
|
||||
)}
|
||||
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 12, lineHeight: 1.5 }}>
|
||||
Requires HTTPS. On iOS, the app must be installed to the home screen first.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main modal ────────────────────────────────────────────────────────────────
|
||||
export default function SettingsModal({ onClose, onFeaturesChanged }) {
|
||||
const [tab, setTab] = useState('registration');
|
||||
@@ -406,7 +332,6 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) {
|
||||
const tabs = [
|
||||
isTeam && { id: 'team', label: 'Team Management' },
|
||||
{ id: 'registration', label: 'Registration' },
|
||||
{ id: 'webpush', label: 'Web Push' },
|
||||
{ id: 'pushdebug', label: 'Push Debug' },
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -431,7 +356,6 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) {
|
||||
|
||||
{tab === 'team' && <TeamManagementTab />}
|
||||
{tab === 'registration' && <RegistrationTab onFeaturesChanged={onFeaturesChanged} />}
|
||||
{tab === 'webpush' && <WebPushTab />}
|
||||
{tab === 'pushdebug' && <PushDebugTab />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,10 +66,21 @@ if ('serviceWorker' in navigator) {
|
||||
currentScale = Math.round(newScale * 100) / 100;
|
||||
document.documentElement.style.setProperty('--font-scale', currentScale);
|
||||
} else if (e.touches.length === 1 && isStandalone) {
|
||||
// Single finger: block pull-to-refresh at top of page
|
||||
// Single finger: block pull-to-refresh only when no scrollable ancestor
|
||||
// has scrolled content above the viewport.
|
||||
// Without this ancestor check, document.scrollTop is always 0 in this
|
||||
// flex layout, so the naive condition blocked ALL upward swipes (dy > 0),
|
||||
// making any scroll container impossible to scroll back up after reaching
|
||||
// the bottom — freezing the window.
|
||||
const dy = e.touches[0].clientY - singleStartY;
|
||||
if (dy > 0 && document.documentElement.scrollTop === 0 && document.body.scrollTop === 0) {
|
||||
e.preventDefault();
|
||||
if (dy > 0) {
|
||||
let el = e.target;
|
||||
let canScrollUp = false;
|
||||
while (el && el !== document.documentElement) {
|
||||
if (el.scrollTop > 0) { canScrollUp = true; break; }
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (!canScrollUp) e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
@@ -71,7 +71,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
|
||||
const load = useCallback(() =>
|
||||
api.getUserGroups().then(({ groups }) => setGroups(groups)).catch(() => {}), []);
|
||||
api.getUserGroups().then(({ groups }) => setGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}), []);
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const selectGroup = async (g) => {
|
||||
@@ -220,7 +220,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
|
||||
const load = useCallback(() =>
|
||||
api.getMultiGroupDms().then(({ dms }) => setDms(dms||[])).catch(() => {}), []);
|
||||
api.getMultiGroupDms().then(({ dms }) => setDms([...(dms||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}), []);
|
||||
useEffect(() => { load(); }, [load, refreshKey]);
|
||||
|
||||
const clearSelection = () => { setSelected(null); setDmName(''); setGroupIds(new Set()); setSavedGroupIds(new Set()); setShowDelete(false); };
|
||||
@@ -556,8 +556,8 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
const onRefresh = () => setRefreshKey(k => k+1);
|
||||
|
||||
useEffect(() => {
|
||||
api.searchUsers('').then(({ users }) => setAllUsers(users.filter(u => u.status==='active'))).catch(() => {});
|
||||
api.getUserGroups().then(({ groups }) => setAllUserGroups(groups)).catch(() => {});
|
||||
api.searchUsers('').then(({ users }) => setAllUsers(users.filter(u => u.status==='active').sort((a, b) => (a.display_name||a.name).localeCompare(b.display_name||b.name)))).catch(() => {});
|
||||
api.getUserGroups().then(({ groups }) => setAllUserGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {});
|
||||
}, [refreshKey]);
|
||||
|
||||
// Nav item helper — matches Schedule page style
|
||||
|
||||
@@ -300,11 +300,13 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
api.getSettings().then(({ settings }) => { if (settings.user_pass) setUserPass(settings.user_pass); }).catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
const filtered = users.filter(u =>
|
||||
const filtered = users
|
||||
.filter(u =>
|
||||
!search || u.name?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
u.display_name?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
u.email?.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// ── Nav item helper (matches Schedule page style) ─────────────────────────
|
||||
const navItem = (label, key) => (
|
||||
@@ -354,7 +356,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex:1, overflowY:'auto', padding:16 }}>
|
||||
<div style={{ flex:1, overflowY:'auto', padding:16, paddingBottom: isMobile ? 72 : 16, overscrollBehavior: 'contain' }}>
|
||||
{tab === 'users' && (
|
||||
<>
|
||||
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user