MM-23853 Support dark mode and light mode syncing with OS theme setting #8804
MM-23853 Support dark mode and light mode syncing with OS theme setting #8804
Conversation
I'm opening as draft because it requires unit tests addition and e2e tests fixing. Right after signing in, default theme is used instead one from user preferences. It's known issue to me, I'm going to fix it. |
This PR has been automatically labelled "stale" because it hasn't had recent activity. |
Hi @komik966. Apologies for the delay in reviews here. I'm not sure why our integration didn't assign reviewers, so this sort of got missed out on |
@hmhealey No problem. Recently I'm busy, so I might be slow to respond. |
@deanwhillier would appreciate your review here considering the theming work you've been doing recently |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. A few nits here and there. Would be nice to have them solved before merge, but not blocking.
import {ThemeKey} from 'mattermost-redux/types/themes'; | ||
|
||
export const getAllowedThemes = createSelector('getAllowedThemes', getConfig, (config) => { | ||
const allowedThemes = (config.AllowedThemes && config.AllowedThemes.split(',')) || []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we simplify this by doing config.AllowedThemes?.split(',') || []
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. This code was moved from:
Line 13 in ffdf5b5
const allowedThemes = (config.AllowedThemes && config.AllowedThemes.split(',')) || []; |
I think
@typescript-eslint/prefer-optional-chain
would be useful inside eslint config.
const hasAllowedThemes = allowedThemes.length > 1 || (allowedThemes[0] && allowedThemes[0].trim().length > 0); | ||
const themesKeys = Object.keys(Preferences.THEMES) as ThemeKey[]; | ||
return themesKeys.filter((key) => !hasAllowedThemes || allowedThemes.indexOf(key) >= 0).map((key) => Preferences.THEMES[key]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am struggling to grasp this logic. Do you mind explaining it to me?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently this logic sits here:
Lines 18 to 25 in ffdf5b5
const allowedThemes = this.props.allowedThemes; | |
const hasAllowedThemes = allowedThemes.length > 1 || (allowedThemes[0] && allowedThemes[0].trim().length > 0); | |
for (const k in Preferences.THEMES) { | |
if (Preferences.THEMES.hasOwnProperty(k)) { | |
if (hasAllowedThemes && allowedThemes.indexOf(k) < 0) { | |
continue; | |
} |
In my implementation filter
function requires negation of hasAllowedThemes && allowedThemes.indexOf(k) < 0
. I used De Morgan's law (https://en.wikipedia.org/wiki/De_Morgan%27s_laws) to change hasAllowedThemes && allowedThemes.indexOf(k) < 0
-> !hasAllowedThemes || allowedThemes.indexOf(key) >= 0
.
components/user_settings/display/user_settings_theme/user_settings_theme.tsx
Show resolved
Hide resolved
return ( | ||
<> | ||
<div className='theme-chooser-header'>{header}</div> | ||
<div className='themes-grid'> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any chance this div may be empty? If so, is there any visual concern that we may want to address? (May be out of the scope of this PR)
> | ||
{theme.type?.toLowerCase() === allowedTheme.type?.toLowerCase() && <span className='fa fa-check-circle themes-grid__thumbnail-active-check'/>} | ||
<ThemeThumbnail | ||
themeKey={allowedTheme.type as string} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is because it may be undefined? Should we instead do something like allowedTheme.type!
?
|
||
import {Preferences} from 'mattermost-redux/constants'; | ||
|
||
import {getAllowedThemes} from '../../../../../selectors/theme'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use a better route?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to use this:
import {getAllowedThemes} from '../../../../../selectors/theme'; | |
import {getAllowedThemes} from 'selectors/theme'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good @komik966! A few things for your consideration. Also, I still need to go through 'components/user_settings/display/user_settings_theme/expanded/expanded.tsx', but thought I'd post what I have for now. Thanks for your work on this and apologies if the recent theme updates added more work for you!
I also have a couple questions to confirm:
- What happens when allowed themes only contain light or dark themes?
- With and without custom themes enabled
- What happens if allowed themes are changed after an os sync’d theme pair has been selected?
@esethna, FYI for followup consideration, the theme thumbnails need to be made accessible. They were already inaccessible before, so a separate ticket/PR would be good. I also need to tweak the theme SVG thumbnail a bit based on the mobile thumbnail I did.
case GeneralTypes.OS_COLOR_SCHEME_INIT: | ||
case GeneralTypes.OS_COLOR_SCHEME_CHANGED: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since ‘OS_COLOR_SCHEME_INIT’ and ‘OS_COLOR_SCHEME_CHANGED’ in essence do the same thing, would it make sense to maybe combine them into one action type using something like ‘SET_OS_COLOR_SCHEME’ or ‘OS_COLOR_SCHEME_SET’ to simplify things a bit?
@@ -3353,14 +3353,14 @@ | |||
"user.settings.advance.sendTitle": "Wysyłaj wiadomość przy użyciu Ctrl+Enter", | |||
"user.settings.advance.sendTitle.mac": "Wysyłaj wiadomość przy użyciu Ctrl+Enter", | |||
"user.settings.advance.title": "Zaawansowane Ustawienia", | |||
"user.settings.custom_theme.advancedThemeEditing": "Zaawansowana edycja motywu", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only i18n/en.json
strings need to be provided, the rest of the language files are auto-generated by the translation build process. If you're interested, feel free to provide these translations using our translation server and get assistance over in our community translation channel.
@@ -12,6 +12,7 @@ export type ThemeType = 'Denim' | 'Sapphire' | 'Quartz' | 'Indigo' | 'Onyx'; | |||
export type Theme = { | |||
[key: string]: string | undefined; | |||
type?: ThemeType | 'custom'; | |||
colorScheme?: 'light' | 'dark'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use the OsColorSchemeName
type from packages/mattermost-redux/src/types/general.ts
here?
type Props = { | ||
selected: boolean; | ||
updateSection: (section: string) => void; | ||
setRequireConfirm: (requireConfirm?: boolean) => void; | ||
setEnforceFocus: (enforceFocus?: boolean) => void; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'm not sure if we have a standard for this yet or not, but I think the Props type usually lives at the top of component from what I've seen so far.
|
||
const UserSettingsTheme: React.FC<Props> = ({selected, updateSection, setRequireConfirm, setEnforceFocus}: Props) => { | ||
return selected ? ( | ||
<Expanded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we maybe use a slightly more descriptive component name here? Something like UserSettingsExpanded
?
onClick={onCustomThemeClick} | ||
> | ||
{customThemeSelected && <span className='fa fa-check-circle themes-grid__thumbnail-active-check'/>} | ||
<span className='fa fa-pencil'/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Palette icon is available in compass-icons, our new custom icon font (already available in the web app)
- the following should do the trick with an additional style update to set the font-size to 31.2px (according to the design)
https://mattermost.github.io/compass-icons/
<span className='fa fa-pencil'/> | |
<i className='icon icon-palette-outline'/> |
})} | ||
onClick={onCustomThemeClick} | ||
> | ||
{customThemeSelected && <span className='fa fa-check-circle themes-grid__thumbnail-active-check'/>} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use our new custom icon font for this now. :)
https://mattermost.github.io/compass-icons/
{customThemeSelected && <span className='fa fa-check-circle themes-grid__thumbnail-active-check'/>} | |
{customThemeSelected && <i className='icon icon-check-circle themes-grid__thumbnail-active-check'/>} |
/> | ||
<div className='header__icon'> | ||
<LocalizedIcon | ||
className='fa fa-plus' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can use compass-icons icon font here.
title={{id: t('generic_icons.expand'), defaultMessage: 'Expand Icon'}} | ||
/> | ||
<LocalizedIcon | ||
className='fa fa-minus' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can use compass-icons icon font here.
|
||
export const getAllowedThemes = createSelector('getAllowedThemes', getConfig, (config) => { | ||
const allowedThemes = (config.AllowedThemes && config.AllowedThemes.split(',')) || []; | ||
const hasAllowedThemes = allowedThemes.length > 1 || (allowedThemes[0] && allowedThemes[0].trim().length > 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: ‘hasAllowedThemes’ seems to be returning undefined instead of false if allowedThemes
haven't been defined, maybe switch to ‘??’ Instead of ‘||’?
@komik966, I was thinking we should maybe also update
|
This PR has been automatically labelled "stale" because it hasn't had recent activity. |
@komik966 let us know if you have questions about Dean's feedback? |
@esethna As far no questions. I'll address all comments this weekend. |
I will continue covering the comments this week |
This PR has been automatically labelled "stale" because it hasn't had recent activity. |
@komik966 Would love to see this PR getting merged :) |
Hey @komik966! How's this coming? We're really excited about this feature and would love to get this in! |
Hi, @esethna! I didn't make progress here. |
Summary
This PR adds support for dark mode and light mode syncing with OS theme setting
Ticket Link
mattermost/mattermost#15975
Related Pull Requests
Screenshots
demo.mp4
Release Note