v0.11.27 message bug fixes
This commit is contained in:
@@ -4,7 +4,7 @@ const multer = require('multer');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { query, queryOne, queryResult, exec, addUserToPublicGroups, getOrCreateSupportGroup } = require('../models/db');
|
const { query, queryOne, queryResult, exec, addUserToPublicGroups, getOrCreateSupportGroup } = require('../models/db');
|
||||||
const { authMiddleware, adminMiddleware, teamManagerMiddleware } = require('../middleware/auth');
|
const { authMiddleware, teamManagerMiddleware } = require('../middleware/auth');
|
||||||
|
|
||||||
const avatarStorage = multer.diskStorage({
|
const avatarStorage = multer.diskStorage({
|
||||||
destination: '/app/uploads/avatars',
|
destination: '/app/uploads/avatars',
|
||||||
@@ -30,7 +30,7 @@ async function resolveUniqueName(schema, baseName, excludeId = null) {
|
|||||||
function isValidEmail(e) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); }
|
function isValidEmail(e) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); }
|
||||||
|
|
||||||
// List users
|
// List users
|
||||||
router.get('/', authMiddleware, adminMiddleware, async (req, res) => {
|
router.get('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const users = await query(req.schema,
|
const users = await query(req.schema,
|
||||||
"SELECT id,name,email,role,status,is_default_admin,must_change_password,avatar,about_me,display_name,allow_dm,created_at,last_online FROM users WHERE status != 'deleted' ORDER BY created_at ASC"
|
"SELECT id,name,email,role,status,is_default_admin,must_change_password,avatar,about_me,display_name,allow_dm,created_at,last_online FROM users WHERE status != 'deleted' ORDER BY created_at ASC"
|
||||||
@@ -81,7 +81,7 @@ router.get('/check-display-name', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
router.post('/', authMiddleware, adminMiddleware, async (req, res) => {
|
router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { name, email, password, role } = req.body;
|
const { name, email, password, role } = req.body;
|
||||||
if (!name || !email) return res.status(400).json({ error: 'Name and email required' });
|
if (!name || !email) return res.status(400).json({ error: 'Name and email required' });
|
||||||
if (!isValidEmail(email)) return res.status(400).json({ error: 'Invalid email address' });
|
if (!isValidEmail(email)) return res.status(400).json({ error: 'Invalid email address' });
|
||||||
@@ -107,7 +107,7 @@ router.post('/', authMiddleware, adminMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Bulk create
|
// Bulk create
|
||||||
router.post('/bulk', authMiddleware, adminMiddleware, async (req, res) => {
|
router.post('/bulk', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { users } = req.body;
|
const { users } = req.body;
|
||||||
const results = { created: [], skipped: [] };
|
const results = { created: [], skipped: [] };
|
||||||
const seenEmails = new Set();
|
const seenEmails = new Set();
|
||||||
@@ -144,7 +144,7 @@ router.post('/bulk', authMiddleware, adminMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Patch name
|
// Patch name
|
||||||
router.patch('/:id/name', authMiddleware, adminMiddleware, async (req, res) => {
|
router.patch('/:id/name', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
if (!name?.trim()) return res.status(400).json({ error: 'Name required' });
|
if (!name?.trim()) return res.status(400).json({ error: 'Name required' });
|
||||||
try {
|
try {
|
||||||
@@ -157,7 +157,7 @@ router.patch('/:id/name', authMiddleware, adminMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Patch role
|
// Patch role
|
||||||
router.patch('/:id/role', authMiddleware, adminMiddleware, async (req, res) => {
|
router.patch('/:id/role', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { role } = req.body;
|
const { role } = req.body;
|
||||||
if (!['member','admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
if (!['member','admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
||||||
try {
|
try {
|
||||||
@@ -174,7 +174,7 @@ router.patch('/:id/role', authMiddleware, adminMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Reset password
|
// Reset password
|
||||||
router.patch('/:id/reset-password', authMiddleware, adminMiddleware, async (req, res) => {
|
router.patch('/:id/reset-password', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
if (!password || password.length < 6) return res.status(400).json({ error: 'Password too short' });
|
if (!password || password.length < 6) return res.status(400).json({ error: 'Password too short' });
|
||||||
try {
|
try {
|
||||||
@@ -185,7 +185,7 @@ router.patch('/:id/reset-password', authMiddleware, adminMiddleware, async (req,
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Suspend / activate / delete
|
// Suspend / activate / delete
|
||||||
router.patch('/:id/suspend', authMiddleware, adminMiddleware, async (req, res) => {
|
router.patch('/:id/suspend', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const t = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
const t = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
||||||
if (!t) return res.status(404).json({ error: 'User not found' });
|
if (!t) return res.status(404).json({ error: 'User not found' });
|
||||||
@@ -196,13 +196,13 @@ router.patch('/:id/suspend', authMiddleware, adminMiddleware, async (req, res)
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
router.patch('/:id/activate', authMiddleware, adminMiddleware, async (req, res) => {
|
router.patch('/:id/activate', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await exec(req.schema, "UPDATE users SET status='active', updated_at=NOW() WHERE id=$1", [req.params.id]);
|
await exec(req.schema, "UPDATE users SET status='active', updated_at=NOW() WHERE id=$1", [req.params.id]);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
router.delete('/:id', authMiddleware, adminMiddleware, async (req, res) => {
|
router.delete('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const t = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
const t = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
||||||
if (!t) return res.status(404).json({ error: 'User not found' });
|
if (!t) return res.status(404).json({ error: 'User not found' });
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.11.26}"
|
VERSION="${1:-0.11.27}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="rosterchirp"
|
IMAGE_NAME="rosterchirp"
|
||||||
|
|||||||
@@ -296,19 +296,28 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{messages.map((msg, i) => (
|
{messages.map((msg, i) => {
|
||||||
<Message
|
// Skip deleted entries when looking for the effective previous message.
|
||||||
key={msg.id}
|
// Deleted messages render null, so they must not affect date separators
|
||||||
message={msg}
|
// or avatar-grouping for the messages that follow them.
|
||||||
prevMessage={messages[i - 1]}
|
let effectivePrev = null;
|
||||||
currentUser={currentUser}
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
onReply={handleReply}
|
if (!messages[j].is_deleted) { effectivePrev = messages[j]; break; }
|
||||||
onDelete={handleDelete}
|
}
|
||||||
onReact={handleReact}
|
return (
|
||||||
onDirectMessage={handleDirectMessage}
|
<Message
|
||||||
isDirect={isDirect}
|
key={msg.id}
|
||||||
onlineUserIds={onlineUserIds} />
|
message={msg}
|
||||||
))}
|
prevMessage={effectivePrev}
|
||||||
|
currentUser={currentUser}
|
||||||
|
onReply={handleReply}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onReact={handleReact}
|
||||||
|
onDirectMessage={handleDirectMessage}
|
||||||
|
isDirect={isDirect}
|
||||||
|
onlineUserIds={onlineUserIds} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{typing.length > 0 && (
|
{typing.length > 0 && (
|
||||||
<div className="typing-indicator">
|
<div className="typing-indicator">
|
||||||
|
|||||||
Reference in New Issue
Block a user