From eea7cb91e74c9ac228c250d07f437be2ef9643e1 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Fri, 27 Mar 2026 15:52:43 -0400 Subject: [PATCH] bug fixes --- backend/src/routes/usergroups.js | 3 ++ frontend/src/components/ProfileModal.jsx | 18 +++++++++++ frontend/src/components/SchedulePage.jsx | 9 ++++++ frontend/src/pages/Chat.jsx | 41 ++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index d9d0a15..63d0cdf 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -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.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); } } @@ -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]); + io.to(R(req.schema,'user',userId)).emit('schedule:refresh'); + 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]); io.in(R(req.schema,'user',userId)).socketsLeave(R(req.schema,'group',ug.dm_group_id)); diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx index 88cf919..6ede2d8 100644 --- a/frontend/src/components/ProfileModal.jsx +++ b/frontend/src/components/ProfileModal.jsx @@ -28,6 +28,7 @@ export default function ProfileModal({ onClose }) { typeof Notification !== 'undefined' ? Notification.permission : 'unsupported' ); const isIOS = /iphone|ipad/i.test(navigator.userAgent); + const isStandalone = window.navigator.standalone === true; const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag); const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0); @@ -213,6 +214,21 @@ export default function ProfileModal({ onClose }) { {tab === 'notifications' && (
+ {isIOS && !isStandalone ? ( +
+
Home Screen required for notifications
+
+ Push notifications on iPhone require RosterChirp to be installed as an app. To do this: +
    +
  1. Tap the Share button () at the bottom of Safari
  2. +
  3. Select "Add to Home Screen"
  4. +
  5. Tap Add, then open RosterChirp from your Home Screen
  6. +
  7. Go to Profile → Notifications to enable push notifications
  8. +
+
+
+ ) : ( + <> {notifPermission !== 'granted' && notifPermission !== 'unsupported' && (
@@ -314,6 +330,8 @@ export default function ProfileModal({ onClose }) { )}
)} + + )}
)} diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index ec160fc..6bbdab2 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import { api } from '../utils/api.js'; import { useToast } from '../contexts/ToastContext.jsx'; import { useAuth } from '../contexts/AuthContext.jsx'; +import { useSocket } from '../contexts/SocketContext.jsx'; import UserFooter from './UserFooter.jsx'; import MobileEventForm from './MobileEventForm.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 }) { const { user } = useAuth(); const toast = useToast(); + const { socket } = useSocket(); // Mobile: only day + schedule views const allowedViews = isMobile ? ['schedule','day'] : ['schedule','day','week','month']; @@ -1502,6 +1504,13 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel 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 useEffect(() => { if (contentRef.current && view !== 'schedule') contentRef.current.scrollTop = 0; }, [selDate, view]); diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index f38572c..50a0473 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -29,6 +29,38 @@ function urlBase64ToUint8Array(base64String) { 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 ( +
+
+
Add to Home Screen
+
+ To receive push notifications, tap the{' '} + {/* iOS share icon */} + + + + + + {' '}Share button, then select "Add to Home Screen". +
+
+ +
+ ); +} + export default function Chat() { const { socket } = useSocket(); const { user } = useAuth(); @@ -47,6 +79,9 @@ export default function Chat() { const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded const [isMobile, setIsMobile] = useState(window.innerWidth < 768); 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 useEffect(() => { @@ -550,6 +585,7 @@ export default function Chat() { {modal === 'branding' && setModal(null)} />} {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} {modal === 'about' && setModal(null)} />} + {showIOSBanner && { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />}
); } @@ -578,6 +614,7 @@ export default function Chat() { {modal === 'branding' && setModal(null)} />} {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} {modal === 'about' && setModal(null)} />} + {showIOSBanner && { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />} ); } @@ -637,6 +674,7 @@ export default function Chat() { {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} {modal === 'about' && setModal(null)} />} {modal === 'newchat' && setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); setPage('chat'); }} />} + {showIOSBanner && { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />} ); } @@ -669,6 +707,7 @@ export default function Chat() { {modal === 'branding' && setModal(null)} />} {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} {modal === 'about' && setModal(null)} />} + {showIOSBanner && { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />} ); } @@ -713,6 +752,7 @@ export default function Chat() { )} {modal === 'about' && setModal(null)} />} {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} + {showIOSBanner && { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />} ); } @@ -780,6 +820,7 @@ export default function Chat() { {modal === 'newchat' && setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); }} />} {modal === 'about' && setModal(null)} />} {modal === 'help' && setModal(null)} dismissed={helpDismissed} />} + {showIOSBanner && { localStorage.setItem(IOS_BANNER_KEY, '1'); setIosBannerDismissed(true); }} />} ); }