diff --git a/.gitignore b/.gitignore index ebd791d30..15c5f26c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +dist .cache .env src/lib/gramjs/build/ diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index f047892df..60a38ef6c 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -1,10 +1,12 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; +import { withGlobal } from '../../lib/teact/teactn'; import { ApiAudio, ApiMessage, ApiVoice, } from '../../api/types'; +import { ISettings } from '../../types'; import { IS_MOBILE_SCREEN } from '../../util/environment'; import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dateFormat'; @@ -49,6 +51,10 @@ type OwnProps = { onDateClick?: (messageId: number, chatId: number) => void; }; +type StateProps = { + theme: ISettings['theme']; +}; + interface ISeekMethods { handleStartSeek: (e: React.MouseEvent) => void; handleSeek: (e: React.MouseEvent) => void; @@ -61,7 +67,8 @@ const MAX_SPIKES = IS_MOBILE_SCREEN ? 50 : 75; // This is needed for browsers requiring user interaction before playing. const PRELOAD = true; -const Audio: FC = ({ +const Audio: FC = ({ + theme, message, senderTitle, uploadProgress, @@ -201,8 +208,8 @@ const Audio: FC = ({ const seekHandlers = { handleStartSeek, handleSeek, handleStopSeek }; const isOwn = isOwnMessage(message); const renderedWaveform = useMemo( - () => voice && renderWaveform(voice, playProgress, isOwn, seekHandlers), - [voice, playProgress, isOwn, seekHandlers], + () => voice && renderWaveform(voice, playProgress, isOwn, seekHandlers, theme), + [voice, playProgress, isOwn, seekHandlers, theme], ); const fullClassName = buildClassName( @@ -346,7 +353,11 @@ function renderVoice(voice: ApiVoice, renderedWaveform: any, isMediaUnread?: boo } function renderWaveform( - voice: ApiVoice, playProgress = 0, isOwn = false, { handleStartSeek, handleSeek, handleStopSeek }: ISeekMethods, + voice: ApiVoice, + playProgress = 0, + isOwn = false, + { handleStartSeek, handleSeek, handleStopSeek }: ISeekMethods, + theme: ISettings['theme'], ) { const { waveform, duration } = voice; @@ -354,14 +365,18 @@ function renderWaveform( return undefined; } + const fillColor = theme === 'dark' ? '#494B75' : '#CBCBCB'; + const fillOwnColor = theme === 'dark' ? '#C69C85' : '#B0DEA6'; + const progressFillColor = theme === 'dark' ? '#868DF5' : '#54a3e6'; + const progressFillOwnColor = theme === 'dark' ? '#FFFFFF' : '#53ad53'; const durationFactor = Math.min(duration / AVG_VOICE_DURATION, 1); const spikesCount = Math.round(MIN_SPIKES + (MAX_SPIKES - MIN_SPIKES) * durationFactor); const decodedWaveform = decodeWaveform(new Uint8Array(waveform)); const { data: spikes, peak } = interpolateArray(decodedWaveform, spikesCount); const { src, width, height } = renderWaveformToDataUri(spikes, playProgress, { peak, - fillStyle: isOwn ? '#B0DEA6' : '#CBCBCB', - progressFillStyle: isOwn ? '#53ad53' : '#54a3e6', + fillStyle: isOwn ? fillOwnColor : fillColor, + progressFillStyle: isOwn ? progressFillOwnColor : progressFillColor, }); return ( @@ -414,4 +429,4 @@ function renderSeekline( ); } -export default memo(Audio); +export default memo(withGlobal((global) => ({ theme: global.settings.byKey.theme }))(Audio)); diff --git a/src/components/common/Avatar.scss b/src/components/common/Avatar.scss index a90a5dd5f..8a1f1465e 100644 --- a/src/components/common/Avatar.scss +++ b/src/components/common/Avatar.scss @@ -6,7 +6,7 @@ width: 3.375rem; height: 3.375rem; border-radius: 50%; - background: linear-gradient(white -125%, var(--color-user)); + background: linear-gradient(var(--color-white) -125%, var(--color-user)); color: white; font-weight: bold; display: flex; @@ -109,7 +109,7 @@ width: 0.875rem; height: 0.875rem; border-radius: 50%; - border: 2px solid white; + border: 2px solid var(--color-background); background-color: #0ac630; flex-shrink: 0; } diff --git a/src/components/common/EmbeddedMessage.scss b/src/components/common/EmbeddedMessage.scss index 256ce04a0..83d196c3e 100644 --- a/src/components/common/EmbeddedMessage.scss +++ b/src/components/common/EmbeddedMessage.scss @@ -21,7 +21,7 @@ padding: 0.5rem; margin: 0; background-color: var(--background-color); - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); &::before { left: .625rem; diff --git a/src/components/common/File.scss b/src/components/common/File.scss index 01bad572e..dd315bee9 100644 --- a/src/components/common/File.scss +++ b/src/components/common/File.scss @@ -158,7 +158,7 @@ } &.smaller { - --background-color: #fff; + --background-color: var(--color-background); --border-radius-messages-small: .3125rem; .icon-download, diff --git a/src/components/common/LastMessageMeta.scss b/src/components/common/LastMessageMeta.scss index 83b045f01..4c1acad82 100644 --- a/src/components/common/LastMessageMeta.scss +++ b/src/components/common/LastMessageMeta.scss @@ -8,7 +8,7 @@ align-items: center; .MessageOutgoingStatus { - color: var(--color-text-green); + color: var(--color-text-meta-colored); margin-right: 0.1rem; font-size: 1.15rem; } diff --git a/src/components/common/StickerSetModal.scss b/src/components/common/StickerSetModal.scss index 06bdd81b6..40bc0c28a 100644 --- a/src/components/common/StickerSetModal.scss +++ b/src/components/common/StickerSetModal.scss @@ -32,7 +32,7 @@ .button-wrapper { padding: 0.5rem 0; border-top: 1px solid var(--color-borders); - box-shadow: 0 0 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 0 2px var(--color-default-shadow); button { display: inline-block; diff --git a/src/components/common/UiLoader.scss b/src/components/common/UiLoader.scss index 1eca85ae1..4149c76f9 100644 --- a/src/components/common/UiLoader.scss +++ b/src/components/common/UiLoader.scss @@ -18,7 +18,7 @@ .left { flex: 1; - background: white; + background: var(--color-background); min-width: 15.5rem; max-width: 26.5rem; @@ -56,10 +56,19 @@ left: 0; bottom: 0; right: 0; - background: rgb(230, 235, 238) url('../../assets/chat-bg.jpg') no-repeat center; + background: no-repeat center; background-size: cover; z-index: -1; transform-origin: left center; + + .theme-dark body.initial & { + background-color: #0f0f0f; + } + + .theme-light body.initial &, + body:not(.initial) & { + background-image: url('../../assets/chat-bg.jpg'); + } } &.with-right-column::before { @@ -97,12 +106,12 @@ min-width: 15.5rem; max-width: 26.5rem; border-left: 1px solid var(--color-borders); - background: white; + background: var(--color-background); } } .blank { flex: 1; - background: white; + background: var(--color-background); } } diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index bba3d1990..9307ab475 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -32,6 +32,7 @@ type OwnProps = { type StateProps = Pick & { hasCustomBackground?: boolean; + isCustomBackgroundColor: boolean; isRightColumnShown?: boolean; }; @@ -84,6 +85,7 @@ const UiLoader: FC = ({ page, children, hasCustomBackground, + isCustomBackgroundColor, isRightColumnShown, setIsUiReady, }) => { @@ -129,7 +131,8 @@ const UiLoader: FC = ({
@@ -149,6 +152,7 @@ export default withGlobal( return { uiReadyState: global.uiReadyState, hasCustomBackground: Boolean(global.settings.byKey.customBackground), + isCustomBackgroundColor: Boolean((global.settings.byKey.customBackground || '').match(/^#[a-f\d]{6,8}$/i)), isRightColumnShown: selectIsRightColumnShown(global), }; }, diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index 9de407746..55860eec1 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -1,5 +1,5 @@ .Chat { - --background-color: white; + --background-color: var(--color-background); position: absolute; top: 0; diff --git a/src/components/left/main/LeftMain.tsx b/src/components/left/main/LeftMain.tsx index 0c87192e4..7966bdad7 100644 --- a/src/components/left/main/LeftMain.tsx +++ b/src/components/left/main/LeftMain.tsx @@ -118,7 +118,6 @@ const LeftMain: FC = ({ onSearchQuery={onSearchQuery} onSelectSettings={handleSelectSettings} onSelectContacts={handleSelectContacts} - onSelectNewGroup={handleSelectNewGroup} onSelectArchived={handleSelectArchived} onReset={onReset} /> diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index 54f9fa9fa..b6aa50157 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -45,7 +45,7 @@ .archived-badge { min-width: 1.5rem; height: 1.5rem; - margin-left: 2rem; + margin-left: auto; background: var(--color-gray); border-radius: 0.75rem; padding: 0 .45rem; diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index 3ef0c5655..a2651c054 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -4,7 +4,7 @@ import React, { import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; -import { LeftColumnContent } from '../../../types'; +import { LeftColumnContent, ISettings } from '../../../types'; import { ApiChat } from '../../../api/types'; import { IS_MOBILE_SCREEN } from '../../../util/environment'; @@ -12,6 +12,7 @@ import buildClassName from '../../../util/buildClassName'; import { pick } from '../../../util/iteratees'; import { isChatArchived } from '../../../modules/helpers'; import { formatDateToString } from '../../../util/dateFormat'; +import switchTheme from '../../../util/switchTheme'; import useLang from '../../../hooks/useLang'; import DropdownMenu from '../../ui/DropdownMenu'; @@ -19,6 +20,7 @@ import MenuItem from '../../ui/MenuItem'; import Button from '../../ui/Button'; import SearchInput from '../../ui/SearchInput'; import PickerSelectedItem from '../../common/PickerSelectedItem'; +import Switcher from '../../ui/Switcher'; import './LeftMainHeader.scss'; @@ -28,7 +30,6 @@ type OwnProps = { onSearchQuery: (query: string) => void; onSelectSettings: () => void; onSelectContacts: () => void; - onSelectNewGroup: () => void; onSelectArchived: () => void; onReset: () => void; }; @@ -39,11 +40,15 @@ type StateProps = { currentUserId?: number; globalSearchChatId?: number; searchDate?: number; + theme: ISettings['theme']; + animationLevel: 0 | 1 | 2; chatsById?: Record; }; type DispatchProps = Pick; +'openChat'| 'openSupportChat' | 'setGlobalSearchDate' | 'setGlobalSearchChatId' | 'setSettingOption'>; + +const ANIMATION_LEVEL_OPTIONS = [0, 1, 2]; const LeftMainHeader: FC = ({ content, @@ -51,7 +56,6 @@ const LeftMainHeader: FC = ({ onSearchQuery, onSelectSettings, onSelectContacts, - onSelectNewGroup, onSelectArchived, setGlobalSearchChatId, onReset, @@ -60,10 +64,13 @@ const LeftMainHeader: FC = ({ currentUserId, globalSearchChatId, searchDate, + theme, + animationLevel, chatsById, openChat, openSupportChat, setGlobalSearchDate, + setSettingOption, }) => { const hasMenu = content === LeftColumnContent.ChatList; const clearedDateSearchParam = { date: undefined }; @@ -113,6 +120,28 @@ const LeftMainHeader: FC = ({ openChat({ id: currentUserId }); }, [currentUserId, openChat]); + const handleDarkModeToggle = useCallback((e: React.SyntheticEvent) => { + e.stopPropagation(); + const newTheme = theme === 'light' ? 'dark' : 'light'; + + setSettingOption({ + theme: newTheme, + customBackground: newTheme === 'dark' ? '#0F0F0F' : undefined, + }); + switchTheme(newTheme, animationLevel > 0); + }, [animationLevel, setSettingOption, theme]); + + const handleAnimationLevelChange = useCallback((e: React.SyntheticEvent) => { + e.stopPropagation(); + + const newLevel = animationLevel === 0 ? 2 : 0; + ANIMATION_LEVEL_OPTIONS.forEach((_, i) => { + document.body.classList.toggle(`animation-level-${i}`, newLevel === i); + }); + + setSettingOption({ animationLevel: newLevel }); + }, [animationLevel, setSettingOption]); + const lang = useLang(); const isSearchFocused = Boolean(globalSearchChatId) @@ -130,10 +159,19 @@ const LeftMainHeader: FC = ({ trigger={MainButton} > - {lang('NewGroup')} + {lang('SavedMessages')} + + + {lang('ArchivedChats')} + {archivedUnreadChatsCount > 0 && ( +
{archivedUnreadChatsCount}
+ )}
= ({ > {lang('Contacts')} - - {lang('Archived')} - {archivedUnreadChatsCount > 0 && ( -
{archivedUnreadChatsCount}
- )} -
- - {lang('Saved')} - {lang('Settings')} + + Dark Mode + + + + {lang('SettingsSearch.Synonyms.Appearance.Animations')} + 0} + /> + ( } = global.globalSearch; const { currentUserId } = global; const { byId: chatsById } = global.chats; + const { theme, animationLevel } = global.settings.byKey; return { searchQuery, @@ -221,6 +267,8 @@ export default memo(withGlobal( chatsById, globalSearchChatId: chatId, searchDate: date, + theme, + animationLevel, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ @@ -228,5 +276,6 @@ export default memo(withGlobal( 'openSupportChat', 'setGlobalSearchDate', 'setGlobalSearchChatId', + 'setSettingOption', ]), )(LeftMainHeader)); diff --git a/src/components/left/search/LeftSearch.scss b/src/components/left/search/LeftSearch.scss index adb84eb73..5747a21c7 100644 --- a/src/components/left/search/LeftSearch.scss +++ b/src/components/left/search/LeftSearch.scss @@ -163,7 +163,7 @@ justify-content: space-between; align-items: flex-end; box-shadow: inset 0 -1px 0 0 var(--color-borders); - background-color: white; + background-color: var(--color-background); -webkit-overflow-scrolling: touch; overflow-x: auto; overflow-y: hidden; diff --git a/src/components/left/settings/SettingsGeneralBackgroundColor.scss b/src/components/left/settings/SettingsGeneralBackgroundColor.scss index 63b9000a8..ecd2406b8 100644 --- a/src/components/left/settings/SettingsGeneralBackgroundColor.scss +++ b/src/components/left/settings/SettingsGeneralBackgroundColor.scss @@ -31,7 +31,7 @@ left: -0.75rem; width: 1.5rem; height: 1.5rem; - border: 0.125rem solid white; + border: 0.125rem solid var(--color-white); border-radius: 0.75rem; cursor: grab; } @@ -66,12 +66,12 @@ div { cursor: pointer; - box-shadow: inset 0 0 0 0 white; + box-shadow: inset 0 0 0 0 var(--color-background); transition: box-shadow 300ms ease; &.active { border: 0.125rem solid var(--color-primary); - box-shadow: inset 0 0 0 0.3125rem white; + box-shadow: inset 0 0 0 0.3125rem var(--color-background); } // A hack to make a square diff --git a/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss b/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss index 4dae4c3cd..9b64dc09e 100644 --- a/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss +++ b/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss @@ -2,7 +2,7 @@ height: calc(100% - var(--header-height)); .picker-header { - box-shadow: 0 0 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 0 2px var(--color-default-shadow); .max-items-reached { margin-bottom: 0.5rem; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 5952e3ea3..7221b38b9 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -9,6 +9,7 @@ import { pick } from '../../util/iteratees'; import { selectIsForwardModalOpen, selectIsMediaViewerOpen, selectIsRightColumnShown } from '../../modules/selectors'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import useShowTransition from '../../hooks/useShowTransition'; +import buildClassName from '../../util/buildClassName'; import LeftColumn from '../left/LeftColumn'; import MiddleColumn from '../middle/MiddleColumn'; @@ -19,7 +20,6 @@ import Notifications from './Notifications.async'; import Errors from './Errors.async'; import './Main.scss'; -import buildClassName from '../../util/buildClassName'; type StateProps = { animationLevel: number; diff --git a/src/components/mediaViewer/ZoomControls.scss b/src/components/mediaViewer/ZoomControls.scss index cbb50ec0c..b268c5cac 100644 --- a/src/components/mediaViewer/ZoomControls.scss +++ b/src/components/mediaViewer/ZoomControls.scss @@ -74,7 +74,7 @@ width: .75rem; height: .75rem; border-radius: 50%; - background-color: #fff; + background-color: var(--color-white); right: 0; top: 50%; transform: translate(.325rem, -50%); diff --git a/src/components/mediaViewer/helpers/formatFileSize.ts b/src/components/mediaViewer/helpers/formatFileSize.ts index b353115db..a9199ad73 100644 --- a/src/components/mediaViewer/helpers/formatFileSize.ts +++ b/src/components/mediaViewer/helpers/formatFileSize.ts @@ -1,7 +1,7 @@ const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']; export default (bytes: number) => { - const number = Math.floor(Math.log(bytes) / Math.log(1024)); + const number = bytes === 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / 1024 ** Math.floor(number)).toFixed(1)} ${units[number]}`; }; diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index 0b7d3868a..b5bbfc65d 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -5,14 +5,17 @@ overflow: scroll; overflow-x: hidden; overflow-y: overlay; + padding-bottom: .3125rem; body.hide-mask-shadow .mask-image-disabled &, .mask-image-enabled & { mask-image: linear-gradient(to top, transparent 0, #000 1rem); } + .custom-bg-color.mask-image-disabled &, .custom-bg-image.mask-image-disabled & { - margin-bottom: .3rem; + margin-bottom: .3125rem; + padding-bottom: 0; } @media (pointer: coarse) { diff --git a/src/components/middle/MessageSelectToolbar.scss b/src/components/middle/MessageSelectToolbar.scss index 823c7cd2b..607042e9f 100644 --- a/src/components/middle/MessageSelectToolbar.scss +++ b/src/components/middle/MessageSelectToolbar.scss @@ -77,9 +77,9 @@ align-items: center; padding: 0.25rem; - background: white; + background: var(--color-background); border-radius: var(--border-radius-messages); - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); font-weight: 500; @media (max-width: 600px) { diff --git a/src/components/middle/MiddleColumn.scss b/src/components/middle/MiddleColumn.scss index 7cddc8096..881f2929d 100644 --- a/src/components/middle/MiddleColumn.scss +++ b/src/components/middle/MiddleColumn.scss @@ -7,7 +7,8 @@ overflow: hidden; z-index: -1; - &::before { + &::before, + &::after { content: ""; display: block; position: absolute; @@ -15,13 +16,18 @@ left: 0; bottom: 0; right: 0; - background-color: rgb(230, 235, 238); + background-color: #F2EBCE; + } + + &::after { background-image: url('../../assets/chat-bg.jpg'); background-position: center; background-repeat: no-repeat; background-size: cover; - transition: background-image .3s ease; + .disable-animations #root & { + transition: opacity .2s !important; + } body.animation-level-0 & { transition: none; @@ -32,19 +38,29 @@ } } - .custom-bg-image > &::before { + .custom-bg-image > &::after { background-image: var(--custom-background) !important; + filter: blur(0); + transform: scale(1.1); + } + + .custom-bg-color > &::before { background-color: var(--custom-background) !important; filter: blur(0); transform: scale(1.1); } - .custom-bg-image.blurred > &::before { + .custom-bg-image.blurred > &::after { filter: blur(12px); } + .custom-bg-color > &::after { + opacity: 0; + } + @media screen and (min-width: 1276px) { - body.animation-level-2 &::before { + body.animation-level-2 &::before, + body.animation-level-2 &::after { margin: -16rem -5rem -20rem 0; overflow: hidden; transform: scale(1); @@ -52,12 +68,12 @@ transition: transform var(--layer-transition); } - body.animation-level-2 .custom-bg-image > &::before { + body.animation-level-2 .custom-bg-image > &::after { margin: -16rem -5rem -20rem -1rem; - transition: transform var(--layer-transition), background .3s ease; + transition: transform var(--layer-transition); } - body.animation-level-2 #Main.right-column-open :not(.custom-bg-image) > &::before { + body.animation-level-2 #Main.right-column-open :not(.custom-bg-image) > &::after { transform: scale(0.67); } } @@ -101,10 +117,10 @@ width: 100%; padding: 1rem; border-radius: var(--border-radius-messages); - background: white; + background: var(--color-background); color: var(--color-text-secondary); text-align: center; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); } } @@ -312,6 +328,7 @@ } } + .custom-bg-color &::before, .custom-bg-image &::before { display: none; } @@ -330,7 +347,7 @@ color: var(--color-black); height: 3.125rem; overflow: visible; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); &:hover { .icon-unpin { diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 134fbd248..7b46b40af 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -59,6 +59,7 @@ type StateProps = { messageSendingRestrictionReason?: string; hasPinnedOrAudioMessage?: boolean; customBackground?: string; + isCustomBackgroundColor?: boolean; isRightColumnShown?: boolean; isBackgroundBlurred?: boolean; isMobileSearchActive?: boolean; @@ -84,6 +85,7 @@ const MiddleColumn: FC = ({ messageSendingRestrictionReason, hasPinnedOrAudioMessage, customBackground, + isCustomBackgroundColor, isRightColumnShown, isBackgroundBlurred, isMobileSearchActive, @@ -166,7 +168,8 @@ const MiddleColumn: FC = ({ const className = buildClassName( hasTools && 'has-header-tools', - customBackground && 'custom-bg-image', + customBackground && !isCustomBackgroundColor && 'custom-bg-image', + customBackground && isCustomBackgroundColor && 'custom-bg-color', customBackground && isBackgroundBlurred && 'blurred', MASK_IMAGE_ENABLED ? 'mask-image-enabled' : 'mask-image-disabled', ); @@ -293,12 +296,14 @@ export default memo(withGlobal( (global): StateProps => { const { isBackgroundBlurred, customBackground } = global.settings.byKey; + const isCustomBackgroundColor = Boolean((customBackground || '').match(/^#[a-f\d]{6,8}$/i)); const currentMessageList = selectCurrentMessageList(global); const { chats: { listIds } } = global; if (!currentMessageList || !listIds.active) { return { customBackground, isBackgroundBlurred, + isCustomBackgroundColor, }; } @@ -320,6 +325,7 @@ export default memo(withGlobal( messageSendingRestrictionReason: chat && getMessageSendingRestrictionReason(chat), hasPinnedOrAudioMessage: Boolean(pinnedIds && pinnedIds.length) || Boolean(audioChatId && audioMessageId), customBackground, + isCustomBackgroundColor, isRightColumnShown: selectIsRightColumnShown(global), isBackgroundBlurred, isMobileSearchActive: Boolean(IS_MOBILE_SCREEN && selectCurrentTextSearch(global)), diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index b059ff6c8..a996e1547 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -9,7 +9,7 @@ right: 0; height: 2.875rem; overflow: hidden; - box-shadow: 0 2px 2px rgba(114, 114, 114, 0.17); + box-shadow: 0 2px 2px var(--color-light-shadow); display: flex; flex-direction: row-reverse; @@ -25,7 +25,7 @@ left: 0; right: 0; height: 2px; - box-shadow: 0 2px 2px rgba(114, 114, 114, 0.17); + box-shadow: 0 2px 2px var(--color-light-shadow); } .HeaderPinnedMessage { @@ -73,8 +73,8 @@ display: flex; align-items: center; width: 100%; - box-shadow: 0 2px 2px rgba(114, 114, 114, 0.17); - background: #fff; + box-shadow: 0 2px 2px var(--color-light-shadow); + background: var(--color-background); padding: .5rem .8125rem .5rem 1.5rem; position: relative; z-index: var(--z-middle-header); diff --git a/src/components/middle/MobileSearch.scss b/src/components/middle/MobileSearch.scss index c26fae35f..205fb09e4 100644 --- a/src/components/middle/MobileSearch.scss +++ b/src/components/middle/MobileSearch.scss @@ -5,7 +5,7 @@ z-index: var(--z-mobile-search); width: 100%; height: 3.5rem; - background: white; + background: var(--color-background); display: flex; align-items: center; padding: 0 0.5rem 0 0.25rem; @@ -23,7 +23,7 @@ z-index: var(--z-mobile-search); width: 100%; height: 3.5rem; - background: white; + background: var(--color-background); display: flex; align-items: center; padding-left: 1rem; diff --git a/src/components/middle/ScrollDownButton.scss b/src/components/middle/ScrollDownButton.scss index ef7d28b4a..375551b60 100644 --- a/src/components/middle/ScrollDownButton.scss +++ b/src/components/middle/ScrollDownButton.scss @@ -27,7 +27,7 @@ align-items: center; > .Button { - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); i { font-size: 1.75rem; diff --git a/src/components/middle/composer/AttachmentModal.scss b/src/components/middle/composer/AttachmentModal.scss index c395ac70a..b952891ca 100644 --- a/src/components/middle/composer/AttachmentModal.scss +++ b/src/components/middle/composer/AttachmentModal.scss @@ -66,7 +66,7 @@ position: relative; .form-control { - background: white; + background: var(--color-background); } .MentionMenu { diff --git a/src/components/middle/composer/BotKeyboardMenu.scss b/src/components/middle/composer/BotKeyboardMenu.scss index a459fbe2e..000cbf8ac 100644 --- a/src/components/middle/composer/BotKeyboardMenu.scss +++ b/src/components/middle/composer/BotKeyboardMenu.scss @@ -32,7 +32,7 @@ min-height: 3.0625rem; border-radius: var(--border-radius-messages-small); border: 2px solid var(--color-primary); - background: #fff; + background: var(--color-background); color: var(--color-primary); font-weight: 500; text-transform: none; diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index fc95ec6c2..b85805caf 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -150,10 +150,10 @@ #message-compose { flex-grow: 1; max-width: calc(100% - 4rem); - background: white; + background: var(--color-background); border-radius: var(--border-radius-messages); border-bottom-right-radius: 0; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); position: relative; z-index: 1; @@ -169,6 +169,10 @@ background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PGZpbHRlciB4PSItNTAlIiB5PSItMTQuNyUiIHdpZHRoPSIyMDAlIiBoZWlnaHQ9IjE0MS4yJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94IiBpZD0iYSI+PGZlT2Zmc2V0IGR5PSIxIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMSIgaW49InNoYWRvd09mZnNldE91dGVyMSIgcmVzdWx0PSJzaGFkb3dCbHVyT3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAuMDYyMTk2MjQ4MiAwIDAgMCAwIDAuMTM4NTc0MTQ0IDAgMCAwIDAgMC4xODUwMzczNjQgMCAwIDAgMC4xNSAwIiBpbj0ic2hhZG93Qmx1ck91dGVyMSIvPjwvZmlsdGVyPjxwYXRoIGQ9Ik0zIDE3aDZWMGMtLjE5MyAyLjg0LS44NzYgNS43NjctMi4wNSA4Ljc4Mi0uOTA0IDIuMzI1LTIuNDQ2IDQuNDg1LTQuNjI1IDYuNDhBMSAxIDAgMDAzIDE3eiIgaWQ9ImIiLz48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PC9nPjwvc3ZnPg==); background-position: bottom left; transform: scaleX(-1); + + .theme-dark & { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIiB3aWR0aD0iMjA1IiBoZWlnaHQ9IjIwMCI+PGZlT2Zmc2V0IHJlc3VsdD0ib2ZmT3V0IiBpbj0iU291cmNlQWxwaGEiIGR5PSIxIi8+PGZlQ29sb3JNYXRyaXggcmVzdWx0PSJtYXRyaXhPdXQiIGluPSJvZmZPdXQiIHZhbHVlcz0iMC4xMyAwIDAgMCAwIDAgMC4xMyAwIDAgMCAwIDAgMC4xMyAwIDAgMCAwIDAgMC42IDAiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJibHVyT3V0IiBpbj0ibWF0cml4T3V0IiBzdGREZXZpYXRpb249IjEiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJibHVyT3V0Ii8+PC9maWx0ZXI+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTMgMTdoNlYwYy0uMTkzIDIuODQtLjg3NiA1Ljc2Ny0yLjA1IDguNzgyLS45MDQgMi4zMjUtMi40NDYgNC40ODUtNC42MjUgNi40OEExIDEgMCAwMDMgMTd6IiBmaWxsPSIjMDAwIiBmaWx0ZXI9InVybCgjYSkiLz48cGF0aCBkPSJNMyAxN2g2VjBjLS4xOTMgMi44NC0uODc2IDUuNzY3LTIuMDUgOC43ODItLjkwNCAyLjMyNS0yLjQ0NiA0LjQ4NS00LjYyNSA2LjQ4QTEgMSAwIDAwMyAxN3oiIGZpbGw9IiMyMTIxMjEiLz48L2c+PC9zdmc+); + } } @media (max-width: 600px) { @@ -203,13 +207,13 @@ position: absolute; top: .75rem; right: .75rem; - border: .1875rem solid #fff; + border: .1875rem solid var(--color-background); box-sizing: content-box; width: .5rem; height: .5rem; border-radius: 50%; background: var(--color-green-darker); - box-shadow: -.375rem -.25rem 0 -.1875rem #fff; + box-shadow: -.375rem -.25rem 0 -.1875rem var(--color-background); @media (max-width: 600px) { top: .5rem; right: .5rem; diff --git a/src/components/middle/composer/DropTarget.scss b/src/components/middle/composer/DropTarget.scss index 4700513be..4b682667f 100644 --- a/src/components/middle/composer/DropTarget.scss +++ b/src/components/middle/composer/DropTarget.scss @@ -1,6 +1,6 @@ .DropTarget { border-radius: var(--border-radius-default); - background: #fff; + background: var(--color-background); padding: 1.25rem; flex: 1 1 auto; width: 100%; @@ -10,7 +10,7 @@ margin-bottom: .3125rem; display: flex; color: #A4ACB3; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); @media (max-height: 350px) { padding: .75rem; diff --git a/src/components/middle/composer/EmojiPicker.scss b/src/components/middle/composer/EmojiPicker.scss index 42a5c9581..a27946c20 100644 --- a/src/components/middle/composer/EmojiPicker.scss +++ b/src/components/middle/composer/EmojiPicker.scss @@ -17,7 +17,7 @@ display: flex; align-items: center; justify-content: space-around; - box-shadow: 0 0 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 0 2px var(--color-default-shadow); @media (max-width: 600px) { overflow-x: auto; diff --git a/src/components/middle/composer/EmojiTooltip.scss b/src/components/middle/composer/EmojiTooltip.scss index 32ead605e..15314fe9b 100644 --- a/src/components/middle/composer/EmojiTooltip.scss +++ b/src/components/middle/composer/EmojiTooltip.scss @@ -3,7 +3,7 @@ bottom: calc(100% + .5rem); left: 0; width: 100%; - background: white; + background: var(--color-background); border-radius: var(--border-radius-messages); padding: 0.5rem 0; max-height: 15rem; @@ -15,7 +15,7 @@ grid-auto-rows: auto; place-items: center; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); opacity: 0; transform: translateY(1.5rem); diff --git a/src/components/middle/composer/MentionMenu.scss b/src/components/middle/composer/MentionMenu.scss index 8e0299f5b..a96fa9c2b 100644 --- a/src/components/middle/composer/MentionMenu.scss +++ b/src/components/middle/composer/MentionMenu.scss @@ -4,14 +4,14 @@ left: 0; width: calc(100% - 4rem); max-width: 20rem; - background: white; + background: var(--color-background); border-radius: var(--border-radius-messages); padding: 0.5rem 0; max-height: 15rem; overflow-x: hidden; overflow-y: auto; - box-shadow: 3px 3px 5px rgba(114, 114, 114, 0.25); + box-shadow: 3px 3px 5px var(--color-default-shadow); z-index: -1; opacity: 0; diff --git a/src/components/middle/composer/StickerPicker.scss b/src/components/middle/composer/StickerPicker.scss index fc5b964a6..06c04f856 100644 --- a/src/components/middle/composer/StickerPicker.scss +++ b/src/components/middle/composer/StickerPicker.scss @@ -19,7 +19,7 @@ overflow-x: auto; overflow-y: hidden; white-space: nowrap; - box-shadow: 0 0 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 0 2px var(--color-default-shadow); scrollbar-width: none; scrollbar-color: rgba(0, 0, 0, 0); diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index 4d44f309e..6b0c8df20 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -32,7 +32,7 @@ display: flex; align-items: center; justify-content: center; - box-shadow: 0 0 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 0 2px var(--color-default-shadow); position: relative; .Button { diff --git a/src/components/middle/composer/TextFormatter.scss b/src/components/middle/composer/TextFormatter.scss index a293f0bfa..1eddb5ec4 100644 --- a/src/components/middle/composer/TextFormatter.scss +++ b/src/components/middle/composer/TextFormatter.scss @@ -3,10 +3,10 @@ &, &-link-control { position: absolute; - background: white; + background: var(--color-background); border-radius: var(--border-radius-messages); padding: 0.5rem 0.375rem; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); } &-link-control { @@ -24,6 +24,8 @@ border: none !important; outline: none !important; width: 100%; + color: var(--color-text); + background-color: var(--color-background); } } @@ -62,12 +64,12 @@ &::before { left: 0; - background: linear-gradient(to right, #fff .25rem, transparent 1rem) + background: linear-gradient(to right, var(--color-background) .25rem, transparent 1rem) } &::after { right: 0; - background: linear-gradient(to left, #fff .25rem, transparent 1rem) + background: linear-gradient(to left, var(--color-background) .25rem, transparent 1rem) } &.mask-left { diff --git a/src/components/middle/message/CommentButton.scss b/src/components/middle/message/CommentButton.scss index 521c9228e..32de3a4c0 100644 --- a/src/components/middle/message/CommentButton.scss +++ b/src/components/middle/message/CommentButton.scss @@ -1,6 +1,6 @@ .CommentButton { - --background-color: #fff; - --hover-color: #f4f4f4; + --background-color: var(--color-background); + --hover-color: var(--color-chat-hover); display: flex; width: 100%; @@ -30,9 +30,13 @@ width: .5625rem; height: 1.25rem; background-position: bottom left; - background-image: url("data:image/svg+xml,%3Csvg width='9' height='20' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cfilter x='-50%25' y='-14.7%25' width='200%25' height='141.2%25' filterUnits='objectBoundingBox' id='a'%3E%3CfeOffset dy='1' in='SourceAlpha' result='shadowOffsetOuter1'/%3E%3CfeGaussianBlur stdDeviation='1' in='shadowOffsetOuter1' result='shadowBlurOuter1'/%3E%3CfeColorMatrix values='0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0' in='shadowBlurOuter1'/%3E%3C/filter%3E%3Cpath d='M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z' id='b'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cuse fill='%23000' filter='url(%23a)' xlink:href='%23b'/%3E%3Cuse fill='%23f4f4f4' xlink:href='%23b'/%3E%3C/g%3E%3C/svg%3E"); + background-image: url('data:image/svg+xml,%3Csvg width="9" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cdefs%3E%3Cfilter x="-50%25" y="-14.7%25" width="200%25" height="141.2%25" filterUnits="objectBoundingBox" id="a"%3E%3CfeOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/%3E%3CfeGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/%3E%3CfeColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/%3E%3C/filter%3E%3Cpath d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" id="b"/%3E%3C/defs%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cuse fill="%23000" filter="url(%23a)" xlink:href="%23b"/%3E%3Cuse fill="%23FFF" xlink:href="%23b"/%3E%3C/g%3E%3C/svg%3E'); opacity: 0; - transition: opacity .15s; + transition: opacity .15s, filter .15s; + + .theme-dark #root & { + filter: invert(.83); + } body.animation-level-0 & { transition: none !important; @@ -162,7 +166,7 @@ .Avatar { transition: border .15s; - border: 2px solid #fff; + border: 2px solid var(--color-background); margin-right: 0; z-index: 3; diff --git a/src/components/middle/message/Invoice.scss b/src/components/middle/message/Invoice.scss index c13171265..2688232b4 100644 --- a/src/components/middle/message/Invoice.scss +++ b/src/components/middle/message/Invoice.scss @@ -16,7 +16,7 @@ height: 10rem; } } - + .description-text { position: absolute; top: 0; @@ -24,10 +24,10 @@ margin: .25rem; background-color: rgba(90, 110, 70, 0.6); border-radius: var(--border-radius-messages-small); - color: var(--color-white); + color: var(--color-text); font-weight: 500; } } - + } } diff --git a/src/components/middle/message/Message.scss b/src/components/middle/message/Message.scss index 2b6ea8cb6..ef80285a6 100644 --- a/src/components/middle/message/Message.scss +++ b/src/components/middle/message/Message.scss @@ -5,7 +5,7 @@ margin-bottom: 0.375rem; position: relative; - --background-color: white; + --background-color: var(--color-background); --hover-color: rgba(var(--color-text-secondary-rgb), 0.08); --active-color: rgba(var(--color-text-secondary-rgb), 0.16); --max-width: 29rem; @@ -81,16 +81,19 @@ &.own { flex-direction: row-reverse; --background-color: var(--color-background-own); - --hover-color: rgba(var(--color-text-green-rgb), 0.12); - --active-color: rgba(var(--color-text-green-rgb), 0.24); + --hover-color: var(--color-reply-own-hover); + --active-color: var(--color-reply-own-active); --max-width: 30rem; - --accent-color: var(--color-text-green); + --accent-color: var(--color-accent-own); --accent-shade-color: var(--color-green); - --secondary-color: var(--color-text-green); + --secondary-color: var(--color-accent-own); --color-code: var(--color-code-own); --color-code-bg: var(--color-code-own-bg); + --color-links: var(--color-own-links); + --color-links-hover: var(--color-own-links); --meta-safe-area-base: 3.5rem; --deleting-translate-x: 50%; + --color-text-green: var(--color-accent-own); @media (max-width: 600px) { padding-right: 0.25rem; @@ -177,6 +180,15 @@ bottom: 0.813rem; } + html.theme-dark &.own .Audio .ProgressSpinner { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMTUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTguMjE4IDcuNWw1LjYzMy01LjYzM2EuNTA4LjUwOCAwIDEwLS43MTgtLjcxOEw3LjUgNi43ODIgMS44NjcgMS4xNDlhLjUwOC41MDggMCAxMC0uNzE4LjcxOEw2Ljc4MiA3LjVsLTUuNjMzIDUuNjMzYS41MDguNTA4IDAgMTAuNzE4LjcxOEw3LjUgOC4yMThsNS42MzMgNS42MzNhLjUwNi41MDYgMCAwMC43MTggMCAuNTA4LjUwOCAwIDAwMC0uNzE4TDguMjE4IDcuNXoiIGZpbGw9IiNGRkYiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjQTQ1RDM3IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+); + + circle { + stroke: var(--background-color); + } + } + + .File { position: relative; diff --git a/src/components/middle/message/MessageMeta.scss b/src/components/middle/message/MessageMeta.scss index 626f78b81..b02e0f1f5 100644 --- a/src/components/middle/message/MessageMeta.scss +++ b/src/components/middle/message/MessageMeta.scss @@ -72,12 +72,15 @@ } .Message.own .has-solid-background & { - color: var(--color-text-green); + color: var(--color-message-meta-own); } .MessageOutgoingStatus { margin-left: -.1875rem; font-size: 1.1875rem; + .Message.own & { + color: var(--color-accent-own); + } } .message-content.has-replies:not(.custom-shape) & { diff --git a/src/components/middle/message/Poll.scss b/src/components/middle/message/Poll.scss index 785bfa356..7ccbd9d1d 100644 --- a/src/components/middle/message/Poll.scss +++ b/src/components/middle/message/Poll.scss @@ -109,7 +109,7 @@ margin-top: -2px; .Avatar { - border: 1px solid #fff; + border: 1px solid var(--color-white); margin-right: 0; box-sizing: content-box; diff --git a/src/components/middle/message/PollOption.scss b/src/components/middle/message/PollOption.scss index 9cb991b46..9f45c8129 100644 --- a/src/components/middle/message/PollOption.scss +++ b/src/components/middle/message/PollOption.scss @@ -35,7 +35,7 @@ width: 1rem; height: 1rem; background: var(--accent-color); - color: #fff; + color: var(--background-color); border-radius: .5rem; font-size: .75rem; text-align: center; diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index 7d6315f83..dca2290e8 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -98,7 +98,7 @@ } &.has-shadow { - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); } &.has-solid-background, .is-album & { @@ -123,6 +123,10 @@ background-position: bottom right; background-image: url('data:image/svg+xml,%3Csvg width="9" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cdefs%3E%3Cfilter x="-50%25" y="-14.7%25" width="200%25" height="141.2%25" filterUnits="objectBoundingBox" id="a"%3E%3CfeOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/%3E%3CfeGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/%3E%3CfeColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/%3E%3C/filter%3E%3Cpath d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" id="b"/%3E%3C/defs%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cuse fill="%23000" filter="url(%23a)" xlink:href="%23b"/%3E%3Cuse fill="%23EEFFDE" xlink:href="%23b"/%3E%3C/g%3E%3C/svg%3E'); } + + .theme-dark &:not([data-has-custom-appendix])::before { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iMCIgeT0iMCIgd2lkdGg9IjIwNSIgaGVpZ2h0PSIyMDAiPjxmZU9mZnNldCByZXN1bHQ9Im9mZk91dCIgaW49IlNvdXJjZUFscGhhIiBkeD0iLTEiIGR5PSIxIi8+PGZlQ29sb3JNYXRyaXggcmVzdWx0PSJtYXRyaXhPdXQiIGluPSJvZmZPdXQiIHZhbHVlcz0iMC4xMyAwIDAgMCAwIDAgMC4xMyAwIDAgMCAwIDAgMC4xMyAwIDAgMCAwIDAgMC42IDAiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJibHVyT3V0IiBpbj0ibWF0cml4T3V0IiBzdGREZXZpYXRpb249IjEiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJibHVyT3V0Ii8+PC9maWx0ZXI+PHBhdGggZD0iTTYgMTdIMFYwYy4xOTMgMi44NC44NzYgNS43NjcgMi4wNSA4Ljc4Mi45MDQgMi4zMjUgMi40NDYgNC40ODUgNC42MjUgNi40OEExIDEgMCAwMTYgMTd6IiBpZD0iYiIvPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjx1c2UgZmlsbD0iIzAwMCIgZmlsdGVyPSJ1cmwoI2EpIiB4bGluazpocmVmPSIjYiIvPjx1c2UgZmlsbD0iIzlBNUYzRiIgeGxpbms6aHJlZj0iI2IiLz48L2c+PC9zdmc+); + } } .Message:not(.own) & { @@ -131,6 +135,10 @@ background-position: bottom left; background-image: url('data:image/svg+xml,%3Csvg width="9" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cdefs%3E%3Cfilter x="-50%25" y="-14.7%25" width="200%25" height="141.2%25" filterUnits="objectBoundingBox" id="a"%3E%3CfeOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/%3E%3CfeGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/%3E%3CfeColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/%3E%3C/filter%3E%3Cpath d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" id="b"/%3E%3C/defs%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cuse fill="%23000" filter="url(%23a)" xlink:href="%23b"/%3E%3Cuse fill="%23FFF" xlink:href="%23b"/%3E%3C/g%3E%3C/svg%3E'); } + + .theme-dark &:not([data-has-custom-appendix])::before { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIiB3aWR0aD0iMjA1IiBoZWlnaHQ9IjIwMCI+PGZlT2Zmc2V0IHJlc3VsdD0ib2ZmT3V0IiBpbj0iU291cmNlQWxwaGEiIGR5PSIxIi8+PGZlQ29sb3JNYXRyaXggcmVzdWx0PSJtYXRyaXhPdXQiIGluPSJvZmZPdXQiIHZhbHVlcz0iMC4xMyAwIDAgMCAwIDAgMC4xMyAwIDAgMCAwIDAgMC4xMyAwIDAgMCAwIDAgMC42IDAiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJibHVyT3V0IiBpbj0ibWF0cml4T3V0IiBzdGREZXZpYXRpb249IjEiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJibHVyT3V0Ii8+PC9maWx0ZXI+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTMgMTdoNlYwYy0uMTkzIDIuODQtLjg3NiA1Ljc2Ny0yLjA1IDguNzgyLS45MDQgMi4zMjUtMi40NDYgNC40ODUtNC42MjUgNi40OEExIDEgMCAwMDMgMTd6IiBmaWxsPSIjMDAwIiBmaWx0ZXI9InVybCgjYSkiLz48cGF0aCBkPSJNMyAxN2g2VjBjLS4xOTMgMi44NC0uODc2IDUuNzY3LTIuMDUgOC43ODItLjkwNCAyLjMyNS0yLjQ0NiA0LjQ4NS00LjYyNSA2LjQ4QTEgMSAwIDAwMyAxN3oiIGZpbGw9IiMyMTIxMjEiLz48L2c+PC9zdmc+); + } } &:not(.has-solid-background) { diff --git a/src/components/payment/PaymentModal.scss b/src/components/payment/PaymentModal.scss index 29ba22ce2..0677777b0 100644 --- a/src/components/payment/PaymentModal.scss +++ b/src/components/payment/PaymentModal.scss @@ -12,7 +12,7 @@ display: flex; align-items: center; flex-direction: row; - background: white; + background: var(--color-background); border-bottom: 1px var(--color-borders) solid; h3 { @@ -51,7 +51,7 @@ border-bottom-right-radius: 10px; width: 100%; padding: .75rem 1rem; - background: white; + background: var(--color-background); border-top: 1px var(--color-borders) solid; button { diff --git a/src/components/right/PollAnswerResults.scss b/src/components/right/PollAnswerResults.scss index 958e7acc6..96371691d 100644 --- a/src/components/right/PollAnswerResults.scss +++ b/src/components/right/PollAnswerResults.scss @@ -14,7 +14,7 @@ padding: 1rem .75rem .5rem 1rem; position: sticky; top: 0; - background: #fff; + background: var(--color-background); @media (max-width: 600px) { padding: .5rem .25rem .5rem .5rem; diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index b44ac9b42..3a33cf147 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -51,7 +51,7 @@ flex-direction: column-reverse; .TabList { - background: #fff; + background: var(--color-background); top: -1px; .Tab { padding: .6875rem .25rem; diff --git a/src/components/right/RightColumn.scss b/src/components/right/RightColumn.scss index b8fd8bdee..93c07a851 100644 --- a/src/components/right/RightColumn.scss +++ b/src/components/right/RightColumn.scss @@ -23,7 +23,7 @@ } @media (max-width: 1275px) { - box-shadow: 0 .25rem .5rem .1rem rgba(114, 114, 114, 0.25); + box-shadow: 0 .25rem .5rem .1rem var(--color-default-shadow); } @media (max-width: 600px) { diff --git a/src/components/ui/AvatarEditable.scss b/src/components/ui/AvatarEditable.scss index a7b5a1d07..c4ed3a652 100644 --- a/src/components/ui/AvatarEditable.scss +++ b/src/components/ui/AvatarEditable.scss @@ -55,7 +55,7 @@ } &.filled { - background-color: white; + background-color: var(--color-background); &::after { content: ''; diff --git a/src/components/ui/Checkbox.scss b/src/components/ui/Checkbox.scss index 9a88bf122..1f88a3172 100644 --- a/src/components/ui/Checkbox.scss +++ b/src/components/ui/Checkbox.scss @@ -79,7 +79,7 @@ &::before { border: 2px solid var(--color-borders); border-radius: .25rem; - background-color: white; + background-color: var(--color-white); transition: border-color .1s ease; } diff --git a/src/components/ui/CropModal.scss b/src/components/ui/CropModal.scss index 03689c537..b9835fd2f 100644 --- a/src/components/ui/CropModal.scss +++ b/src/components/ui/CropModal.scss @@ -26,7 +26,7 @@ position: absolute; bottom: 1rem; right: 1rem; - box-shadow: 0 1px 2px rgba(114, 114, 114, 0.25); + box-shadow: 0 1px 2px var(--color-default-shadow); } #avatar-crop { diff --git a/src/components/ui/Menu.scss b/src/components/ui/Menu.scss index a0e7308d2..d91aa7f87 100644 --- a/src/components/ui/Menu.scss +++ b/src/components/ui/Menu.scss @@ -19,8 +19,8 @@ padding: 0.5rem 0; margin: 0; position: absolute; - background-color: white; - box-shadow: 0 .25rem .5rem .1rem rgba(114, 114, 114, 0.25); + background-color: var(--color-background); + box-shadow: 0 .25rem .5rem .1rem var(--color-default-shadow); border-radius: var(--border-radius-default); min-width: 13.5rem; z-index: var(--z-menu-bubble); diff --git a/src/components/ui/MenuItem.scss b/src/components/ui/MenuItem.scss index 37a22df63..a11141138 100644 --- a/src/components/ui/MenuItem.scss +++ b/src/components/ui/MenuItem.scss @@ -24,6 +24,10 @@ color: var(--color-text-secondary); } + .menu-item-name { + margin-right: 2rem; + } + &.disabled { opacity: 0.5 !important; cursor: default !important; @@ -41,4 +45,8 @@ background-color: var(--color-chat-active); transition: none !important; } + + & > .Switcher { + margin-left: auto; + } } diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index fde311faf..74dfd7270 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -47,8 +47,8 @@ max-width: 35rem; min-width: 17.5rem; margin: 2rem auto; - background-color: white; - box-shadow: 0 .25rem .5rem .1rem rgba(114, 114, 114, 0.25); + background-color: var(--color-background); + box-shadow: 0 .25rem .5rem .1rem var(--color-default-shadow); border-radius: var(--border-radius-default); transform: translate3d(0, -1rem, 0); diff --git a/src/components/ui/Radio.scss b/src/components/ui/Radio.scss index 4f0e11622..f6daa18ac 100644 --- a/src/components/ui/Radio.scss +++ b/src/components/ui/Radio.scss @@ -42,7 +42,7 @@ &::before { border: 2px solid var(--color-borders); border-radius: 50%; - background-color: white; + background-color: var(--color-white); opacity: 1; transition: border-color .1s ease, opacity .1s ease; } diff --git a/src/components/ui/SearchInput.scss b/src/components/ui/SearchInput.scss index fd5bc29f2..6b81a769a 100644 --- a/src/components/ui/SearchInput.scss +++ b/src/components/ui/SearchInput.scss @@ -22,7 +22,7 @@ &.has-focus { border-color: var(--color-primary); caret-color: var(--color-primary); - background-color: #fff; + background-color: var(--color-background); input { & + i { diff --git a/src/components/ui/Switcher.scss b/src/components/ui/Switcher.scss new file mode 100644 index 000000000..a0b9a0591 --- /dev/null +++ b/src/components/ui/Switcher.scss @@ -0,0 +1,53 @@ +.Switcher { + display: inline-flex; + align-items: center; + position: relative; + margin: 0; + + &.disabled { + pointer-events: none; + opacity: 0.5; + } + + input { + height: 0; + width: 0; + visibility: hidden; + position: absolute; + z-index: var(--z-below); + opacity: 0; + } + + .widget { + cursor: pointer; + text-indent: -999px; + width: 2.125rem; + height: 0.875rem; + background: var(--color-gray); + display: inline-block; + border-radius: .5rem; + position: relative; + } + + .widget:after { + content: ''; + position: absolute; + top: -.125rem; + left: 0; + width: 1.125rem; + height: 1.125rem; + background: var(--color-background); + border-radius: .75rem; + border: .125rem solid var(--color-gray); + } + + input:checked + .widget { + background: var(--color-primary); + } + + input:checked + .widget:after { + left: calc(100% - 1.125rem); + transform: translateX(calc(-100% + 1.125rem)); + border-color: var(--color-primary); + } +} diff --git a/src/components/ui/Switcher.tsx b/src/components/ui/Switcher.tsx new file mode 100644 index 000000000..705748cea --- /dev/null +++ b/src/components/ui/Switcher.tsx @@ -0,0 +1,60 @@ +import { ChangeEvent } from 'react'; +import React, { FC, memo, useCallback } from '../../lib/teact/teact'; + +import buildClassName from '../../util/buildClassName'; + +import './Switcher.scss'; + +type OwnProps = { + id?: string; + name?: string; + value?: string; + label: string; + checked?: boolean; + disabled?: boolean; + onChange?: (e: ChangeEvent) => void; + onCheck?: (isChecked: boolean) => void; +}; + +const Switcher: FC = ({ + id, + name, + value, + label, + checked = false, + disabled, + onChange, + onCheck, +}) => { + const handleChange = useCallback((event: ChangeEvent) => { + if (onChange) { + onChange(event); + } + + if (onCheck) { + onCheck(event.currentTarget.checked); + } + }, [onChange, onCheck]); + + const className = buildClassName( + 'Switcher', + disabled && 'disabled', + ); + + return ( + + ); +}; + +export default memo(Switcher); diff --git a/src/components/ui/TabList.scss b/src/components/ui/TabList.scss index 313c565c6..73db6fa4d 100644 --- a/src/components/ui/TabList.scss +++ b/src/components/ui/TabList.scss @@ -7,8 +7,8 @@ align-items: flex-end; font-size: 0.875rem; flex-wrap: nowrap; - box-shadow: 0 2px 2px rgba(114, 114, 114, 0.17); - background-color: white; + box-shadow: 0 2px 2px var(--color-light-shadow); + background-color: var(--color-background); overflow-x: auto; overflow-y: hidden; diff --git a/src/components/ui/Transition.scss b/src/components/ui/Transition.scss index 33af30b12..3b1f03e14 100644 --- a/src/components/ui/Transition.scss +++ b/src/components/ui/Transition.scss @@ -5,6 +5,7 @@ width: 100%; height: 100%; animation-fill-mode: forwards !important; + transition: background-color .2s; &.from, &.to { position: absolute; @@ -245,7 +246,7 @@ */ &.slide-layers { - --background-color: var(--color-white); + --background-color: var(--color-background); background: black; > div { @@ -290,7 +291,7 @@ &.push-slide { > div { - background: white; + background: var(--color-background); } > .from { diff --git a/src/global/initial.ts b/src/global/initial.ts index d9f1d358f..93e1916ed 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -103,6 +103,7 @@ export const INITIAL_STATE: GlobalState = { isBackgroundBlurred: true, animationLevel: ANIMATION_LEVEL_DEFAULT, messageSendKeyCombo: 'enter', + theme: 'light', shouldAutoDownloadMediaFromContacts: true, shouldAutoDownloadMediaInPrivateChats: true, shouldAutoDownloadMediaInGroups: true, diff --git a/src/modules/actions/ui/initial.ts b/src/modules/actions/ui/initial.ts index 882a7f619..030ec893d 100644 --- a/src/modules/actions/ui/initial.ts +++ b/src/modules/actions/ui/initial.ts @@ -4,15 +4,20 @@ import { IS_ANDROID, IS_IOS, IS_SAFARI, IS_TOUCH_ENV, } from '../../../util/environment'; import { setLanguage } from '../../../util/langProvider'; +import switchTheme from '../../../util/switchTheme'; addReducer('init', (global) => { - const { animationLevel, messageTextSize, language } = global.settings.byKey; + const { + theme, animationLevel, messageTextSize, language, + } = global.settings.byKey; setLanguage(language); document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`); + document.body.classList.add('initial'); document.body.classList.add(`animation-level-${animationLevel}`); document.body.classList.add(IS_TOUCH_ENV ? 'is-touch-env' : 'is-pointer-env'); + switchTheme(theme, animationLevel > 0); if (IS_SAFARI) { document.body.classList.add('is-safari'); @@ -27,6 +32,10 @@ addReducer('init', (global) => { addReducer('setIsUiReady', (global, actions, payload) => { const { uiReadyState } = payload!; + if (uiReadyState === 2) { + document.body.classList.remove('initial'); + } + return { ...global, uiReadyState, diff --git a/src/styles/_forms.scss b/src/styles/_forms.scss index d5ca35059..c35d0235d 100644 --- a/src/styles/_forms.scss +++ b/src/styles/_forms.scss @@ -70,9 +70,10 @@ width: 100%; height: 3.375rem; padding: calc(0.75rem - var(--border-width)) calc(.9rem - var(--border-width)); - border: var(--border-width) solid var(--color-borders); + border: var(--border-width) solid var(--color-borders-input); border-radius: var(--border-radius-default); color: var(--color-text); + background-color: var(--color-background); outline: none; transition: border-color 0.15s ease; word-break: break-word; @@ -82,7 +83,11 @@ line-height: 1.25rem; &:hover { - border-color: var(--color-gray); + border-color: var(--color-primary); + + & + label { + color: var(--color-primary); + } } &:focus, diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 2e805f692..1ec066c92 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -64,9 +64,11 @@ $color-user-8: #faa774; --color-text-secondary-rgb: #{toRGB($color-text-secondary)}; --color-text-meta: #{$color-text-meta}; --color-text-meta-rgb: #{toRGB($color-text-meta)}; + --color-text-meta-colored: #{$color-text-green}; --color-text-green: #{$color-text-green}; --color-text-green-rgb: #{toRGB($color-text-green)}; --color-borders: #{$color-borders}; + --color-borders-input: #{$color-borders}; --color-webpage-initial-background: #{$color-dark-gray}; --color-interactive-active: var(--color-primary); --color-interactive-inactive: rgba(var(--color-text-secondary-rgb), 0.25); @@ -94,6 +96,8 @@ $color-user-8: #faa774; --color-links-darker: #{darken($color-links, 15%)}; --color-links-darker-hover: #{darken($color-links, 23%)}; + --color-own-links: #{$color-white}; + --color-placeholders: #{$color-placeholders}; --color-code: #4a729a; @@ -101,6 +105,9 @@ $color-user-8: #faa774; --color-code-own: #3c7940; --color-code-own-bg: #{rgba($color-text-secondary, .08)}; + --color-accent-own: #{$color-text-green}; + --color-message-meta-own: #{$color-text-green}; + --color-reply-hover: #{blend-normal(rgba($color-text-secondary, 0.08), $color-white)}; --color-reply-active: #{blend-normal(rgba($color-text-secondary, 0.16), $color-white)}; --color-reply-own-hover: #{blend-normal(rgba($color-text-green, 0.12), $color-light-green)}; @@ -123,6 +130,9 @@ $color-user-8: #faa774; --color-user-7: #{$color-user-7}; --color-user-8: #{$color-user-8}; + --color-default-shadow: #72727240; + --color-light-shadow: #7272722B; + --border-radius-default: 0.75rem; --border-radius-default-small: 0.625rem; --border-radius-default-tiny: 0.375rem; diff --git a/src/styles/index.scss b/src/styles/index.scss index 71f8cd05e..b027840f2 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -83,6 +83,10 @@ body.cursor-grabbing, body.cursor-grabbing * { box-sizing: border-box; } +.disable-animations #root * { + transition: none !important; +} + .custom-scroll, .custom-scroll-x { scrollbar-width: thin; diff --git a/src/styles/themes.json b/src/styles/themes.json new file mode 100644 index 000000000..6ab3e67b6 --- /dev/null +++ b/src/styles/themes.json @@ -0,0 +1,23 @@ +{ + "--color-primary": ["#50A2E9", "#868DF5"], + "--color-background": ["#FFFFFF", "#212121"], + "--color-background-own": ["#EEFEDF", "#A45D37"], + "--color-chat-hover": ["#F4F4F5", "#2C2C2C"], + "--color-chat-active": ["#ededed", "#292929"], + "--color-text": ["#000000", "#FFFFFF"], + "--color-text-secondary": ["#707579", "#AAAAAA"], + "--color-borders": ["#DADCE0", "#100F10"], + "--color-borders-input": ["#DADCE0", "#5B5B5A"], + "--color-links": ["#52A1EF", "#868DF6"], + "--color-gray": ["#C4C9CC", "#808080"], + "--color-default-shadow": ["#72727240", "#00000099"], + "--color-light-shadow": ["#7272722B", "#00000040"], + "--color-green": ["#4DCD5E", "#868DF5"], + "--color-text-meta-colored": ["#4DCD5E", "#868DF5"], + "--color-reply-own-hover": ["#DBF4CE", "#A26947"], + "--color-reply-own-active": ["#C8EBBC", "#B0714C"], + "--color-accent-own": ["#4FAE4E", "#FFFFFF"], + "--color-message-meta-own": ["#4FAE4E", "#D9BDAD"], + "--color-own-links": ["#52A1EF", "#FFFFFF"], + "--color-code-own": ["#3C7940", "#FFFFFF"] +} diff --git a/src/types/index.ts b/src/types/index.ts index 803a00131..9f8ce25c8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -26,6 +26,7 @@ export interface ISettings extends Record { isBackgroundBlurred?: boolean; animationLevel: 0 | 1 | 2; messageSendKeyCombo: 'enter' | 'ctrl-enter'; + theme: 'light' | 'dark'; shouldAutoDownloadMediaFromContacts: boolean; shouldAutoDownloadMediaInPrivateChats: boolean; shouldAutoDownloadMediaInGroups: boolean; diff --git a/src/util/switchTheme.ts b/src/util/switchTheme.ts new file mode 100644 index 000000000..3090fc20b --- /dev/null +++ b/src/util/switchTheme.ts @@ -0,0 +1,85 @@ +import { ISettings } from '../types'; + +import { animateSingle } from './animation'; + +import themeColors from '../styles/themes.json'; + +type RGBAColor = { + r: number; + g: number; + b: number; + a?: number; +}; + +let isInitialized = false; + +const HEX_COLOR_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i; +const DURATION_MS = 200; +const ENABLE_ANIMATION_DELAY_MS = 500; + +const lerp = (start: number, end: number, interpolationRatio: number) => { + return (1 - interpolationRatio) * start + interpolationRatio * end; +}; + +const colors = (Object.keys(themeColors) as Array).map((property) => ({ + property, + colors: [hexToRgb(themeColors[property][0]), hexToRgb(themeColors[property][1])], +})); + +export default (theme: ISettings['theme'], withAnimation: boolean) => { + const shouldAnimate = isInitialized && withAnimation; + const startIndex = theme === 'dark' ? 0 : 1; + const endIndex = theme === 'dark' ? 1 : 0; + const startAt = Date.now(); + + document.documentElement.classList.remove(`theme-${theme === 'dark' ? 'light' : 'dark'}`); + if (isInitialized) { + document.documentElement.classList.add('disable-animations'); + } + document.documentElement.classList.add(`theme-${theme}`); + + setTimeout(() => { + document.documentElement.classList.remove('disable-animations'); + }, ENABLE_ANIMATION_DELAY_MS); + + isInitialized = true; + + if (shouldAnimate) { + animateSingle(() => { + const t = Math.min((Date.now() - startAt) / DURATION_MS, 1); + + applyColorAnimationStep(startIndex, endIndex, transition(t)); + + return t < 1; + }); + } else { + applyColorAnimationStep(startIndex, endIndex); + } +}; + +function transition(t: number) { + return 1 - ((1 - t) ** 3.5); +} + +function hexToRgb(hex: string): RGBAColor { + const result = HEX_COLOR_REGEX.exec(hex)!; + + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + a: result[4] ? parseInt(result[4], 16) : undefined, + }; +} + +function applyColorAnimationStep(startIndex: number, endIndex: number, interpolationRatio: number = 1) { + colors.forEach(({ property, colors: propertyColors }) => { + const r = Math.round(lerp(propertyColors[startIndex].r, propertyColors[endIndex].r, interpolationRatio)); + const g = Math.round(lerp(propertyColors[startIndex].g, propertyColors[endIndex].g, interpolationRatio)); + const b = Math.round(lerp(propertyColors[startIndex].b, propertyColors[endIndex].b, interpolationRatio)); + const a = propertyColors[startIndex].a + && Math.round(lerp(propertyColors[startIndex].a!, propertyColors[endIndex].a!, interpolationRatio)); + + document.documentElement.style.setProperty(property, a ? `rgba(${r},${g},${b},${a / 255})` : `rgb(${r},${g},${b})`); + }); +}