chore: bump version to 1.4.5
This commit is contained in:
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.4.5 (2026-03-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Calendar**: Add prev/next navigation buttons and date label to desktop calendar toolbar
|
||||||
|
- **Calendar**: Add pending event preview functionality to calendar views and event modal
|
||||||
|
- **Calendar**: Add setting to show event start time in month view
|
||||||
|
- **Contacts**: Implement pagination for fetching contacts with maxObjectsInGet capability
|
||||||
|
- **Email**: Add attachment position setting in email settings
|
||||||
|
- **Layout**: Add mobile visibility toggle for sidebar apps
|
||||||
|
- **Error**: Add NotFound component to handle 404 errors and redirect unauthenticated users
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- **Auth**: Enhance account switching logic and clear stores on account change
|
||||||
|
- **Auth**: Improve account restoration logic and handle stale accounts
|
||||||
|
- **Auth**: Improve draft handling in email composer and enhance session cookie verification
|
||||||
|
- **Calendar**: Expand recurring events in CalendarEvent/query so individual occurrences are returned (#65)
|
||||||
|
- **Calendar**: Validate event start field when fetching calendar events
|
||||||
|
- **Calendar**: Auto-scroll agenda view to today's events and include today's date in groups
|
||||||
|
- **Calendar**: Correct JSX syntax in CalendarToolbar component
|
||||||
|
- **Dependencies**: Update flatted to 3.4.2
|
||||||
|
- **DevOps**: Use native ARM runners instead of QEMU for Docker builds
|
||||||
|
- **DevOps**: Enhance health check with detailed memory diagnostics and stable liveness probe
|
||||||
|
|
||||||
## 1.4.4 (2026-03-19)
|
## 1.4.4 (2026-03-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+6
-24
@@ -70,12 +70,9 @@ export default function Home() {
|
|||||||
const [isLoadingConversation, setIsLoadingConversation] = useState(false);
|
const [isLoadingConversation, setIsLoadingConversation] = useState(false);
|
||||||
const [previewAttachment, setPreviewAttachment] = useState<{ blobId: string; name: string; type?: string } | null>(null);
|
const [previewAttachment, setPreviewAttachment] = useState<{ blobId: string; name: string; type?: string } | null>(null);
|
||||||
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading, connectionLost, activeAccountId } = useAuthStore();
|
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading, connectionLost } = useAuthStore();
|
||||||
const { identities } = useIdentityStore();
|
const { identities } = useIdentityStore();
|
||||||
|
|
||||||
// Track account switches to force data reload
|
|
||||||
const lastLoadedAccountRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
// Mobile/tablet responsive hooks
|
// Mobile/tablet responsive hooks
|
||||||
const { isMobile, isTablet } = useDeviceDetection();
|
const { isMobile, isTablet } = useDeviceDetection();
|
||||||
const { activeView, sidebarOpen, setSidebarOpen, setActiveView, tabletListVisible, setTabletListVisible, sidebarWidth, emailListWidth, setSidebarWidth, setEmailListWidth, persistColumnWidths, sidebarCollapsed, resetSidebarWidth, resetEmailListWidth } = useUIStore();
|
const { activeView, sidebarOpen, setSidebarOpen, setActiveView, tabletListVisible, setTabletListVisible, sidebarWidth, emailListWidth, setSidebarWidth, setEmailListWidth, persistColumnWidths, sidebarCollapsed, resetSidebarWidth, resetEmailListWidth } = useUIStore();
|
||||||
@@ -292,19 +289,9 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [initialCheckDone, isAuthenticated, authLoading, router]);
|
}, [initialCheckDone, isAuthenticated, authLoading, router]);
|
||||||
|
|
||||||
// Load mailboxes and emails when authenticated
|
// Load mailboxes and emails when authenticated (only if not already loaded)
|
||||||
// Re-fetches on initial load (mailboxes empty) or when account switches
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const accountChanged = lastLoadedAccountRef.current !== null && lastLoadedAccountRef.current !== activeAccountId;
|
if (isAuthenticated && client && mailboxes.length === 0) {
|
||||||
if (isAuthenticated && client && (mailboxes.length === 0 || accountChanged)) {
|
|
||||||
lastLoadedAccountRef.current = activeAccountId;
|
|
||||||
|
|
||||||
// Clear stale selected email from the previous account so the viewer
|
|
||||||
// doesn't flash old content while fresh data loads.
|
|
||||||
if (accountChanged) {
|
|
||||||
selectEmail(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
// First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes)
|
// First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes)
|
||||||
@@ -350,18 +337,15 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadData();
|
loadData();
|
||||||
} else if (isAuthenticated && client && lastLoadedAccountRef.current === null) {
|
|
||||||
// First render with existing data (e.g. restored from snapshot) — just record the account
|
|
||||||
lastLoadedAccountRef.current = activeAccountId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup push notifications on unmount or client change
|
// Cleanup push notifications on unmount
|
||||||
return () => {
|
return () => {
|
||||||
if (client) {
|
if (client) {
|
||||||
client.closePushNotifications();
|
client.closePushNotifications();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isAuthenticated, client, mailboxes.length, activeAccountId, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts, handleStateChange, setPushConnected, selectEmail]);
|
}, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts, handleStateChange, setPushConnected]);
|
||||||
|
|
||||||
// Auto-fetch full email content when an email is auto-selected (e.g. after delete/archive)
|
// Auto-fetch full email content when an email is auto-selected (e.g. after delete/archive)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -487,9 +471,7 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditDraft = (email?: Email) => {
|
const handleEditDraft = (email?: Email) => {
|
||||||
// Guard: when used directly as an onClick handler, the click event is passed
|
const draft = email || selectedEmail;
|
||||||
// as the first argument. Detect this and fall back to selectedEmail.
|
|
||||||
const draft = (email && 'mailboxIds' in email) ? email : selectedEmail;
|
|
||||||
if (!draft) return;
|
if (!draft) return;
|
||||||
const bodyText = draft.bodyValues
|
const bodyText = draft.bodyValues
|
||||||
? Object.values(draft.bodyValues).map(v => v.value).join('\n')
|
? Object.values(draft.bodyValues).map(v => v.value).join('\n')
|
||||||
|
|||||||
@@ -137,8 +137,7 @@
|
|||||||
"show_all": "All",
|
"show_all": "All",
|
||||||
"no_icons_found": "No icons found",
|
"no_icons_found": "No icons found",
|
||||||
"inline_badge": "Inline",
|
"inline_badge": "Inline",
|
||||||
"tab_badge": "Tab",
|
"tab_badge": "Tab"
|
||||||
"show_on_mobile": "Show on Mobile"
|
|
||||||
},
|
},
|
||||||
"email_list": {
|
"email_list": {
|
||||||
"no_emails": "No messages found",
|
"no_emails": "No messages found",
|
||||||
@@ -1827,9 +1826,7 @@
|
|||||||
"notification_sound": "Notification sound",
|
"notification_sound": "Notification sound",
|
||||||
"notification_sound_desc": "Play a sound for calendar alerts",
|
"notification_sound_desc": "Play a sound for calendar alerts",
|
||||||
"invitation_parsing": "Parse email invitations",
|
"invitation_parsing": "Parse email invitations",
|
||||||
"invitation_parsing_desc": "Detect calendar invitations in email attachments and show calendar actions",
|
"invitation_parsing_desc": "Detect calendar invitations in email attachments and show calendar actions"
|
||||||
"show_time_in_month_view": "Show start time in month view",
|
|
||||||
"show_time_in_month_view_desc": "Display the event start time alongside the title in month view"
|
|
||||||
},
|
},
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "Monday",
|
"monday": "Monday",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bulwark-webmail",
|
"name": "bulwark-webmail",
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server",
|
"description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server",
|
||||||
"author": "Bulwark Webmail <bulwark@rbm.systems>",
|
"author": "Bulwark Webmail <bulwark@rbm.systems>",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export interface SidebarApp {
|
|||||||
url: string;
|
url: string;
|
||||||
icon: string; // Lucide icon name (e.g. 'Globe', 'Rss')
|
icon: string; // Lucide icon name (e.g. 'Globe', 'Rss')
|
||||||
openMode: 'tab' | 'inline'; // Open in new tab or embed inline
|
openMode: 'tab' | 'inline'; // Open in new tab or embed inline
|
||||||
showOnMobile?: boolean; // Show in mobile bottom nav (default false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available color palette for keywords
|
// Available color palette for keywords
|
||||||
@@ -107,9 +106,6 @@ interface SettingsState {
|
|||||||
sessionTimeout: number; // minutes (0 = never)
|
sessionTimeout: number; // minutes (0 = never)
|
||||||
trustedSenders: string[]; // Email addresses that can load external content
|
trustedSenders: string[]; // Email addresses that can load external content
|
||||||
|
|
||||||
// Calendar
|
|
||||||
showTimeInMonthView: boolean;
|
|
||||||
|
|
||||||
// Calendar Notifications
|
// Calendar Notifications
|
||||||
calendarNotificationsEnabled: boolean;
|
calendarNotificationsEnabled: boolean;
|
||||||
calendarNotificationSound: boolean;
|
calendarNotificationSound: boolean;
|
||||||
@@ -205,9 +201,6 @@ const DEFAULT_SETTINGS = {
|
|||||||
sessionTimeout: 0, // Never
|
sessionTimeout: 0, // Never
|
||||||
trustedSenders: [] as string[],
|
trustedSenders: [] as string[],
|
||||||
|
|
||||||
// Calendar
|
|
||||||
showTimeInMonthView: false,
|
|
||||||
|
|
||||||
// Calendar Notifications
|
// Calendar Notifications
|
||||||
calendarNotificationsEnabled: true,
|
calendarNotificationsEnabled: true,
|
||||||
calendarNotificationSound: true,
|
calendarNotificationSound: true,
|
||||||
@@ -288,7 +281,6 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
sendConfirmation: state.sendConfirmation,
|
sendConfirmation: state.sendConfirmation,
|
||||||
defaultReplyMode: state.defaultReplyMode,
|
defaultReplyMode: state.defaultReplyMode,
|
||||||
sessionTimeout: state.sessionTimeout,
|
sessionTimeout: state.sessionTimeout,
|
||||||
showTimeInMonthView: state.showTimeInMonthView,
|
|
||||||
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
||||||
calendarNotificationSound: state.calendarNotificationSound,
|
calendarNotificationSound: state.calendarNotificationSound,
|
||||||
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user