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
16 changes: 16 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3012,6 +3012,22 @@
"messageformat": "System",
"description": "Label text for system theme"
},
"icu:Preferences--time-format": {
"messageformat": "Time format",
"description": "Header for time format settings"
},
"icu:timeFormatSystem": {
"messageformat": "System",
"description": "Label text for system locale based time format"
},
"icu:timeFormat12Hour": {
"messageformat": "12-hour",
"description": "Label text for 12-hour time format"
},
"icu:timeFormat24Hour": {
"messageformat": "24-hour",
"description": "Label text for 24-hour time format"
},
"icu:noteToSelf": {
"messageformat": "Note to Self",
"description": "Name for the conversation with your own phone number"
Expand Down
41 changes: 34 additions & 7 deletions app/main.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,21 @@ async function getResolvedThemeSetting(
return ThemeType[theme];
}

type HourCycleSettingType = 'system' | '12' | '24';

async function getHourCycleSetting(): Promise<HourCycleSettingType> {
const value = ephemeralConfig.get('hour-cycle-preference');
if (value === '12' || value === '24' || value === 'system') {
log.info('got fast hour-cycle-preference value', value);
return value;
}

// Default to 'system' if setting doesn't exist or is invalid
ephemeralConfig.set('hour-cycle-preference', 'system');
log.info('initializing hour-cycle-preference setting', 'system');
return 'system';
}

type GetBackgroundColorOptionsType = GetThemeSettingOptionsType &
Readonly<{
signalColors?: boolean;
Expand Down Expand Up @@ -473,16 +488,28 @@ function getResolvedMessagesLocale(): LocaleType {
return resolvedTranslationsLocale;
}

function getHourCyclePreference(): HourCyclePreference {
if (process.platform !== 'darwin') {
return HourCyclePreference.UnknownPreference;
async function getHourCyclePreference(): Promise<HourCyclePreference> {
const userSetting = await getHourCycleSetting();
if (userSetting === '12') {
return HourCyclePreference.Prefer12;
}
if (systemPreferences.getUserDefault('AppleICUForce24HourTime', 'boolean')) {
if (userSetting === '24') {
return HourCyclePreference.Prefer24;
}
if (systemPreferences.getUserDefault('AppleICUForce12HourTime', 'boolean')) {
return HourCyclePreference.Prefer12;

if (OS.isMacOS()) {
if (
systemPreferences.getUserDefault('AppleICUForce24HourTime', 'boolean')
) {
return HourCyclePreference.Prefer24;
}
if (
systemPreferences.getUserDefault('AppleICUForce12HourTime', 'boolean')
) {
return HourCyclePreference.Prefer12;
}
}

return HourCyclePreference.UnknownPreference;
}

Expand Down Expand Up @@ -2082,7 +2109,7 @@ app.on('ready', async () => {

localeOverride = await getLocaleOverrideSetting();

const hourCyclePreference = getHourCyclePreference();
const hourCyclePreference = await getHourCyclePreference();
log.info(`app.ready: hour cycle preference: ${hourCyclePreference}`);

log.info('app.ready: preferred system locales:', preferredSystemLocales);
Expand Down
12 changes: 12 additions & 0 deletions ts/components/Preferences.dom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ export default {
sentMediaQualitySetting: 'standard',
shouldShowUpdateDialog: false,
themeSetting: 'system',
hourCyclePreference: 'system',
theme: ThemeType.light,
universalExpireTimer: DurationInSeconds.HOUR,
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
Expand Down Expand Up @@ -591,6 +592,7 @@ export default {
onStartUpdate: action('onStartUpdate'),
onTextFormattingChange: action('onTextFormattingChange'),
onThemeChange: action('onThemeChange'),
onHourCycleChange: action('onHourCycleChange'),
onToggleNavTabsCollapse: action('onToggleNavTabsCollapse'),
onUniversalExpireTimerChange: action('onUniversalExpireTimerChange'),
onWhoCanSeeMeChange: action('onWhoCanSeeMeChange'),
Expand Down Expand Up @@ -666,6 +668,16 @@ export const Appearance = Template.bind({});
Appearance.args = {
settingsLocation: { page: SettingsPage.Appearance },
};
export const Appearance12HourFormat = Template.bind({});
Appearance12HourFormat.args = {
settingsLocation: { page: SettingsPage.Appearance },
hourCyclePreference: '12',
};
export const Appearance24HourFormat = Template.bind({});
Appearance24HourFormat.args = {
settingsLocation: { page: SettingsPage.Appearance },
hourCyclePreference: '24',
};
export const Chats = Template.bind({});
Chats.args = {
settingsLocation: { page: SettingsPage.Chats },
Expand Down
36 changes: 36 additions & 0 deletions ts/components/Preferences.dom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import type {
SentMediaQualityType,
ThemeType,
} from '../types/Util.std.js';
import type { HourCycleSettingType } from '../util/preload.preload.js';
import type {
BackupMediaDownloadStatusType,
BackupsSubscriptionType,
Expand Down Expand Up @@ -162,6 +163,7 @@ export type PropsDataType = {
selectedSpeaker?: AudioDevice;
sentMediaQualitySetting: SentMediaQualitySettingType;
themeSetting: ThemeSettingType | undefined;
hourCyclePreference: HourCycleSettingType | undefined;
universalExpireTimer: DurationInSeconds;
whoCanFindMe: PhoneNumberDiscoverability;
whoCanSeeMe: PhoneNumberSharingMode;
Expand Down Expand Up @@ -320,6 +322,7 @@ type PropsFunctionType = {
onSpellCheckChange: CheckboxChangeHandlerType;
onTextFormattingChange: CheckboxChangeHandlerType;
onThemeChange: SelectChangeHandlerType<ThemeType>;
onHourCycleChange: SelectChangeHandlerType<HourCycleSettingType>;
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
onUniversalExpireTimerChange: SelectChangeHandlerType<number>;
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
Expand Down Expand Up @@ -486,6 +489,7 @@ export function Preferences({
onSpellCheckChange,
onTextFormattingChange,
onThemeChange,
onHourCycleChange,
onToggleNavTabsCollapse,
onUniversalExpireTimerChange,
onWhoCanSeeMeChange,
Expand Down Expand Up @@ -526,6 +530,7 @@ export function Preferences({
localeOverride,
theme,
themeSetting,
hourCyclePreference,
universalExpireTimer = DurationInSeconds.ZERO,
validateBackup,
whoCanFindMe,
Expand All @@ -539,6 +544,7 @@ export function Preferences({
}: PropsType): JSX.Element {
const storiesId = useId();
const themeSelectId = useId();
const hourCycleSelectId = useId();
const zoomSelectId = useId();
const languageId = useId();

Expand Down Expand Up @@ -1035,6 +1041,36 @@ export function Preferences({
{i18n('icu:Preferences__LanguageModal__Restart__Description')}
</ConfirmationDialog>
)}
<Control
icon
left={
<label htmlFor={hourCycleSelectId}>
{i18n('icu:Preferences--time-format')}
</label>
}
right={
<Select
id={hourCycleSelectId}
disabled={hourCyclePreference === undefined}
onChange={onHourCycleChange}
options={[
{
text: i18n('icu:timeFormatSystem'),
value: 'system',
},
{
text: i18n('icu:timeFormat12Hour'),
value: '12',
},
{
text: i18n('icu:timeFormat24Hour'),
value: '24',
},
]}
value={hourCyclePreference}
/>
}
/>
<Control
icon
left={
Expand Down
1 change: 1 addition & 0 deletions ts/main/settingsChannel.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class SettingsChannel extends EventEmitter {
this.#installEphemeralSetting('localeOverride');
this.#installEphemeralSetting('spellCheck');
this.#installEphemeralSetting('contentProtection');
this.#installEphemeralSetting('hourCyclePreference');

installPermissionsHandler({ session: session.defaultSession, userConfig });

Expand Down
18 changes: 18 additions & 0 deletions ts/state/smart/Preferences.preload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import OS from '../../util/os/osPreload.preload.js';
import { themeChanged } from '../../shims/themeChanged.dom.js';
import * as Settings from '../../types/Settings.std.js';
import * as universalExpireTimerUtil from '../../util/universalExpireTimer.preload.js';
import type { HourCycleSettingType } from '../../util/preload.preload.js';
import {
parseSystemTraySetting,
shouldMinimizeToSystemTray,
Expand Down Expand Up @@ -393,6 +394,8 @@ export function SmartPreferences(): JSX.Element | null {
React.useState<boolean>();
const [hasSpellCheck, setSpellCheck] = React.useState<boolean>();
const [themeSetting, setThemeSetting] = React.useState<ThemeType>();
const [hourCyclePreference, setHourCyclePreference] =
React.useState<HourCycleSettingType>();

useEffect(() => {
let canceled = false;
Expand Down Expand Up @@ -439,6 +442,15 @@ export function SmartPreferences(): JSX.Element | null {
};
drop(loadThemeSetting());

const loadHourCyclePreference = async () => {
const value = await window.Events.getHourCyclePreference();
if (canceled) {
return;
}
setHourCyclePreference(value);
};
drop(loadHourCyclePreference());

return () => {
canceled = true;
};
Expand Down Expand Up @@ -479,6 +491,10 @@ export function SmartPreferences(): JSX.Element | null {
drop(window.Events.setThemeSetting(value));
drop(themeChanged());
};
const onHourCycleChange = (value: HourCycleSettingType) => {
setHourCyclePreference(value);
drop(window.Events.setHourCyclePreference(value));
};

// Async IPC for electron configuration, all can be modified

Expand Down Expand Up @@ -907,6 +923,7 @@ export function SmartPreferences(): JSX.Element | null {
onSpellCheckChange={onSpellCheckChange}
onTextFormattingChange={onTextFormattingChange}
onThemeChange={onThemeChange}
onHourCycleChange={onHourCycleChange}
onToggleNavTabsCollapse={toggleNavTabsCollapse}
onUniversalExpireTimerChange={onUniversalExpireTimerChange}
onWhoCanFindMeChange={onWhoCanFindMeChange}
Expand Down Expand Up @@ -952,6 +969,7 @@ export function SmartPreferences(): JSX.Element | null {
startPlaintextExport={startPlaintextExport}
theme={theme}
themeSetting={themeSetting}
hourCyclePreference={hourCyclePreference}
universalExpireTimer={universalExpireTimer}
validateBackup={validateBackup}
whoCanFindMe={whoCanFindMe}
Expand Down
29 changes: 29 additions & 0 deletions ts/test-node/util/hourCyclePreference_test.std.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { assert } from 'chai';
import { HourCyclePreference } from '../../types/I18N.std.js';

describe('HourCyclePreference', () => {
describe('enum values', () => {
it('should have Prefer24 value', () => {
assert.equal(HourCyclePreference.Prefer24, 'Prefer24');
});

it('should have Prefer12 value', () => {
assert.equal(HourCyclePreference.Prefer12, 'Prefer12');
});

it('should have UnknownPreference value', () => {
assert.equal(HourCyclePreference.UnknownPreference, 'UnknownPreference');
});

it('should only contain three values', () => {
const values = Object.values(HourCyclePreference);
assert.lengthOf(values, 3);
assert.include(values, HourCyclePreference.Prefer24);
assert.include(values, HourCyclePreference.Prefer12);
assert.include(values, HourCyclePreference.UnknownPreference);
});
});
});
24 changes: 24 additions & 0 deletions ts/test-node/util/hourCycleSetting_test.std.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { assert } from 'chai';
import type { HourCycleSettingType } from '../../util/preload.preload.js';

describe('HourCycleSettingType', () => {
describe('valid setting values', () => {
it('should accept "system" as a valid setting', () => {
const setting: HourCycleSettingType = 'system';
assert.equal(setting, 'system');
});

it('should accept "12" as a valid setting', () => {
const setting: HourCycleSettingType = '12';
assert.equal(setting, '12');
});

it('should accept "24" as a valid setting', () => {
const setting: HourCycleSettingType = '24';
assert.equal(setting, '24');
});
});
});
10 changes: 10 additions & 0 deletions ts/util/createIPCEvents.preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { fromWebSafeBase64 } from './webSafeBase64.std.js';
import { showConfirmationDialog } from './showConfirmationDialog.dom.js';
import type {
EphemeralSettings,
HourCycleSettingType,
SettingsValuesType,
ThemeType,
} from './preload.preload.js';
Expand Down Expand Up @@ -92,6 +93,7 @@ type ValuesWithGetters = Omit<
| 'contentProtection'
| 'systemTraySetting'
| 'themeSetting'
| 'hourCyclePreference'
| 'zoomFactor'
>;

Expand Down Expand Up @@ -126,6 +128,7 @@ export type IPCEventsGettersType = {
getContentProtection: () => Promise<boolean>;
getSystemTraySetting: () => Promise<SystemTraySetting>;
getThemeSetting: () => Promise<ThemeType>;
getHourCyclePreference: () => Promise<HourCycleSettingType>;
getZoomFactor: () => Promise<ZoomFactorType>;
// Events
onZoomFactorChange: (callback: ZoomFactorChangeCallback) => void;
Expand Down Expand Up @@ -236,6 +239,13 @@ export function createIPCEvents(
setThemeSetting: async (value: ThemeType) => {
await setEphemeralSetting('themeSetting', value);
},
getHourCyclePreference: async () => {
return (await getEphemeralSetting('hourCyclePreference')) ?? 'system';
},
setHourCyclePreference: async (value: HourCycleSettingType) => {
await setEphemeralSetting('hourCyclePreference', value);
window.SignalContext.restartApp();
},

// From IPCEventsCallbacksType
addDarkOverlay: () => {
Expand Down
3 changes: 3 additions & 0 deletions ts/util/preload.preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ export type SettingType<Value> = Readonly<{

export type ThemeType = 'light' | 'dark' | 'system';

export type HourCycleSettingType = 'system' | '12' | '24';

export type EphemeralSettings = {
localeOverride: string | null;
spellCheck: boolean;
contentProtection: boolean;
systemTraySetting: SystemTraySetting;
themeSetting: ThemeType;
hourCyclePreference: HourCycleSettingType;
};

export type SettingsValuesType = IPCEventsValuesType & EphemeralSettings;
Expand Down
1 change: 1 addition & 0 deletions ts/windows/preload.preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ installEphemeralSetting('localeOverride');
installEphemeralSetting('spellCheck');
installEphemeralSetting('systemTraySetting');
installEphemeralSetting('themeSetting');
installEphemeralSetting('hourCyclePreference');