Brand Refresh
This commit is contained in:
parent
b88100d32a
commit
b97e67121f
181 changed files with 828 additions and 131 deletions
36
ts/components/About.stories.tsx
Normal file
36
ts/components/About.stories.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import type { AboutProps } from './About';
|
||||
import { About } from './About';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/About',
|
||||
component: About,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
closeAbout: action('showWhatsNewModal'),
|
||||
appEnv: 'production',
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
version: '1.2.3',
|
||||
},
|
||||
} satisfies ComponentMeta<AboutProps>;
|
||||
|
||||
export function Basic(args: AboutProps): JSX.Element {
|
||||
return (
|
||||
<div style={{ height: '100vh' }}>
|
||||
<About {...args} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -6,28 +6,46 @@ import React from 'react';
|
|||
import type { LocalizerType } from '../types/Util';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
|
||||
export type PropsType = {
|
||||
export type AboutProps = Readonly<{
|
||||
closeAbout: () => unknown;
|
||||
environment: string;
|
||||
appEnv: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
i18n: LocalizerType;
|
||||
version: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export function About({
|
||||
closeAbout,
|
||||
environment,
|
||||
appEnv,
|
||||
arch,
|
||||
platform,
|
||||
i18n,
|
||||
version,
|
||||
}: PropsType): JSX.Element {
|
||||
}: AboutProps): JSX.Element {
|
||||
useEscapeHandling(closeAbout);
|
||||
|
||||
let env: string;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
if (arch === 'arm64') {
|
||||
env = i18n('icu:About__AppEnvironment--AppleSilicon', { appEnv });
|
||||
} else {
|
||||
env = i18n('icu:About__AppEnvironment--AppleIntel', { appEnv });
|
||||
}
|
||||
} else {
|
||||
env = i18n('icu:About__AppEnvironment', { appEnv });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="About">
|
||||
<div className="module-splash-screen">
|
||||
<div className="module-splash-screen__logo module-img--150" />
|
||||
<div className="module-splash-screen__logo module-splash-screen__logo--128" />
|
||||
|
||||
<h1 className="About__Title">{i18n('icu:signalDesktop')}</h1>
|
||||
<div className="version">{version}</div>
|
||||
<div className="environment">{environment}</div>
|
||||
<div className="environment">{env}</div>
|
||||
<br />
|
||||
<div>
|
||||
<a href="https://signal.org">signal.org</a>
|
||||
</div>
|
||||
|
|
46
ts/components/ChatsTab.stories.tsx
Normal file
46
ts/components/ChatsTab.stories.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import type { ChatsTabProps } from './ChatsTab';
|
||||
import { ChatsTab } from './ChatsTab';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/ChatsTab',
|
||||
component: ChatsTab,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
otherTabsUnreadStats: {
|
||||
unreadCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
markedUnread: false,
|
||||
},
|
||||
isStaging: false,
|
||||
hasPendingUpdate: false,
|
||||
hasFailedStorySends: false,
|
||||
navTabsCollapsed: false,
|
||||
onToggleNavTabsCollapse: action('onToggleNavTabsCollapse'),
|
||||
renderConversationView: () => <>{null}</>,
|
||||
renderLeftPane: () => <>{null}</>,
|
||||
renderMiniPlayer: () => <>{null}</>,
|
||||
selectedConversationId: undefined,
|
||||
showWhatsNewModal: action('showWhatsNewModal'),
|
||||
},
|
||||
} satisfies ComponentMeta<ChatsTabProps>;
|
||||
|
||||
export function Basic(args: ChatsTabProps): JSX.Element {
|
||||
return (
|
||||
<div style={{ height: '100vh' }}>
|
||||
<ChatsTab {...args} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -7,7 +7,7 @@ import type { NavTabPanelProps } from './NavTabs';
|
|||
import { WhatsNewLink } from './WhatsNewLink';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
|
||||
type ChatsTabProps = Readonly<{
|
||||
export type ChatsTabProps = Readonly<{
|
||||
otherTabsUnreadStats: UnreadStats;
|
||||
i18n: LocalizerType;
|
||||
isStaging: boolean;
|
||||
|
@ -59,7 +59,7 @@ export function ChatsTab({
|
|||
) : (
|
||||
<div className="Inbox__no-conversation-open">
|
||||
{renderMiniPlayer({ shouldFlow: false })}
|
||||
<div className="module-splash-screen__logo module-img--80 module-logo-blue" />
|
||||
<div className="module-splash-screen__logo module-splash-screen__logo--96" />
|
||||
<h3 className="Inbox__welcome">
|
||||
{isStaging
|
||||
? 'THIS IS A STAGING DESKTOP'
|
||||
|
|
|
@ -150,7 +150,9 @@ export function Inbox({
|
|||
}
|
||||
logo = <div className="Inbox__logo">{parts}</div>;
|
||||
} else {
|
||||
logo = <div className="module-splash-screen__logo module-img--150" />;
|
||||
logo = (
|
||||
<div className="module-splash-screen__logo module-splash-screen__logo--128" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
import type { NativeThemeState } from '../types/NativeThemeNotifier.d';
|
||||
import { SystemThemeType } from '../types/Util';
|
||||
|
||||
export type Callback = (change: NativeThemeState) => void;
|
||||
|
||||
|
@ -19,9 +20,6 @@ export interface MinimalIPC {
|
|||
listener: (event: unknown, ...args: ReadonlyArray<any>) => void
|
||||
): this;
|
||||
}
|
||||
|
||||
type SystemThemeType = 'dark' | 'light';
|
||||
|
||||
export type SystemThemeHolder = { systemTheme: SystemThemeType };
|
||||
|
||||
export type NativeThemeType = {
|
||||
|
@ -41,7 +39,9 @@ export function createNativeThemeListener(
|
|||
let systemTheme: SystemThemeType;
|
||||
|
||||
function update(): SystemThemeType {
|
||||
const nextSystemTheme = theme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
const nextSystemTheme = theme.shouldUseDarkColors
|
||||
? SystemThemeType.dark
|
||||
: SystemThemeType.light;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
holder.systemTheme = nextSystemTheme;
|
||||
return nextSystemTheme;
|
||||
|
|
297
ts/scripts/generate-tray-icons.ts
Normal file
297
ts/scripts/generate-tray-icons.ts
Normal file
|
@ -0,0 +1,297 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createCanvas, GlobalFonts, loadImage } from '@napi-rs/canvas';
|
||||
import { join } from 'node:path';
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { SystemThemeType } from '../types/Util';
|
||||
|
||||
const cwd = __dirname;
|
||||
const fontsDir = join(cwd, '..', '..', 'fonts');
|
||||
const imagesDir = join(cwd, '..', '..', 'images');
|
||||
const trayIconsDir = join(imagesDir, 'tray-icons');
|
||||
const trayIconsBaseDir = join(trayIconsDir, 'base');
|
||||
const trayIconsAlertsDir = join(trayIconsDir, 'alert');
|
||||
|
||||
enum TrayIconSize {
|
||||
Size16 = '16',
|
||||
Size32 = '32',
|
||||
Size48 = '48',
|
||||
Size256 = '256',
|
||||
}
|
||||
|
||||
type TrayIconValue = number | string | null;
|
||||
|
||||
type TrayIconImageRequest = Readonly<{
|
||||
size: TrayIconSize;
|
||||
theme: SystemThemeType;
|
||||
value: TrayIconValue;
|
||||
}>;
|
||||
|
||||
type TrayIconVariant = {
|
||||
size: number;
|
||||
maxCount: number;
|
||||
badgePadding: number;
|
||||
fontSize: number;
|
||||
fontWeight: string;
|
||||
fontOffsetY: number;
|
||||
badgeShadowBlur: number;
|
||||
badgeShadowOffsetY: number;
|
||||
images: Record<SystemThemeType, string>;
|
||||
};
|
||||
|
||||
GlobalFonts.loadFontsFromDir(fontsDir);
|
||||
|
||||
const Inter = GlobalFonts.families.find(family => {
|
||||
return family.family === 'Inter';
|
||||
});
|
||||
|
||||
strictAssert(Inter != null, `Failed to load fonts from ${fontsDir}`);
|
||||
|
||||
const Constants = {
|
||||
fontFamily: 'Inter',
|
||||
badgeColor: 'rgb(244, 67, 54)',
|
||||
badgeShadowColor: 'rgba(0, 0, 0, 0.25)',
|
||||
};
|
||||
|
||||
const Variants: Record<TrayIconSize, TrayIconVariant> = {
|
||||
[TrayIconSize.Size16]: {
|
||||
size: 16,
|
||||
maxCount: 9,
|
||||
badgePadding: 2,
|
||||
fontSize: 8,
|
||||
fontWeight: '500',
|
||||
fontOffsetY: 0,
|
||||
badgeShadowBlur: 0,
|
||||
badgeShadowOffsetY: 0,
|
||||
images: {
|
||||
light: join(trayIconsBaseDir, 'signal-tray-icon-16x16-light-base.png'),
|
||||
dark: join(trayIconsBaseDir, 'signal-tray-icon-16x16-dark-base.png'),
|
||||
},
|
||||
},
|
||||
[TrayIconSize.Size32]: {
|
||||
size: 32,
|
||||
maxCount: 9,
|
||||
badgePadding: 4,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
fontOffsetY: 0,
|
||||
badgeShadowBlur: 1,
|
||||
badgeShadowOffsetY: 1,
|
||||
images: {
|
||||
light: join(trayIconsBaseDir, 'signal-tray-icon-32x32-light-base.png'),
|
||||
dark: join(trayIconsBaseDir, 'signal-tray-icon-32x32-dark-base.png'),
|
||||
},
|
||||
},
|
||||
[TrayIconSize.Size48]: {
|
||||
size: 48,
|
||||
maxCount: 9,
|
||||
badgePadding: 6,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
fontOffsetY: -1,
|
||||
badgeShadowBlur: 1,
|
||||
badgeShadowOffsetY: 1,
|
||||
images: {
|
||||
light: join(trayIconsBaseDir, 'signal-tray-icon-48x48-light-base.png'),
|
||||
dark: join(trayIconsBaseDir, 'signal-tray-icon-48x48-dark-base.png'),
|
||||
},
|
||||
},
|
||||
[TrayIconSize.Size256]: {
|
||||
size: 256,
|
||||
maxCount: 9,
|
||||
fontSize: 72,
|
||||
fontWeight: '600',
|
||||
fontOffsetY: 0,
|
||||
badgePadding: 32,
|
||||
badgeShadowBlur: 8,
|
||||
badgeShadowOffsetY: 8,
|
||||
images: {
|
||||
light: join(trayIconsBaseDir, 'signal-tray-icon-256x256-light-base.png'),
|
||||
dark: join(trayIconsBaseDir, 'signal-tray-icon-256x256-dark-base.png'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function trayIconValueToText(
|
||||
value: TrayIconValue,
|
||||
variant: TrayIconVariant
|
||||
): string {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new RangeError(`Unread count must be positive integer ${value}`);
|
||||
}
|
||||
|
||||
if (value === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (value > variant.maxCount) {
|
||||
return `${variant.maxCount}+`;
|
||||
}
|
||||
|
||||
return `${value}`;
|
||||
}
|
||||
throw new TypeError(`Invalid value ${value}`);
|
||||
}
|
||||
|
||||
async function generateTrayIconImage(
|
||||
request: TrayIconImageRequest
|
||||
): Promise<Buffer> {
|
||||
const variant = Variants[request.size];
|
||||
if (variant == null) {
|
||||
throw new TypeError(`Invalid variant size (${request.size})`);
|
||||
}
|
||||
|
||||
const imagePath = variant.images[request.theme];
|
||||
if (imagePath == null) {
|
||||
throw new TypeError(`Invalid theme (theme: ${request.theme})`);
|
||||
}
|
||||
|
||||
const text = trayIconValueToText(request.value, variant);
|
||||
|
||||
const image = await loadImage(imagePath);
|
||||
const canvas = createCanvas(variant.size, variant.size);
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (context == null) {
|
||||
throw new Error('Failed to create 2d canvas context');
|
||||
}
|
||||
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.imageSmoothingQuality = 'high';
|
||||
context.drawImage(image, 0, 0, variant.size, variant.size);
|
||||
|
||||
if (text !== '') {
|
||||
// Decrements by 1 until the badge fits within the canvas.
|
||||
let currentFontSize = variant.fontSize;
|
||||
|
||||
while (currentFontSize > 4) {
|
||||
const font = `${variant.fontWeight} ${currentFontSize}px ${Constants.fontFamily}`;
|
||||
|
||||
context.font = font;
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
// @ts-expect-error Missing types
|
||||
context.textRendering = 'optimizeLegibility';
|
||||
context.fontKerning = 'normal';
|
||||
|
||||
// All font settings should be set before now and should not change.
|
||||
const capMetrics = context.measureText('X');
|
||||
const textMetrics = context.measureText(text);
|
||||
const textWidth = Math.ceil(
|
||||
textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft
|
||||
);
|
||||
const textHeight = Math.ceil(
|
||||
capMetrics.actualBoundingBoxAscent + capMetrics.actualBoundingBoxDescent
|
||||
);
|
||||
|
||||
const boxHeight = textHeight + variant.badgePadding * 2;
|
||||
const boxWidth = Math.max(
|
||||
boxHeight, // Ensures the badge is a circle
|
||||
textWidth + variant.badgePadding * 2
|
||||
);
|
||||
|
||||
// Needed to avoid cutting off the shadow blur
|
||||
const boxMargin = variant.badgeShadowBlur;
|
||||
const boxWidthWithMargins = boxWidth + boxMargin * 2;
|
||||
|
||||
if (boxWidthWithMargins > variant.size) {
|
||||
currentFontSize -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const boxX = variant.size - boxWidth - boxMargin; // right aligned
|
||||
const boxY = boxMargin;
|
||||
const boxMidX = boxX + boxWidth / 2;
|
||||
const boxMidY = boxY + boxHeight / 2;
|
||||
const boxRadius = Math.ceil(boxHeight / 2);
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.roundRect(boxX, boxY, boxWidth, boxHeight, boxRadius);
|
||||
context.fillStyle = Constants.badgeColor;
|
||||
if (variant.badgeShadowBlur !== 0 || variant.badgeShadowOffsetY !== 0) {
|
||||
context.shadowBlur = variant.badgeShadowBlur;
|
||||
context.shadowOffsetX = 0;
|
||||
context.shadowOffsetY = variant.badgeShadowOffsetY;
|
||||
context.shadowColor = Constants.badgeShadowColor;
|
||||
}
|
||||
context.fill();
|
||||
context.restore();
|
||||
|
||||
context.fillStyle = 'white';
|
||||
context.fillText(text, boxMidX, boxMidY + variant.fontOffsetY);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentFontSize <= 4) {
|
||||
throw new Error(
|
||||
`Badge text is too large for canvas size ${variant.size} (${text})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return canvas.toBuffer('image/png');
|
||||
}
|
||||
|
||||
function range(start: number, end: number): Array<number> {
|
||||
const length = end - start + 1;
|
||||
return Array.from({ length }, (_, index) => start + index);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await rm(trayIconsAlertsDir, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const requests: Array<TrayIconImageRequest> = [];
|
||||
for (const size of Object.values(TrayIconSize)) {
|
||||
const variant = Variants[size];
|
||||
const { maxCount } = variant;
|
||||
const values = range(1, maxCount + 1);
|
||||
for (const theme of Object.values(SystemThemeType)) {
|
||||
for (const value of values) {
|
||||
requests.push({ size, theme, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
requests.map(async ({ size, theme, value }) => {
|
||||
const variant = Variants[size];
|
||||
const text = trayIconValueToText(value, variant);
|
||||
|
||||
const fileDir = join(trayIconsAlertsDir);
|
||||
const fileName = `signal-tray-icon-${size}x${size}-${theme}-alert-${text}.png`;
|
||||
const filePath = join(fileDir, fileName);
|
||||
|
||||
const fileContents = await generateTrayIconImage({ size, theme, value });
|
||||
|
||||
console.log(`Writing "${fileName}"`);
|
||||
await mkdir(fileDir, { recursive: true });
|
||||
await writeFile(filePath, fileContents);
|
||||
})
|
||||
);
|
||||
|
||||
console.log('Done');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
|
@ -10,6 +10,7 @@ import type {
|
|||
} from '../../context/createNativeThemeListener';
|
||||
import { createNativeThemeListener } from '../../context/createNativeThemeListener';
|
||||
import type { NativeThemeState } from '../../types/NativeThemeNotifier.d';
|
||||
import { SystemThemeType } from '../../types/Util';
|
||||
|
||||
class FakeIPC extends EventEmitter implements MinimalIPC {
|
||||
constructor(private readonly state: NativeThemeState) {
|
||||
|
@ -29,7 +30,7 @@ class FakeIPC extends EventEmitter implements MinimalIPC {
|
|||
}
|
||||
|
||||
describe('NativeThemeListener', () => {
|
||||
const holder: SystemThemeHolder = { systemTheme: 'dark' };
|
||||
const holder: SystemThemeHolder = { systemTheme: SystemThemeType.dark };
|
||||
|
||||
it('syncs the initial native theme', () => {
|
||||
const dark = createNativeThemeListener(
|
||||
|
|
|
@ -5,7 +5,6 @@ import { assert } from 'chai';
|
|||
import * as sinon from 'sinon';
|
||||
import type { MenuItem } from 'electron';
|
||||
import { BrowserWindow, Tray, nativeImage } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
|
||||
import type { SystemTrayServiceOptionsType } from '../../../app/SystemTrayService';
|
||||
|
@ -218,26 +217,17 @@ describe('SystemTrayService', function (this: Mocha.Suite) {
|
|||
// can't spy on `Tray.prototype.setImage` because it's not defined that way. So we
|
||||
// spy on the specific instance, just to get the image.
|
||||
const setImageSpy = sandbox.spy(tray, 'setImage');
|
||||
const getImagePath = (): string => {
|
||||
const result = setImageSpy.lastCall?.firstArg;
|
||||
if (!result) {
|
||||
throw new Error('Expected tray.setImage to be called at least once');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
for (let i = 9; i >= 1; i -= 1) {
|
||||
service.setUnreadCount(i);
|
||||
assert.strictEqual(path.parse(getImagePath()).base, `${i}.png`);
|
||||
}
|
||||
|
||||
for (let i = 10; i < 13; i += 1) {
|
||||
service.setUnreadCount(i);
|
||||
assert.strictEqual(path.parse(getImagePath()).base, '10.png');
|
||||
}
|
||||
|
||||
service.setUnreadCount(1);
|
||||
assert.strictEqual(setImageSpy.callCount, 1);
|
||||
service.setUnreadCount(1);
|
||||
assert.strictEqual(setImageSpy.callCount, 1);
|
||||
service.setUnreadCount(2);
|
||||
assert.strictEqual(setImageSpy.callCount, 2);
|
||||
service.setUnreadCount(2);
|
||||
assert.strictEqual(setImageSpy.callCount, 2);
|
||||
service.setUnreadCount(0);
|
||||
assert.match(path.parse(getImagePath()).base, /^icon_\d+\.png$/);
|
||||
assert.strictEqual(setImageSpy.callCount, 3);
|
||||
});
|
||||
|
||||
it('uses a fallback image if the icon file cannot be found', () => {
|
||||
|
@ -251,7 +241,7 @@ describe('SystemTrayService', function (this: Mocha.Suite) {
|
|||
}
|
||||
|
||||
const setImageStub = sandbox.stub(tray, 'setImage');
|
||||
setImageStub.withArgs(sinon.match.string).throws('Failed to load');
|
||||
setImageStub.onFirstCall().throws('Failed to load');
|
||||
|
||||
service.setUnreadCount(4);
|
||||
|
||||
|
@ -259,7 +249,7 @@ describe('SystemTrayService', function (this: Mocha.Suite) {
|
|||
const NativeImage = nativeImage.createEmpty().constructor;
|
||||
|
||||
sinon.assert.calledTwice(setImageStub);
|
||||
sinon.assert.calledWith(setImageStub, sinon.match.string);
|
||||
sinon.assert.calledWith(setImageStub, sinon.match.instanceOf(NativeImage));
|
||||
sinon.assert.calledWith(setImageStub, sinon.match.instanceOf(NativeImage));
|
||||
});
|
||||
|
||||
|
|
|
@ -60,6 +60,11 @@ export enum ThemeType {
|
|||
'dark' = 'dark',
|
||||
}
|
||||
|
||||
export enum SystemThemeType {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
}
|
||||
|
||||
// These are strings so they can be interpolated into class names.
|
||||
export enum ScrollBehavior {
|
||||
Default = 'default',
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { SystemThemeType, ThemeType } from '../types/Util';
|
||||
|
||||
export async function getThemeType(): Promise<ThemeType> {
|
||||
const themeSetting = await window.Events.getThemeSetting();
|
||||
|
||||
if (themeSetting === 'light') {
|
||||
if (
|
||||
themeSetting === 'light' ||
|
||||
window.systemTheme === SystemThemeType.light
|
||||
) {
|
||||
return ThemeType.light;
|
||||
}
|
||||
|
||||
if (themeSetting === 'dark') {
|
||||
if (themeSetting === 'dark' || window.systemTheme === SystemThemeType.dark) {
|
||||
return ThemeType.dark;
|
||||
}
|
||||
|
||||
|
|
6
ts/window.d.ts
vendored
6
ts/window.d.ts
vendored
|
@ -27,7 +27,7 @@ import type * as Crypto from './Crypto';
|
|||
import type * as Curve from './Curve';
|
||||
import type * as RemoteConfig from './RemoteConfig';
|
||||
import type { OSType } from './util/os/shared';
|
||||
import type { LocalizerType, ThemeType } from './types/Util';
|
||||
import type { LocalizerType, SystemThemeType, ThemeType } from './types/Util';
|
||||
import type { Receipt } from './types/Receipt';
|
||||
import type { ConversationController } from './ConversationController';
|
||||
import type { ReduxActions } from './state/types';
|
||||
|
@ -104,8 +104,8 @@ export type FeatureFlagType = {
|
|||
};
|
||||
|
||||
type AboutWindowPropsType = {
|
||||
appEnv: string;
|
||||
arch: string;
|
||||
environmentText: string;
|
||||
platform: string;
|
||||
};
|
||||
|
||||
|
@ -224,7 +224,7 @@ declare global {
|
|||
sendChallengeRequest: (request: IPCChallengeRequest) => void;
|
||||
showKeyboardShortcuts: () => void;
|
||||
storage: Storage;
|
||||
systemTheme: ThemeType;
|
||||
systemTheme: SystemThemeType;
|
||||
|
||||
Signal: SignalCoreType;
|
||||
|
||||
|
|
|
@ -12,21 +12,12 @@ const { AboutWindowProps } = window.Signal;
|
|||
|
||||
strictAssert(AboutWindowProps, 'window values not provided');
|
||||
|
||||
let platform = '';
|
||||
if (AboutWindowProps.platform === 'darwin') {
|
||||
if (AboutWindowProps.arch === 'arm64') {
|
||||
platform = ` (${i18n('icu:appleSilicon')})`;
|
||||
} else {
|
||||
platform = ' (Intel)';
|
||||
}
|
||||
}
|
||||
|
||||
const environmentText = `${AboutWindowProps.environmentText}${platform}`;
|
||||
|
||||
ReactDOM.render(
|
||||
<About
|
||||
closeAbout={() => window.SignalContext.executeMenuRole('close')}
|
||||
environment={environmentText}
|
||||
appEnv={AboutWindowProps.appEnv}
|
||||
platform={AboutWindowProps.platform}
|
||||
arch={AboutWindowProps.arch}
|
||||
i18n={i18n}
|
||||
version={window.SignalContext.getVersion()}
|
||||
/>,
|
||||
|
|
|
@ -14,8 +14,8 @@ if (config.appInstance) {
|
|||
|
||||
const Signal = {
|
||||
AboutWindowProps: {
|
||||
appEnv: environments.join(' - '),
|
||||
arch: process.arch,
|
||||
environmentText: environments.join(' - '),
|
||||
platform: process.platform,
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue