v0.7.7 bugs fixes
This commit is contained in:
@@ -233,7 +233,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
|
||||
{group.type === 'public' ? '#' : group.is_direct ? (group.peer_real_name || group.name)[0]?.toUpperCase() : group.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
{!!group.is_direct && group.peer_id && onlineUserIds.has(group.peer_id) && (
|
||||
{!!group.is_direct && group.peer_id && (onlineUserIds instanceof Set ? onlineUserIds.has(Number(group.peer_id)) : false) && (
|
||||
<span style={{
|
||||
position: 'absolute', bottom: 1, right: 1,
|
||||
width: 11, height: 11, borderRadius: '50%',
|
||||
|
||||
@@ -334,6 +334,43 @@
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* Triple-dot pin button wrapper */
|
||||
.msg-pin-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.msg-pin-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
color: var(--text-tertiary);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.msg-pin-btn:hover,
|
||||
.msg-pin-btn.active {
|
||||
opacity: 1 !important;
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-variant);
|
||||
}
|
||||
|
||||
/* Show pin button on message row hover */
|
||||
.msg-bubble-wrap:hover .msg-pin-btn,
|
||||
.msg-pin-btn.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* On touch devices always show it so users know it's there */
|
||||
@media (hover: none) {
|
||||
.msg-pin-btn {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
/* Message pin/options popup menu */
|
||||
.msg-options-menu {
|
||||
position: absolute;
|
||||
@@ -344,7 +381,8 @@
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 4px 0;
|
||||
min-width: 170px;
|
||||
top: calc(100% + 4px);
|
||||
bottom: calc(100% + 4px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.msg-options-menu.options-left {
|
||||
|
||||
@@ -108,8 +108,9 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
|
||||
setShowEmojiPicker(p => !p);
|
||||
};
|
||||
|
||||
// Long press for mobile action menu
|
||||
// Long press for mobile action menu (DMs only)
|
||||
const handleTouchStart = () => {
|
||||
if (!isDirect) return;
|
||||
longPressTimer.current = setTimeout(() => setShowOptionsMenu(true), 500);
|
||||
};
|
||||
const handleTouchEnd = () => {
|
||||
@@ -165,13 +166,13 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
|
||||
{!isOwn && !prevSameUser && (
|
||||
<div
|
||||
ref={avatarRef}
|
||||
style={{ position: 'relative', cursor: 'pointer', borderRadius: '50%', transition: 'box-shadow 0.15s', flexShrink: 0 }}
|
||||
style={{ position: 'relative', cursor: 'pointer', transition: 'box-shadow 0.15s', flexShrink: 0, borderRadius: '50%', display: 'inline-flex' }}
|
||||
onClick={() => setShowProfile(p => !p)}
|
||||
onMouseEnter={e => e.currentTarget.style.boxShadow = '0 0 0 2px var(--primary)'}
|
||||
onMouseLeave={e => e.currentTarget.style.boxShadow = 'none'}
|
||||
>
|
||||
<Avatar user={msgUser} size="sm" className="msg-avatar" />
|
||||
{onlineUserIds.has(msg.user_id) && (
|
||||
{!!(onlineUserIds instanceof Set ? onlineUserIds.has(Number(msg.user_id)) : false) && (
|
||||
<span style={{
|
||||
position: 'absolute', bottom: 0, right: 0,
|
||||
width: 9, height: 9, borderRadius: '50%',
|
||||
@@ -211,10 +212,10 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
|
||||
<div className="msg-bubble-wrap">
|
||||
<div className="msg-bubble-with-actions"
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => { if (!showEmojiPicker) setShowActions(false); }}
|
||||
onTouchStart={isDirect ? handleTouchStart : undefined}
|
||||
onTouchEnd={isDirect ? handleTouchEnd : undefined}
|
||||
onTouchMove={isDirect ? handleTouchEnd : undefined}
|
||||
onMouseLeave={() => { if (!showEmojiPicker && !showOptionsMenu) setShowActions(false); }}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchMove={handleTouchEnd}
|
||||
>
|
||||
{/* Actions toolbar — floats above the bubble, aligned to correct side */}
|
||||
{!isDeleted && (showActions || showEmojiPicker) && (
|
||||
@@ -238,14 +239,6 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
|
||||
</button>
|
||||
)}
|
||||
{isDirect && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button className="btn-icon action-btn" onClick={() => setShowOptionsMenu(p => !p)} title="More options">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Emoji picker anchored to the toolbar */}
|
||||
{showEmojiPicker && (
|
||||
<div
|
||||
@@ -282,27 +275,37 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
|
||||
|
||||
<span className="msg-time">{formatTime(msg.created_at)}</span>
|
||||
|
||||
{/* Pin/unpin options menu — desktop triple-dot + mobile long-press */}
|
||||
{isDirect && showOptionsMenu && (
|
||||
<div
|
||||
ref={optionsMenuRef}
|
||||
className={`msg-options-menu ${isOwn ? 'options-left' : 'options-right'}`}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
>
|
||||
{isPinned ? (
|
||||
<button onClick={() => { onUnpin(msg.id); setShowOptionsMenu(false); }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 2v4l-3 3v7l-4-4-4 4V9L2 6V2h14zm2 0h2v2h-2V2z"/></svg>
|
||||
Unpin message
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => { if (pinCount < 5) { onPin(msg.id); setShowOptionsMenu(false); } }}
|
||||
disabled={pinCount >= 5}
|
||||
title={pinCount >= 5 ? 'Maximum 5 pinned messages reached' : ''}
|
||||
{/* Triple-dot pin button — sits outside the hover toolbar so mouse-leave doesn't race */}
|
||||
{isDirect && (
|
||||
<div className="msg-pin-wrap" ref={optionsMenuRef}>
|
||||
<button
|
||||
className={`btn-icon msg-pin-btn ${showOptionsMenu ? 'active' : ''}`}
|
||||
onClick={e => { e.stopPropagation(); setShowOptionsMenu(p => !p); }}
|
||||
title="Message options"
|
||||
>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><circle cx="12" cy="5" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="12" cy="19" r="1.2" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{showOptionsMenu && (
|
||||
<div
|
||||
className={`msg-options-menu ${isOwn ? 'options-left' : 'options-right'}`}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 2v4l-3 3v7l-4-4-4 4V9L2 6V2h14zm2 0h2v2h-2V2z"/></svg>
|
||||
{pinCount >= 5 ? 'Pin (5/5)' : `Pin (${pinCount + 1}/5)`}
|
||||
</button>
|
||||
{isPinned ? (
|
||||
<button onClick={() => { onUnpin(msg.id); setShowOptionsMenu(false); }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 2v4l-3 3v7l-4-4-4 4V9L2 6V2h14zm2 0h2v2h-2V2z"/></svg>
|
||||
Unpin message
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => { if (pinCount < 5) { onPin(msg.id); setShowOptionsMenu(false); } }}
|
||||
disabled={pinCount >= 5}
|
||||
title={pinCount >= 5 ? 'Maximum 5 pinned messages reached' : ''}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 2v4l-3 3v7l-4-4-4 4V9L2 6V2h14zm2 0h2v2h-2V2z"/></svg>
|
||||
{pinCount >= 5 ? 'Pin (5/5)' : `Pin (${pinCount + 1}/5)`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
const unreadCount = unreadGroups.get(group.id) || 0;
|
||||
const hasUnread = unreadCount > 0;
|
||||
const isActive = group.id === activeGroupId;
|
||||
const isOnline = !!group.is_direct && !!group.peer_id && onlineUserIds.has(group.peer_id);
|
||||
const isOnline = !!group.is_direct && !!group.peer_id && (onlineUserIds instanceof Set ? onlineUserIds.has(Number(group.peer_id)) : false);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -207,9 +207,9 @@ export default function Chat() {
|
||||
};
|
||||
|
||||
// Online presence
|
||||
const handleUserOnline = ({ userId }) => setOnlineUserIds(prev => new Set([...prev, userId]));
|
||||
const handleUserOffline = ({ userId }) => setOnlineUserIds(prev => { const n = new Set(prev); n.delete(userId); return n; });
|
||||
const handleUsersOnline = ({ userIds }) => setOnlineUserIds(new Set(userIds));
|
||||
const handleUserOnline = ({ userId }) => setOnlineUserIds(prev => new Set([...prev, Number(userId)]));
|
||||
const handleUserOffline = ({ userId }) => setOnlineUserIds(prev => { const n = new Set(prev); n.delete(Number(userId)); return n; });
|
||||
const handleUsersOnline = ({ userIds }) => setOnlineUserIds(new Set((userIds || []).map(Number)));
|
||||
|
||||
socket.on('user:online', handleUserOnline);
|
||||
socket.on('user:offline', handleUserOffline);
|
||||
|
||||
Reference in New Issue
Block a user