bug fixes
This commit is contained in:
@@ -301,6 +301,7 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) =>
|
|||||||
io.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',ug.dm_group_id));
|
io.in(R(req.schema,'user',uid)).socketsLeave(R(req.schema,'group',ug.dm_group_id));
|
||||||
io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id });
|
io.to(R(req.schema,'user',uid)).emit('group:deleted', { groupId: ug.dm_group_id });
|
||||||
}
|
}
|
||||||
|
io.to(R(req.schema,'user',uid)).emit('schedule:refresh');
|
||||||
removedUids.push(uid);
|
removedUids.push(uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +419,8 @@ router.delete('/:id/members/:userId', authMiddleware, teamManagerMiddleware, asy
|
|||||||
|
|
||||||
await exec(req.schema, 'DELETE FROM user_group_members WHERE user_group_id=$1 AND user_id=$2', [ug.id, userId]);
|
await exec(req.schema, 'DELETE FROM user_group_members WHERE user_group_id=$1 AND user_id=$2', [ug.id, userId]);
|
||||||
|
|
||||||
|
io.to(R(req.schema,'user',userId)).emit('schedule:refresh');
|
||||||
|
|
||||||
if (ug.dm_group_id) {
|
if (ug.dm_group_id) {
|
||||||
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [ug.dm_group_id, userId]);
|
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [ug.dm_group_id, userId]);
|
||||||
io.in(R(req.schema,'user',userId)).socketsLeave(R(req.schema,'group',ug.dm_group_id));
|
io.in(R(req.schema,'user',userId)).socketsLeave(R(req.schema,'group',ug.dm_group_id));
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default function ProfileModal({ onClose }) {
|
|||||||
typeof Notification !== 'undefined' ? Notification.permission : 'unsupported'
|
typeof Notification !== 'undefined' ? Notification.permission : 'unsupported'
|
||||||
);
|
);
|
||||||
const isIOS = /iphone|ipad/i.test(navigator.userAgent);
|
const isIOS = /iphone|ipad/i.test(navigator.userAgent);
|
||||||
|
const isStandalone = window.navigator.standalone === true;
|
||||||
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
||||||
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
||||||
|
|
||||||
@@ -213,6 +214,21 @@ export default function ProfileModal({ onClose }) {
|
|||||||
|
|
||||||
{tab === 'notifications' && (
|
{tab === 'notifications' && (
|
||||||
<div className="flex-col gap-3">
|
<div className="flex-col gap-3">
|
||||||
|
{isIOS && !isStandalone ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, padding: '12px 14px', borderRadius: 8, background: 'var(--surface-variant)', border: '1px solid var(--border)' }}>
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>Home Screen required for notifications</div>
|
||||||
|
<div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6 }}>
|
||||||
|
Push notifications on iPhone require RosterChirp to be installed as an app. To do this:
|
||||||
|
<ol style={{ margin: '8px 0 0', paddingLeft: 18, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
<li>Tap the <strong>Share</strong> button (<svg style={{ display: 'inline', verticalAlign: 'middle', margin: '0 2px' }} width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>) at the bottom of Safari</li>
|
||||||
|
<li>Select <strong>"Add to Home Screen"</strong></li>
|
||||||
|
<li>Tap <strong>Add</strong>, then open RosterChirp from your Home Screen</li>
|
||||||
|
<li>Go to <strong>Profile → Notifications</strong> to enable push notifications</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{notifPermission !== 'granted' && notifPermission !== 'unsupported' && (
|
{notifPermission !== 'granted' && notifPermission !== 'unsupported' && (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px', borderRadius: 8, background: 'var(--surface-variant)' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px', borderRadius: 8, background: 'var(--surface-variant)' }}>
|
||||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
|
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
|
||||||
@@ -314,6 +330,8 @@ export default function ProfileModal({ onClose }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import { api } from '../utils/api.js';
|
import { api } from '../utils/api.js';
|
||||||
import { useToast } from '../contexts/ToastContext.jsx';
|
import { useToast } from '../contexts/ToastContext.jsx';
|
||||||
import { useAuth } from '../contexts/AuthContext.jsx';
|
import { useAuth } from '../contexts/AuthContext.jsx';
|
||||||
|
import { useSocket } from '../contexts/SocketContext.jsx';
|
||||||
import UserFooter from './UserFooter.jsx';
|
import UserFooter from './UserFooter.jsx';
|
||||||
import MobileEventForm from './MobileEventForm.jsx';
|
import MobileEventForm from './MobileEventForm.jsx';
|
||||||
import ColourPickerSheet from './ColourPickerSheet.jsx';
|
import ColourPickerSheet from './ColourPickerSheet.jsx';
|
||||||
@@ -1471,6 +1472,7 @@ function MonthView({ events: rawEvents, selectedDate, onSelect, onSelectDay }) {
|
|||||||
export default function SchedulePage({ isToolManager, isMobile, onProfile, onHelp, onAbout }) {
|
export default function SchedulePage({ isToolManager, isMobile, onProfile, onHelp, onAbout }) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const { socket } = useSocket();
|
||||||
|
|
||||||
// Mobile: only day + schedule views
|
// Mobile: only day + schedule views
|
||||||
const allowedViews = isMobile ? ['schedule','day'] : ['schedule','day','week','month'];
|
const allowedViews = isMobile ? ['schedule','day'] : ['schedule','day','week','month'];
|
||||||
@@ -1502,6 +1504,13 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
// Re-fetch when removed from a user group (private event visibility may change)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket) return;
|
||||||
|
socket.on('schedule:refresh', load);
|
||||||
|
return () => socket.off('schedule:refresh', load);
|
||||||
|
}, [socket, load]);
|
||||||
|
|
||||||
// Reset scroll to top on date/view change; schedule view scrolls to today via ScheduleView's own effect
|
// Reset scroll to top on date/view change; schedule view scrolls to today via ScheduleView's own effect
|
||||||
useEffect(() => { if (contentRef.current && view !== 'schedule') contentRef.current.scrollTop = 0; }, [selDate, view]);
|
useEffect(() => { if (contentRef.current && view !== 'schedule') contentRef.current.scrollTop = 0; }, [selDate, view]);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,38 @@ function urlBase64ToUint8Array(base64String) {
|
|||||||
return outputArray;
|
return outputArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── iOS "Add to Home Screen" install banner ───────────────────────────────────
|
||||||
|
// iOS Safari does not fire beforeinstallprompt. Push notifications on iOS require
|
||||||
|
// the app to be installed as a PWA (added to Home Screen). This banner guides users.
|
||||||
|
const IOS_BANNER_KEY = 'rc_ios_install_dismissed';
|
||||||
|
function IOSInstallBanner({ onDismiss }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 9999,
|
||||||
|
background: 'var(--primary)', color: '#fff',
|
||||||
|
padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12,
|
||||||
|
boxShadow: '0 -2px 12px rgba(0,0,0,0.25)',
|
||||||
|
}}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 2 }}>Add to Home Screen</div>
|
||||||
|
<div style={{ fontSize: 12, lineHeight: 1.4, opacity: 0.9 }}>
|
||||||
|
To receive push notifications, tap the{' '}
|
||||||
|
{/* iOS share icon */}
|
||||||
|
<svg style={{ display: 'inline', verticalAlign: 'middle', margin: '0 2px' }} width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||||
|
<polyline points="16 6 12 2 8 6"/>
|
||||||
|
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||||
|
</svg>
|
||||||
|
{' '}Share button, then select <strong>"Add to Home Screen"</strong>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onDismiss} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#fff', padding: 4, flexShrink: 0, opacity: 0.9 }}>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -47,6 +79,9 @@ export default function Chat() {
|
|||||||
const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded
|
const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
const [showSidebar, setShowSidebar] = useState(true);
|
const [showSidebar, setShowSidebar] = useState(true);
|
||||||
|
const isIOSSafari = /iphone|ipad/i.test(navigator.userAgent) && !window.navigator.standalone;
|
||||||
|
const [iosBannerDismissed, setIosBannerDismissed] = useState(() => localStorage.getItem(IOS_BANNER_KEY) === '1');
|
||||||
|
const showIOSBanner = isIOSSafari && !iosBannerDismissed;
|
||||||
|
|
||||||
// Check if help should be shown on login
|
// Check if help should be shown on login
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -550,6 +585,7 @@ export default function Chat() {
|
|||||||
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
||||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||||
|
{showIOSBanner && <IOSInstallBanner onDismiss={() => { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -578,6 +614,7 @@ export default function Chat() {
|
|||||||
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
||||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||||
|
{showIOSBanner && <IOSInstallBanner onDismiss={() => { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -637,6 +674,7 @@ export default function Chat() {
|
|||||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||||
{modal === 'newchat' && <NewChatModal onClose={() => setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); setPage('chat'); }} />}
|
{modal === 'newchat' && <NewChatModal onClose={() => setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); setPage('chat'); }} />}
|
||||||
|
{showIOSBanner && <IOSInstallBanner onDismiss={() => { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -669,6 +707,7 @@ export default function Chat() {
|
|||||||
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
|
||||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||||
|
{showIOSBanner && <IOSInstallBanner onDismiss={() => { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -713,6 +752,7 @@ export default function Chat() {
|
|||||||
)}
|
)}
|
||||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||||
|
{showIOSBanner && <IOSInstallBanner onDismiss={() => { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -780,6 +820,7 @@ export default function Chat() {
|
|||||||
{modal === 'newchat' && <NewChatModal onClose={() => setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); }} />}
|
{modal === 'newchat' && <NewChatModal onClose={() => setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); }} />}
|
||||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||||
|
{showIOSBanner && <IOSInstallBanner onDismiss={() => { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user