diff --git a/backend/package.json b/backend/package.json
index 55c1b6c..1df4df5 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-backend",
- "version": "0.12.8",
+ "version": "0.12.9",
"description": "RosterChirp backend server",
"main": "src/index.js",
"scripts": {
diff --git a/build.sh b/build.sh
index 01e5ee6..4ff347c 100644
--- a/build.sh
+++ b/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"
diff --git a/frontend/package.json b/frontend/package.json
index 060141c..7a5eb47 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-frontend",
- "version": "0.12.8",
+ "version": "0.12.9",
"private": true,
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/GroupInfoModal.jsx b/frontend/src/components/GroupInfoModal.jsx
index 44f5be1..34985fd 100644
--- a/frontend/src/components/GroupInfoModal.jsx
+++ b/frontend/src/components/GroupInfoModal.jsx
@@ -194,7 +194,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
Members ({members.length})
- {members.map(m => (
+ {[...members].sort((a, b) => a.name.localeCompare(b.name)).map(m => (
{m.name}
diff --git a/frontend/src/components/NewChatModal.jsx b/frontend/src/components/NewChatModal.jsx
index 285b4bf..f5a344f 100644
--- a/frontend/src/components/NewChatModal.jsx
+++ b/frontend/src/components/NewChatModal.jsx
@@ -141,7 +141,7 @@ export default function NewChatModal({ onClose, onCreated }) {
)}
- {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 => (
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index fbe12ad..ad840d8 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -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 });
diff --git a/frontend/src/pages/GroupManagerPage.jsx b/frontend/src/pages/GroupManagerPage.jsx
index 8b9c25d..9b534b9 100644
--- a/frontend/src/pages/GroupManagerPage.jsx
+++ b/frontend/src/pages/GroupManagerPage.jsx
@@ -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
diff --git a/frontend/src/pages/UserManagerPage.jsx b/frontend/src/pages/UserManagerPage.jsx
index 43414e1..9e8abc0 100644
--- a/frontend/src/pages/UserManagerPage.jsx
+++ b/frontend/src/pages/UserManagerPage.jsx
@@ -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 =>
- !search || u.name?.toLowerCase().includes(search.toLowerCase()) ||
- u.display_name?.toLowerCase().includes(search.toLowerCase()) ||
- u.email?.toLowerCase().includes(search.toLowerCase())
- );
+ 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 */}
-