diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 3081e93405..b0b0e733b9 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -5021,11 +5021,17 @@ button.module-conversation-details__action-button { align-items: center; display: flex; justify-content: center; - background-color: $color-gray-75; border-radius: 48px; height: 48px; width: 48px; + @include light-theme { + background-color: $color-gray-65; + } + @include dark-theme { + background-color: $color-gray-75; + } + &:after { content: ''; height: 17px; diff --git a/ts/background.ts b/ts/background.ts index bc6a49828d..c94b893128 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -326,7 +326,7 @@ type WhatIsThis = import('./window.d').WhatIsThis; window.Events = { getDeviceName: () => window.textsecure.storage.user.getDeviceName(), - getThemeSetting: () => + getThemeSetting: (): 'light' | 'dark' | 'system' => window.storage.get( 'theme-setting', window.platform === 'darwin' ? 'system' : 'light' @@ -751,6 +751,7 @@ type WhatIsThis = import('./window.d').WhatIsThis; platform: window.platform, i18n: window.i18n, interactionMode: window.getInteractionMode(), + theme: window.Events.getThemeSetting(), }, }; @@ -2162,6 +2163,11 @@ type WhatIsThis = import('./window.d').WhatIsThis; if (view) { view.applyTheme(); } + + const theme = window.Events.getThemeSetting(); + window.reduxActions.user.userChanged({ + theme: theme === 'system' ? window.systemTheme : theme, + }); } const FIVE_MINUTES = 5 * 60 * 1000; diff --git a/ts/components/conversation/Image.stories.tsx b/ts/components/conversation/Image.stories.tsx index 56f149f9a2..a5e3f8fe1e 100644 --- a/ts/components/conversation/Image.stories.tsx +++ b/ts/components/conversation/Image.stories.tsx @@ -10,6 +10,7 @@ import { storiesOf } from '@storybook/react'; import { pngUrl } from '../../storybook/Fixtures'; import { Image, Props } from './Image'; import { IMAGE_PNG } from '../../types/MIME'; +import { ThemeType } from '../../types/Util'; import { setup as setupI18n } from '../../../js/modules/i18n'; import enMessages from '../../../_locales/en/messages.json'; @@ -56,6 +57,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ ), softCorners: boolean('softCorners', overrideProps.softCorners || false), tabIndex: number('tabIndex', overrideProps.tabIndex || 0), + theme: text('theme', overrideProps.theme || 'light') as ThemeType, url: text('url', overrideProps.url || pngUrl), width: number('width', overrideProps.width || 100), }); @@ -191,6 +193,32 @@ story.add('Blurhash', () => { return ; }); +story.add('undefined blurHash (light)', () => { + const defaultProps = createProps(); + const props = { + ...defaultProps, + blurHash: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + url: undefined as any, + theme: ThemeType.light, + }; + + return ; +}); + +story.add('undefined blurHash (dark)', () => { + const defaultProps = createProps(); + const props = { + ...defaultProps, + blurHash: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + url: undefined as any, + theme: ThemeType.dark, + }; + + return ; +}); + story.add('Missing Image', () => { const defaultProps = createProps(); const props = { diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index f863cb79e1..874a7e1969 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames'; import { Blurhash } from 'react-blurhash'; import { Spinner } from '../Spinner'; -import { LocalizerType } from '../../types/Util'; +import { LocalizerType, ThemeType } from '../../types/Util'; import { AttachmentType, hasNotDownloaded } from '../../types/Attachment'; export type Props = { @@ -37,6 +37,7 @@ export type Props = { blurHash?: string; i18n: LocalizerType; + theme?: ThemeType; onClick?: (attachment: AttachmentType) => void; onClickClose?: (attachment: AttachmentType) => void; onError?: () => void; @@ -44,10 +45,10 @@ export type Props = { export class Image extends React.Component { private canClick() { - const { onClick, attachment, blurHash, url } = this.props; + const { onClick, attachment } = this.props; const { pending } = attachment || { pending: true }; - return Boolean(onClick && !pending && (url || blurHash)); + return Boolean(onClick && !pending); } public handleClick = (event: React.MouseEvent): void => { @@ -150,6 +151,7 @@ export class Image extends React.Component { smallCurveTopLeft, softCorners, tabIndex, + theme, url, width = 0, } = this.props; @@ -158,6 +160,12 @@ export class Image extends React.Component { const canClick = this.canClick(); const imgNotDownloaded = hasNotDownloaded(attachment); + const defaulBlurHash = + theme === ThemeType.dark + ? 'L05OQnoffQofoffQfQfQfQfQfQfQ' + : 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ'; + const resolvedBlurHash = blurHash || defaulBlurHash; + const overlayClassName = classNames('module-image__border-overlay', { 'module-image__border-overlay--with-border': !noBorder, 'module-image__border-overlay--with-click-handler': canClick, @@ -210,9 +218,9 @@ export class Image extends React.Component { width={width} src={url} /> - ) : blurHash ? ( + ) : resolvedBlurHash ? ( ; @@ -28,6 +28,7 @@ export type Props = { tabIndex?: number; i18n: LocalizerType; + theme?: ThemeType; onError: () => void; onClick?: (attachment: AttachmentType) => void; @@ -42,6 +43,7 @@ export const ImageGrid = ({ onError, onClick, tabIndex, + theme, withContentAbove, withContentBelow, }: Props): JSX.Element | null => { @@ -75,6 +77,7 @@ export const ImageGrid = ({ {getAlt(attachments[0], { showVisualAttachment, isSticker, text, + theme, } = this.props; + const { imageBroken } = this.state; if (!attachments || !attachments[0]) { @@ -693,9 +695,7 @@ export class Message extends React.PureComponent { if ( displayImage && !imageBroken && - ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && - (hasVideoBlurHash(attachments) || hasVideoScreenshot(attachments)))) + (isImage(attachments) || isVideo(attachments)) ) { const prefix = isSticker ? 'sticker' : 'attachment'; const bottomOverlay = !isSticker && !collapseMetadata; @@ -725,6 +725,7 @@ export class Message extends React.PureComponent { stickerSize={STICKER_SIZE} bottomOverlay={bottomOverlay} i18n={i18n} + theme={theme} onError={this.handleImageError} tabIndex={tabIndex} onClick={attachment => { @@ -783,7 +784,11 @@ export class Message extends React.PureComponent { event.stopPropagation(); event.preventDefault(); - if (!firstAttachment.url) { + if (hasNotDownloaded(firstAttachment)) { + kickOffAttachmentDownload({ + attachment: firstAttachment, + messageId: id, + }); return; } @@ -841,6 +846,7 @@ export class Message extends React.PureComponent { openLink, previews, quote, + theme, } = this.props; // Attachments take precedence over Link Previews @@ -885,6 +891,7 @@ export class Message extends React.PureComponent { withContentBelow onError={this.handleImageError} i18n={i18n} + theme={theme} /> ) : null}
@@ -1546,12 +1553,7 @@ export class Message extends React.PureComponent { if (attachments && attachments.length) { const displayImage = canDisplayImage(attachments); - return ( - displayImage && - ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && - (hasVideoBlurHash(attachments) || hasVideoScreenshot(attachments)))) - ); + return displayImage && (isImage(attachments) || isVideo(attachments)); } if (previews && previews.length) { @@ -2012,8 +2014,7 @@ export class Message extends React.PureComponent { !isAttachmentPending && canDisplayImage(attachments) && ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && - (hasVideoBlurHash(attachments) || hasVideoScreenshot(attachments)))) + (isVideo(attachments) && hasVideoScreenshot(attachments))) ) { event.preventDefault(); event.stopPropagation(); diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index c617251408..409914b0a6 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import { LocalizerType } from '../../types/Util'; +import { LocalizerType, ThemeType } from '../../types/Util'; import { Message, @@ -126,6 +126,7 @@ type PropsLocalType = { selectMessage: (messageId: string, conversationId: string) => unknown; renderContact: SmartContactRendererType; i18n: LocalizerType; + theme?: ThemeType; }; type PropsActionsType = MessageActionsType & @@ -145,6 +146,7 @@ export class TimelineItem extends React.PureComponent { isSelected, item, i18n, + theme, messageSizeChanged, renderContact, returnToActiveCall, @@ -159,7 +161,9 @@ export class TimelineItem extends React.PureComponent { } if (item.type === 'message') { - return ; + return ( + + ); } let notification; diff --git a/ts/state/ducks/user.ts b/ts/state/ducks/user.ts index e40d9db7fd..186beea952 100644 --- a/ts/state/ducks/user.ts +++ b/ts/state/ducks/user.ts @@ -4,7 +4,7 @@ import { trigger } from '../../shims/events'; import { NoopActionType } from './noop'; -import { LocalizerType } from '../../types/Util'; +import { LocalizerType, ThemeType } from '../../types/Util'; // State @@ -19,6 +19,7 @@ export type UserStateType = { regionCode: string; i18n: LocalizerType; interactionMode: 'mouse' | 'keyboard'; + theme: ThemeType; }; // Actions @@ -31,6 +32,7 @@ type UserChangedActionType = { ourNumber?: string; regionCode?: string; interactionMode?: 'mouse' | 'keyboard'; + theme?: ThemeType; }; }; @@ -45,10 +47,11 @@ export const actions = { function userChanged(attributes: { interactionMode?: 'mouse' | 'keyboard'; - ourConversationId: string; - ourNumber: string; - ourUuid: string; - regionCode: string; + ourConversationId?: string; + ourNumber?: string; + ourUuid?: string; + regionCode?: string; + theme?: ThemeType; }): UserChangedActionType { return { type: 'USER_CHANGED', @@ -78,6 +81,7 @@ export function getEmptyState(): UserStateType { regionCode: 'missing', platform: 'missing', interactionMode: 'mouse', + theme: ThemeType.light, i18n: () => 'missing', }; } diff --git a/ts/state/selectors/user.ts b/ts/state/selectors/user.ts index 253a91faca..988e18ec35 100644 --- a/ts/state/selectors/user.ts +++ b/ts/state/selectors/user.ts @@ -3,7 +3,7 @@ import { createSelector } from 'reselect'; -import { LocalizerType } from '../../types/Util'; +import { LocalizerType, ThemeType } from '../../types/Util'; import { StateType } from '../reducer'; import { UserStateType } from '../ducks/user'; @@ -59,3 +59,8 @@ export const getTempPath = createSelector( getUser, (state: UserStateType): string => state.tempPath ); + +export const getTheme = createSelector( + getUser, + (state: UserStateType): ThemeType => state.theme +); diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 27b67aaa91..96d793b399 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -8,7 +8,7 @@ import { mapDispatchToProps } from '../actions'; import { StateType } from '../reducer'; import { TimelineItem } from '../../components/conversation/TimelineItem'; -import { getIntl } from '../selectors/user'; +import { getIntl, getTheme } from '../selectors/user'; import { getMessageSelector, getSelectedMessage, @@ -47,6 +47,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { isSelected, renderContact, i18n: getIntl(state), + theme: getTheme(state), }; }; diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index e5e392d798..11326ad98a 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -170,7 +170,7 @@ export function isVideoAttachment( } export function hasNotDownloaded(attachment?: AttachmentType): boolean { - return Boolean(attachment && !attachment.url && attachment.blurHash); + return Boolean(attachment && !attachment.url); } export function hasVideoBlurHash(attachments?: Array): boolean { diff --git a/ts/types/Util.ts b/ts/types/Util.ts index daa7319f14..fc7b487542 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -24,3 +24,8 @@ export type LocalizerType = ( key: string, values?: Array | ReplacementValuesType ) => string; + +export enum ThemeType { + 'light' = 'light', + 'dark' = 'dark', +}