v0.8.5 fix pinning

This commit is contained in:
2026-03-13 10:51:27 -04:00
parent a02facff1a
commit b3aac1981c
13 changed files with 153 additions and 504 deletions

View File

@@ -76,6 +76,35 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
const appName = settings.app_name || 'jama';
const logoUrl = settings.logo_url;
// Conversation pinning — derive from groups data (is_pinned flag from backend)
const [pinnedConvIds, setPinnedConvIds] = useState(new Set());
// Sync pinnedConvIds whenever groups data changes
useEffect(() => {
const allG = [...(groups.publicGroups || []), ...(groups.privateGroups || [])];
setPinnedConvIds(new Set(allG.filter(g => g.is_pinned).map(g => g.id)));
}, [groups]);
const handlePinConversation = async (e, groupId) => {
e.stopPropagation();
try {
await api.pinConversation(groupId);
setPinnedConvIds(prev => new Set([...prev, groupId]));
} catch (err) {
toast('Could not pin conversation', 'error');
}
};
const handleUnpinConversation = async (e, groupId) => {
e.stopPropagation();
try {
await api.unpinConversation(groupId);
setPinnedConvIds(prev => { const n = new Set(prev); n.delete(groupId); return n; });
} catch (err) {
toast('Could not unpin conversation', 'error');
}
};
const allGroups = [
...(groups.publicGroups || []),
...(groups.privateGroups || [])
@@ -83,14 +112,17 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
const publicFiltered = allGroups.filter(g => g.type === 'public');
// All private groups (DMs + group chats) sorted together by most recent message
const privateFiltered = allGroups
.filter(g => g.type === 'private')
.sort((a, b) => {
if (!a.last_message_at && !b.last_message_at) return 0;
if (!a.last_message_at) return 1;
if (!b.last_message_at) return -1;
return new Date(b.last_message_at) - new Date(a.last_message_at);
});
const sortWithPinned = (arr) => [...arr].sort((a, b) => {
const aPinned = pinnedConvIds.has(a.id) ? 1 : 0;
const bPinned = pinnedConvIds.has(b.id) ? 1 : 0;
if (bPinned !== aPinned) return bPinned - aPinned;
if (!a.last_message_at && !b.last_message_at) return 0;
if (!a.last_message_at) return 1;
if (!b.last_message_at) return -1;
return new Date(b.last_message_at) - new Date(a.last_message_at);
});
const privateFiltered = sortWithPinned(allGroups.filter(g => g.type === 'private'));
const getNotifCount = (groupId) => notifications.filter(n => n.groupId === groupId).length;
@@ -102,11 +134,24 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
const hasUnread = unreadCount > 0;
const isActive = group.id === activeGroupId;
const isOnline = !!group.is_direct && !!group.peer_id && (onlineUserIds instanceof Set ? onlineUserIds.has(Number(group.peer_id)) : false);
const isPinned = pinnedConvIds.has(group.id);
// Long-press for mobile pin
const longPressTimer = useRef(null);
const handleTouchStart = () => {
longPressTimer.current = setTimeout(() => {
isPinned ? handleUnpinConversation({ stopPropagation: () => {} }, group.id)
: handlePinConversation({ stopPropagation: () => {} }, group.id);
}, 600);
};
const handleTouchEnd = () => clearTimeout(longPressTimer.current);
return (
<div
className={`group-item ${isActive ? 'active' : ''} ${hasUnread ? 'has-unread' : ''}`}
className={`group-item ${isActive ? 'active' : ''} ${hasUnread ? 'has-unread' : ''} ${isPinned ? 'is-pinned' : ''}`}
onClick={() => onSelectGroup(group.id)}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchEnd}
>
<div className="group-icon-wrap">
{group.is_direct && group.peer_avatar ? (
@@ -126,13 +171,29 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
<div className="group-info flex-1 overflow-hidden">
<div className="flex items-center justify-between">
<span className={`group-name truncate ${hasUnread ? 'unread-name' : ''}`}>
{isPinned && (
<svg className="conv-pin-indicator" width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 2v4l-3 3v6l-2-2-2 2V9L6 6V2h10z"/>
</svg>
)}
{group.is_direct && group.peer_display_name
? <>{group.peer_display_name}<span className="dm-real-name"> ({group.peer_real_name})</span></>
: group.is_direct && group.peer_real_name ? group.peer_real_name : group.name}
</span>
{group.last_message_at && (
<span className="group-time">{formatTime(group.last_message_at)}</span>
)}
<div className="group-item-actions">
{group.last_message_at && (
<span className="group-time">{formatTime(group.last_message_at)}</span>
)}
<button
className={`conv-pin-btn ${isPinned ? 'pinned' : ''}`}
onClick={(e) => isPinned ? handleUnpinConversation(e, group.id) : handlePinConversation(e, group.id)}
title={isPinned ? 'Unpin conversation' : 'Pin to top'}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 2v4l-3 3v6l-2-2-2 2V9L6 6V2h10z"/>
</svg>
</button>
</div>
</div>
<div className="flex items-center justify-between gap-2">
<span className="group-last-msg truncate">