v0.7.4 bug fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user