diff --git a/locales/rn/de.json b/locales/rn/de.json
index f810eca3..234ed8ae 100644
--- a/locales/rn/de.json
+++ b/locales/rn/de.json
@@ -1261,6 +1261,10 @@
"mobile_description": "Wählen Sie ein Farbdesign. Designs folgen der Hell/Dunkel-Einstellung unter Darstellung; eigene Designpakete werden im Webmail verwaltet."
},
"vacation": {
+ "status": {
+ "ended": "Beendet",
+ "scheduled": "Geplant"
+ },
"warnings": {
"date_format": "Datumsangaben müssen das Format \"YYYY-MM-DD\" oder \"YYYY-MM-DD HH:MM\" haben"
}
diff --git a/locales/rn/en.json b/locales/rn/en.json
index 8b53ba9e..730e7571 100644
--- a/locales/rn/en.json
+++ b/locales/rn/en.json
@@ -1626,6 +1626,10 @@
"description": "The message that will be sent as a reply",
"title": "Auto-Reply Message"
},
+ "status": {
+ "ended": "Ended",
+ "scheduled": "Scheduled"
+ },
"warnings": {
"date_format": "Dates must be \"YYYY-MM-DD\" or \"YYYY-MM-DD HH:MM\""
}
diff --git a/src/components/settings/VacationSettings.tsx b/src/components/settings/VacationSettings.tsx
index 09caed0c..1e96a348 100644
--- a/src/components/settings/VacationSettings.tsx
+++ b/src/components/settings/VacationSettings.tsx
@@ -29,6 +29,7 @@ import {
utcIsoToLocalInput,
} from '../../lib/vacation-dates';
import { STALWART_VACATION_LIMITS, vacationOversize } from '../../lib/vacation-limits';
+import { vacationStatus } from '../../lib/vacation-status';
import { jmapClient } from '../../api/jmap-client';
import { getSieveCapabilities } from '../../api/sieve';
@@ -139,6 +140,8 @@ export function VacationSettings() {
const toParsed = parseLocalInput(toDate);
const formatError = !isValidLocalInput(fromDate) || !isValidLocalInput(toDate);
const endBeforeStart = !!(fromParsed && toParsed && toParsed <= fromParsed);
+ // "Scheduled" while the start date is ahead, not "Active".
+ const status = vacationStatus(enabled, fromParsed, toParsed);
const htmlText = htmlEnabled ? htmlToPlainText(htmlBody).trim() : '';
const hasHtmlContent = htmlText.length > 0;
const emptyBody = enabled && !body.trim() && !hasHtmlContent;
@@ -235,6 +238,9 @@ export function VacationSettings() {
forwardChanged, forwardSettings, audienceChanged, audienceSettings,
]);
+ const saveLabel = store.isSaving
+ ? t('settings.vacation.saving', 'Saving...')
+ : t('settings.vacation.save', 'Save Changes');
const title = t('settings.vacation.title', 'Vacation Responder');
const description = t('settings.vacation.description', "Automatically reply to incoming emails while you're away");
@@ -284,11 +290,15 @@ export function VacationSettings() {
description={t('settings.vacation.status.description', 'Send an automatic reply to people who email you')}
>
-
-
- {enabled
+
+
+ {status === 'active'
? t('settings.vacation.status.active', 'Active')
- : t('settings.vacation.status.inactive', 'Inactive')}
+ : status === 'scheduled'
+ ? t('settings.vacation.status.scheduled', 'Scheduled')
+ : status === 'ended'
+ ? t('settings.vacation.status.ended', 'Ended')
+ : t('settings.vacation.status.inactive', 'Inactive')}
@@ -373,7 +383,7 @@ export function VacationSettings() {
placeholder={t('settings.vacation.message.subject_placeholder', 'Out of Office')}
placeholderTextColor={c.mutedForeground}
accessibilityLabel={t('settings.vacation.message.subject_label', 'Subject')}
- style={[styles.subjectInput, oversize.subject && styles.inputInvalid]}
+ style={[styles.subjectInput, !subject && styles.placeholderShown, oversize.subject && styles.inputInvalid]}
/>
{subjectTooLong && {subjectTooLong}}
@@ -386,7 +396,7 @@ export function VacationSettings() {
onChangeText={setBody}
placeholder={t('settings.vacation.message.body_placeholder', 'Thank you for your email. I am currently out of the office and will respond when I return.')}
placeholderTextColor={c.mutedForeground}
- style={[styles.bodyInput, oversize.body && styles.inputInvalid]}
+ style={[styles.bodyInput, !body && styles.placeholderShown, oversize.body && styles.inputInvalid]}
multiline
numberOfLines={6}
textAlignVertical="top"
@@ -528,10 +538,16 @@ export function VacationSettings() {
)}
-
@@ -597,6 +613,11 @@ function makeStyles(c: ThemePalette) {
pillText: { fontSize: 12 },
pillTextActive: { color: c.success },
pillTextInactive: { color: c.mutedForeground },
+ pillScheduled: { backgroundColor: c.primaryBg },
+ pillTextScheduled: { color: c.primary },
+ // An empty field's placeholder in italics: the sample reply read as a
+ // message already typed, next to the warning that the body is empty.
+ placeholderShown: { fontStyle: 'italic' },
dateInput: {
minWidth: 180,
paddingHorizontal: spacing.md,
diff --git a/src/lib/__tests__/vacation-status.test.ts b/src/lib/__tests__/vacation-status.test.ts
new file mode 100644
index 00000000..f736114c
--- /dev/null
+++ b/src/lib/__tests__/vacation-status.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'node:fs';
+import path from 'node:path';
+import { vacationStatus } from '../vacation-status';
+
+const NOW = new Date(2026, 10, 1, 12, 0);
+const day = (offset: number) => new Date(2026, 10, 1 + offset, 12, 0);
+
+describe('vacationStatus', () => {
+ it('is scheduled, not active, while the start date is ahead', () => {
+ expect(vacationStatus(true, day(3), day(10), NOW)).toBe('scheduled');
+ expect(vacationStatus(true, day(3), null, NOW)).toBe('scheduled');
+ });
+
+ it('is active inside the range or without dates', () => {
+ expect(vacationStatus(true, day(-1), day(2), NOW)).toBe('active');
+ expect(vacationStatus(true, null, null, NOW)).toBe('active');
+ expect(vacationStatus(true, null, day(1), NOW)).toBe('active');
+ });
+
+ it('has ended after the end date', () => {
+ expect(vacationStatus(true, day(-5), day(-1), NOW)).toBe('ended');
+ });
+
+ it('is inactive when switched off, whatever the dates', () => {
+ expect(vacationStatus(false, day(3), day(10), NOW)).toBe('inactive');
+ });
+});
+
+// Out of office in the phone walkthrough: the pill said "Active" for a range
+// that starts next week; the sample reply in the empty body looked typed in,
+// next to the warning that the body is empty; and after a save TalkBack read
+// the disabled Save as "busy".
+describe('the out-of-office pane', () => {
+ const source = fs.readFileSync(path.resolve(__dirname, '../../components/settings/VacationSettings.tsx'), 'utf8')
+ .split('\r\n').join('\n');
+
+ it('shows the status from the dates', () => {
+ expect(source).toContain('vacationStatus(enabled, fromParsed, toParsed)');
+ expect(source).toContain("t('settings.vacation.status.scheduled', 'Scheduled')");
+ });
+
+ it('sets an empty subject or body placeholder apart from typed text', () => {
+ expect(source).toContain('!body && styles.placeholderShown');
+ expect(source).toContain('!subject && styles.placeholderShown');
+ expect(source).toMatch(/placeholderShown: \{ fontStyle: 'italic' \}/);
+ });
+
+ it('names the Save button, so its old "busy" description is replaced', () => {
+ const save = source.slice(source.indexOf(' void handleSave()}'));
+ expect(save.slice(0, save.indexOf(''))).toContain('accessibilityLabel={saveLabel}');
+ });
+});
diff --git a/src/lib/vacation-status.ts b/src/lib/vacation-status.ts
new file mode 100644
index 00000000..557720a5
--- /dev/null
+++ b/src/lib/vacation-status.ts
@@ -0,0 +1,18 @@
+/**
+ * What the out-of-office status pill says. Switched on with a start date
+ * still ahead, the server sends no replies yet: the pill said "Active" all
+ * the same. It says "Scheduled" until the start and "Ended" after the end.
+ */
+export type VacationStatus = 'inactive' | 'scheduled' | 'active' | 'ended';
+
+export function vacationStatus(
+ enabled: boolean,
+ from: Date | null,
+ until: Date | null,
+ now: Date = new Date(),
+): VacationStatus {
+ if (!enabled) return 'inactive';
+ if (from && from.getTime() > now.getTime()) return 'scheduled';
+ if (until && until.getTime() <= now.getTime()) return 'ended';
+ return 'active';
+}