v0.7.4 bug fixes

This commit is contained in:
2026-03-11 18:36:07 -04:00
parent 6ad9584ea9
commit 8202c838f5
11 changed files with 132 additions and 71 deletions

View File

@@ -22,20 +22,19 @@ function BuiltWithValue({ value }) {
}
export default function AboutModal({ onClose }) {
const [settings, setSettings] = useState({ app_name: 'jama', app_version: '' });
const [about, setAbout] = useState(null);
useEffect(() => {
api.getSettings().then(({ settings }) => setSettings(settings)).catch(() => {});
fetch('/api/about')
.then(r => r.json())
.then(({ about }) => setAbout(about))
.catch(() => {});
}, []);
const appName = settings.app_name || 'jama';
// Version always mirrors Settings window — from settings API (env var)
const version = settings.app_version || about?.version || '';
// Always use the original app identity — not the user-customised settings name/logo
const appName = about?.default_app_name || 'jama';
const logoSrc = about?.default_logo || '/icons/jama.png';
const version = about?.version || '';
const a = about || {};
const rows = [
@@ -55,7 +54,7 @@ export default function AboutModal({ onClose }) {
</button>
<div className="about-hero">
<img src="/icons/jama.png" alt="jama" className="about-logo" />
<img src={logoSrc} alt={appName} className="about-logo" />
<h1 className="about-appname">{appName}</h1>
<p className="about-tagline">just another messaging app</p>
</div>

View File

@@ -43,7 +43,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
}, []);
useEffect(() => {
if (!group) { setMessages([]); return; }
if (!group) { setMessages([]); setPinnedMsgIds(new Set()); setPinCount(0); return; }
setMessages([]);
setHasMore(false);
setLoading(true);
@@ -55,8 +55,40 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
})
.catch(e => toast(e.message, 'error'))
.finally(() => setLoading(false));
// Load pinned messages for DMs
if (group.is_direct) {
api.getPinnedMessages(group.id)
.then(({ pinned, count }) => {
setPinnedMsgIds(new Set(pinned.map(p => p.id)));
setPinCount(count);
})
.catch(() => {});
} else {
setPinnedMsgIds(new Set());
setPinCount(0);
}
}, [group?.id]);
const handlePinMessage = async (msgId) => {
try {
const { count } = await api.pinMessage(msgId);
setPinnedMsgIds(prev => new Set([...prev, msgId]));
setPinCount(count);
} catch (e) {
toast(e.message || 'Could not pin message', 'error');
}
};
const handleUnpinMessage = async (msgId) => {
try {
const { count } = await api.unpinMessage(msgId);
setPinnedMsgIds(prev => { const n = new Set(prev); n.delete(msgId); return n; });
setPinCount(count);
} catch (e) {
toast(e.message || 'Could not unpin message', 'error');
}
};
// Socket events
useEffect(() => {
if (!socket || !group) return;

View File

@@ -62,6 +62,60 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
msg.group_owner_id === currentUser.id
);
// Close emoji picker when clicking outside
useEffect(() => {
if (!showEmojiPicker) return;
const handler = (e) => {
if (pickerRef.current && !pickerRef.current.contains(e.target)) {
setShowEmojiPicker(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [showEmojiPicker]);
// Close options menu on outside click
useEffect(() => {
if (!showOptionsMenu) return;
const close = (e) => {
if (optionsMenuRef.current && !optionsMenuRef.current.contains(e.target)) {
setShowOptionsMenu(false);
}
};
document.addEventListener('mousedown', close);
document.addEventListener('touchstart', close);
return () => {
document.removeEventListener('mousedown', close);
document.removeEventListener('touchstart', close);
};
}, [showOptionsMenu]);
const handleReact = (emoji) => {
onReact(msg.id, emoji);
setShowEmojiPicker(false);
};
const handleCopy = () => {
if (!msg.content) return;
navigator.clipboard.writeText(msg.content).catch(() => {});
};
const handleTogglePicker = () => {
if (!showEmojiPicker && wrapperRef.current) {
const rect = wrapperRef.current.getBoundingClientRect();
setPickerOpensDown(rect.top < 400);
}
setShowEmojiPicker(p => !p);
};
// Long press for mobile action menu
const handleTouchStart = () => {
longPressTimer.current = setTimeout(() => setShowOptionsMenu(true), 500);
};
const handleTouchEnd = () => {
if (longPressTimer.current) clearTimeout(longPressTimer.current);
};
// Deleted messages are filtered out by ChatWindow, but guard here too
if (isDeleted) return null;
@@ -85,61 +139,6 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
if (r.user_id === currentUser.id) reactionMap[r.emoji].hasMe = true;
}
// Close emoji picker when clicking outside
useEffect(() => {
if (!showEmojiPicker) return;
const handler = (e) => {
if (pickerRef.current && !pickerRef.current.contains(e.target)) {
setShowEmojiPicker(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [showEmojiPicker]);
const handleReact = (emoji) => {
onReact(msg.id, emoji);
setShowEmojiPicker(false);
};
const handleCopy = () => {
if (!msg.content) return;
navigator.clipboard.writeText(msg.content).catch(() => {});
};
const handleTogglePicker = () => {
if (!showEmojiPicker && wrapperRef.current) {
// If the message is in the top 400px of viewport, open picker downward
const rect = wrapperRef.current.getBoundingClientRect();
setPickerOpensDown(rect.top < 400);
}
setShowEmojiPicker(p => !p);
};
// Long press for mobile action menu
const handleTouchStart = () => {
longPressTimer.current = setTimeout(() => setShowOptionsMenu(true), 500);
};
const handleTouchEnd = () => {
if (longPressTimer.current) clearTimeout(longPressTimer.current);
};
// Close options menu on outside click
useEffect(() => {
if (!showOptionsMenu) return;
const close = (e) => {
if (optionsMenuRef.current && !optionsMenuRef.current.contains(e.target)) {
setShowOptionsMenu(false);
}
};
document.addEventListener('mousedown', close);
document.addEventListener('touchstart', close);
return () => {
document.removeEventListener('mousedown', close);
document.removeEventListener('touchstart', close);
};
}, [showOptionsMenu]);
const msgUser = {
id: msg.user_id,
name: msg.user_name,

View File

@@ -327,7 +327,7 @@ export default function UserManagerModal({ onClose }) {
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12 }}>
<div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Temp Password <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(blank = default)</span></label>
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Temp Password <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(blank = {userPass || 'USER_PASS'})</span></label>
<input className="input" type="text" value={form.password} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} />
</div>
<div className="flex-col gap-1">