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:
+
+ - Tap the Share button () at the bottom of Safari
+ - Select "Add to Home Screen"
+ - Tap Add, then open RosterChirp from your Home Screen
+ - Go to Profile → Notifications to enable push notifications
+
+
+
+ ) : (
+ <>
{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); }} />}
);
}