Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions backend/src/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ export async function patchPreferences(req, res) {
showAppBadge, showFaviconBadge, replyDefault, sidebarWidth,
categorizationEnabled, markReadBehavior, markReadDelay, aiActions,
autoLockMinutes, showMobileAvatars, gravatarAvatars, folderSyncInterval,
folderOrder, senderFavicons } = req.body;
folderOrder, senderFavicons, showMessagePreviews } = req.body;
// GTD content and generic right-sidebar layout preferences are independent flat
// top-level keys with separate allow-lists. gtdEnabled is intentionally NOT a user
// preference — it lives per-account in email_accounts.gtd_enabled.
Expand Down Expand Up @@ -851,6 +851,7 @@ export async function patchPreferences(req, res) {
|| CASE WHEN $38::text IS NOT NULL THEN jsonb_build_object('folderSyncInterval', $38::text) ELSE '{}'::jsonb END
|| CASE WHEN $39::jsonb IS NOT NULL THEN jsonb_build_object('folderOrder', $39::jsonb) ELSE '{}'::jsonb END
|| CASE WHEN $40::boolean IS NOT NULL THEN jsonb_build_object('senderFavicons', $40::boolean) ELSE '{}'::jsonb END
|| CASE WHEN $41::boolean IS NOT NULL THEN jsonb_build_object('showMessagePreviews', $41::boolean) ELSE '{}'::jsonb END
WHERE id = $1
`, [req.session.userId, theme ?? null, font ?? null, layout ?? null, notificationSound ?? null,
pageSize ?? null, scrollMode ?? null, syncInterval ?? null,
Expand All @@ -860,7 +861,8 @@ export async function patchPreferences(req, res) {
showAppBadge ?? null, showFaviconBadge ?? null, replyDefaultVal, sidebarWidthVal,
categorizationEnabled ?? null, markReadBehaviorVal, markReadDelayVal, aiActionsJson,
rightSidebarWidth, rightSidebarHidden, gtdCollapsedSectionsJson, gtdPetSlug, autoLockMinutesVal,
showMobileAvatars ?? null, gravatarAvatars ?? null, folderSyncIntervalVal, folderOrderJson, senderFaviconsVal]);
showMobileAvatars ?? null, gravatarAvatars ?? null, folderSyncIntervalVal, folderOrderJson, senderFaviconsVal,
showMessagePreviews ?? null]);

if (syncInterval != null) {
const ms = parseInt(syncInterval) * 1000;
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/components/AdminPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1474,7 +1474,7 @@ function SwipeActionIcon({ action, size = 17 }) {
function LayoutsTab() {
const { t } = useTranslation();
const isMobile = useMobile();
const { layout, setLayout, pageSize, setPageSize, scrollMode, setScrollMode, swipeActions, setSwipeAction, syncInterval, setSyncInterval, folderSyncInterval, setFolderSyncInterval, threadedView, setThreadedView, plaintextEmail, setPlaintextEmail, hoverQuickActions, setHoverQuickActions, showMobileAvatars, setShowMobileAvatars, gravatarAvatars, setGravatarAvatars, replyDefault, setReplyDefault, markReadBehavior, setMarkReadBehavior, markReadDelay, setMarkReadDelay, senderFavicons, senderFaviconsSaving, setSenderFavicons } = useStore();
const { layout, setLayout, pageSize, setPageSize, scrollMode, setScrollMode, swipeActions, setSwipeAction, syncInterval, setSyncInterval, folderSyncInterval, setFolderSyncInterval, threadedView, setThreadedView, plaintextEmail, setPlaintextEmail, hoverQuickActions, setHoverQuickActions, showMobileAvatars, setShowMobileAvatars, gravatarAvatars, setGravatarAvatars, replyDefault, setReplyDefault, markReadBehavior, setMarkReadBehavior, markReadDelay, setMarkReadDelay, senderFavicons, senderFaviconsSaving, setSenderFavicons, showMessagePreviews, setShowMessagePreviews } = useStore();
const [senderFaviconsError, setSenderFaviconsError] = useState('');

// "Set MailFlow as your default email app": registerProtocolHandler is the
Expand Down Expand Up @@ -1697,6 +1697,37 @@ function LayoutsTab() {
</div>
</div>

<div style={{ marginTop: 22, paddingTop: 18, borderTop: '1px solid var(--border-subtle)' }}>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 8 }}>
{t('admin.messageList.showMessagePreviews')}
</div>
<div style={{ display: 'flex', gap: 8 }}>
{[
{ id: false, label: t('admin.messageList.previewOff'), desc: t('admin.messageList.previewOffDesc') },
{ id: true, label: t('admin.messageList.previewOn'), desc: t('admin.messageList.previewOnDesc') },
].map(({ id, label, desc }) => {
const active = showMessagePreviews === id;
return (
<button
key={String(id)}
onClick={() => setShowMessagePreviews(id)}
style={{
flex: 1, padding: '10px 12px', textAlign: 'left',
background: active ? 'var(--bg-hover)' : 'var(--bg-tertiary)',
border: `2px solid ${active ? 'var(--accent)' : 'var(--border-subtle)'}`,
borderRadius: 8, cursor: 'pointer', transition: 'all 0.15s', outline: 'none',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.borderColor = 'var(--border)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = 'var(--border-subtle)'; }}
>
<div style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-primary)', marginBottom: 2 }}>{label}</div>
<div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{desc}</div>
</button>
);
})}
</div>
</div>

<div style={{ marginTop: 22, paddingTop: 18, borderTop: '1px solid var(--border-subtle)' }}>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 8 }}>
{t('admin.messageList.gravatarMode')}
Expand Down
45 changes: 26 additions & 19 deletions frontend/src/components/MessageList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default function MessageList() {
setMobileSidebarOpen, unreadCounts, showContacts, setShowContacts,
threadedView, expandedThreadId, setExpandedThreadId,
threadMessages, setThreadMessages, loadingThread, setLoadingThread,
hoverQuickActions, showMobileAvatars,
hoverQuickActions, showMobileAvatars, showMessagePreviews,
swipeActions,
folders, favoriteFolders, addFavoriteFolder, removeFavoriteFolder, setSelectedAccount,
categorizationEnabled, categoryCounts, setCategoryCounts, adjustCategoryCount,
Expand Down Expand Up @@ -3436,6 +3436,7 @@ export default function MessageList() {
isNarrow={isNarrow}
onThreadClick={() => handleThreadClick(message)}
showMobileAvatars={showMobileAvatars}
showMessagePreviews={showMessagePreviews}
onSelect={handleSelect}
onMarkRead={handleThreadMarkRead}
onStar={handleStar}
Expand Down Expand Up @@ -3479,6 +3480,7 @@ export default function MessageList() {
onRangeSelect={handleRangeSelect}
onAvatarClick={!isMobile ? handleAvatarClick : undefined}
showMobileAvatars={showMobileAvatars}
showMessagePreviews={showMessagePreviews}
onMarkRead={handleMarkRead}
onStar={handleStar}
onDelete={handleDelete}
Expand Down Expand Up @@ -3912,7 +3914,7 @@ function EmptyState({ folderSyncing, searchQuery, unreadOnly, selectedFolder, ac
);
}

function ThreadRow({ message, isExpanded, threadMsgs, isLoadingThread, selectedMessageId, selectedMid, lastViewedMessageId, showAccount, isNarrow, onThreadClick, showMobileAvatars, onSelect, onMarkRead, onStar, onDelete, hoverQuickActions, onContextMenu, onMove, onGtdDone, isMobile, swipeLeftAction, swipeRightAction, onSwipeLeft, onSwipeRight, isChecked, selectionMode, onToggleSelect, onRangeSelect, onLongPress }) {
function ThreadRow({ message, isExpanded, threadMsgs, isLoadingThread, selectedMessageId, selectedMid, lastViewedMessageId, showAccount, isNarrow, onThreadClick, showMobileAvatars, showMessagePreviews, onSelect, onMarkRead, onStar, onDelete, hoverQuickActions, onContextMenu, onMove, onGtdDone, isMobile, swipeLeftAction, swipeRightAction, onSwipeLeft, onSwipeRight, isChecked, selectionMode, onToggleSelect, onRangeSelect, onLongPress }) {
const { t } = useTranslation();
const [hovered, setHovered] = useState(false);
const messageCount = message.message_count || 1;
Expand Down Expand Up @@ -4105,12 +4107,14 @@ function ThreadRow({ message, isExpanded, threadMsgs, isLoadingThread, selectedM
{message.subject || t('common.noSubject')}
</div>
{/* Row 3: snippet */}
<div style={{
fontSize: 12, color: 'var(--text-tertiary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{message.snippet || ''}
</div>
{showMessagePreviews && (
<div style={{
fontSize: 12, color: 'var(--text-tertiary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{message.snippet || ''}
</div>
)}
</div>
{hovered && hoverQuickActions && (
<RowHoverActions
Expand Down Expand Up @@ -4174,12 +4178,13 @@ function ThreadRow({ message, isExpanded, threadMsgs, isLoadingThread, selectedM
{formatDate(msg.date)}
</span>
</div>
{showMessagePreviews && (
<div style={{
fontSize: 11, color: 'var(--text-tertiary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 1,
}}>
{msg.snippet || ''}
</div>
</div>)}
</div>
</div>
))}
Expand All @@ -4189,7 +4194,7 @@ function ThreadRow({ message, isExpanded, threadMsgs, isLoadingThread, selectedM
);
}

function MessageRow({ message, selected, lastViewed, isChecked, selectionMode, showAccount, isNarrow, onSelect, onToggleSelect, onRangeSelect, onAvatarClick, showMobileAvatars, onMarkRead, onStar, onDelete, hoverQuickActions, onContextMenu, onMove, onGtdDone, onDragStart, isMobile, swipeLeftAction, swipeRightAction, onSwipeLeft, onSwipeRight, onLongPress }) {
function MessageRow({ message, selected, lastViewed, isChecked, selectionMode, showAccount, isNarrow, onSelect, onToggleSelect, onRangeSelect, onAvatarClick, showMobileAvatars, showMessagePreviews, onMarkRead, onStar, onDelete, hoverQuickActions, onContextMenu, onMove, onGtdDone, onDragStart, isMobile, swipeLeftAction, swipeRightAction, onSwipeLeft, onSwipeRight, onLongPress }) {
const { t } = useTranslation();
const [hovered, setHovered] = useState(false);
const [avatarHovered, setAvatarHovered] = useState(false);
Expand Down Expand Up @@ -4417,15 +4422,17 @@ function MessageRow({ message, selected, lastViewed, isChecked, selectionMode, s
</div>

{/* Row 3: Snippet */}
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{
fontSize: 12, color: 'var(--text-tertiary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
flex: 1,
}}>
{message.snippet || '\u00a0'}
</span>
</div>
{showMessagePreviews && (
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{
fontSize: 12, color: 'var(--text-tertiary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
flex: 1,
}}>
{message.snippet || '\u00a0'}
</span>
</div>
)}
</div>
</div>

Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,11 @@
"hoverQuickActionsOffDesc": "Schnellaktionen ausblenden, bis das Kontextmenü oder Tastenkürzel verwendet werden.",
"hoverQuickActionsOn": "Ein",
"hoverQuickActionsOnDesc": "Schnellaktion-Symbole in Nachrichtenzeilen anzeigen, wenn der Mauszeiger darüber ist.",
"showMessagePreviews": "Nachrichten-Vorschau",
"previewOff": "Aus",
"previewOffDesc": "Die Vorschauzeile unter jeder Betreffzeile ausblenden.",
"previewOn": "Ein",
"previewOnDesc": "Die Vorschauzeile unter jeder Betreffzeile einblenden.",
"mobileAvatarsMode": "Absender-Avatare (mobil)",
"mobileAvatarsOff": "Aus",
"mobileAvatarsOffDesc": "Kein Avatar — maximaler Platz für Absender und Betreff.",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,11 @@
"hoverQuickActionsOffDesc": "Keep Quick Actions hidden until you use the context menu or shortcuts.",
"hoverQuickActionsOn": "On",
"hoverQuickActionsOnDesc": "Show Quick Action icons in message rows when the pointer is over them.",
"showMessagePreviews": "Message previews",
"previewOff": "Off",
"previewOffDesc": "Hide the preview line beneath each subject.",
"previewOn": "On",
"previewOnDesc": "Show the preview line beneath each subject.",
"mobileAvatarsMode": "Sender avatars (mobile)",
"mobileAvatarsOff": "Off",
"mobileAvatarsOffDesc": "No avatar — maximum room for sender and subject.",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,11 @@
"hoverQuickActionsOffDesc": "Mantener las acciones rápidas ocultas hasta usar el menú contextual o atajos.",
"hoverQuickActionsOn": "Activado",
"hoverQuickActionsOnDesc": "Mostrar iconos de acción rápida en las filas de mensajes cuando el cursor esté sobre ellas.",
"showMessagePreviews": "Previsualización de mensajes",
"previewOff": "Desactivado",
"previewOffDesc": "Ocultar la línea de vista previa debajo de cada asunto.",
"previewOn": "Activado",
"previewOnDesc": "Mostrar la línea de vista previa debajo de cada asunto.",
"mobileAvatarsMode": "Avatares del remitente (móvil)",
"mobileAvatarsOff": "Desactivado",
"mobileAvatarsOffDesc": "Sin avatar: máximo espacio para remitente y asunto.",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,11 @@
"hoverQuickActionsOffDesc": "Garder les actions rapides masquées jusqu'à l'utilisation du menu contextuel ou des raccourcis.",
"hoverQuickActionsOn": "Activé",
"hoverQuickActionsOnDesc": "Afficher les icônes d'action rapide dans les lignes de messages lorsque le pointeur les survole.",
"showMessagePreviews": "Aperçu des messages",
"previewOff": "Désactivé",
"previewOffDesc": "Masquer la ligne d’aperçu sous chaque objet.",
"previewOn": "Activé",
"previewOnDesc": "Afficher la ligne d’aperçu sous chaque objet.",
"mobileAvatarsMode": "Avatars de l'expéditeur (mobile)",
"mobileAvatarsOff": "Désactivé",
"mobileAvatarsOffDesc": "Pas d'avatar — espace maximal pour l'expéditeur et l'objet.",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,11 @@
"hoverQuickActionsOffDesc": "Mantieni le azioni rapide nascoste fino all'uso del menu contestuale o delle scorciatoie.",
"hoverQuickActionsOn": "Attivato",
"hoverQuickActionsOnDesc": "Mostra le icone delle azioni rapide nelle righe dei messaggi quando il puntatore è sopra di esse.",
"showMessagePreviews": "Anteprima dei messaggi",
"previewOff": "Disattivato",
"previewOffDesc": "Nascondi la riga di anteprima sotto ogni oggetto.",
"previewOn": "Attivato",
"previewOnDesc": "Mostra la riga di anteprima sotto ogni oggetto.",
"mobileAvatarsMode": "Avatar del mittente (mobile)",
"mobileAvatarsOff": "Disattivato",
"mobileAvatarsOffDesc": "Nessun avatar: massimo spazio per mittente e oggetto.",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,11 @@
"hoverQuickActionsOffDesc": "Скрывать быстрые действия до использования контекстного меню или горячих клавиш.",
"hoverQuickActionsOn": "Вкл",
"hoverQuickActionsOnDesc": "Показывать значки быстрых действий в строках сообщений при наведении указателя.",
"showMessagePreviews": "Просмотр сообщений",
"previewOff": "Выкл.",
"previewOffDesc": "Скрыть строку предварительного просмотра под каждой темой.",
"previewOn": "Вкл.",
"previewOnDesc": "Показывать строку предварительного просмотра под каждой темой.",
"mobileAvatarsMode": "Аватары отправителя (моб.)",
"mobileAvatarsOff": "Выкл.",
"mobileAvatarsOffDesc": "Без аватара — максимум места для отправителя и темы.",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/locales/zhCN.json
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,11 @@
"hoverQuickActionsOffDesc": "隐藏快速操作,直到使用右键菜单或快捷键。",
"hoverQuickActionsOn": "开启",
"hoverQuickActionsOnDesc": "当指针悬停在消息行上时,显示快速操作图标。",
"showMessagePreviews": "消息预览",
"previewOff": "关闭",
"previewOffDesc": "隐藏每个主题下方的预览行。",
"previewOn": "开启",
"previewOnDesc": "显示每个主题下方的预览行。",
"mobileAvatarsMode": "发件人头像(移动端)",
"mobileAvatarsOff": "关闭",
"mobileAvatarsOffDesc": "不显示头像 — 为发件人和主题留出最大空间。",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,14 @@ export const useStore = create((set, get) => ({
schedulePrefSave({ hoverQuickActions: val });
},

// Show message preview snippets in the message list (on by default).
showMessagePreviews: localStorage.getItem('mailflow_show_message_previews') !== 'false',
setShowMessagePreviews: (val) => {
localStorage.setItem('mailflow_show_message_previews', String(val));
set({ showMessagePreviews: val });
schedulePrefSave({ showMessagePreviews: val });
},

// Show sender avatars in the mobile message list (off by default — they cost row width
// on a narrow screen; opt-in for users who prefer the scannability). Desktop always shows them.
showMobileAvatars: localStorage.getItem('mailflow_show_mobile_avatars') === 'true',
Expand Down