diff --git a/.env.example b/.env.example index 94aff1b..dfb50c0 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ TZ=UTC # Copy this file to .env and customize # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.6.5 +JAMA_VERSION=0.6.6 # Default admin credentials (used on FIRST RUN only) ADMIN_NAME=Admin User diff --git a/backend/package.json b/backend/package.json index 62bbed9..57c1069 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.6.5", + "version": "0.6.6", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/index.js b/backend/src/index.js index 6cdd09e..2b187ec 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -127,6 +127,9 @@ io.on('connection', (socket) => { if (!onlineUsers.has(userId)) onlineUsers.set(userId, new Set()); onlineUsers.get(userId).add(socket.id); + // Record last_online timestamp + getDb().prepare("UPDATE users SET last_online = datetime('now') WHERE id = ?").run(userId); + // Broadcast online status io.emit('user:online', { userId }); @@ -200,6 +203,7 @@ io.on('connection', (socket) => { title: senderName, body: (content || (imageUrl ? '📷 Image' : '')).slice(0, 100), url: '/', + groupId, badge: 1, }).catch(() => {}); } else { @@ -340,6 +344,7 @@ io.on('connection', (socket) => { onlineUsers.get(userId).delete(socket.id); if (onlineUsers.get(userId).size === 0) { onlineUsers.delete(userId); + getDb().prepare("UPDATE users SET last_online = datetime('now') WHERE id = ?").run(userId); io.emit('user:offline', { userId }); } } diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 18bcf25..71b58ff 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -182,10 +182,10 @@ function initDb() { console.log('[DB] Migration: added direct_peer2_id column'); } catch (e) { /* column already exists */ } - // Migration: last_login timestamp per user + // Migration: last_online timestamp per user try { - db.exec("ALTER TABLE users ADD COLUMN last_login TEXT"); - console.log('[DB] Migration: added last_login column'); + db.exec("ALTER TABLE users ADD COLUMN last_online TEXT"); + console.log('[DB] Migration: added last_online column'); } catch (e) { /* column already exists */ } // Migration: help_dismissed preference per user diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 8901747..8a1ace5 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -24,9 +24,6 @@ router.post('/login', (req, res) => { const valid = bcrypt.compareSync(password, user.password); if (!valid) return res.status(401).json({ error: 'Invalid credentials' }); - // Record last login timestamp - db.prepare("UPDATE users SET last_login = datetime('now') WHERE id = ?").run(user.id); - const token = generateToken(user.id); const ua = req.headers['user-agent'] || ''; const device = setActiveSession(user.id, token, ua); // displaces prior session on same device class diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 9fc4b64..79b67ec 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -49,7 +49,7 @@ function getDefaultPassword(db) { router.get('/', authMiddleware, adminMiddleware, (req, res) => { const db = getDb(); const users = db.prepare(` - SELECT id, name, email, role, status, is_default_admin, must_change_password, avatar, about_me, display_name, created_at, last_login + SELECT id, name, email, role, status, is_default_admin, must_change_password, avatar, about_me, display_name, created_at, last_online FROM users WHERE status != 'deleted' ORDER BY created_at ASC `).all(); diff --git a/build.sh b/build.sh index 870a09c..e7fb74e 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.6.5}" +VERSION="${1:-0.6.6}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/data/help.md b/data/help.md index 1546cc8..b07dc29 100644 --- a/data/help.md +++ b/data/help.md @@ -1,6 +1,6 @@ -# Getting Started with Jama +# Getting Started with JAMA -Welcome to **Jama** — your private, self-hosted team messaging app. +Welcome to **JAMA** — your private, self-hosted team messaging app. --- @@ -22,7 +22,7 @@ Type your message in the input box at the bottom and press **Enter** to send. - **Shift + Enter** adds a new line without sending - Tap the **+** button to attach a photo or emoji -- Use the **camera** icon to take a photo directly (mobile) +- Use the **camera** icon to take a photo directly (mobile only) ### Mentioning Someone Type **@** followed by the person's name to mention them. Select from the dropdown that appears. Mentioned users receive a notification. @@ -39,11 +39,13 @@ Hover over any message and click the **emoji** button to react with an emoji. ## Direct Messages -To start a private conversation with one person: +Two ways to start a private conversation with one person: -1. Click the **pencil / new chat** icon in the sidebar +1. Click the **New Chat** icon in the sidebar 2. Select one user from the list 3. Click **Start Conversation** +4. Click the users avatar in a message to bring up the profile +5. Click **Direct Message** --- @@ -51,12 +53,12 @@ To start a private conversation with one person: To create a group conversation: -1. Click the **pencil / new chat** icon -2. Select two or more users -3. Enter a **Group Name** +1. Click the **new chat** icon +2. Select two or more users from the +3. Enter a **Message Name** 4. Click **Create** -> If a group with the exact same members already exists, you will be redirected to it automatically. +> If a group with the exact same members already exists, you will be redirected to it automatically to help avoid duplication. --- @@ -64,7 +66,7 @@ To create a group conversation: Click your name or avatar at the bottom of the sidebar to: -- Update your **display name** (shown to others instead of your username) +- Update your **display name** (displayed in message windows) - Add an **about me** note - Upload a **profile photo** - Change your **password** @@ -75,12 +77,12 @@ Click your name or avatar at the bottom of the sidebar to: You can set a personal display name for any group that only you will see: -1. Open the group -2. Click the **ⓘ info** icon in the top bar +1. Open the message +2. Click the **message info** icon in the top right 3. Enter your custom name under **Your custom name** 4. Click **Save** -Other members still see the original group name. +Other members still see the original group name, unless they change to customised name. --- @@ -88,8 +90,8 @@ Other members still see the original group name. Admins can access **Settings** from the user menu to configure: -- App name and logo -- Default user password +- Branding a new app name and logo +- Set new user password - Notification preferences --- @@ -98,8 +100,4 @@ Admins can access **Settings** from the user menu to configure: - 🌙 Toggle **dark mode** from the user menu - 🔔 Enable **push notifications** when prompted to receive alerts when the app is closed -- 📱 Install Jama as a **PWA** on your device — tap *Add to Home Screen* in your browser menu for an app-like experience - ---- - -*This help file can be updated by your administrator at any time.* +- 📱 Install JAMA as a **PWA** on your device — tap *Add to Home Screen* in your browser menu for an app-like experience diff --git a/frontend/package.json b/frontend/package.json index fa8384d..e228d01 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.6.5", + "version": "0.6.6", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 63066d8..68ced58 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -27,36 +27,53 @@ self.addEventListener('fetch', (event) => { ); }); -// Track badge count in SW +// Track badge count in SW scope let badgeCount = 0; self.addEventListener('push', (event) => { if (!event.data) return; - const data = event.data.json(); + + let data = {}; + try { data = event.data.json(); } catch (e) { return; } badgeCount++; - // Update app badge (supported on Android Chrome and some desktop) - if (navigator.setAppBadge) { - navigator.setAppBadge(badgeCount).catch(() => {}); + // Update app badge + if (self.navigator && self.navigator.setAppBadge) { + self.navigator.setAppBadge(badgeCount).catch(() => {}); } - event.waitUntil( - self.registration.showNotification(data.title || 'New Message', { + // Check if app is currently visible — if so, skip the notification + const showNotification = clients.matchAll({ + type: 'window', + includeUncontrolled: true, + }).then((clientList) => { + const appVisible = clientList.some( + (c) => c.visibilityState === 'visible' + ); + // Still show if app is open but hidden (minimized), skip only if truly visible + if (appVisible) return; + + return self.registration.showNotification(data.title || 'New Message', { body: data.body || '', icon: '/icons/icon-192.png', - badge: '/icons/icon-192.png', + badge: '/icons/icon-192-maskable.png', data: { url: data.url || '/' }, - tag: 'jama-message', // replaces previous notification instead of stacking - renotify: true, // still vibrate/sound even if replacing - }) - ); + // Use unique tag per group so notifications group by conversation + tag: data.groupId ? `jama-group-${data.groupId}` : 'jama-message', + renotify: true, + }); + }); + + event.waitUntil(showNotification); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); badgeCount = 0; - if (navigator.clearAppBadge) navigator.clearAppBadge().catch(() => {}); + if (self.navigator && self.navigator.clearAppBadge) { + self.navigator.clearAppBadge().catch(() => {}); + } event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { const url = event.notification.data?.url || '/'; @@ -71,10 +88,22 @@ self.addEventListener('notificationclick', (event) => { ); }); -// Clear badge when user opens the app +// Clear badge when app signals it self.addEventListener('message', (event) => { if (event.data?.type === 'CLEAR_BADGE') { badgeCount = 0; - if (navigator.clearAppBadge) navigator.clearAppBadge().catch(() => {}); + if (self.navigator && self.navigator.clearAppBadge) { + self.navigator.clearAppBadge().catch(() => {}); + } + } + if (event.data?.type === 'SET_BADGE') { + badgeCount = event.data.count || 0; + if (self.navigator && self.navigator.setAppBadge) { + if (badgeCount > 0) { + self.navigator.setAppBadge(badgeCount).catch(() => {}); + } else { + self.navigator.clearAppBadge().catch(() => {}); + } + } } }); diff --git a/frontend/src/components/UserManagerModal.jsx b/frontend/src/components/UserManagerModal.jsx index e6f1555..33038fc 100644 --- a/frontend/src/components/UserManagerModal.jsx +++ b/frontend/src/components/UserManagerModal.jsx @@ -91,11 +91,18 @@ function UserRow({ u, onUpdated }) { {u.status !== 'active' && {u.status}} {!!u.is_default_admin && Default Admin} -