pinch zoom bug fix
This commit is contained in:
@@ -56,7 +56,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
(SELECT COUNT(*) FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE) AS message_count,
|
(SELECT COUNT(*) FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE) AS message_count,
|
||||||
(SELECT m.content FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message,
|
(SELECT m.content FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message,
|
||||||
(SELECT m.created_at FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_at,
|
(SELECT m.created_at FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_at,
|
||||||
(SELECT m.user_id FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_user_id
|
(SELECT m.user_id FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_user_id,
|
||||||
|
(SELECT json_agg(t) FROM (
|
||||||
|
SELECT u2.id, u2.name, u2.avatar
|
||||||
|
FROM group_members gm2
|
||||||
|
JOIN users u2 ON gm2.user_id = u2.id
|
||||||
|
WHERE gm2.group_id = g.id AND u2.name != 'Deleted User'
|
||||||
|
ORDER BY u2.name LIMIT 4
|
||||||
|
) t) AS member_previews
|
||||||
FROM groups g JOIN group_members gm ON g.id=gm.group_id AND gm.user_id=$1
|
FROM groups g JOIN group_members gm ON g.id=gm.group_id AND gm.user_id=$1
|
||||||
LEFT JOIN users u ON g.owner_id=u.id WHERE g.type='private'
|
LEFT JOIN users u ON g.owner_id=u.id WHERE g.type='private'
|
||||||
ORDER BY last_message_at DESC NULLS LAST
|
ORDER BY last_message_at DESC NULLS LAST
|
||||||
|
|||||||
@@ -4,16 +4,31 @@ import { createPortal } from 'react-dom';
|
|||||||
export default function ImageLightbox({ src, onClose }) {
|
export default function ImageLightbox({ src, onClose }) {
|
||||||
const overlayRef = useRef(null);
|
const overlayRef = useRef(null);
|
||||||
|
|
||||||
// Close on Escape; signal global pinch handler to stand down while open
|
// Close on Escape; enable native pinch-zoom on the image while open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// Signal the global font-scale pinch handler in main.jsx to stand down
|
||||||
document.documentElement.dataset.lightboxOpen = '1';
|
document.documentElement.dataset.lightboxOpen = '1';
|
||||||
|
|
||||||
|
// Enable native browser pinch-to-zoom by removing the scale restrictions.
|
||||||
|
// The original content is restored exactly on close.
|
||||||
|
const viewport = document.querySelector('meta[name="viewport"]');
|
||||||
|
const originalContent = viewport?.content ?? '';
|
||||||
|
if (viewport) {
|
||||||
|
viewport.content = originalContent
|
||||||
|
.replace(/,?\s*maximum-scale=[^,]*/g, '')
|
||||||
|
.replace(/,?\s*user-scalable=[^,]*/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handler);
|
window.removeEventListener('keydown', handler);
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
delete document.documentElement.dataset.lightboxOpen;
|
delete document.documentElement.dataset.lightboxOpen;
|
||||||
|
if (viewport) viewport.content = originalContent;
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,73 @@ function nameToColor(name) {
|
|||||||
return AVATAR_COLORS[(name || '').charCodeAt(0) % AVATAR_COLORS.length];
|
return AVATAR_COLORS[(name || '').charCodeAt(0) % AVATAR_COLORS.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Layouts for composite avatars inside a 44×44 circle (all values in px)
|
||||||
|
const COMPOSITE_LAYOUTS = {
|
||||||
|
1: [{ top: 4, left: 4, size: 36 }],
|
||||||
|
2: [
|
||||||
|
{ top: 11, left: 1, size: 21 },
|
||||||
|
{ top: 11, right: 1, size: 21 },
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ top: 2, left: 3, size: 19 },
|
||||||
|
{ top: 2, right: 3, size: 19 },
|
||||||
|
{ bottom: 2, left: 12, size: 19 },
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ top: 1, left: 1, size: 20 },
|
||||||
|
{ top: 1, right: 1, size: 20 },
|
||||||
|
{ bottom: 1, left: 1, size: 20 },
|
||||||
|
{ bottom: 1, right: 1, size: 20 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function GroupAvatarComposite({ memberPreviews, fallbackLabel, fallbackColor }) {
|
||||||
|
const members = (memberPreviews || []).slice(0, 4);
|
||||||
|
const n = members.length;
|
||||||
|
const positions = COMPOSITE_LAYOUTS[n];
|
||||||
|
|
||||||
|
if (!positions) {
|
||||||
|
return (
|
||||||
|
<div className="group-icon" style={{ background: fallbackColor, borderRadius: 8, fontSize: 11, fontWeight: 700 }}>
|
||||||
|
{fallbackLabel}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group-icon" style={{ background: '#1a1a2e', position: 'relative', padding: 0, overflow: 'hidden' }}>
|
||||||
|
{members.map((m, i) => {
|
||||||
|
const pos = positions[i];
|
||||||
|
const base = {
|
||||||
|
position: 'absolute',
|
||||||
|
width: pos.size,
|
||||||
|
height: pos.size,
|
||||||
|
borderRadius: '50%',
|
||||||
|
...(pos.top !== undefined ? { top: pos.top } : {}),
|
||||||
|
...(pos.bottom !== undefined ? { bottom: pos.bottom } : {}),
|
||||||
|
...(pos.left !== undefined ? { left: pos.left } : {}),
|
||||||
|
...(pos.right !== undefined ? { right: pos.right } : {}),
|
||||||
|
overflow: 'hidden',
|
||||||
|
flexShrink: 0,
|
||||||
|
};
|
||||||
|
if (m.avatar) {
|
||||||
|
return <img key={m.id} src={m.avatar} alt={m.name} style={{ ...base, objectFit: 'cover' }} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={m.id} style={{
|
||||||
|
...base,
|
||||||
|
background: nameToColor(m.name),
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: Math.round(pos.size * 0.42), fontWeight: 700, color: 'white',
|
||||||
|
}}>
|
||||||
|
{(m.name || '')[0]?.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function useAppSettings() {
|
function useAppSettings() {
|
||||||
const [settings, setSettings] = useState({ app_name: 'rosterchirp', logo_url: '', color_avatar_public: '', color_avatar_dm: '' });
|
const [settings, setSettings] = useState({ app_name: 'rosterchirp', logo_url: '', color_avatar_public: '', color_avatar_dm: '' });
|
||||||
const fetchSettings = () => {
|
const fetchSettings = () => {
|
||||||
@@ -98,9 +165,9 @@ 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 ? (
|
||||||
<div className="group-icon" style={{ background: settings.color_avatar_dm || '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>MG</div>
|
<GroupAvatarComposite memberPreviews={group.member_previews} fallbackLabel="MG" fallbackColor={settings.color_avatar_dm || '#a142f4'} />
|
||||||
) : group.is_managed ? (
|
) : group.is_managed ? (
|
||||||
<div className="group-icon" style={{ background: settings.color_avatar_dm || '#a142f4', borderRadius: 8, fontSize: 11, fontWeight: 700 }}>UG</div>
|
<GroupAvatarComposite memberPreviews={group.member_previews} fallbackLabel="UG" fallbackColor={settings.color_avatar_dm || '#a142f4'} />
|
||||||
) : (
|
) : (
|
||||||
<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()}
|
||||||
|
|||||||
Reference in New Issue
Block a user