Skip to content

Commit a15ffe5

Browse files
redjexclaude
andcommitted
Themes: a cloud theme on the Day base shows its wallpaper rather than plain white
The server gives the Day base of its cloud themes a white wallPaperNoFile, so only the factory theme had a wallpaper there. Its own wallpaper, on the Classic base, takes that place when a theme is applied, on the theme tiles and in a chat with a theme of its own; a white the user picked stays. A theme with no wallpaper at all no longer throws in the chat background, and keeps the current one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c2c6e5e commit a15ffe5

5 files changed

Lines changed: 109 additions & 13 deletions

File tree

‎src/components/chat/bubbles/chatBackground.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ function resolveBackgroundSync(
170170
return untrack(() => {
171171
const globalTheme = themeController.getTheme();
172172
const theme = options.theme ?? globalTheme;
173-
const wallPaper = options.wallPaper ?? themeController.getThemeSettings(theme).wallpaper;
173+
const wallPaper = options.wallPaper ?? themeController.getThemeWallpaper(theme);
174174
return {theme, wallPaper};
175175
});
176176
}

‎src/components/chatThemesPicker.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import ListenerSetter from '@helpers/listenerSetter';
88
import createMiddleware from '@helpers/solid/createMiddleware';
99
import {IS_SAFARI} from '@environment/userAgent';
1010
import {DEFAULT_THEME} from '@config/state';
11-
import {blendWallpaperForTinted} from '@config/themePresets';
11+
import {blendWallpaperForTinted, getCloudThemeWallpaper} from '@config/themePresets';
1212
import {BaseTheme, Theme} from '@layer';
1313
import rootScope from '@lib/rootScope';
1414
import I18n from '@lib/langPack';
@@ -195,7 +195,7 @@ export default function ChatThemesPicker(props: ChatThemesPickerProps) {
195195
.filter((themeSettings) => AVAILABLE_BASE_THEMES.has(themeSettings.base_theme._))
196196
.map((themeSettings) => {
197197
const shouldBlend = themeSettings.base_theme._ === 'baseThemeTinted' && !isCurated && themeSettings.wallpaper;
198-
const wp = shouldBlend ? blendWallpaperForTinted(themeSettings.wallpaper, themeSettings.accent_color) : themeSettings.wallpaper;
198+
const wp = shouldBlend ? blendWallpaperForTinted(themeSettings.wallpaper, themeSettings.accent_color) : getCloudThemeWallpaper(theme, themeSettings);
199199
const result = AppBackgroundTab.addWallPaper(wp, undefined, themeSettings.base_theme._);
200200
// addWallPaper returns undefined for a pattern wallpaper with no colors —
201201
// skip rather than deref (matches the tinted-fallback guard below).

‎src/config/themePresets.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* separate "no preset" choice.
1111
*/
1212

13-
import type {BaseTheme, ThemeSettings, WallPaper} from '@layer';
13+
import type {BaseTheme, Theme, ThemeSettings, WallPaper} from '@layer';
1414
import {hsvToRgb, rgbToHsv} from '@helpers/color';
1515

1616
export type AccentPresetWallpaper = {
@@ -144,6 +144,17 @@ export function tintedBaseWallpaperFor(baseColor: BaseColorName): AccentPresetWa
144144
return TINTED_BASE_WALLPAPERS[baseColor];
145145
}
146146

147+
// The server gives the Day base of its cloud themes a plain white instead of a wallpaper; the theme's
148+
// own wallpaper is on its Classic base, which is a light one too. A white picked by the user is
149+
// kept on the AppTheme and never comes through here.
150+
export function getCloudThemeWallpaper(theme: Theme, themeSettings: ThemeSettings): WallPaper {
151+
const wallPaper = themeSettings.wallpaper;
152+
const isPlainDay = themeSettings.base_theme._ === 'baseThemeDay' &&
153+
wallPaper?._ === 'wallPaperNoFile' &&
154+
wallPaper.settings?.background_color === 0xffffff;
155+
return isPlainDay ? theme.settings.find((s) => s.base_theme._ === 'baseThemeClassic')?.wallpaper ?? wallPaper : wallPaper;
156+
}
157+
147158
// Blend an arbitrary wallpaper toward the iOS Dark Blue palette so it reads navy-dominant on
148159
// tinted base. Used everywhere a non-curated wallpaper lands on tinted: cloud themes from the
149160
// carousel (applyNewTheme), grid picks from Chat Wallpaper, color-picker single-color picks,

‎src/helpers/themeController.ts‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {AppTheme, AppThemeSettings} from '@config/state';
22
import type {AccentPreset} from '@config/themePresets';
3-
import type {BaseTheme, Theme, ThemeSettings} from '@layer';
4-
import {blendWallpaperForTinted, presetThemeId, presetToThemeSettings} from '@config/themePresets';
3+
import type {BaseTheme, Theme, ThemeSettings, WallPaper} from '@layer';
4+
import {blendWallpaperForTinted, getCloudThemeWallpaper, presetThemeId, presetToThemeSettings} from '@config/themePresets';
55
import type {AppBackgroundTab} from '@components/sidebarLeft/tabs/background';
66
import type {AppChatBackground} from '@components/chat/bubbles/chatBackground';
77
import {applyOutgoingMessageGradient, getOutgoingMessageGradient, keepReadableOnOutgoingMessageGradient, MESSAGE_OUT_TINT} from '@helpers/outgoingMessageGradient';
@@ -638,13 +638,9 @@ export class ThemeController {
638638
public applyAccentPreset(preset: AccentPreset) {
639639
const currentTheme = this.getTheme();
640640
const baseTheme = this.getBaseThemeForName(currentTheme.name);
641-
const settings = presetToThemeSettings(preset, baseTheme);
642641
// iOS dayColorPresets have `wallpaper: nil` — those presets only swap accent + bubbles
643-
// and keep the current wallpaper. Mirror that here: setBackgroundDocument requires a
644-
// WallPaper, so fall back to the current AppTheme's wallpaper when the preset omits one.
645-
if(!settings.wallpaper) {
646-
settings.wallpaper = this.getThemeSettings(currentTheme)?.wallpaper;
647-
}
642+
// and keep the current wallpaper, which applyNewTheme does for any theme without one.
643+
const settings = presetToThemeSettings(preset, baseTheme);
648644
const synthetic: Theme = {
649645
_: 'theme',
650646
id: presetThemeId(preset),
@@ -690,10 +686,12 @@ export class ThemeController {
690686
// (accent preset). Server cloud themes carry numeric/hash ids.
691687
const themeId = String(theme.id ?? '');
692688
const isCurated = themeId === '' || themeId.startsWith('preset:');
693-
let effectiveWallpaper = themeSettings.wallpaper;
689+
let effectiveWallpaper = getCloudThemeWallpaper(theme, themeSettings);
694690
if(themeName === 'tinted' && !isCurated && effectiveWallpaper) {
695691
effectiveWallpaper = blendWallpaperForTinted(effectiveWallpaper, themeSettings.accent_color);
696692
}
693+
// setBackgroundDocument requires a WallPaper; the current one is already of this base
694+
effectiveWallpaper ??= this.getThemeWallpaper(currentTheme);
697695

698696
// Preserve every base entry from the cloud theme (mirrors iOS' `[TelegramThemeSettings]`).
699697
// Only the entry matching the current base gets the (optionally tinted-blended) wallpaper.
@@ -748,6 +746,18 @@ export class ThemeController {
748746
theme.settings.find((s) => s.base_theme._ === ((isNight ?? this.isNightThemeName(themeName)) ? 'baseThemeNight' : 'baseThemeClassic'));
749747
}
750748

749+
// What the chat is drawn on under a theme. Cloud themes often give a base no wallpaper of its own
750+
// (mostly baseThemeDay's): those keep the current one, or the factory one where a theme without
751+
// one was saved as the current theme.
752+
public getThemeWallpaper(theme: Theme | AppTheme): WallPaper {
753+
const settings = this.getThemeSettings(theme);
754+
// * an AppTheme keeps what was picked; a theme of a chat comes as the server gave it
755+
const own = settings && ((theme as AppTheme).name ? settings.wallpaper : getCloudThemeWallpaper(theme as Theme, settings as ThemeSettings));
756+
return own ??
757+
this.getThemeSettings(this.getTheme())?.wallpaper ??
758+
this.getThemeSettings(SETTINGS_INIT.themes.find((t) => t.name === this.getResolvedThemeName())).wallpaper;
759+
}
760+
751761
// Persist a wallpaper pick (Chat Wallpaper tab grid click / upload) onto the *current*
752762
// AppTheme's matching base-theme settings entry. Must go through the appSettings store
753763
// setter: the object returned by getThemeSettings() is a readonly Solid store node, so a
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {SETTINGS_INIT} from '@config/state';
2+
import {getCloudThemeWallpaper} from '@config/themePresets';
3+
import themeController from '@helpers/themeController';
4+
import type {Theme} from '@layer';
5+
6+
const factory = SETTINGS_INIT.themes.find((theme) => theme.name === 'light');
7+
const factoryWallpaper = factory.settings.find((settings) => settings.base_theme._ === 'baseThemeDay').wallpaper;
8+
9+
const makeTheme = (wallpaper?: Theme['settings'][number]['wallpaper']) => ({
10+
_: 'theme',
11+
id: '1',
12+
access_hash: '',
13+
slug: 'theme',
14+
title: 'Theme',
15+
pFlags: {},
16+
settings: [{_: 'themeSettings', pFlags: {}, base_theme: {_: 'baseThemeDay'}, accent_color: 0x7e5fe5, message_colors: [0x7e5fe5, 0x2d7ed5], wallpaper}]
17+
} as Theme);
18+
19+
describe('the plain white the server gives the Day base of a cloud theme', () => {
20+
const white = {_: 'wallPaperNoFile', id: 1, pFlags: {}, settings: {_: 'wallPaperSettings', pFlags: {}, background_color: 0xffffff}} as const;
21+
const classic = {...factoryWallpaper, slug: 'classic'};
22+
const make = (base: 'baseThemeDay' | 'baseThemeNight', wallpaper: any) => {
23+
const entry = {_: 'themeSettings', pFlags: {}, base_theme: {_: base}, accent_color: 0, wallpaper} as any;
24+
return {...makeTheme(), settings: [entry, {...entry, base_theme: {_: 'baseThemeClassic'}, wallpaper: classic}]} as Theme;
25+
};
26+
27+
test('gives way to the wallpaper of the theme\'s Classic base', () => {
28+
const theme = make('baseThemeDay', white);
29+
expect(getCloudThemeWallpaper(theme, theme.settings[0])).toBe(classic);
30+
});
31+
32+
test('another colour, or another base, stays', () => {
33+
const colored = {...white, settings: {...white.settings, background_color: 0xff0000}};
34+
const day = make('baseThemeDay', colored);
35+
expect(getCloudThemeWallpaper(day, day.settings[0])).toBe(colored);
36+
const night = make('baseThemeNight', white);
37+
expect(getCloudThemeWallpaper(night, night.settings[0])).toBe(white);
38+
});
39+
40+
test('a white picked for an AppTheme stays, a chat\'s theme gets the wallpaper', () => {
41+
vi.spyOn(themeController as any, 'getResolvedThemeName').mockReturnValue('light');
42+
const theme = make('baseThemeDay', white);
43+
expect(themeController.getThemeWallpaper({...theme, name: 'light'} as any)).toBe(white);
44+
expect(themeController.getThemeWallpaper(theme)).toBe(classic);
45+
vi.restoreAllMocks();
46+
});
47+
});
48+
49+
describe('a theme without a wallpaper of its own', () => {
50+
beforeEach(() => {
51+
vi.spyOn(themeController as any, 'getResolvedThemeName').mockReturnValue('light');
52+
});
53+
54+
afterEach(() => {
55+
vi.restoreAllMocks();
56+
});
57+
58+
test('keeps the current wallpaper', () => {
59+
const current = {...makeTheme(factoryWallpaper), name: 'light'} as any;
60+
vi.spyOn(themeController, 'getTheme').mockReturnValue(current);
61+
expect(themeController.getThemeWallpaper(makeTheme())).toBe(factoryWallpaper);
62+
});
63+
64+
test('saved as the current theme, it gets the factory wallpaper', () => {
65+
const current = {...makeTheme(), name: 'light'} as any;
66+
vi.spyOn(themeController, 'getTheme').mockReturnValue(current);
67+
expect(themeController.getThemeWallpaper(current)).toBe(factoryWallpaper);
68+
});
69+
70+
test('a wallpaper of its own stays', () => {
71+
const own = {...factoryWallpaper, slug: 'own'};
72+
vi.spyOn(themeController, 'getTheme').mockReturnValue(factory);
73+
expect(themeController.getThemeWallpaper(makeTheme(own))).toBe(own);
74+
});
75+
});

0 commit comments

Comments
 (0)