priviate group avatars update
This commit is contained in:
5
backend/src/models/migrations/012_composite_avatar.sql
Normal file
5
backend/src/models/migrations/012_composite_avatar.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- Migration 012: Add composite_members to groups for private group avatar composites
|
||||||
|
-- Stores up to 4 member previews (id, name, avatar) as a JSONB snapshot.
|
||||||
|
-- Only set for non-managed, non-direct private groups with 3+ members.
|
||||||
|
-- Updated only when a member is added and pre-add membership count was ≤3.
|
||||||
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS composite_members JSONB;
|
||||||
@@ -13,6 +13,21 @@ function deleteImageFile(imageUrl) {
|
|||||||
// Schema-aware room name helper
|
// Schema-aware room name helper
|
||||||
const R = (schema, type, id) => `${schema}:${type}:${id}`;
|
const R = (schema, type, id) => `${schema}:${type}:${id}`;
|
||||||
|
|
||||||
|
// Compute and store composite_members for a non-managed private group.
|
||||||
|
// Captures up to 4 current members (excluding deleted users), ordered by name.
|
||||||
|
async function computeAndStoreComposite(schema, groupId) {
|
||||||
|
const members = await query(schema,
|
||||||
|
`SELECT u.id, u.name, u.avatar FROM group_members gm
|
||||||
|
JOIN users u ON gm.user_id = u.id
|
||||||
|
WHERE gm.group_id = $1 AND u.name != 'Deleted User'
|
||||||
|
ORDER BY u.name LIMIT 4`,
|
||||||
|
[groupId]
|
||||||
|
);
|
||||||
|
await exec(schema, 'UPDATE groups SET composite_members=$1 WHERE id=$2',
|
||||||
|
[JSON.stringify(members), groupId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (io) => {
|
module.exports = (io) => {
|
||||||
|
|
||||||
async function emitGroupNew(schema, io, groupId) {
|
async function emitGroupNew(schema, io, groupId) {
|
||||||
@@ -202,6 +217,11 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, uid]);
|
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, uid]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Generate composite avatar for non-managed private groups with 3+ members
|
||||||
|
const totalCount = await queryOne(req.schema, 'SELECT COUNT(*) AS cnt FROM group_members WHERE group_id=$1', [groupId]);
|
||||||
|
if (parseInt(totalCount.cnt) >= 3) {
|
||||||
|
await computeAndStoreComposite(req.schema, groupId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await emitGroupNew(req.schema, io, groupId);
|
await emitGroupNew(req.schema, io, groupId);
|
||||||
res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [groupId]) });
|
res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [groupId]) });
|
||||||
@@ -246,6 +266,8 @@ router.post('/:id/members', authMiddleware, async (req, res) => {
|
|||||||
if (group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner can add members' });
|
if (group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner can add members' });
|
||||||
const targetUser = await queryOne(req.schema, 'SELECT is_default_admin FROM users WHERE id=$1', [userId]);
|
const targetUser = await queryOne(req.schema, 'SELECT is_default_admin FROM users WHERE id=$1', [userId]);
|
||||||
if (targetUser?.is_default_admin) return res.status(400).json({ error: 'Default admin cannot be added to private groups' });
|
if (targetUser?.is_default_admin) return res.status(400).json({ error: 'Default admin cannot be added to private groups' });
|
||||||
|
// Capture pre-add count to decide if composite should regenerate
|
||||||
|
const preAddCount = await queryOne(req.schema, 'SELECT COUNT(*) AS cnt FROM group_members WHERE group_id=$1', [group.id]);
|
||||||
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [group.id, userId]);
|
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [group.id, userId]);
|
||||||
const addedUser = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [userId]);
|
const addedUser = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [userId]);
|
||||||
const addedName = addedUser?.display_name || addedUser?.name || 'Unknown';
|
const addedName = addedUser?.display_name || addedUser?.name || 'Unknown';
|
||||||
@@ -259,6 +281,14 @@ router.post('/:id/members', authMiddleware, async (req, res) => {
|
|||||||
);
|
);
|
||||||
sysMsg.reactions = [];
|
sysMsg.reactions = [];
|
||||||
io.to(R(req.schema,'group',group.id)).emit('message:new', sysMsg);
|
io.to(R(req.schema,'group',group.id)).emit('message:new', sysMsg);
|
||||||
|
// Regenerate composite if pre-add count was ≤3 and group is non-managed private
|
||||||
|
if (!group.is_managed && !group.is_direct && parseInt(preAddCount.cnt) <= 3) {
|
||||||
|
const newTotal = parseInt(preAddCount.cnt) + 1;
|
||||||
|
if (newTotal >= 3) {
|
||||||
|
await computeAndStoreComposite(req.schema, group.id);
|
||||||
|
}
|
||||||
|
await emitGroupUpdated(req.schema, io, group.id);
|
||||||
|
}
|
||||||
io.in(R(req.schema,'user',userId)).socketsJoin(R(req.schema,'group',group.id));
|
io.in(R(req.schema,'user',userId)).socketsJoin(R(req.schema,'group',group.id));
|
||||||
io.to(R(req.schema,'user',userId)).emit('group:new', { group });
|
io.to(R(req.schema,'user',userId)).emit('group:new', { group });
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|||||||
@@ -33,15 +33,15 @@ const COMPOSITE_LAYOUTS = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
function GroupAvatarComposite({ memberPreviews, fallbackLabel, fallbackColor }) {
|
function GroupAvatarComposite({ memberPreviews }) {
|
||||||
const members = (memberPreviews || []).slice(0, 4);
|
const members = (memberPreviews || []).slice(0, 4);
|
||||||
const n = members.length;
|
const n = members.length;
|
||||||
const positions = COMPOSITE_LAYOUTS[n];
|
const positions = COMPOSITE_LAYOUTS[n];
|
||||||
|
|
||||||
if (!positions) {
|
if (!positions) {
|
||||||
return (
|
return (
|
||||||
<div className="group-icon" style={{ background: fallbackColor, borderRadius: 8, fontSize: 11, fontWeight: 700 }}>
|
<div className="group-icon" style={{ background: '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>
|
||||||
{fallbackLabel}
|
?
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,9 +165,11 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
|||||||
{(group.peer_real_name || group.name)[0]?.toUpperCase()}
|
{(group.peer_real_name || group.name)[0]?.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
) : group.is_managed && group.is_multi_group ? (
|
) : group.is_managed && group.is_multi_group ? (
|
||||||
<GroupAvatarComposite memberPreviews={group.member_previews} fallbackLabel="MG" fallbackColor={settings.color_avatar_dm || '#a142f4'} />
|
<div className="group-icon" style={{ background: settings.color_avatar_dm || '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>MG</div>
|
||||||
) : group.is_managed ? (
|
) : group.is_managed ? (
|
||||||
<GroupAvatarComposite memberPreviews={group.member_previews} fallbackLabel="UG" fallbackColor={settings.color_avatar_dm || '#a142f4'} />
|
<div className="group-icon" style={{ background: settings.color_avatar_dm || '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>UG</div>
|
||||||
|
) : group.composite_members?.length > 0 ? (
|
||||||
|
<GroupAvatarComposite memberPreviews={group.composite_members} />
|
||||||
) : (
|
) : (
|
||||||
<div className="group-icon" style={{ background: group.type === 'public' ? (settings.color_avatar_public || '#1a73e8') : (settings.color_avatar_dm || '#a142f4') }}>
|
<div className="group-icon" style={{ background: group.type === 'public' ? (settings.color_avatar_public || '#1a73e8') : (settings.color_avatar_dm || '#a142f4') }}>
|
||||||
{group.type === 'public' ? '#' : group.name[0]?.toUpperCase()}
|
{group.type === 'public' ? '#' : group.name[0]?.toUpperCase()}
|
||||||
@@ -231,7 +233,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
|||||||
)}
|
)}
|
||||||
{groupMessagesMode && privateFiltered.length > 0 && (
|
{groupMessagesMode && privateFiltered.length > 0 && (
|
||||||
<div className="group-section">
|
<div className="group-section">
|
||||||
<div className="section-label">PRIVATE GROUP MESSAGES</div>
|
<div className="section-label">USER GROUP MESSAGES</div>
|
||||||
{privateFiltered.map(g => <GroupItem key={g.id} group={g} />)}
|
{privateFiltered.map(g => <GroupItem key={g.id} group={g} />)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user