diff --git a/.storybook/config.js b/.storybook/config.js index 83ad39d5bed0..1b697b307e6c 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -31,17 +31,6 @@ const makeThemeKnob = pane => ) ); -const makeDeviceThemeKnob = pane => - persistKnob(`${pane}-pane-device-theme`)(localValue => - optionsKnob( - `${pane} Pane Device Theme`, - { Android: '', iOS: 'ios-theme' }, - localValue || '', - optionsConfig, - `${pane} Pane` - ) - ); - const makeModeKnob = pane => persistKnob(`${pane}-pane-mode`)(localValue => optionsKnob( @@ -58,7 +47,6 @@ addDecorator(withKnobs); addDecorator((storyFn /* , context */) => { const contents = storyFn(); const firstPaneTheme = makeThemeKnob('First'); - const firstPaneDeviceTheme = makeDeviceThemeKnob('First'); const firstPaneMode = makeModeKnob('First'); const secondPane = persistKnob('second-pane-active')(localValue => @@ -66,7 +54,6 @@ addDecorator((storyFn /* , context */) => { ); const secondPaneTheme = makeThemeKnob('Second'); - const secondPaneDeviceTheme = makeDeviceThemeKnob('Second'); const secondPaneMode = makeModeKnob('Second'); // Adding it to the body as well so that we can cover modals and other @@ -77,12 +64,6 @@ addDecorator((storyFn /* , context */) => { document.body.classList.add('dark-theme'); } - if (firstPaneDeviceTheme === '') { - document.body.classList.remove('ios-theme'); - } else { - document.body.classList.add('ios-theme'); - } - if (firstPaneMode === 'mouse-mode') { document.body.classList.remove('keyboard-mode'); document.body.classList.add('mouse-mode'); @@ -95,24 +76,14 @@ addDecorator((storyFn /* , context */) => {
{contents}
{secondPane ? (
{contents}
diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e00737b269ae..08ada9069ae6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -175,6 +175,10 @@ "message": "View Archive", "description": "One of the menu options available in the Avatar Popup menu" }, + "avatarMenuChatColors": { + "message": "Chat Color", + "description": "One of the menu options available in the Avatar Popup menu" + }, "loading": { "message": "Loading...", "description": "Message shown on the loading screen before we've loaded any messages" @@ -4785,6 +4789,10 @@ "message": "Remove from group", "description": "Button text for remove from group button in Group Contact Details modal" }, + "showChatColorEditor": { + "message": "Chat color", + "description": "This is a button in the conversation context menu to show the chat color editor" + }, "showConversationDetails": { "message": "Group settings", "description": "This is a button in the conversation context menu to show group settings" @@ -5292,5 +5300,91 @@ "deleteForEveryoneFailed": { "message": "Failed to delete message for everyone. Please retry later.", "description": "Displayed when delete-for-everyone has failed to send to all recepients" + }, + "ChatColorPicker__delete--title": { + "message": "Delete color", + "description": "Confirm title for deleting custom color" + }, + "ChatColorPicker__delete--message": { + "message": "This custom color is used in $num$ chats. Do you want to delete it for all chats?", + "description": "Confirm message for deleting custom color", + "placeholders": { + "num": { + "content": "$1", + "example": "5" + } + } + }, + "ChatColorPicker__global-chat-color": { + "message": "Global Chat Color", + "description": "Modal title for the chat color picker and editor for all conversations" + }, + "ChatColorPicker__menu-title": { + "message": "Chat Color", + "description": "View title for the chat color picker and editor" + }, + "ChatColorPicker__reset": { + "message": "Reset chat color", + "description": "Button label for resetting chat colors" + }, + "ChatColorPicker__resetAll": { + "message": "Reset all chat colors", + "description": "Button label for resetting all chat colors" + }, + "ChatColorPicker__confirm-reset": { + "message": "Reset", + "description": "Confirm button label for resetting chat colors" + }, + "ChatColorPicker__confirm-reset-message": { + "message": "Would you like to override all chat colors?", + "description": "Modal message text for confirming resetting of chat colors" + }, + "ChatColorPicker__custom-color--label": { + "message": "Show custom color editor", + "description": "aria-label for custom color editor button" + }, + "ChatColorPicker__sampleBubble1": { + "message": "Here's a preview of the chat color.", + "description": "An example message bubble for selecting the chat color" + }, + "ChatColorPicker__sampleBubble2": { + "message": "Another bubble.", + "description": "An example message bubble for selecting the chat color" + }, + "ChatColorPicker__sampleBubble3": { + "message": "The color is visible to only you.", + "description": "An example message bubble for selecting the chat color" + }, + "ChatColorPicker__context--edit": { + "message": "Edit color", + "description": "Option in the custom color bubble context menu" + }, + "ChatColorPicker__context--duplicate": { + "message": "Duplicate", + "description": "Option in the custom color bubble context menu" + }, + "ChatColorPicker__context--delete": { + "message": "Delete", + "description": "Option in the custom color bubble context menu" + }, + "CustomColorEditor__solid": { + "message": "Solid", + "description": "Tab label for selecting solid colors" + }, + "CustomColorEditor__gradient": { + "message": "Gradient", + "description": "Tab label for selecting a gradient" + }, + "CustomColorEditor__hue": { + "message": "Hue", + "description": "Label for the hue slider" + }, + "CustomColorEditor__saturation": { + "message": "Saturation", + "description": "Label for the saturation slider" + }, + "CustomColorEditor__title": { + "message": "Custom Color", + "description": "Modal title for the custom color editor" } } diff --git a/images/icons/v2/color-outline-24.svg b/images/icons/v2/color-outline-24.svg new file mode 100644 index 000000000000..6340336c3bfb --- /dev/null +++ b/images/icons/v2/color-outline-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/color-solid-24.svg b/images/icons/v2/color-solid-24.svg new file mode 100644 index 000000000000..e8041715eb55 --- /dev/null +++ b/images/icons/v2/color-solid-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/modules/signal.js b/js/modules/signal.js index 1ec6cc182d3e..f5c6f411fc3f 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -30,6 +30,7 @@ const { AttachmentList, } = require('../../ts/components/conversation/AttachmentList'); const { CaptionEditor } = require('../../ts/components/CaptionEditor'); +const { ChatColorPicker } = require('../../ts/components/ChatColorPicker'); const { ConfirmationDialog, } = require('../../ts/components/ConfirmationDialog'); @@ -61,6 +62,9 @@ const { // State const { createTimeline } = require('../../ts/state/roots/createTimeline'); +const { + createChatColorPicker, +} = require('../../ts/state/roots/createChatColorPicker'); const { createCompositionArea, } = require('../../ts/state/roots/createCompositionArea'); @@ -77,6 +81,9 @@ const { createCallManager } = require('../../ts/state/roots/createCallManager'); const { createForwardMessageModal, } = require('../../ts/state/roots/createForwardMessageModal'); +const { + createGlobalModalContainer, +} = require('../../ts/state/roots/createGlobalModalContainer'); const { createGroupLinkManagement, } = require('../../ts/state/roots/createGroupLinkManagement'); @@ -324,6 +331,7 @@ exports.setup = (options = {}) => { const Components = { AttachmentList, CaptionEditor, + ChatColorPicker, ConfirmationDialog, ContactDetail, ContactListItem, @@ -345,11 +353,13 @@ exports.setup = (options = {}) => { const Roots = { createCallManager, + createChatColorPicker, createCompositionArea, createContactModal, createConversationDetails, createConversationHeader, createForwardMessageModal, + createGlobalModalContainer, createGroupLinkManagement, createGroupV1MigrationModal, createGroupV2JoinModal, diff --git a/js/views/app_view.js b/js/views/app_view.js index bf12e61f61e9..ef812c6fc49c 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -1,4 +1,4 @@ -// Copyright 2017-2020 Signal Messenger, LLC +// Copyright 2017-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* global Backbone, Whisper, storage, _, ConversationController, $ */ @@ -34,17 +34,10 @@ }, applyTheme() { const theme = resolveTheme(); - const iOS = storage.get('userAgent') === 'OWI'; this.$el .removeClass('light-theme') .removeClass('dark-theme') .addClass(`${theme}-theme`); - - if (iOS) { - this.$el.addClass('ios-theme'); - } else { - this.$el.removeClass('ios-theme'); - } }, applyHideMenu() { const hideMenuBar = storage.get('hide-menu-bar', false); diff --git a/js/views/identicon_svg_view.js b/js/views/identicon_svg_view.js index 7bad681e717f..bcf027f31f0f 100644 --- a/js/views/identicon_svg_view.js +++ b/js/views/identicon_svg_view.js @@ -48,18 +48,18 @@ }); const COLORS = { - red: '#cc163d', - deep_orange: '#c73800', - brown: '#746c53', - pink: '#a23474', - purple: '#862caf', - indigo: '#5951c8', - blue: '#336ba3', - teal: '#067589', - green: '#3b7845', - light_green: '#1c8260', - blue_grey: '#895d66', - grey: '#6b6b78', - ultramarine: '#2c6bed', + blue: '#0a69c7', + burlap: '#866118', + crimson: '#d00b2c', + forest: '#067919', + indigo: '#5151f6', + plum: '#c70a88', + steel: '#077288', + taupe: '#cb0b6b', + teal: '#077288', + ultramarine: '#0d59f2', + vermilion: '#c72a0a', + violet: '#a20ced', + wintergreen: '#067953', }; })(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index fe79e88df0ab..250e36b480b0 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -1,4 +1,4 @@ -// Copyright 2014-2020 Signal Messenger, LLC +// Copyright 2014-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* global @@ -115,6 +115,7 @@ } else { this.setupLeftPane(); this.setupCallManagerUI(); + this.setupGlobalModalContainer(); } Whisper.events.on('pack-install-failed', () => { @@ -141,6 +142,18 @@ }); this.$('.call-manager-placeholder').append(this.callManagerView.el); }, + setupGlobalModalContainer() { + if (this.globalModalContainerView) { + return; + } + this.globalModalContainerView = new Whisper.ReactWrapperView({ + JSX: Signal.State.Roots.createGlobalModalContainer(window.reduxStore), + }); + const node = document.querySelector('.inbox-container'); + if (node) { + node.appendChild(this.globalModalContainerView.el); + } + }, setupLeftPane() { if (this.leftPaneView) { return; @@ -182,6 +195,7 @@ onEmpty() { this.setupLeftPane(); this.setupCallManagerUI(); + this.setupGlobalModalContainer(); const view = this.appLoadingScreen; if (view) { diff --git a/sticker-creator/app/stages/MetaStage.scss b/sticker-creator/app/stages/MetaStage.scss index e26c05bba021..84c3e7f66fab 100644 --- a/sticker-creator/app/stages/MetaStage.scss +++ b/sticker-creator/app/stages/MetaStage.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../../stylesheets/variables'; @@ -66,10 +66,10 @@ composes: cover-frame; @include light-theme() { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } @include dark-theme() { - border-color: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; } } diff --git a/sticker-creator/components/StickerFrame.scss b/sticker-creator/components/StickerFrame.scss index 3e0fb31b907f..b650d52b7a3f 100644 --- a/sticker-creator/components/StickerFrame.scss +++ b/sticker-creator/components/StickerFrame.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../mixins'; @@ -36,11 +36,11 @@ $border-width: 1px; composes: container; @include light-theme() { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } @include dark-theme() { - border-color: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; } } diff --git a/sticker-creator/elements/Button.scss b/sticker-creator/elements/Button.scss index 62df15a36cbf..a0c6a5b992f5 100644 --- a/sticker-creator/elements/Button.scss +++ b/sticker-creator/elements/Button.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../mixins'; @@ -37,12 +37,12 @@ composes: base; @include light-theme() { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; color: $color-white; } @include dark-theme() { - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; color: $color-white; } } @@ -71,13 +71,13 @@ @include light-theme() { border: none; - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; color: $color-white; } @include dark-theme() { border: none; - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; color: $color-white; } } diff --git a/sticker-creator/elements/ConfirmDialog.scss b/sticker-creator/elements/ConfirmDialog.scss index c5418c554df7..56dbb7b8f8e5 100644 --- a/sticker-creator/elements/ConfirmDialog.scss +++ b/sticker-creator/elements/ConfirmDialog.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../mixins'; @@ -89,13 +89,13 @@ @include light-theme() { color: $color-white; - border-color: $ultramarine-ui-light; - background: $ultramarine-ui-light; + border-color: $color-ultramarine; + background: $color-ultramarine; } @include dark-theme() { color: $color-white; - border-color: $ultramarine-ui-dark; - background: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; + background: $color-ultramarine-light; } } diff --git a/sticker-creator/elements/DropZone.scss b/sticker-creator/elements/DropZone.scss index 89dde2d0929c..69c5eaa6774c 100644 --- a/sticker-creator/elements/DropZone.scss +++ b/sticker-creator/elements/DropZone.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../stylesheets/variables'; @@ -55,10 +55,10 @@ composes: standalone; @include light-theme() { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } @include dark-theme() { - border-color: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; } } diff --git a/sticker-creator/elements/LabeledCheckbox.scss b/sticker-creator/elements/LabeledCheckbox.scss index 92f06cef6f42..6243070e04ae 100644 --- a/sticker-creator/elements/LabeledCheckbox.scss +++ b/sticker-creator/elements/LabeledCheckbox.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../stylesheets/variables'; @@ -41,7 +41,7 @@ .checkbox-checked { composes: checkbox; border: none; - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; color: $color-white; } diff --git a/sticker-creator/elements/LabeledInput.scss b/sticker-creator/elements/LabeledInput.scss index 1769e2824d80..b89bc988c938 100644 --- a/sticker-creator/elements/LabeledInput.scss +++ b/sticker-creator/elements/LabeledInput.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../stylesheets/variables'; @@ -48,11 +48,11 @@ padding: 0 11px; @include light-theme() { - border: 2px solid $ultramarine-ui-light; + border: 2px solid $color-ultramarine; } @include dark-theme() { - border: 2px solid $ultramarine-ui-dark; + border: 2px solid $color-ultramarine-light; } } } diff --git a/sticker-creator/elements/MessageBubble.scss b/sticker-creator/elements/MessageBubble.scss index 652a66509304..c4ebc776c6e1 100644 --- a/sticker-creator/elements/MessageBubble.scss +++ b/sticker-creator/elements/MessageBubble.scss @@ -1,10 +1,10 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../stylesheets/variables'; .base { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; padding: 6px 12px; border-radius: 16px; color: $color-white-alpha-90; diff --git a/sticker-creator/elements/ProgressBar.scss b/sticker-creator/elements/ProgressBar.scss index 055df01d98af..9b0bb42db093 100644 --- a/sticker-creator/elements/ProgressBar.scss +++ b/sticker-creator/elements/ProgressBar.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../stylesheets/variables'; @@ -21,7 +21,7 @@ .bar { height: 4px; width: 0px; - background: $ultramarine-ui-light; + background: $color-ultramarine; transition: width 100ms ease-out; } diff --git a/sticker-creator/elements/Typography.scss b/sticker-creator/elements/Typography.scss index 73ac8978a960..5610943a1278 100644 --- a/sticker-creator/elements/Typography.scss +++ b/sticker-creator/elements/Typography.scss @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import '../../stylesheets/variables'; @@ -57,7 +57,7 @@ } a { - color: $ultramarine-ui-light; + color: $color-ultramarine; text-decoration: none; } } diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index d06d23e12fb2..d0fc74613eaa 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -1,4 +1,4 @@ -// Copyright 2015-2020 Signal Messenger, LLC +// Copyright 2015-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only @import './mixins'; @@ -130,43 +130,6 @@ padding-bottom: 40px; } -.bottom-bar { - .module-quote { - margin: 0; - - border-left-style: none; - @include ios-dark-theme { - background-color: $ultramarine-brand-dark; - border-left-color: $color-ios-blue-tint; - } - @include ios-theme { - background-color: $color-ios-blue-tint; - border-left-color: $color-white; - } - } - - @include ios-dark-theme { - .module-quote__primary__author { - color: $color-gray-05; - } - - .module-quote__primary__type-label { - color: $color-gray-05; - } - - .module-quote__generic-file__text { - color: $color-gray-05; - } - - .module-quote__primary__text { - color: $color-gray-05; - a { - color: $color-gray-05; - } - } - } -} - // We need to use the wrapper because the conversation view calculates the height of all // things in the composition area. A margin on an inner div won't be included in that // height calculation. @@ -189,7 +152,7 @@ form.active { textarea { - border: solid 1px $ultramarine-ui-light; + border: solid 1px $color-ultramarine; } } diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 8736f0d9b0df..107c5e12fb95 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -156,7 +156,7 @@ button.grey { } a { - color: $ultramarine-ui-light; + color: $color-ultramarine; } .file-input { @@ -318,7 +318,7 @@ $loading-height: 16px; right: 0; top: 0; bottom: 0; - background-color: $ultramarine-brand-light; + background-color: $color-ultramarine-icon; color: $color-white; display: flex; flex-direction: column; @@ -374,7 +374,7 @@ $loading-height: 16px; color: $color-black; a { - color: $ultramarine-ui-light; + color: $color-ultramarine; } background: linear-gradient( to bottom, @@ -393,7 +393,7 @@ $loading-height: 16px; input { margin-top: 1em; font-size: 12pt; - border: 2px solid $ultramarine-ui-light; + border: 2px solid $color-ultramarine; padding: 0.5em; text-align: center; width: 20em; @@ -411,7 +411,7 @@ $loading-height: 16px; display: inline-block; &.ready { - border: 5px solid $ultramarine-ui-light; + border: 5px solid $color-ultramarine; box-shadow: 2px 2px 4px $color-black-alpha-40; } @@ -430,7 +430,7 @@ $loading-height: 16px; .dot { width: 14px; height: 14px; - border: 3px solid $ultramarine-ui-light; + border: 3px solid $color-ultramarine; border-radius: 50%; float: left; margin: 0 6px; @@ -598,7 +598,7 @@ $loading-height: 16px; margin-left: 0.5em; margin-right: 0.5em; color: $color-white; - background: $ultramarine-ui-light; + background: $color-ultramarine; box-shadow: 2px 2px 4px $color-black-alpha-40; font-size: 12pt; @@ -620,7 +620,7 @@ $loading-height: 16px; cursor: pointer; text-decoration: underline; margin: 0.5em; - color: $ultramarine-ui-light; + color: $color-ultramarine; } .progress { diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss index 411339945aea..21375975e754 100644 --- a/stylesheets/_mixins.scss +++ b/stylesheets/_mixins.scss @@ -94,18 +94,6 @@ } } -@mixin ios-theme() { - .ios-theme & { - @content; - } -} - -@mixin ios-dark-theme() { - .dark-theme.ios-theme & { - @content; - } -} - // Utilities @mixin rounded-corners() { @@ -201,28 +189,12 @@ @content; } } -@mixin ios-keyboard-mode() { - .ios-theme.keyboard-mode & { - @content; - } -} @mixin dark-mouse-mode() { .dark-theme.mouse-mode & { @content; } } -@mixin ios-mouse-mode() { - .ios-theme.mouse-mode & { - @content; - } -} - -@mixin dark-ios-keyboard-mode() { - .dark-theme.ios-theme.keyboard-mode & { - @content; - } -} // Other @@ -249,27 +221,27 @@ @mixin button-focus-outline { &:focus { @include keyboard-mode { - box-shadow: 0px 0px 0px 3px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 3px $color-ultramarine; } @include dark-keyboard-mode { - box-shadow: 0px 0px 0px 3px $ultramarine-ui-dark; + box-shadow: 0px 0px 0px 3px $color-ultramarine-light; } } } @mixin button-blue-text { @include light-theme { - color: $ultramarine-ui-light; + color: $color-ultramarine; } @include dark-theme { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; } } // Complete button styles @mixin button-primary { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; // Note: the background colors here need to match the parent component @include light-theme { @@ -283,11 +255,11 @@ &:hover { @include mouse-mode { - background-color: mix($color-black, $ultramarine-ui-light, 15%); + background-color: mix($color-black, $color-ultramarine, 15%); } @include dark-mouse-mode { - background-color: mix($color-white, $ultramarine-ui-light, 15%); + background-color: mix($color-white, $color-ultramarine, 15%); } } @@ -295,17 +267,17 @@ // We need to include all four here for specificity precedence @include mouse-mode { - background-color: mix($color-black, $ultramarine-ui-light, 25%); + background-color: mix($color-black, $color-ultramarine, 25%); } @include dark-mouse-mode { - background-color: mix($color-white, $ultramarine-ui-light, 25%); + background-color: mix($color-white, $color-ultramarine, 25%); } @include keyboard-mode { - background-color: mix($color-black, $ultramarine-ui-light, 25%); + background-color: mix($color-black, $color-ultramarine, 25%); } @include dark-keyboard-mode { - background-color: mix($color-black, $ultramarine-ui-light, 25%); + background-color: mix($color-black, $color-ultramarine, 25%); } } @@ -526,10 +498,37 @@ &:focus { @include keyboard-mode { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } @include dark-keyboard-mode { - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; + } + } +} + +@mixin color-bubble($bubble-size) { + background-clip: content-box; + border-color: transparent; + border-radius: $bubble-size + 12px; + border-style: solid; + border-width: 4px; + cursor: pointer; + height: $bubble-size + 12px; + padding: 2px; + width: $bubble-size + 12px; + + @each $color, $value in $conversation-colors { + &--#{$color} { + background-color: $value; + } + } + @each $color, $value in $conversation-colors-gradient { + &--#{$color} { + background-image: linear-gradient( + map-get($value, 'deg'), + map-get($value, 'start'), + map-get($value, 'end') + ); } } } diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 7a060e71ed22..aeafe80556a8 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -45,7 +45,7 @@ } &.module-logo-blue { - background-color: $ultramarine-brand-light; + background-color: $color-ultramarine-icon; } } @@ -352,7 +352,7 @@ } .module-message:focus .module-message__container { @include keyboard-mode { - box-shadow: 0 0 0 3px $ultramarine-ui-light; + box-shadow: 0 0 0 3px $color-ultramarine; } } @@ -361,10 +361,10 @@ box-shadow: 0 0 0 5px transparent; } 10% { - box-shadow: 0 0 0 5px $ultramarine-ui-light; + box-shadow: 0 0 0 5px $color-ultramarine; } 70% { - box-shadow: 0 0 0 5px $ultramarine-ui-light; + box-shadow: 0 0 0 5px $color-ultramarine; } 100% { box-shadow: 0 0 0 5px transparent; @@ -400,38 +400,48 @@ .module-message__container--outgoing { @include light-theme { - background-color: $color-gray-05; - } - @include dark-theme { - background-color: $color-gray-75; - } - @include ios-theme { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; color: $color-white; } - @include ios-dark-theme { - background-color: $ultramarine-ui-light; + @include dark-theme { + background-color: $color-ultramarine; color: $color-gray-05; } } // In case the color gets messed up .module-message__container--incoming { - background-color: $color-conversation-grey; + background-color: $color-gray-05; + color: $color-gray-90; - @include ios-theme { - background-color: $color-gray-05; - color: $color-gray-90; - } - @include ios-dark-theme { + @include dark-theme { background-color: $color-gray-75; color: $color-gray-05; } } @each $color, $value in $conversation-colors { - .module-message__container--incoming-#{$color} { + .module-message__container--outgoing-#{$color} { background-color: $value; + + @include dark-theme { + background-color: $value; + } + } +} + +.module-message__container--outgoing-custom { + background-attachment: fixed; +} + +@each $color, $value in $conversation-colors-gradient { + .module-message__container--outgoing-#{$color} { + background-attachment: fixed; + background-image: linear-gradient( + map-get($value, 'deg'), + map-get($value, 'start'), + map-get($value, 'end') + ); } } @@ -441,20 +451,7 @@ } .module-message__container--with-tap-to-view-pending { - @include ios-theme { - background-color: $color-gray-15; - } -} - -// In case the color gets messed up -.module-message__container--incoming--tap-to-view-pending { - background-color: $color-conversation-grey-shade; -} - -@each $color, $value in $conversation-colors-shade { - .module-message__container--incoming-#{$color}-tap-to-view-pending { - background-color: $value; - } + background-color: $color-gray-15; } .module-message__container--with-tap-to-view-pending { @@ -470,14 +467,6 @@ border: 1px solid $color-gray-60; background-color: $color-gray-95; } - @include ios-theme { - border: 1px solid $color-gray-15; - background-color: $color-white; - } - @include ios-dark-theme { - border: 1px solid $color-gray-60; - background-color: $color-gray-95; - } } .module-message__container--with-tap-to-view-error { @@ -486,17 +475,9 @@ @include light-theme { background-color: $color-white; - border: 1px solid $color-accent-red; + border: 1px solid $color-deep-red; } @include dark-theme { - background-color: $color-gray-95; - border: 1px solid $color-deep-red; - } - @include ios-theme { - background-color: $color-white; - border: 1px solid $color-deep-red; - } - @include ios-dark-theme { background-color: $color-black; border: 1px solid $color-deep-red; } @@ -534,28 +515,19 @@ height: 20px; @include light-theme { - @include color-svg('../images/icons/v2/view-once-24.svg', $color-white); - } - @include dark-theme { - @include color-svg('../images/icons/v2/view-once-24.svg', $color-gray-05); - } - @include ios-theme { @include color-svg('../images/icons/v2/view-once-24.svg', $color-gray-90); } - @include ios-dark-theme { + @include dark-theme { @include color-svg('../images/icons/v2/view-once-24.svg', $color-gray-05); } } .module-message__tap-to-view__icon--outgoing { @include light-theme { - background-color: $color-gray-75; + background-color: $color-white; } @include dark-theme { background-color: $color-gray-05; } - @include ios-theme { - background-color: $color-white; - } } .module-message__tap-to-view__icon--expired { @include light-theme { @@ -564,12 +536,6 @@ @include dark-theme { @include color-svg('../images/icons/v2/viewed-once-24.svg', $color-gray-05); } - @include ios-theme { - @include color-svg('../images/icons/v2/viewed-once-24.svg', $color-gray-75); - } - @include ios-dark-theme { - @include color-svg('../images/icons/v2/viewed-once-24.svg', $color-gray-05); - } } .module-message__tap-to-view__text { @include font-body-1-bold; @@ -580,21 +546,12 @@ @include dark-theme { color: $color-gray-05; } - @include ios-theme { - color: $color-gray-90; - } } .module-message__tap-to-view__text--incoming { @include light-theme { - color: $color-white; - } - @include dark-theme { - color: $color-gray-05; - } - @include ios-theme { color: $color-gray-90; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } } @@ -605,12 +562,6 @@ @include dark-theme { color: $color-gray-05; } - @include ios-theme { - color: $color-gray-90; - } - @include ios-dark-theme { - color: $color-gray-05; - } } .module-message__tap-to-view__text--incoming-error { @include light-theme { @@ -619,21 +570,16 @@ @include dark-theme { color: $color-gray-25; } - @include ios-theme { - color: $color-gray-60; - } } .module-message__tap-to-view__text--outgoing { - @include ios-theme { - color: $color-white; - } + color: $color-white; } .module-message__tap-to-view__text--outgoing-expired { - @include ios-theme { + @include light-theme { color: $color-gray-90; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } } @@ -718,7 +664,7 @@ @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -819,23 +765,20 @@ text-overflow: ellipsis; @include light-theme { - color: $color-gray-90; + color: $color-white; } @include dark-theme { color: $color-gray-02; } - @include ios-theme { - color: $color-white; - } } .module-message__generic-attachment__file-name--incoming { color: $color-white; - @include ios-theme { + @include light-theme { color: $color-gray-90; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-25; } } @@ -846,23 +789,20 @@ margin-top: 3px; @include light-theme { - color: $color-gray-90; + color: $color-white; } @include dark-theme { color: $color-gray-02; } - @include ios-theme { - color: $color-white; - } } .module-message__generic-attachment__file-size--incoming { color: $color-white; - @include ios-theme { + @include light-theme { color: $color-gray-90; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-25; } } @@ -890,7 +830,7 @@ @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -998,13 +938,6 @@ color: $color-white-alpha-90; - @include ios-theme { - color: $color-gray-90; - } - @include ios-dark-theme { - color: $color-gray-05; - } - &__profile-name { @include font-caption-bold-italic; } @@ -1068,15 +1001,9 @@ white-space: pre-wrap; @include light-theme { - color: $color-gray-90; - } - @include dark-theme { - color: $color-gray-05; - } - @include ios-theme { color: $color-white-alpha-90; } - @include ios-dark-theme { + @include dark-theme { color: $color-white-alpha-90; } @@ -1085,7 +1012,7 @@ outline: none; @include light-theme { - color: $color-gray-90; + color: $color-white-alpha-90; } @include keyboard-mode { &:focus { @@ -1094,21 +1021,9 @@ } @include dark-theme { - color: $color-gray-05; + color: $color-white-alpha-90; } @include dark-keyboard-mode { - &:focus { - outline: 1px solid $color-gray-05; - } - } - - @include ios-theme { - color: $color-white-alpha-90; - } - @include ios-dark-theme { - color: $color-white-alpha-90; - } - @include ios-keyboard-mode { &:focus { outline: 1px solid $color-white-alpha-90; } @@ -1118,15 +1033,9 @@ .module-message__text--incoming { @include light-theme { - color: $color-white; - } - @include ios-theme { color: $color-gray-90; } @include dark-theme { - color: $color-white-alpha-90; - } - @include ios-dark-theme { color: $color-gray-05; } @@ -1135,36 +1044,18 @@ outline: none; @include light-theme { - color: $color-white; - } - @include keyboard-mode { - &:focus { - outline: 1px solid $color-white; - } - } - - @include dark-theme { - color: $color-white-alpha-90; - } - @include dark-keyboard-mode { - &:focus { - outline: 1px solid $color-white-alpha-90; - } - } - - @include ios-theme { color: $color-gray-90; } - @include ios-keyboard-mode { + @include keyboard-mode { &:focus { outline: 1px solid $color-gray-90; } } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } - @include dark-ios-keyboard-mode { + @include dark-keyboard-mode { &:focus { outline: 1px solid $color-gray-05; } @@ -1211,17 +1102,11 @@ @include font-caption; @include light-theme { - color: $color-gray-60; + color: $color-white-alpha-80; } @include dark-theme { color: $color-white-alpha-80; } - @include ios-theme { - color: $color-white-alpha-80; - } - @include ios-dark-theme { - color: $color-white-alpha-80; - } } .module-message__metadata__tapable { @include button-reset; @@ -1229,10 +1114,10 @@ .module-message__metadata__date--incoming { color: $color-white-alpha-80; - @include ios-theme { + @include light-theme { color: $color-gray-60; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-25; } } @@ -1243,9 +1128,6 @@ @include dark-theme { color: $color-white-alpha-80; } - @include ios-theme { - color: $color-white; - } } .module-message__metadata__date.module-message__metadata__date--incoming-with-tap-to-view-expired { color: $color-gray-75; @@ -1263,7 +1145,7 @@ } .module-message__metadata__date--with-sticker { - @include ios-theme { + @include light-theme { color: $color-gray-60; } } @@ -1281,15 +1163,9 @@ animation: module-message__metadata__status-icon--spinning 4s linear infinite; @include light-theme { - @include color-svg('../images/sending.svg', $color-gray-60); - } - @include dark-theme { - @include color-svg('../images/sending.svg', $color-white-alpha-80); - } - @include ios-theme { @include color-svg('../images/sending.svg', $color-white); } - @include ios-dark-theme { + @include dark-theme { @include color-svg('../images/sending.svg', $color-white); } } @@ -1303,7 +1179,10 @@ .module-message__metadata__status-icon--sent { @include light-theme { - @include color-svg('../images/check-circle-outline.svg', $color-gray-60); + @include color-svg( + '../images/check-circle-outline.svg', + $color-white-alpha-80 + ); } @include dark-theme { @include color-svg( @@ -1311,71 +1190,44 @@ $color-white-alpha-80 ); } - @include ios-theme { - @include color-svg( - '../images/check-circle-outline.svg', - $color-white-alpha-80 - ); - } - @include ios-dark-theme { - @include color-svg( - '../images/check-circle-outline.svg', - $color-white-alpha-80 - ); - } } .module-message__metadata__status-icon--delivered { width: 18px; @include light-theme { - @include color-svg('../images/double-check.svg', $color-gray-60); + @include color-svg('../images/double-check.svg', $color-white-alpha-80); } @include dark-theme { @include color-svg('../images/double-check.svg', $color-white-alpha-80); } - @include ios-theme { - @include color-svg('../images/double-check.svg', $color-white-alpha-80); - } - @include ios-dark-theme { - @include color-svg('../images/double-check.svg', $color-white-alpha-80); - } } .module-message__metadata__status-icon--read { width: 18px; @include light-theme { - @include color-svg('../images/read.svg', $color-gray-60); + @include color-svg('../images/read.svg', $color-white-alpha-80); } @include dark-theme { @include color-svg('../images/read.svg', $color-white-alpha-80); } - @include ios-theme { - @include color-svg('../images/read.svg', $color-white-alpha-80); - } - @include ios-dark-theme { - @include color-svg('../images/read.svg', $color-white-alpha-80); - } } // When status indicators are overlaid on top of an image, they use different colors .module-message__metadata__status-icon--with-image-no-caption { - @include light-theme { - background-color: $color-white; - } @include dark-theme { background-color: $color-gray-02; } - @include ios-theme { + @include light-theme { background-color: $color-white; } } .module-message__metadata__status-icon--with-sticker { - @include ios-theme { + @include light-theme { background-color: $color-gray-60; } } .module-message__metadata__status-icon--with-tap-to-view-expired { - @include ios-theme { + @include light-theme { background-color: $color-gray-75; } } @@ -1403,19 +1255,19 @@ border-bottom-right-radius: 16px; @include light-theme { - color: $ultramarine-ui-light; + color: $color-ultramarine; background-color: $color-gray-02; border: 1px solid $color-black-alpha-20; } @include dark-theme { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; background-color: $color-gray-75; border: 1px solid $color-gray-45; } @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -1441,7 +1293,7 @@ .module-Avatar { @include keyboard-mode { - box-shadow: 0 0 0 3px $ultramarine-ui-light; + box-shadow: 0 0 0 3px $color-ultramarine; } } } @@ -1506,15 +1358,11 @@ } &--is-me { - @include light-theme { - color: $color-gray-75; - } - @include dark-theme { color: $color-gray-15; } - @include ios-theme { + @include light-theme { color: $color-white-alpha-90; } } @@ -1526,7 +1374,7 @@ @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -1541,15 +1389,11 @@ } &--is-me { - @include light-theme() { - background: $color-gray-25; - } - @include dark-theme() { background: $color-gray-45; } - @include ios-theme() { + @include light-theme() { background: $color-accent-blue; } } @@ -1564,16 +1408,13 @@ margin-left: 6px; margin-bottom: 2px; - @include light-theme { - @include color-svg('../images/icons/v2/timer-60-12.svg', $color-gray-60); - } @include dark-theme { @include color-svg( '../images/icons/v2/timer-60-12.svg', $color-white-alpha-80 ); } - @include ios-theme { + @include light-theme { @include color-svg( '../images/icons/v2/timer-60-12.svg', $color-white-alpha-80 @@ -1586,19 +1427,13 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', @each $timer-icon in $timer-icons { .module-expire-timer--#{$timer-icon} { - @include light-theme { - @include color-svg( - '../images/icons/v2/timer-#{$timer-icon}-12.svg', - $color-gray-60 - ); - } @include dark-theme { @include color-svg( '../images/icons/v2/timer-#{$timer-icon}-12.svg', $color-white-alpha-80 ); } - @include ios-theme { + @include light-theme { @include color-svg( '../images/icons/v2/timer-#{$timer-icon}-12.svg', $color-white-alpha-80 @@ -1610,10 +1445,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', .module-expire-timer--incoming { background-color: $color-white-alpha-80; - @include ios-theme { + @include light-theme { background-color: $color-gray-60; } - @include ios-dark-theme { + @include dark-theme { background-color: $color-gray-25; } } @@ -1632,7 +1467,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } } .module-expire-timer--with-sticker { - @include ios-theme { + @include light-theme { background-color: $color-gray-60; } } @@ -1642,9 +1477,6 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', @include light-theme { background-color: $color-white; } - @include ios-theme { - background-color: $color-white; - } @include dark-theme { background-color: $color-gray-02; } @@ -1684,7 +1516,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -1704,102 +1536,69 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } .module-quote--outgoing { - border-left-color: $color-conversation-grey; - @include light-theme { - background-color: $color-conversation-grey-tint; - } - @include dark-theme { - background-color: $color-conversation-grey-shade; - } + border-left-color: $color-steel; + background-color: $color-steel; // To preserve contrast - @include ios-keyboard-mode { + @include keyboard-mode { &:focus { box-shadow: 0px 0px 0px 2px $color-white; } } - - // Note: both of these override all of the specific color classes below - @include ios-dark-theme { - background-color: $ultramarine-brand-dark; - border-left-color: $color-black; - } - @include ios-theme { - background-color: $color-ios-blue-tint; - border-left-color: $color-white; - } -} - -.module-composition-area__row { - .module-quote--outgoing { - border-left-style: solid; - - @include ios-dark-theme { - background-color: $ultramarine-brand-dark; - border-left-color: $color-ios-blue-tint; - } - @include ios-theme { - background-color: $color-ios-blue-tint; - border-left-color: $ultramarine-ui-light; - } - } } @each $color, $value in $conversation-colors { - .module-quote--outgoing-#{$color} { + .module-quote--incoming-#{$color} { + background-color: scale-color($value, $lightness: 60%); border-left-color: $value; - } -} -@each $color, $value in $conversation-colors-tint { - .module-quote--outgoing-#{$color} { - @include light-theme { - background-color: $value; - } - } -} -@each $color, $value in $conversation-colors-shade { - .module-quote--outgoing-#{$color} { - @include dark-theme { - background-color: $value; - } - } -} -.module-quote--incoming { - @include light-theme { + @include dark-theme { + background-color: scale-color($value, $lightness: -40%); + } + } + + .module-quote--outgoing-#{$color} { + background-color: scale-color($value, $lightness: 60%); border-left-color: $color-white; - background-color: $color-conversation-grey-tint; - } - @include dark-theme { - border-left-color: $color-black; - background-color: $color-conversation-grey-shade; - } - // Note: both of these override all of the specific color classes below - @include ios-theme { - background-color: $color-ios-blue-tint; - border-left-color: $ultramarine-ui-light; - } - @include ios-dark-theme { - background-color: $ultramarine-brand-dark; - border-left-color: $color-ios-blue-tint; - } -} -@each $color, $value in $conversation-colors-tint { - .module-quote--incoming-#{$color} { - @include light-theme { - background-color: $value; - } - } -} -@each $color, $value in $conversation-colors-shade { - .module-quote--incoming-#{$color} { @include dark-theme { - background-color: $value; + background-color: scale-color($value, $lightness: -40%); + border-left-color: $color-white; } } } +.module-quote--outgoing-custom { + background-attachment: fixed; +} + +@each $color, $value in $conversation-colors-gradient { + .module-quote--incoming-#{$color} { + border-left-color: map-get($value, 'start'); + } + .module-quote--incoming-#{$color}, + .module-quote--outgoing-#{$color} { + background-attachment: fixed; + @include light-theme { + background-image: linear-gradient( + map-get($value, 'deg'), + scale-color(map-get($value, 'start'), $lightness: 60%), + scale-color(map-get($value, 'end'), $lightness: 60%) + ); + } + @include dark-theme { + background-image: linear-gradient( + map-get($value, 'deg'), + scale-color(map-get($value, 'start'), $lightness: -40%), + scale-color(map-get($value, 'end'), $lightness: -40%) + ); + } + } + .module-quote--outgoing-#{$color} { + border-left-color: $color-white; + } +} + .module-quote__primary { flex-grow: 1; padding-left: 8px; @@ -1822,15 +1621,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', color: $color-gray-90; } @include dark-theme { - color: $color-gray-02; - } - @include ios-dark-theme { color: $color-gray-05; } } .module-quote__primary__author--incoming { - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } } @@ -1851,12 +1647,6 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } } @include dark-theme { - color: $color-gray-02; - a { - color: $color-gray-02; - } - } - @include ios-dark-theme { color: $color-gray-05; a { color: $color-gray-05; @@ -1881,7 +1671,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } .module-quote__primary__text--incoming { - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; a { color: $color-gray-05; @@ -1896,15 +1686,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', color: $color-gray-90; } @include dark-theme { - color: $color-gray-02; - } - @include ios-dark-theme { color: $color-gray-05; } } .module-quote__primary__type-label--incoming { - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } } @@ -1926,7 +1713,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', @include keyboard-mode { &:focus-within { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } } } @@ -1985,25 +1772,25 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } .module-quote__icon-container__icon--file { - @include color-svg('../images/file.svg', $ultramarine-ui-light); + @include color-svg('../images/file.svg', $color-ultramarine); } .module-quote__icon-container__icon--image { - @include color-svg('../images/image.svg', $ultramarine-ui-light); + @include color-svg('../images/image.svg', $color-ultramarine); } .module-quote__icon-container__icon--microphone { @include color-svg( '../images/icons/v2/mic-outline-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } .module-quote__icon-container__icon--play { @include color-svg( '../images/icons/v2/play-solid-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } .module-quote__icon-container__icon--movie { - @include color-svg('../images/movie.svg', $ultramarine-ui-light); + @include color-svg('../images/movie.svg', $color-ultramarine); } .module-quote__generic-file { @@ -2033,15 +1820,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', color: $color-gray-90; } @include dark-theme { - color: $color-gray-02; - } - @include ios-dark-theme { color: $color-gray-05; } } .module-quote__generic-file__text--incoming { - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } } @@ -2055,29 +1839,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; + border-left-style: solid; + border-left-width: 4px; padding-left: 8px; padding-right: 8px; - - background-color: $color-white-alpha-80; - @include dark-theme { - background-color: $color-white-alpha-20; - } - @include ios-theme { - background-color: $color-ios-ref-warning-light; - } - @include ios-dark-theme { - background-color: $color-ios-ref-warning-dark; - } -} - -.module-quote__reference-warning--incoming { - color: $color-gray-90; - @include ios-theme { - background-color: $color-ios-ref-warning-light; - } - @include ios-dark-theme { - background-color: $color-ios-ref-warning-dark; - } } .module-quote__reference-warning__icon { @@ -2085,25 +1850,19 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', width: 16px; @include light-theme { - @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-60); + @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90); } @include dark-theme { - @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-25); - } - @include ios-theme { - @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90); - } - @include ios-dark-theme { - @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90); + @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-05); } } .module-quote__reference-warning__icon--incoming { - @include ios-theme { + @include light-theme { @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90); } - @include ios-dark-theme { - @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90); + @include dark-theme { + @include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-05); } } @@ -2116,16 +1875,13 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', color: $color-gray-90; } @include dark-theme { - color: $color-gray-02; - } - @include ios-dark-theme { - color: $color-gray-90; + color: $color-gray-05; } } .module-quote__reference-warning__text--incoming { - @include ios-dark-theme { - color: $color-gray-90; + @include dark-theme { + color: $color-gray-05; } } @@ -2177,13 +1933,13 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } .module-embedded-contact--outgoing { - @include ios-keyboard-mode { + @include keyboard-mode { &:focus { box-shadow: 0px 0px 0px 2px $color-white; } @@ -2221,15 +1977,9 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', text-overflow: ellipsis; @include light-theme { - color: $color-gray-90; - } - @include dark-theme { - color: $color-gray-02; - } - @include ios-theme { color: $color-white; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-05; } } @@ -2237,10 +1987,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', .module-embedded-contact__contact-name--incoming { color: $color-white; - @include ios-theme { + @include light-theme { color: $color-gray-90; } - @include ios-dark-theme { + @include dark-theme { color: $color-gray-25; } } @@ -2254,13 +2004,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', overflow-x: hidden; text-overflow: ellipsis; - @include light-theme { - color: $color-gray-60; - } @include dark-theme { color: $color-white-alpha-80; } - @include ios-theme { + @include light-theme { color: $color-white-alpha-80; } } @@ -2268,7 +2015,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', .module-embedded-contact__contact-method--incoming { color: $color-white-alpha-80; - @include ios-theme { + @include light-theme { color: $color-gray-60; } } @@ -2300,7 +2047,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', @include button-reset; border-radius: 4px; - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; display: inline-block; padding: 6px; margin-top: 20px; @@ -2463,7 +2210,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', padding: 12px; border-radius: 4px; - color: $ultramarine-ui-light; + color: $color-ultramarine; @include light-theme { background-color: $color-gray-02; @@ -2858,7 +2605,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', &:focus { @include color-svg( '../images/icons/v2/profile-circle-outline-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } } @@ -2948,6 +2695,10 @@ button.module-conversation-details__action-button { } } + &__chat-color { + @include color-bubble(20px); + } + &-membership-list { &__add-members-icon { @mixin plus-icon($color) { @@ -3060,6 +2811,21 @@ button.module-conversation-details__action-button { -webkit-mask-size: 100%; } + &--color { + &::after { + -webkit-mask: url(../images/icons/v2/color-outline-24.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + &--timer { &::after { -webkit-mask: url(../images/icons/v2/timer-disabled-24.svg) no-repeat @@ -3312,10 +3078,10 @@ button.module-conversation-details__action-button { } @include keyboard-mode { - @include keyboard-focus-state($ultramarine-ui-light); + @include keyboard-focus-state($color-ultramarine); } @include dark-keyboard-mode { - @include keyboard-focus-state($ultramarine-ui-dark); + @include keyboard-focus-state($color-ultramarine-light); } } @@ -3430,7 +3196,7 @@ button.module-conversation-details__action-button { } &:focus { - border: 3px solid $ultramarine-ui-light; + border: 3px solid $color-ultramarine; line-height: 14px; padding-left: 10px; } @@ -3706,7 +3472,7 @@ button.module-conversation-details__action-button { } .module-media-gallery__tab--active { - border-bottom: 2px solid $ultramarine-ui-light; + border-bottom: 2px solid $color-ultramarine; } .module-media-gallery__content { @@ -3768,7 +3534,7 @@ button.module-conversation-details__action-button { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -3815,7 +3581,7 @@ button.module-conversation-details__action-button { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -3868,7 +3634,7 @@ button.module-conversation-details__action-button { width: 24px; @include color-svg( '../images/icons/v2/play-solid-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } @@ -4101,7 +3867,7 @@ button.module-conversation-details__action-button { } &:focus { - border: solid 1px $ultramarine-ui-light; + border: solid 1px $color-ultramarine; outline: none; } @@ -4162,7 +3928,7 @@ button.module-conversation-details__action-button { width: 16px; border-radius: 8px; - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } &__avatar { @@ -4229,7 +3995,7 @@ button.module-conversation-details__action-button { } &:focus { - @include color-svg($icon, $ultramarine-ui-light); + @include color-svg($icon, $color-ultramarine); } } } @@ -4269,7 +4035,7 @@ button.module-conversation-details__action-button { &:focus { span { background-color: $color-gray-75; - border: 4px solid $ultramarine-ui-light; + border: 4px solid $color-ultramarine; box-sizing: border-box; outline: none; } @@ -4450,13 +4216,13 @@ button.module-conversation-details__action-button { right: 1px; border-radius: 10px; - box-shadow: 0 0 0 3px $ultramarine-ui-light; + box-shadow: 0 0 0 3px $color-ultramarine; } } button.module-image__border-overlay:focus { @include keyboard-mode { - box-shadow: inset 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: inset 0px 0px 0px 2px $color-ultramarine; } } @@ -4532,7 +4298,7 @@ button.module-image__border-overlay:focus { width: 24px; @include color-svg( '../images/icons/v2/play-solid-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } @@ -4568,7 +4334,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - outline: 2px solid $ultramarine-ui-light; + outline: 2px solid $color-ultramarine; } } } @@ -4624,13 +4390,10 @@ button.module-image__border-overlay:focus { width: 6px; opacity: 0.4; - @include light-theme { - background-color: $color-gray-60; - } @include dark-theme { background-color: $color-white; } - @include ios-theme { + @include light-theme { background-color: $color-gray-60; } } @@ -4644,7 +4407,7 @@ button.module-image__border-overlay:focus { background-color: $color-white; - @include ios-theme { + @include light-theme { background-color: $color-gray-60; } } @@ -4726,7 +4489,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - @include color-svg('../images/icons/v2/x-24.svg', $ultramarine-ui-light); + @include color-svg('../images/icons/v2/x-24.svg', $color-ultramarine); } } } @@ -4934,7 +4697,7 @@ button.module-image__border-overlay:focus { color: $color-white-alpha-80; } &:focus { - border: 1px solid $ultramarine-ui-light; + border: 1px solid $color-ultramarine; outline: none; } } @@ -4943,7 +4706,7 @@ button.module-image__border-overlay:focus { @include button-reset; position: absolute; - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; color: $color-white; height: 28px; @@ -4979,7 +4742,7 @@ button.module-image__border-overlay:focus { } @include keyboard-mode { &:focus { - box-shadow: inset 0 0 0 2px $ultramarine-ui-light; + box-shadow: inset 0 0 0 2px $color-ultramarine; } } @@ -4992,7 +4755,7 @@ button.module-image__border-overlay:focus { } @include dark-keyboard-mode { &:focus { - box-shadow: inset 0 0 0 2px $ultramarine-ui-light; + box-shadow: inset 0 0 0 2px $color-ultramarine; } } } @@ -5125,7 +4888,7 @@ button.module-image__border-overlay:focus { } @include keyboard-mode { &:focus { - @include color-svg('../images/icons/v2/x-24.svg', $ultramarine-ui-light); + @include color-svg('../images/icons/v2/x-24.svg', $color-ultramarine); } } @@ -5134,7 +4897,10 @@ button.module-image__border-overlay:focus { } @include dark-keyboard-mode { &:focus { - @include color-svg('../images/icons/v2/x-24.svg', $ultramarine-ui-dark); + @include color-svg( + '../images/icons/v2/x-24.svg', + $color-ultramarine-light + ); } } } @@ -5213,38 +4979,26 @@ button.module-image__border-overlay:focus { } .module-spinner__arc--incoming { @include light-theme { - background-color: $color-white; - } - @include dark-theme { - background-color: $color-gray-02; - } - @include ios-theme { background-color: $color-gray-60; } - @include ios-dark-theme { + @include dark-theme { background-color: $color-gray-02; } } .module-spinner__circle--outgoing { + @include light-theme { + background-color: $color-white-alpha-40; + } @include dark-theme { background-color: $color-white-alpha-40; } - @include ios-theme { - background-color: $color-white-alpha-40; - } - @include ios-dark-theme { - background-color: $color-white-alpha-40; - } } .module-spinner__arc--outgoing { - @include dark-theme { - background-color: $color-gray-05; - } - @include ios-theme { + @include light-theme { background-color: $color-white; } - @include ios-dark-theme { + @include dark-theme { background-color: $color-gray-05; } } @@ -5282,7 +5036,7 @@ button.module-image__border-overlay:focus { } } .module-spinner__arc--on-progress-dialog { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } .module-spinner__arc--on-avatar { background-color: $color-white; @@ -5313,18 +5067,6 @@ button.module-image__border-overlay:focus { } .module-message-body__at-mention--incoming { - @include ios-theme { - @include light-theme { - background-color: $color-gray-20; - } - } - - @include ios-dark-theme { - background-color: $color-gray-60; - } -} - -.module-message-body__at-mention--outgoing { @include light-theme { background-color: $color-gray-20; } @@ -5332,10 +5074,6 @@ button.module-image__border-overlay:focus { @include dark-theme { background-color: $color-gray-60; } - - @include ios-theme { - background-color: $ultramarine-brand-dark; - } } // Module: Reaction Viewer @@ -5387,7 +5125,7 @@ button.module-image__border-overlay:focus { } @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -5580,7 +5318,7 @@ button.module-image__border-overlay:focus { display: block; width: 4px; height: 4px; - background: $ultramarine-ui-light; + background: $color-ultramarine; border-radius: 2px; position: absolute; bottom: 4px; @@ -5756,13 +5494,13 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 4px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 4px $color-ultramarine; } } @include mouse-mode { &:hover { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -5781,13 +5519,13 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 4px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 4px $color-ultramarine; } } @include mouse-mode { &:hover { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -5803,13 +5541,13 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 4px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 4px $color-ultramarine; } } @include mouse-mode { &:hover { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -5828,13 +5566,13 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 4px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 4px $color-ultramarine; } } @include mouse-mode { &:hover { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -6639,7 +6377,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - outline: 2px solid $ultramarine-ui-light; + outline: 2px solid $color-ultramarine; } } } @@ -6816,11 +6554,11 @@ button.module-image__border-overlay:focus { padding-left: 12px; @include light-theme { - border-left: 4px solid $ultramarine-ui-light; + border-left: 4px solid $color-ultramarine; } @include dark-theme { - border-left: 4px solid $ultramarine-ui-dark; + border-left: 4px solid $color-ultramarine-light; } } @@ -6859,11 +6597,11 @@ button.module-image__border-overlay:focus { color: $color-white; @include light-theme { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; box-shadow: 0px 0px 0px 1px $color-gray-02; } @include dark-theme { - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; box-shadow: 0px 0px 0px 1px $color-gray-90; } } @@ -7136,7 +6874,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { border-width: 2px; - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; &:checked { box-shadow: inset 0 0 0px 1px $color-white; } @@ -7145,7 +6883,7 @@ button.module-image__border-overlay:focus { @include dark-keyboard-mode { &:focus { border-width: 2px; - border-color: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; &:checked { box-shadow: inset 0 0 0px 1px $color-black; @@ -7160,7 +6898,7 @@ button.module-image__border-overlay:focus { &:checked { $icon: '../images/icons/v2/check-24.svg'; - background: $ultramarine-ui-light; + background: $color-ultramarine; display: flex; align-items: center; justify-content: center; @@ -7292,7 +7030,7 @@ button.module-image__border-overlay:focus { &:focus { @include color-svg( '../images/icons/v2/chevron-left-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } } @@ -7307,7 +7045,7 @@ button.module-image__border-overlay:focus { &:hover { @include color-svg( '../images/icons/v2/chevron-left-24.svg', - $ultramarine-ui-dark + $color-ultramarine-light ); } } @@ -7669,7 +7407,7 @@ button.module-image__border-overlay:focus { width: 14px; height: 14px; border-radius: 7px; - background: $ultramarine-ui-light; + background: $color-ultramarine; } } } @@ -7780,11 +7518,11 @@ button.module-image__border-overlay:focus { &--hint { @include light-theme() { - color: $ultramarine-ui-light; + color: $color-ultramarine; } @include dark-theme() { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; } } @@ -7907,7 +7645,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -8011,11 +7749,11 @@ button.module-image__border-overlay:focus { &--blue { @include light-theme { - background: $ultramarine-ui-light; + background: $color-ultramarine; color: $color-white; } @include dark-theme { - background: $ultramarine-ui-dark; + background: $color-ultramarine-light; color: $color-white; } } @@ -8447,13 +8185,13 @@ button.module-image__border-overlay:focus { &:focus { @include keyboard-mode { - box-shadow: 0 0 0 3px $ultramarine-ui-light; + box-shadow: 0 0 0 3px $color-ultramarine; } } &:hover { @include mouse-mode { - box-shadow: 0 0 0 3px $ultramarine-ui-light; + box-shadow: 0 0 0 3px $color-ultramarine; } } } @@ -8552,7 +8290,7 @@ button.module-image__border-overlay:focus { border-color: $color-gray-60; &:focus { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } &:placeholder { @@ -8566,7 +8304,7 @@ button.module-image__border-overlay:focus { color: $color-gray-05; &:focus { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } &:placeholder { @@ -8917,11 +8655,11 @@ button.module-image__border-overlay:focus { border-radius: 4px; @include light-theme { - color: $ultramarine-ui-light; + color: $color-ultramarine; background-color: $color-gray-02; } @include dark-theme { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; background-color: $color-gray-75; } } @@ -9060,11 +8798,11 @@ button.module-image__border-overlay:focus { &:focus-within { @include light-theme() { - border: 1px solid $ultramarine-ui-light; + border: 1px solid $color-ultramarine; } @include dark-theme() { - border: 1px solid $ultramarine-ui-light; + border: 1px solid $color-ultramarine; } } } @@ -9203,10 +8941,7 @@ button.module-image__border-overlay:focus { width: 24px; height: 24px; flex-shrink: 0; - @include color-svg( - '../images/icons/v2/send-24.svg', - $ultramarine-ui-light - ); + @include color-svg('../images/icons/v2/send-24.svg', $color-ultramarine); } } &__input { @@ -9398,10 +9133,10 @@ button.module-image__border-overlay:focus { } .module-scroll-down__button--new-messages { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; &:hover { - background-color: $ultramarine-brand-dark; + background-color: $color-ultramarine-dark; } } @@ -9547,6 +9282,17 @@ button.module-image__border-overlay:focus { ); } } +.module-avatar-popup__item__icon-colors { + @include light-theme { + @include color-svg( + '../images/icons/v2/color-outline-24.svg', + $color-gray-75 + ); + } + @include dark-theme { + @include color-svg('../images/icons/v2/color-solid-24.svg', $color-gray-15); + } +} .module-avatar-popup__item__icon-archive { @include light-theme { @include color-svg( @@ -9650,10 +9396,13 @@ button.module-image__border-overlay:focus { &:focus { @include keyboard-mode { - @include color-svg('../images/icons/v2/x-24.svg', $ultramarine-ui-light); + @include color-svg('../images/icons/v2/x-24.svg', $color-ultramarine); } @include dark-keyboard-mode { - @include color-svg('../images/icons/v2/x-24.svg', $ultramarine-ui-dark); + @include color-svg( + '../images/icons/v2/x-24.svg', + $color-ultramarine-light + ); } } } @@ -9811,7 +9560,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - outline: 2px solid $ultramarine-ui-light; + outline: 2px solid $color-ultramarine; } } } @@ -10129,10 +9878,10 @@ button.module-image__border-overlay:focus { &:focus { @include keyboard-mode { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } @include dark-keyboard-mode { - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; } } } @@ -10229,7 +9978,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @@ -10262,7 +10011,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -10279,7 +10028,7 @@ button.module-image__border-overlay:focus { @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -10487,7 +10236,7 @@ $contact-modal-padding: 18px; &:focus { @include keyboard-mode { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } } } @@ -10497,7 +10246,7 @@ $contact-modal-padding: 18px; background-color: $color-black-alpha-40; } - @each $color, $value in $conversation-colors { + @each $color, $value in $avatar-colors { &__#{$color} { background-color: $value; } diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index 746778cc7c09..eeea2ec60a98 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -42,148 +42,144 @@ $color-black-alpha-60: rgba($color-black, 0.6); $color-black-alpha-70: rgba($color-black, 0.7); $color-black-alpha-80: rgba($color-black, 0.8); -$ultramarine-brand-light: #3a76f0; -$ultramarine-brand-dark: #1851b4; -$ultramarine-ui-light: #2c6bed; -$ultramarine-ui-dark: #6191f3; +$color-ultramarine-dark: #1851b4; +$color-ultramarine-icon: #3a76f0; +$color-ultramarine-light: #6191f3; +$color-ultramarine: #2c6bed; -$color-crimson: #cc163d; -$color-vermilion: #c73800; -$color-burlap: #746c53; +// Flat colors + +$color-crimson: #cf163e; +$color-vermilion: #c73f0a; +$color-burlap: #6f6a58; $color-forest: #3b7845; -$color-wintergreen: #1c8260; -$color-teal: #067589; +$color-wintergreen: #1d8663; +$color-teal: #077d92; $color-blue: #336ba3; -$color-indigo: #5951c8; -$color-violet: #862caf; -$color-plum: #a23474; -$color-taupe: #895d66; -$color-steel: #6b6b78; +$color-indigo: #6058ca; +$color-violet: #9932c8; +$color-plum: #aa377a; +$color-taupe: #8f616a; +$color-steel: #71717f; -// Tints and shades +// Gradient colors -// Used for iOS theme and the safety number change warning banner -$color-ios-blue-tint: #b0c8f9; +$color-ultramarine-gradient: ( + deg: 180deg, + start: #0552f0, + end: $color-ultramarine, +); +$color-basil: ( + deg: 180deg, + start: #2f9373, + end: #077343, +); +$color-ember: ( + deg: 168deg, + start: #e57c00, + end: #5e0000, +); +$color-fluorescent: ( + deg: 192deg, + start: #ec13dd, + end: #1b36c6, +); +$color-infrared: ( + deg: 192deg, + start: #f65560, + end: #442ced, +); +$color-lagoon: ( + deg: 180deg, + start: #004066, + end: #32867d, +); +$color-midnight: ( + deg: 180deg, + start: #2c2c3a, + end: #787891, +); +$color-sea: ( + deg: 180deg, + start: #498fd4, + end: #2c66a0, +); +$color-sublime: ( + deg: 180deg, + start: #6281d5, + end: #974460, +); +$color-tangerine: ( + deg: 192deg, + start: #db7133, + end: #911231, +); -// Used for scroll down button hover state when it has new messages -$color-ios-ref-warning-light: #d2def8; -$color-ios-ref-warning-dark: #7b97cd; +// Avatars -$color-crimson-tint: #eda6ae; -$color-vermilion-tint: #eba78e; -$color-burlap-tint: #c4b997; -$color-forest-tint: #8fcc9a; -$color-wintergreen-tint: #9bcfbd; -$color-teal-tint: #a5cad5; -$color-blue-tint: #adc8e1; -$color-indigo-tint: #c2c1e7; -$color-violet-tint: #cdaddc; -$color-plum-tint: #dcb2ca; -$color-taupe-tint: #cfb5bb; -$color-steel-tint: #bebec6; - -$color-crimson-shade: #8a0f29; -$color-vermilion-shade: #872600; -$color-burlap-shade: #58513c; -$color-forest-shade: #2b5934; -$color-wintergreen-shade: #36544a; -$color-teal-shade: #055968; -$color-blue-shade: #285480; -$color-indigo-shade: #4840a0; -$color-violet-shade: #6b248a; -$color-plum-shade: #881b5b; -$color-taupe-shade: #6a4e54; -$color-steel-shade: #5a5a63; - -// Semantic conversation colors - -$color-conversation-red: $color-crimson; -$color-conversation-deep_orange: $color-vermilion; -$color-conversation-brown: $color-burlap; -$color-conversation-pink: $color-plum; -$color-conversation-purple: $color-violet; -$color-conversation-indigo: $color-indigo; -$color-conversation-blue: $color-blue; -$color-conversation-teal: $color-teal; -$color-conversation-green: $color-forest; -$color-conversation-light_green: $color-wintergreen; -$color-conversation-blue_grey: $color-taupe; -$color-conversation-grey: $color-steel; -$color-conversation-ultramarine: $ultramarine-ui-light; - -$color-conversation-red-tint: $color-crimson-tint; -$color-conversation-deep_orange-tint: $color-vermilion-tint; -$color-conversation-brown-tint: $color-burlap-tint; -$color-conversation-pink-tint: $color-plum-tint; -$color-conversation-purple-tint: $color-violet-tint; -$color-conversation-indigo-tint: $color-indigo-tint; -$color-conversation-blue-tint: $color-blue-tint; -$color-conversation-teal-tint: $color-teal-tint; -$color-conversation-green-tint: $color-forest-tint; -$color-conversation-light_green-tint: $color-wintergreen-tint; -$color-conversation-blue_grey-tint: $color-taupe-tint; -$color-conversation-grey-tint: $color-steel-tint; -$color-conversation-ultramarine-tint: $color-ios-blue-tint; - -$color-conversation-red-shade: $color-crimson-shade; -$color-conversation-deep_orange-shade: $color-vermilion-shade; -$color-conversation-brown-shade: $color-burlap-shade; -$color-conversation-pink-shade: $color-plum-shade; -$color-conversation-purple-shade: $color-violet-shade; -$color-conversation-indigo-shade: $color-indigo-shade; -$color-conversation-blue-shade: $color-blue-shade; -$color-conversation-teal-shade: $color-teal-shade; -$color-conversation-green-shade: $color-forest-shade; -$color-conversation-light_green-shade: $color-wintergreen-shade; -$color-conversation-blue_grey-shade: $color-taupe-shade; -$color-conversation-grey-shade: $color-steel-shade; -$color-conversation-ultramarine-shade: $ultramarine-brand-dark; +$avatar-color-crimson: #d00b2c; +$avatar-color-vermilion: #c72a0a; +$avatar-color-burlap: #866118; +$avatar-color-forest: #067919; +$avatar-color-wintergreen: #067953; +$avatar-color-teal: #077288; +$avatar-color-blue: #0a69c7; +$avatar-color-indigo: #5151f6; +$avatar-color-violet: #a20ced; +$avatar-color-plum: #c70a88; +$avatar-color-taupe: #cb0b6b; +$avatar-color-steel: $color-gray-60; +$avatar-color-ultramarine: #0d59f2; // Maps for easy manipulation +$avatar-colors: ( + blue: $avatar-color-blue, + burlap: $avatar-color-burlap, + crimson: $avatar-color-crimson, + forest: $avatar-color-forest, + indigo: $avatar-color-indigo, + plum: $avatar-color-plum, + steel: $avatar-color-steel, + taupe: $avatar-color-taupe, + teal: $avatar-color-teal, + ultramarine: $avatar-color-ultramarine, + vermilion: $avatar-color-vermilion, + violet: $avatar-color-violet, + wintergreen: $avatar-color-wintergreen, +); + $conversation-colors: ( - 'red': $color-conversation-red, - 'deep_orange': $color-conversation-deep_orange, - 'brown': $color-conversation-brown, - 'pink': $color-conversation-pink, - 'purple': $color-conversation-purple, - 'indigo': $color-conversation-indigo, - 'blue': $color-conversation-blue, - 'teal': $color-conversation-teal, - 'green': $color-conversation-green, - 'light_green': $color-conversation-light_green, - 'blue_grey': $color-conversation-blue_grey, - 'ultramarine': $color-conversation-ultramarine, + blue: $color-blue, + burlap: $color-burlap, + crimson: $color-crimson, + forest: $color-forest, + indigo: $color-indigo, + plum: $color-plum, + steel: $color-steel, + taupe: $color-taupe, + teal: $color-teal, + vermilion: $color-vermilion, + violet: $color-violet, + wintergreen: $color-wintergreen, ); -$conversation-colors-tint: ( - 'red': $color-conversation-red-tint, - 'deep_orange': $color-conversation-deep_orange-tint, - 'brown': $color-conversation-brown-tint, - 'pink': $color-conversation-pink-tint, - 'purple': $color-conversation-purple-tint, - 'indigo': $color-conversation-indigo-tint, - 'blue': $color-conversation-blue-tint, - 'teal': $color-conversation-teal-tint, - 'green': $color-conversation-green-tint, - 'light_green': $color-conversation-light_green-tint, - 'blue_grey': $color-conversation-blue_grey-tint, - 'ultramarine': $color-conversation-ultramarine-tint, -); -$conversation-colors-shade: ( - 'red': $color-conversation-red-shade, - 'deep_orange': $color-conversation-deep_orange-shade, - 'brown': $color-conversation-brown-shade, - 'pink': $color-conversation-pink-shade, - 'purple': $color-conversation-purple-shade, - 'indigo': $color-conversation-indigo-shade, - 'blue': $color-conversation-blue-shade, - 'teal': $color-conversation-teal-shade, - 'green': $color-conversation-green-shade, - 'light_green': $color-conversation-light_green-shade, - 'blue_grey': $color-conversation-blue_grey-shade, - 'ultramarine': $color-conversation-ultramarine-shade, + +$conversation-colors-gradient: ( + ultramarine: $color-ultramarine-gradient, + basil: $color-basil, + ember: $color-ember, + fluorescent: $color-fluorescent, + infrared: $color-infrared, + lagoon: $color-lagoon, + midnight: $color-midnight, + sea: $color-sea, + sublime: $color-sublime, + tangerine: $color-tangerine, ); +// Used for the safety number change warning banner +$color-ios-blue-tint: #b0c8f9; + // -- Non-V3 colors // Used in spinners diff --git a/stylesheets/components/Avatar.scss b/stylesheets/components/Avatar.scss index 3537095f173d..509a63c49206 100644 --- a/stylesheets/components/Avatar.scss +++ b/stylesheets/components/Avatar.scss @@ -23,7 +23,7 @@ @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } } @@ -119,27 +119,20 @@ } &--no-image { - @include light-theme { - background-color: $color-conversation-grey; - } - @include dark-theme { - background-color: $color-conversation-grey-shade; - } + background-color: $avatar-color-steel; } &--signal-blue { - background-color: $ultramarine-ui-light; - } - - @each $color, $value in $conversation-colors { - &--#{$color} { - @include light-theme { - background-color: $value; - } + background-color: $avatar-color-ultramarine; + @include dark-theme { + background-color: $avatar-color-ultramarine; } } - @each $color, $value in $conversation-colors-shade { + + @each $color, $value in $avatar-colors { &--#{$color} { + background-color: $value; + @include dark-theme { background-color: $value; } diff --git a/stylesheets/components/AvatarInput.scss b/stylesheets/components/AvatarInput.scss index c355ba9d1fc9..e011e705e7d0 100644 --- a/stylesheets/components/AvatarInput.scss +++ b/stylesheets/components/AvatarInput.scss @@ -27,7 +27,7 @@ background: $color-white; @at-root '#{$dark-selector} #{&}' { - background: $ultramarine-ui-light; + background: $color-ultramarine; } &::before { @@ -36,7 +36,7 @@ display: block; @include color-svg( '../images/icons/v2/camera-outline-24.svg', - $ultramarine-ui-light, + $color-ultramarine, false ); -webkit-mask-size: 24px 24px; @@ -70,18 +70,18 @@ padding-top: 4px; @include light-theme { - color: $ultramarine-ui-light; + color: $color-ultramarine; } @include dark-theme { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; } } @include keyboard-mode { &:focus { .module-AvatarInput__avatar { - box-shadow: inset 0 0 0 2px $ultramarine-ui-light; + box-shadow: inset 0 0 0 2px $color-ultramarine; } .module-AvatarInput__label { diff --git a/stylesheets/components/Button.scss b/stylesheets/components/Button.scss index 609a899db297..6f3da927a6c3 100644 --- a/stylesheets/components/Button.scss +++ b/stylesheets/components/Button.scss @@ -24,11 +24,11 @@ user-select: none; @include keyboard-mode { - @include focus-box-shadow($color-white, $ultramarine-ui-light); + @include focus-box-shadow($color-white, $color-ultramarine); } @include dark-keyboard-mode { - @include focus-box-shadow($color-black, $ultramarine-brand-light); + @include focus-box-shadow($color-black, $color-ultramarine-icon); } &:disabled { @@ -47,7 +47,7 @@ &--primary { $color: $color-white; - $background-color: $ultramarine-ui-light; + $background-color: $color-ultramarine; color: $color; background: $background-color; @@ -80,7 +80,7 @@ } &--affirmative { - color: $ultramarine-ui-light; + color: $color-ultramarine; } &--destructive { @@ -103,7 +103,7 @@ } &--affirmative { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; } &--destructive { diff --git a/stylesheets/components/CallingSelectPresentingSourcesModal.scss b/stylesheets/components/CallingSelectPresentingSourcesModal.scss index 4ee39ce980f1..577e7d1b0448 100644 --- a/stylesheets/components/CallingSelectPresentingSourcesModal.scss +++ b/stylesheets/components/CallingSelectPresentingSourcesModal.scss @@ -47,8 +47,8 @@ width: 200px; &--selected { - background-color: $ultramarine-ui-dark; - border: 1px solid $ultramarine-ui-dark; + background-color: $color-ultramarine-dark; + border: 1px solid $color-ultramarine-dark; } img { diff --git a/stylesheets/components/ChatColorPicker.scss b/stylesheets/components/ChatColorPicker.scss new file mode 100644 index 000000000000..0db45f6ecb60 --- /dev/null +++ b/stylesheets/components/ChatColorPicker.scss @@ -0,0 +1,58 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.ChatColorPicker { + $bubble-size: 40px; + + &__bubbles { + align-items: center; + display: grid; + grid-gap: 24px; + grid-template-columns: repeat(auto-fit, $bubble-size); + justify-content: center; + margin: 20px 0; + } + + &__bubble { + align-items: center; + background-color: $color-gray-05; + display: flex; + justify-content: center; + @include color-bubble($bubble-size); + + &--selected { + border-color: $color-gray-75; + + @include dark-theme { + border-color: $color-white; + } + + &:hover { + &::after { + content: ''; + display: block; + height: 24px; + width: 24px; + @include color-svg( + '../images/icons/v2/more-horiz-24.svg', + $color-gray-05 + ); + } + } + } + + @include keyboard-mode { + &:focus { + border-color: $color-ultramarine; + outline: none; + } + } + } + + &__add-icon { + @include color-svg('../images/icons/v2/plus-24.svg', $color-gray-90); + display: block; + height: 24px; + width: 24px; + } +} diff --git a/stylesheets/components/ContactName.scss b/stylesheets/components/ContactName.scss new file mode 100644 index 000000000000..b5cdb951f621 --- /dev/null +++ b/stylesheets/components/ContactName.scss @@ -0,0 +1,256 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.module-contact-name { + &--000 { + color: #d00b0b; + @include dark-theme { + color: #f76e6e; + } + } + + &--120 { + color: #067906; + @include dark-theme { + color: #0ab80a; + } + } + + &--240 { + color: #5151f6; + @include dark-theme { + color: #8b8bf9; + } + } + + &--040 { + color: #866118; + @include dark-theme { + color: #d08f0b; + } + } + + &--160 { + color: #067953; + @include dark-theme { + color: #09b37b; + } + } + + &--280 { + color: #a20ced; + @include dark-theme { + color: #cb72f8; + } + } + + &--080 { + color: #507406; + @include dark-theme { + color: #77ae09; + } + } + + &--200 { + color: #086da0; + @include dark-theme { + color: #0da6f2; + } + } + + &--320 { + color: #c70a88; + @include dark-theme { + color: #f76ec9; + } + } + + &--020 { + color: #b34209; + @include dark-theme { + color: #f4702f; + } + } + + &--140 { + color: #06792d; + @include dark-theme { + color: #0ab844; + } + } + + &--240 { + color: #7a3df5; + @include dark-theme { + color: #ac86f9; + } + } + + &--060 { + color: #6c6c13; + @include dark-theme { + color: #a5a509; + } + } + + &--180 { + color: #067474; + @include dark-theme { + color: #09aeae; + } + } + + &--300 { + color: #b80ab8; + @include dark-theme { + color: #f75ff7; + } + } + + &--100 { + color: #2d7906; + @include dark-theme { + color: #42b309; + } + } + + &--220 { + color: #0d59f2; + @include dark-theme { + color: #6495f7; + } + } + + &--340 { + color: #d00b4d; + @include dark-theme { + color: #f76998; + } + } + + &--010 { + color: #c72a0a; + @include dark-theme { + color: #f67055; + } + } + + &--130 { + color: #067919; + @include dark-theme { + color: #0ab827; + } + } + + &--250 { + color: #6447f5; + @include dark-theme { + color: #9986f9; + } + } + + &--050 { + color: #76681e; + @include dark-theme { + color: #b89b0a; + } + } + + &--170 { + color: #067462; + @include dark-theme { + color: #09b397; + } + } + + &--290 { + color: #af0bd0; + @include dark-theme { + color: #e06ef7; + } + } + + &--090 { + color: #3d7406; + @include dark-theme { + color: #5eb309; + } + } + + &--210 { + color: #0a69c7; + @include dark-theme { + color: #429cf5; + } + } + + &--330 { + color: #cb0b6b; + @include dark-theme { + color: #f76eb2; + } + } + + &--030 { + color: #9c5711; + @include dark-theme { + color: #e97a0c; + } + } + + &--150 { + color: #067940; + @include dark-theme { + color: #09b35e; + } + } + + &--270 { + color: #8f2af4; + @include dark-theme { + color: #bd81f8; + } + } + + &--070 { + color: #5e6e0c; + @include dark-theme { + color: #8faa09; + } + } + + &--190 { + color: #077288; + @include dark-theme { + color: #0babcb; + } + } + + &--310 { + color: #c20aa3; + @include dark-theme { + color: #f75fdd; + } + } + + &--110 { + color: #1a7906; + @include dark-theme { + color: #27b80a; + } + } + + &--230 { + color: #3454f4; + @include dark-theme { + color: #778df8; + } + } + + &--350 { + color: #d00b2c; + @include dark-theme { + color: #f76e85; + } + } +} diff --git a/stylesheets/components/ContactPill.scss b/stylesheets/components/ContactPill.scss index d2ebd440aea3..91feff97f7c7 100644 --- a/stylesheets/components/ContactPill.scss +++ b/stylesheets/components/ContactPill.scss @@ -55,7 +55,7 @@ background: $color-gray-15; &::before { - @include color-svg($icon, $ultramarine-ui-light); + @include color-svg($icon, $color-ultramarine); } } } @@ -64,7 +64,7 @@ background: $color-gray-65; &::before { - @include color-svg($icon, $ultramarine-ui-dark); + @include color-svg($icon, $color-ultramarine-light); } } } diff --git a/stylesheets/components/ConversationHeader.scss b/stylesheets/components/ConversationHeader.scss index fdf6efb2fc9d..a67ddb491d35 100644 --- a/stylesheets/components/ConversationHeader.scss +++ b/stylesheets/components/ConversationHeader.scss @@ -87,12 +87,12 @@ @include keyboard-mode { &:focus { - color: $ultramarine-ui-light; + color: $color-ultramarine; } } @include dark-keyboard-mode { &:focus { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; } } } @@ -216,12 +216,12 @@ @include keyboard-mode { &:focus { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } } @include dark-keyboard-mode { &:focus { - border-color: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; } } diff --git a/stylesheets/components/CustomColorEditor.scss b/stylesheets/components/CustomColorEditor.scss new file mode 100644 index 000000000000..87511c27c284 --- /dev/null +++ b/stylesheets/components/CustomColorEditor.scss @@ -0,0 +1,58 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.CustomColorEditor { + &__messages { + border-radius: 8px; + border: 1px solid $color-gray-15; + padding: 27px 0; + margin-top: 16px; + position: relative; + } + + &__tabs { + margin-left: -16px; + margin-right: -16px; + } + + &__gradient-knob { + @include color-bubble(30px); + cursor: move; + position: absolute; + } + + &__slider-container { + margin-top: 26px; + } + + // .Slider for specificity + &__hue-slider.Slider { + background-image: linear-gradient( + 90deg, + hsl(0, 100%, 45%), + hsl(45, 100%, 30%), + hsl(90, 100%, 30%), + hsl(135, 100%, 30%), + hsl(180, 100%, 30%), + hsl(270, 100%, 50%), + hsl(360, 100%, 45%) + ); + margin-top: 8px; + margin-bottom: 30px; + } + + &__saturation-slider.Slider { + margin-top: 8px; + margin-bottom: 30px; + } + + &__footer { + display: flex; + justify-content: flex-end; + margin-top: 16px; + + .module-Button { + margin-left: 8px; + } + } +} diff --git a/stylesheets/components/ForwardMessageModal.scss b/stylesheets/components/ForwardMessageModal.scss index 2d16fe63f131..9bfeed6a1b7b 100644 --- a/stylesheets/components/ForwardMessageModal.scss +++ b/stylesheets/components/ForwardMessageModal.scss @@ -52,7 +52,7 @@ @include keyboard-mode { &:focus-within { - border: solid 1px $ultramarine-ui-light; + border: solid 1px $color-ultramarine; } } } @@ -99,7 +99,7 @@ @include keyboard-mode { &:focus { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } } } @@ -123,7 +123,7 @@ &:focus { @include color-svg( '../images/icons/v2/chevron-left-24.svg', - $ultramarine-ui-light + $color-ultramarine ); } } @@ -138,7 +138,7 @@ &:hover { @include color-svg( '../images/icons/v2/chevron-left-24.svg', - $ultramarine-ui-dark + $color-ultramarine-light ); } } diff --git a/stylesheets/components/GradientDial.scss b/stylesheets/components/GradientDial.scss new file mode 100644 index 000000000000..a820f83df912 --- /dev/null +++ b/stylesheets/components/GradientDial.scss @@ -0,0 +1,52 @@ +.GradientDial { + &__container { + height: 100%; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 100%; + } + + &__bar { + &--container { + height: 100%; + width: 100%; + overflow: hidden; + position: relative; + z-index: 1; + } + + &--node { + background: $color-white; + border: 1px solid $color-black-alpha-20; + height: 100%; + height: 1000px; + left: 50%; + position: absolute; + top: 50%; + transform-origin: center; + width: 4px; + z-index: 1; + } + } + + &__knob { + @include color-bubble(30px); + box-shadow: 0 0 4px $color-black-alpha-20; + cursor: move; + margin-left: -20px; + margin-top: -20px; + padding: 2px; + position: absolute; + z-index: 10; + + &--selected { + border-color: $color-gray-75; + + @include dark-theme { + border-color: $color-white; + } + } + } +} diff --git a/stylesheets/components/GroupDialog.scss b/stylesheets/components/GroupDialog.scss index 5ec114a9e6b7..a66e8a23b9e6 100644 --- a/stylesheets/components/GroupDialog.scss +++ b/stylesheets/components/GroupDialog.scss @@ -43,10 +43,10 @@ &:focus { @include keyboard-mode { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } @include dark-keyboard-mode { - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; } } } diff --git a/stylesheets/components/GroupTitleInput.scss b/stylesheets/components/GroupTitleInput.scss index 8b30d0e4cdb0..3c8a46bf11e8 100644 --- a/stylesheets/components/GroupTitleInput.scss +++ b/stylesheets/components/GroupTitleInput.scss @@ -37,10 +37,10 @@ outline: none; @include light-theme { - border-color: $ultramarine-ui-light; + border-color: $color-ultramarine; } @include dark-theme { - border-color: $ultramarine-ui-dark; + border-color: $color-ultramarine-light; } } } diff --git a/stylesheets/components/MessageAudio.scss b/stylesheets/components/MessageAudio.scss index ae3f63610852..423bfdd191b9 100644 --- a/stylesheets/components/MessageAudio.scss +++ b/stylesheets/components/MessageAudio.scss @@ -17,41 +17,16 @@ margin-bottom: 7px; .module-message__audio-attachment--incoming & { - @mixin android { - border-color: $color-white-alpha-20; - } - @include light-theme { - @include android; - } - @include dark-theme { - @include android; - } - @include ios-theme { border-color: $color-black-alpha-20; } - @include ios-dark-theme { + @include dark-theme { border-color: $color-white-alpha-20; } } .module-message__container--outgoing & { - @mixin ios { - border-color: $color-white-alpha-20; - } - - @include light-theme { - border-color: $color-black-alpha-20; - } - @include dark-theme { - border-color: $color-white-alpha-20; - } - @include ios-theme { - @include ios; - } - @include ios-dark-theme { - @include ios; - } + border-color: $color-white-alpha-20; } } @@ -98,24 +73,12 @@ } .module-message__audio-attachment--incoming & { - @mixin android { - background: $color-white-alpha-20; - - @include all-audio-icons($color-white); - } - @include light-theme { - @include android; - } - @include dark-theme { - @include android; - } - @include ios-theme { background: $color-white; @include all-audio-icons($color-gray-60); } - @include ios-dark-theme { + @include dark-theme { background: $color-gray-60; @include all-audio-icons($color-gray-15); @@ -123,30 +86,8 @@ } .module-message__audio-attachment--outgoing & { - @mixin android { - background: $color-white; - - @include all-audio-icons($color-gray-60); - } - - @mixin ios { - background: $color-white-alpha-20; - - @include all-audio-icons($color-white); - } - - @include light-theme { - @include android; - } - @include dark-theme { - @include android; - } - @include ios-theme { - @include ios; - } - @include ios-dark-theme { - @include ios; - } + background: $color-white-alpha-20; + @include all-audio-icons($color-white); } } @@ -166,19 +107,13 @@ .module-message__audio-attachment__waveform { &:focus { @include keyboard-mode { - outline: 2px solid $color-white-alpha-60; - } - @include ios-keyboard-mode { - outline: 2px solid $ultramarine-ui-light; + outline: 2px solid $color-ultramarine; } } .module-message__audio-attachment--outgoing & { &:focus { @include keyboard-mode { - outline: 2px solid $ultramarine-ui-light; - } - @include ios-keyboard-mode { outline: 2px solid $color-white-alpha-60; } } @@ -197,26 +132,13 @@ } .module-message__audio-attachment--incoming & { - @mixin android { - background: $color-white-alpha-40; - &--active { - background: $color-white-alpha-80; - } - } - @include light-theme { - @include android; - } - @include dark-theme { - @include android; - } - @include ios-theme { background: $color-black-alpha-40; &--active { background: $color-black-alpha-80; } } - @include ios-dark-theme { + @include dark-theme { background: $color-white-alpha-40; &--active { background: $color-white-alpha-70; @@ -225,30 +147,9 @@ } .module-message__audio-attachment--outgoing & { - @mixin ios { - background: $color-white-alpha-40; - &--active { - background: $color-white-alpha-80; - } - } - - @include light-theme { - background: $color-black-alpha-20; - &--active { - background: $color-black-alpha-50; - } - } - @include dark-theme { - background: $color-white-alpha-40; - &--active { - background: $color-white-alpha-80; - } - } - @include ios-theme { - @include ios; - } - @include ios-dark-theme { - @include ios; + background: $color-white-alpha-40; + &--active { + background: $color-white-alpha-80; } } } @@ -265,40 +166,16 @@ @include font-caption; .module-message__audio-attachment--incoming & { - @mixin android { - color: $color-white-alpha-80; - } @include light-theme { - @include android; - } - @include dark-theme { - @include android; - } - @include ios-theme { color: $color-black-alpha-60; } - @include ios-dark-theme { + @include dark-theme { color: $color-white-alpha-80; } } .module-message__audio-attachment--outgoing & { - @mixin ios { - color: $color-white-alpha-80; - } - - @include light-theme { - color: $color-gray-60; - } - @include dark-theme { - color: $color-white-alpha-80; - } - @include ios-theme { - @include ios; - } - @include ios-dark-theme { - @include ios; - } + color: $color-white-alpha-80; } } diff --git a/stylesheets/components/Modal.scss b/stylesheets/components/Modal.scss index 626d7881a3d0..d2f45170f41f 100644 --- a/stylesheets/components/Modal.scss +++ b/stylesheets/components/Modal.scss @@ -55,10 +55,10 @@ &:focus { @include keyboard-mode { - background-color: $ultramarine-ui-light; + background-color: $color-ultramarine; } @include dark-keyboard-mode { - background-color: $ultramarine-ui-dark; + background-color: $color-ultramarine-light; } } } diff --git a/stylesheets/components/SafetyNumberChangeDialog.scss b/stylesheets/components/SafetyNumberChangeDialog.scss index 92ef066e2749..dfe5c9292af4 100644 --- a/stylesheets/components/SafetyNumberChangeDialog.scss +++ b/stylesheets/components/SafetyNumberChangeDialog.scss @@ -62,16 +62,16 @@ @include keyboard-mode { &:focus { - box-shadow: 0px 0px 0px 2px $ultramarine-ui-light; + box-shadow: 0px 0px 0px 2px $color-ultramarine; } } @include light-theme { - color: $ultramarine-ui-light; + color: $color-ultramarine; } @include dark-theme { - color: $ultramarine-ui-dark; + color: $color-ultramarine-light; } } } diff --git a/stylesheets/components/SafetyNumberViewer.scss b/stylesheets/components/SafetyNumberViewer.scss index 68386b93019b..53abdede5443 100644 --- a/stylesheets/components/SafetyNumberViewer.scss +++ b/stylesheets/components/SafetyNumberViewer.scss @@ -101,7 +101,7 @@ @include keyboard-mode { &:focus { - border: 1px solid $ultramarine-ui-light; + border: 1px solid $color-ultramarine; } } diff --git a/stylesheets/components/SearchInput.scss b/stylesheets/components/SearchInput.scss index 106c6eb60db0..f9beff1a0b3a 100644 --- a/stylesheets/components/SearchInput.scss +++ b/stylesheets/components/SearchInput.scss @@ -24,7 +24,7 @@ } &:focus-within { - border: solid 1px $ultramarine-ui-light; + border: solid 1px $color-ultramarine; outline: none; } } diff --git a/stylesheets/components/Slider.scss b/stylesheets/components/Slider.scss new file mode 100644 index 000000000000..0c8c990a742d --- /dev/null +++ b/stylesheets/components/Slider.scss @@ -0,0 +1,22 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.Slider { + background-color: $color-gray-15; + cursor: pointer; + height: 8px; + position: relative; + width: 100%; + + &__handle { + background-color: $color-gray-90; + border-radius: 16px; + border: 1px solid $color-white; + cursor: move; + height: 16px; + margin-left: -4px; + margin-top: -4px; + position: absolute; + width: 16px; + } +} diff --git a/stylesheets/components/Tabs.scss b/stylesheets/components/Tabs.scss new file mode 100644 index 000000000000..9c69885a4fac --- /dev/null +++ b/stylesheets/components/Tabs.scss @@ -0,0 +1,26 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.Tabs { + border-bottom: 1px solid $color-gray-15; + display: flex; + justify-content: space-around; + user-select: none; + + &__tab { + @include font-body-1; + cursor: pointer; + padding: 10px; + + &:focus { + @include mouse-mode { + outline: none; + } + } + + &--selected { + @include font-body-1-bold; + border-bottom: 2px solid $color-black; + } + } +} diff --git a/stylesheets/components/TimelineWarning.scss b/stylesheets/components/TimelineWarning.scss index 75870bab4262..eed9d13d235c 100644 --- a/stylesheets/components/TimelineWarning.scss +++ b/stylesheets/components/TimelineWarning.scss @@ -54,7 +54,7 @@ text-decoration: none; @include light-theme { - color: $ultramarine-brand-light; + color: $color-ultramarine-icon; } @include dark-theme { color: $color-ios-blue-tint; diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 3ac0d53b413b..11769e0c33a7 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -33,13 +33,17 @@ @import './components/Button.scss'; @import './components/CallingScreenSharingController.scss'; @import './components/CallingSelectPresentingSourcesModal.scss'; +@import './components/ChatColorPicker.scss'; +@import './components/ContactName.scss'; @import './components/ContactPill.scss'; @import './components/ContactPills.scss'; @import './components/ContactSpoofingReviewDialog.scss'; @import './components/ContactSpoofingReviewDialogPerson.scss'; @import './components/ConversationHeader.scss'; +@import './components/CustomColorEditor.scss'; @import './components/EditConversationAttributesModal.scss'; @import './components/ForwardMessageModal.scss'; +@import './components/GradientDial.scss'; @import './components/GroupDialog.scss'; @import './components/GroupTitleInput.scss'; @import './components/MessageAudio.scss'; @@ -49,5 +53,7 @@ @import './components/SearchInput.scss'; @import './components/SearchResultsLoadingFakeHeader.scss'; @import './components/SearchResultsLoadingFakeRow.scss'; +@import './components/Slider.scss'; +@import './components/Tabs.scss'; @import './components/TimelineWarning.scss'; @import './components/TimelineWarnings.scss'; diff --git a/ts/background.ts b/ts/background.ts index be2be398b633..2b477e79021b 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -2533,7 +2533,6 @@ export async function startApp(): Promise { conversation.set({ name: details.name, - color: details.color, inbox_position: details.inboxPosition, }); @@ -2640,7 +2639,6 @@ export async function startApp(): Promise { const updates = { name: details.name, members, - color: details.color, type: 'group', inbox_position: details.inboxPosition, } as WhatIsThis; diff --git a/ts/components/Avatar.stories.tsx b/ts/components/Avatar.stories.tsx index 0c3cd7abced0..ef0d52ed9701 100644 --- a/ts/components/Avatar.stories.tsx +++ b/ts/components/Avatar.stories.tsx @@ -11,13 +11,13 @@ import { action } from '@storybook/addon-actions'; import { Avatar, AvatarBlur, Props } from './Avatar'; import { setup as setupI18n } from '../../js/modules/i18n'; import enMessages from '../../_locales/en/messages.json'; -import { Colors, ColorType } from '../types/Colors'; +import { AvatarColors, AvatarColorType } from '../types/Colors'; const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Avatar', module); -const colorMap: Record = Colors.reduce( +const colorMap: Record = AvatarColors.reduce( (m, color) => ({ ...m, [color]: color, @@ -129,12 +129,14 @@ story.add('Group Icon', () => { story.add('Colors', () => { const props = createProps(); - return Colors.map(color => ); + return AvatarColors.map(color => ( + + )); }); story.add('Broken Color', () => { const props = createProps({ - color: 'nope' as ColorType, + color: 'nope' as AvatarColorType, }); return sizes.map(size => ); diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 9d141798b41b..fa5011e5b4e0 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -14,7 +14,7 @@ import { Spinner } from './Spinner'; import { getInitials } from '../util/getInitials'; import { LocalizerType } from '../types/Util'; -import { ColorType } from '../types/Colors'; +import { AvatarColorType } from '../types/Colors'; import * as log from '../logging/log'; import { assert } from '../util/assert'; import { shouldBlurAvatar } from '../util/shouldBlurAvatar'; @@ -37,7 +37,7 @@ export enum AvatarSize { export type Props = { avatarPath?: string; blur?: AvatarBlur; - color?: ColorType; + color?: AvatarColorType; loading?: boolean; acceptedMessageRequest: boolean; diff --git a/ts/components/AvatarPopup.stories.tsx b/ts/components/AvatarPopup.stories.tsx index 18fa8dba3bc7..d8473b7838a3 100644 --- a/ts/components/AvatarPopup.stories.tsx +++ b/ts/components/AvatarPopup.stories.tsx @@ -8,13 +8,13 @@ import { action } from '@storybook/addon-actions'; import { boolean, select, text } from '@storybook/addon-knobs'; import { AvatarPopup, Props } from './AvatarPopup'; -import { Colors, ColorType } from '../types/Colors'; +import { AvatarColors, AvatarColorType } from '../types/Colors'; import { setup as setupI18n } from '../../js/modules/i18n'; import enMessages from '../../_locales/en/messages.json'; const i18n = setupI18n('en', enMessages); -const colorMap: Record = Colors.reduce( +const colorMap: Record = AvatarColors.reduce( (m, color) => ({ ...m, [color]: color, @@ -41,6 +41,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ name: text('name', overrideProps.name || ''), noteToSelf: boolean('noteToSelf', overrideProps.noteToSelf || false), onClick: action('onClick'), + onSetChatColor: action('onSetChatColor'), onViewArchive: action('onViewArchive'), onViewPreferences: action('onViewPreferences'), phoneNumber: text('phoneNumber', overrideProps.phoneNumber || ''), diff --git a/ts/components/AvatarPopup.tsx b/ts/components/AvatarPopup.tsx index ada0979d033f..e6d3abaee35d 100644 --- a/ts/components/AvatarPopup.tsx +++ b/ts/components/AvatarPopup.tsx @@ -12,6 +12,7 @@ import { LocalizerType } from '../types/Util'; export type Props = { readonly i18n: LocalizerType; + onSetChatColor: () => unknown; onViewPreferences: () => unknown; onViewArchive: () => unknown; @@ -28,6 +29,7 @@ export const AvatarPopup = (props: Props): JSX.Element => { profileName, phoneNumber, title, + onSetChatColor, onViewPreferences, onViewArchive, style, @@ -72,6 +74,21 @@ export const AvatarPopup = (props: Props): JSX.Element => { {i18n('mainMenuSettings')}
+ + + + + )} + + + ); +}; diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx new file mode 100644 index 000000000000..26a699a0deae --- /dev/null +++ b/ts/components/GlobalModalContainer.tsx @@ -0,0 +1,43 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { Modal } from './Modal'; +import { LocalizerType } from '../types/Util'; +import { ConversationColorType } from '../types/Colors'; + +type PropsType = { + i18n: LocalizerType; + isChatColorEditorVisible: boolean; + renderChatColorPicker: (actions: { + setAllConversationColors: (color: ConversationColorType) => unknown; + }) => JSX.Element; + setAllConversationColors: (color: ConversationColorType) => unknown; + toggleChatColorEditor: () => unknown; +}; + +export const GlobalModalContainer = ({ + i18n, + isChatColorEditorVisible, + renderChatColorPicker, + setAllConversationColors, + toggleChatColorEditor, +}: PropsType): JSX.Element | null => { + if (isChatColorEditorVisible) { + return ( + + {renderChatColorPicker({ + setAllConversationColors, + })} + + ); + } + + return null; +}; diff --git a/ts/components/GradientDial.tsx b/ts/components/GradientDial.tsx new file mode 100644 index 000000000000..786ff3ef937e --- /dev/null +++ b/ts/components/GradientDial.tsx @@ -0,0 +1,309 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// eslint-disable-next-line max-len +/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */ + +import React, { CSSProperties, useEffect, useRef, useState } from 'react'; +import classNames from 'classnames'; + +export enum KnobType { + start = 'start', + end = 'end', +} + +export type PropsType = { + deg?: number; + knob1Style: CSSProperties; + knob2Style: CSSProperties; + onChange: (deg: number) => unknown; + onClick: (knob: KnobType) => unknown; + selectedKnob: KnobType; +}; + +// Converts from degrees to radians. +function toRadians(degrees: number): number { + return (degrees * Math.PI) / 180; +} + +// Converts from radians to degrees. +function toDegrees(radians: number): number { + return (radians * 180) / Math.PI; +} + +type CSSPosition = { left: number; top: number }; + +function getKnobCoordinates( + degrees: number, + rect: ClientRect +): { start: CSSPosition; end: CSSPosition } { + const center = { + x: rect.width / 2, + y: rect.height / 2, + }; + const alpha = toDegrees(Math.atan(rect.height / rect.width)); + const beta = (360.0 - alpha * 4) / 4; + + if (degrees < alpha) { + // Right top + const a = center.x; + const b = a * Math.tan(toRadians(degrees)); + + return { + start: { + left: rect.width, + top: center.y - b, + }, + end: { + left: 0, + top: center.y + b, + }, + }; + } + + if (degrees < 90) { + // Top right + const phi = 90 - degrees; + const a = center.y; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: center.x + b, + top: 0, + }, + end: { + left: center.x - b, + top: rect.height, + }, + }; + } + + if (degrees < 90 + beta) { + // Top left + const phi = degrees - 90; + const a = center.y; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: center.x - b, + top: 0, + }, + end: { + left: center.x + b, + top: rect.height, + }, + }; + } + + if (degrees < 180) { + // left top + const phi = 180 - degrees; + const a = center.x; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: 0, + top: center.y - b, + }, + end: { + left: rect.width, + top: center.y + b, + }, + }; + } + + if (degrees < 180 + alpha) { + // left bottom + const phi = degrees - 180; + const a = center.x; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: 0, + top: center.y + b, + }, + end: { + left: rect.width, + top: center.y - b, + }, + }; + } + + if (degrees < 270) { + // bottom left + const phi = 270 - degrees; + const a = center.y; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: center.x - b, + top: rect.height, + }, + end: { + left: center.x + b, + top: 0, + }, + }; + } + + if (degrees < 270 + beta) { + // bottom right + const phi = degrees - 270; + const a = center.y; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: center.x + b, + top: rect.height, + }, + end: { + left: center.x - b, + top: 0, + }, + }; + } + + // right bottom + const phi = 360 - degrees; + const a = center.x; + const b = a * Math.tan(toRadians(phi)); + + return { + start: { + left: rect.width, + top: center.y + b, + }, + end: { + left: 0, + top: center.y - b, + }, + }; +} + +export const GradientDial = ({ + deg = 180, + knob1Style, + knob2Style, + onChange, + onClick, + selectedKnob, +}: PropsType): JSX.Element => { + const containerRef = useRef(null); + + const [knobDim, setKnobDim] = useState<{ + start?: CSSPosition; + end?: CSSPosition; + }>({}); + + const handleMouseMove = (ev: MouseEvent) => { + if (!containerRef || !containerRef.current) { + return; + } + + const rect = containerRef.current.getBoundingClientRect(); + const center = { + x: rect.width / 2, + y: rect.height / 2, + }; + + const a = { x: ev.clientX - center.x, y: ev.clientY - center.y }; + const b = { x: center.x, y: 0 }; + const dot = a.x * b.x + a.y * b.y; + const det = a.x * b.y - a.y * b.x; + + const offset = selectedKnob === KnobType.end ? 180 : 0; + const degrees = (toDegrees(Math.atan2(det, dot)) + 360 + offset) % 360; + + onChange(degrees); + + ev.preventDefault(); + ev.stopPropagation(); + }; + + const handleMouseUp = () => { + document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('mousemove', handleMouseMove); + }; + + // We want to use React.MouseEvent here because above we + // use the regular MouseEvent + const handleMouseDown = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + useEffect(() => { + if (!containerRef || !containerRef.current) { + return; + } + + const containerRect = containerRef.current.getBoundingClientRect(); + setKnobDim(getKnobCoordinates(deg, containerRect)); + }, [containerRef, deg]); + + return ( +
+ {knobDim.start && ( +
{ + if (selectedKnob === KnobType.start) { + handleMouseDown(ev); + } + }} + onClick={() => { + onClick(KnobType.start); + }} + role="button" + style={{ + ...knob1Style, + ...knobDim.start, + }} + /> + )} + {knobDim.end && ( +
{ + if (selectedKnob === KnobType.end) { + handleMouseDown(ev); + } + }} + onClick={() => { + onClick(KnobType.end); + }} + role="button" + style={{ + ...knob2Style, + ...knobDim.end, + }} + /> + )} + {knobDim.start && knobDim.end && ( +
+
+
+ )} +
+ ); +}; diff --git a/ts/components/IncomingCallBar.stories.tsx b/ts/components/IncomingCallBar.stories.tsx index 2e5a1ee13c8a..332507fb5086 100644 --- a/ts/components/IncomingCallBar.stories.tsx +++ b/ts/components/IncomingCallBar.stories.tsx @@ -7,7 +7,7 @@ import { boolean, select, text } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; import { IncomingCallBar } from './IncomingCallBar'; -import { Colors, ColorType } from '../types/Colors'; +import { AvatarColors } from '../types/Colors'; import { setup as setupI18n } from '../../js/modules/i18n'; import enMessages from '../../_locales/en/messages.json'; import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; @@ -25,7 +25,7 @@ const defaultProps = { conversation: getDefaultConversation({ id: '3051234567', avatarPath: undefined, - color: 'ultramarine' as ColorType, + color: AvatarColors[0], name: 'Rick Sanchez', phoneNumber: '3051234567', profileName: 'Rick Sanchez', @@ -53,7 +53,7 @@ const permutations = [ storiesOf('Components/IncomingCallBar', module) .add('Knobs Playground', () => { - const color = select('color', Colors, 'ultramarine'); + const color = select('color', AvatarColors, 'ultramarine'); const isVideoCall = boolean('isVideoCall', false); const name = text( 'name', diff --git a/ts/components/MainHeader.stories.tsx b/ts/components/MainHeader.stories.tsx index 22f8d5d0d0bb..7b6d6cf8ba1b 100644 --- a/ts/components/MainHeader.stories.tsx +++ b/ts/components/MainHeader.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2020 Signal Messenger, LLC +// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -58,6 +58,7 @@ const createProps = (overrideProps: Partial = {}): PropsType => ({ showArchivedConversations: action('showArchivedConversations'), startComposing: action('startComposing'), + toggleChatColorEditor: action('toggleChatColorEditor'), }); story.add('Basic', () => { diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 24ba85e2ff47..59d0cface6f2 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -11,7 +11,7 @@ import { showSettings } from '../shims/Whisper'; import { Avatar } from './Avatar'; import { AvatarPopup } from './AvatarPopup'; import { LocalizerType } from '../types/Util'; -import { ColorType } from '../types/Colors'; +import { AvatarColorType } from '../types/Colors'; import { ConversationType } from '../state/ducks/conversations'; export type PropsType = { @@ -31,7 +31,7 @@ export type PropsType = { phoneNumber?: string; isMe?: boolean; name?: string; - color?: ColorType; + color?: AvatarColorType; disabled?: boolean; isVerified?: boolean; profileName?: string; @@ -64,6 +64,7 @@ export type PropsType = { showArchivedConversations: () => void; startComposing: () => void; + toggleChatColorEditor: () => void; }; type StateType = { @@ -351,6 +352,7 @@ export class MainHeader extends React.Component { searchConversationName, searchTerm, showArchivedConversations, + toggleChatColorEditor, } = this.props; const { showingAvatarPopup, popperRoot } = this.state; @@ -408,6 +410,10 @@ export class MainHeader extends React.Component { size={28} // See the comment above about `sharedGroupNames`. sharedGroupNames={[]} + onSetChatColor={() => { + toggleChatColorEditor(); + this.hideAvatarPopup(); + }} onViewPreferences={() => { showSettings(); this.hideAvatarPopup(); diff --git a/ts/components/Modal.tsx b/ts/components/Modal.tsx index a0ac67978436..882817bd0d6c 100644 --- a/ts/components/Modal.tsx +++ b/ts/components/Modal.tsx @@ -16,6 +16,7 @@ type PropsType = { hasXButton?: boolean; i18n: LocalizerType; moduleClassName?: string; + noMouseClose?: boolean; onClose?: () => void; title?: ReactNode; theme?: Theme; @@ -28,6 +29,7 @@ export function Modal({ hasXButton, i18n, moduleClassName, + noMouseClose, onClose = noop, title, theme, @@ -38,7 +40,7 @@ export function Modal({ const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName); return ( - +
unknown; readonly onClose: () => unknown; readonly children: React.ReactElement; @@ -14,7 +15,7 @@ export type PropsType = { }; export const ModalHost = React.memo( - ({ onEscape, onClose, children, theme }: PropsType) => { + ({ onEscape, onClose, children, noMouseClose, theme }: PropsType) => { const [root, setRoot] = React.useState(null); useEffect(() => { @@ -67,7 +68,7 @@ export const ModalHost = React.memo( 'module-modal-host__overlay', theme ? themeClassName(theme) : undefined )} - onClick={handleCancel} + onClick={noMouseClose ? undefined : handleCancel} > {children}
, diff --git a/ts/components/SafetyNumberChangeDialog.stories.tsx b/ts/components/SafetyNumberChangeDialog.stories.tsx index 2afd69b218ff..80421eba216f 100644 --- a/ts/components/SafetyNumberChangeDialog.stories.tsx +++ b/ts/components/SafetyNumberChangeDialog.stories.tsx @@ -15,7 +15,7 @@ const i18n = setupI18n('en', enMessages); const contactWithAllData = getDefaultConversation({ id: 'abc', avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', profileName: '-*Smartest Dude*-', title: 'Rick Sanchez', name: 'Rick Sanchez', @@ -25,7 +25,7 @@ const contactWithAllData = getDefaultConversation({ const contactWithJustProfile = getDefaultConversation({ id: 'def', avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', title: '-*Smartest Dude*-', profileName: '-*Smartest Dude*-', name: undefined, @@ -35,7 +35,7 @@ const contactWithJustProfile = getDefaultConversation({ const contactWithJustNumber = getDefaultConversation({ id: 'xyz', avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', profileName: undefined, name: undefined, title: '(305) 123-4567', @@ -45,7 +45,7 @@ const contactWithJustNumber = getDefaultConversation({ const contactWithNothing = getDefaultConversation({ id: 'some-guid', avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', profileName: undefined, name: undefined, phoneNumber: undefined, diff --git a/ts/components/SafetyNumberViewer.stories.tsx b/ts/components/SafetyNumberViewer.stories.tsx index 9a3d3924100f..c67496fda9d7 100644 --- a/ts/components/SafetyNumberViewer.stories.tsx +++ b/ts/components/SafetyNumberViewer.stories.tsx @@ -22,7 +22,7 @@ const contactWithAllData = { const contactWithJustProfile = { avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', title: '-*Smartest Dude*-', profileName: '-*Smartest Dude*-', name: undefined, @@ -31,7 +31,7 @@ const contactWithJustProfile = { const contactWithJustNumber = { avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', profileName: undefined, name: undefined, title: '(305) 123-4567', @@ -41,7 +41,7 @@ const contactWithJustNumber = { const contactWithNothing = { id: 'some-guid', avatarPath: undefined, - color: 'signal-blue', + color: 'ultramarine', profileName: undefined, title: 'Unknown contact', name: undefined, diff --git a/ts/components/SampleMessageBubbles.tsx b/ts/components/SampleMessageBubbles.tsx new file mode 100644 index 000000000000..b387ea72f32d --- /dev/null +++ b/ts/components/SampleMessageBubbles.tsx @@ -0,0 +1,112 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { CSSProperties } from 'react'; +import { ConversationColorType } from '../types/Colors'; +import { LocalizerType } from '../types/Util'; +import { formatRelativeTime } from '../util/formatRelativeTime'; + +export type PropsType = { + backgroundStyle?: CSSProperties; + color?: ConversationColorType; + i18n: LocalizerType; + includeAnotherBubble?: boolean; +}; + +const A_FEW_DAYS_AGO = 60 * 60 * 24 * 5 * 1000; + +const SampleMessage = ({ + color = 'ultramarine', + direction, + i18n, + text, + timestamp, + status, + style, +}: { + color?: ConversationColorType; + direction: 'incoming' | 'outgoing'; + i18n: LocalizerType; + text: string; + timestamp: number; + status: 'delivered' | 'read' | 'sent'; + style?: CSSProperties; +}): JSX.Element => ( +
+
+
+
+ {text} +
+
+ + {formatRelativeTime(timestamp, { extended: true, i18n })} + + {direction === 'outgoing' && ( +
+ )} +
+
+
+
+); + +export const SampleMessageBubbles = ({ + backgroundStyle = {}, + color, + i18n, + includeAnotherBubble = false, +}: PropsType): JSX.Element => { + const firstBubbleStyle = includeAnotherBubble ? backgroundStyle : undefined; + return ( + <> + +
+ {includeAnotherBubble ? ( + <> +
+
+ +
+
+ + ) : null} + +
+ + ); +}; diff --git a/ts/components/Slider.stories.tsx b/ts/components/Slider.stories.tsx new file mode 100644 index 000000000000..bc788b04c083 --- /dev/null +++ b/ts/components/Slider.stories.tsx @@ -0,0 +1,29 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useState } from 'react'; + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import { Slider, PropsType } from './Slider'; + +const story = storiesOf('Components/Slider', module); + +const createProps = (): PropsType => ({ + label: 'Slider Handle', + onChange: action('onChange'), + value: 30, +}); + +story.add('Default', () => ); + +story.add('Draggable Test', () => { + function StatefulSliderController(props: PropsType): JSX.Element { + const [value, setValue] = useState(30); + + return ; + } + + return ; +}); diff --git a/ts/components/Slider.tsx b/ts/components/Slider.tsx new file mode 100644 index 000000000000..e6709d963654 --- /dev/null +++ b/ts/components/Slider.tsx @@ -0,0 +1,126 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { CSSProperties, KeyboardEvent, useRef } from 'react'; +import { getClassNamesFor } from '../util/getClassNamesFor'; + +export type PropsType = { + containerStyle?: CSSProperties; + label: string; + handleStyle?: CSSProperties; + moduleClassName?: string; + onChange: (value: number) => unknown; + value: number; +}; + +export const Slider = ({ + containerStyle = {}, + label, + handleStyle = {}, + moduleClassName, + onChange, + value, +}: PropsType): JSX.Element => { + const diff = useRef(0); + const handleRef = useRef(null); + const sliderRef = useRef(null); + + const getClassName = getClassNamesFor('Slider', moduleClassName); + + const handleValueChange = (ev: MouseEvent | React.MouseEvent) => { + if (!sliderRef || !sliderRef.current) { + return; + } + + let x = + ev.clientX - + diff.current - + sliderRef.current.getBoundingClientRect().left; + + const max = sliderRef.current.offsetWidth; + + x = Math.min(max, Math.max(0, x)); + + const nextValue = (100 * x) / max; + + onChange(nextValue); + + ev.preventDefault(); + ev.stopPropagation(); + }; + + const handleMouseUp = () => { + document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('mousemove', handleValueChange); + }; + + // We want to use React.MouseEvent here because above we + // use the regular MouseEvent + const handleMouseDown = (ev: React.MouseEvent) => { + if (!handleRef || !handleRef.current) { + return; + } + + diff.current = ev.clientX - handleRef.current.getBoundingClientRect().left; + + document.addEventListener('mousemove', handleValueChange); + document.addEventListener('mouseup', handleMouseUp); + }; + + const handleKeyDown = (ev: KeyboardEvent) => { + let preventDefault = false; + + if (ev.key === 'ArrowRight') { + const nextValue = value + 1; + onChange(Math.min(nextValue, 100)); + + preventDefault = true; + } + + if (ev.key === 'ArrowLeft') { + const nextValue = value - 1; + onChange(Math.max(0, nextValue)); + + preventDefault = true; + } + + if (ev.key === 'Home') { + onChange(0); + preventDefault = true; + } + + if (ev.key === 'End') { + onChange(100); + preventDefault = true; + } + + if (preventDefault) { + ev.preventDefault(); + ev.stopPropagation(); + } + }; + + return ( +
+
+
+ ); +}; diff --git a/ts/components/Tabs.tsx b/ts/components/Tabs.tsx new file mode 100644 index 000000000000..efe4e0d8178a --- /dev/null +++ b/ts/components/Tabs.tsx @@ -0,0 +1,68 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { KeyboardEvent, ReactNode, useState } from 'react'; +import classNames from 'classnames'; +import { assert } from '../util/assert'; +import { getClassNamesFor } from '../util/getClassNamesFor'; + +type Tab = { + id: string; + label: string; +}; + +type PropsType = { + children: (renderProps: { selectedTab: string }) => ReactNode; + initialSelectedTab?: string; + moduleClassName?: string; + onTabChange?: (selectedTab: string) => unknown; + tabs: Array; +}; + +export const Tabs = ({ + children, + initialSelectedTab, + moduleClassName, + onTabChange, + tabs, +}: PropsType): JSX.Element => { + assert(tabs.length, 'Tabs needs more than 1 tab present'); + + const [selectedTab, setSelectedTab] = useState( + initialSelectedTab || tabs[0].id + ); + + const getClassName = getClassNamesFor('Tabs', moduleClassName); + + return ( + <> +
+ {tabs.map(({ id, label }) => ( +
{ + setSelectedTab(id); + onTabChange?.(id); + }} + onKeyUp={(e: KeyboardEvent) => { + if (e.target === e.currentTarget && e.keyCode === 13) { + setSelectedTab(id); + e.preventDefault(); + e.stopPropagation(); + } + }} + role="tab" + tabIndex={0} + > + {label} +
+ ))} +
+ {children({ selectedTab })} + + ); +}; diff --git a/ts/components/conversation/ContactName.stories.tsx b/ts/components/conversation/ContactName.stories.tsx index 52f6e9aa9407..f99ed9677288 100644 --- a/ts/components/conversation/ContactName.stories.tsx +++ b/ts/components/conversation/ContactName.stories.tsx @@ -8,6 +8,7 @@ import { storiesOf } from '@storybook/react'; import { setup as setupI18n } from '../../../js/modules/i18n'; import enMessages from '../../../_locales/en/messages.json'; import { ContactName } from './ContactName'; +import { ContactNameColors } from '../../types/Colors'; const i18n = setupI18n('en', enMessages); @@ -42,6 +43,18 @@ storiesOf('Components/Conversation/ContactName', module) /> ); }) + .add('Colors', () => { + return ContactNameColors.map(color => ( +
+ +
+ )); + }) .add('No data provided', () => { return ; }); diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index e4b132769f5a..ec20df724188 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -2,11 +2,15 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; +import classNames from 'classnames'; -import { LocalizerType } from '../../types/Util'; import { Emojify } from './Emojify'; +import { ContactNameColorType } from '../../types/Colors'; +import { LocalizerType } from '../../types/Util'; +import { getClassNamesFor } from '../../util/getClassNamesFor'; export type PropsType = { + contactNameColor?: ContactNameColorType; firstName?: string; i18n: LocalizerType; module?: string; @@ -18,12 +22,13 @@ export type PropsType = { }; export const ContactName = ({ + contactNameColor, firstName, module, preferFirstName, title, }: PropsType): JSX.Element => { - const prefix = module || 'module-contact-name'; + const getClassName = getClassNamesFor('module-contact-name', module); let text: string; if (preferFirstName) { @@ -33,7 +38,13 @@ export const ContactName = ({ } return ( - + ); diff --git a/ts/components/conversation/ConversationHeader.stories.tsx b/ts/components/conversation/ConversationHeader.stories.tsx index 70c9173d6d50..b4956d634b08 100644 --- a/ts/components/conversation/ConversationHeader.stories.tsx +++ b/ts/components/conversation/ConversationHeader.stories.tsx @@ -48,6 +48,7 @@ const commonProps = { 'onOutgoingVideoCallInConversation' ), + onShowChatColorEditor: action('onShowChatColorEditor'), onShowSafetyNumber: action('onShowSafetyNumber'), onShowAllMedia: action('onShowAllMedia'), onShowContactModal: action('onShowContactModal'), @@ -70,7 +71,7 @@ const stories: Array = [ title: 'With name and profile, verified', props: { ...commonProps, - color: 'red', + color: 'crimson', isVerified: true, avatarPath: gifUrl, title: 'Someone 🔥 Somewhere', @@ -114,7 +115,7 @@ const stories: Array = [ title: 'Profile, no name', props: { ...commonProps, - color: 'teal', + color: 'wintergreen', isVerified: false, phoneNumber: '(202) 555-0003', type: 'direct', @@ -140,7 +141,7 @@ const stories: Array = [ props: { ...commonProps, showBackButton: true, - color: 'deep_orange', + color: 'vermilion', phoneNumber: '(202) 555-0004', title: '(202) 555-0004', type: 'direct', @@ -212,7 +213,7 @@ const stories: Array = [ title: 'Basic', props: { ...commonProps, - color: 'signal-blue', + color: 'ultramarine', title: 'Typescript support group', name: 'Typescript support group', phoneNumber: '', @@ -227,7 +228,7 @@ const stories: Array = [ title: 'In a group you left - no disappearing messages', props: { ...commonProps, - color: 'signal-blue', + color: 'ultramarine', title: 'Typescript support group', name: 'Typescript support group', phoneNumber: '', @@ -243,7 +244,7 @@ const stories: Array = [ title: 'In a group with an active group call', props: { ...commonProps, - color: 'signal-blue', + color: 'ultramarine', title: 'Typescript support group', name: 'Typescript support group', phoneNumber: '', @@ -258,7 +259,7 @@ const stories: Array = [ title: 'In a forever muted group', props: { ...commonProps, - color: 'signal-blue', + color: 'ultramarine', title: 'Way too many messages', name: 'Way too many messages', phoneNumber: '', diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 19eb80fd179a..755f130881b8 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -72,6 +72,7 @@ export type PropsActionsType = { onOutgoingVideoCallInConversation: () => void; onSetPin: (value: boolean) => void; + onShowChatColorEditor: () => void; onShowConversationDetails: () => void; onShowSafetyNumber: () => void; onShowAllMedia: () => void; @@ -368,6 +369,7 @@ export class ConversationHeader extends React.Component { onSetDisappearingMessages, onSetMuteNotifications, onShowAllMedia, + onShowChatColorEditor, onShowConversationDetails, onShowGroupMembers, onShowSafetyNumber, @@ -456,6 +458,11 @@ export class ConversationHeader extends React.Component { ))} + {!isGroup ? ( + + {i18n('showChatColorEditor')} + + ) : null} {hasGV2AdminEnabled ? ( {i18n('showConversationDetails')} diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index c3298c558cd1..358d0bbfe47f 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -9,7 +9,7 @@ import { boolean, number, select, text } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import { SignalService } from '../../protobuf'; -import { Colors } from '../../types/Colors'; +import { ConversationColors } from '../../types/Colors'; import { EmojiPicker } from '../emoji/EmojiPicker'; import { Message, Props, AudioAttachmentProps } from './Message'; import { @@ -70,10 +70,7 @@ const renderAudioAttachment: Props['renderAudioAttachment'] = props => ( const createProps = (overrideProps: Partial = {}): Props => ({ attachments: overrideProps.attachments, - author: overrideProps.author || { - ...getDefaultConversation(), - color: select('authorColor', Colors, 'red'), - }, + author: overrideProps.author || getDefaultConversation(), reducedMotion: boolean('reducedMotion', false), bodyRanges: overrideProps.bodyRanges, canReply: true, @@ -81,6 +78,9 @@ const createProps = (overrideProps: Partial = {}): Props => ({ canDeleteForEveryone: overrideProps.canDeleteForEveryone || false, clearSelectedMessage: action('clearSelectedMessage'), collapseMetadata: overrideProps.collapseMetadata, + conversationColor: + overrideProps.conversationColor || + select('conversationColor', ConversationColors, ConversationColors[0]), conversationId: text('conversationId', overrideProps.conversationId || ''), conversationType: overrideProps.conversationType || 'direct', deletedForEveryone: overrideProps.deletedForEveryone, @@ -137,7 +137,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ showMessageDetail: action('showMessageDetail'), showVisualAttachment: action('showVisualAttachment'), status: overrideProps.status || 'sent', - text: text('text', overrideProps.text || ''), + text: overrideProps.text || text('text', ''), textPending: boolean('textPending', overrideProps.textPending || false), timestamp: number('timestamp', overrideProps.timestamp || Date.now()), }); @@ -1007,14 +1007,15 @@ story.add('Dangerous File Type', () => { story.add('Colors', () => { return ( <> - {Colors.map(color => ( - + {ConversationColors.map(color => ( +
+ {renderBothDirections( + createProps({ + conversationColor: color, + text: `Here is a preview of the chat color: ${color}. The color is visible to only you.`, + }) + )} +
))} ); @@ -1081,3 +1082,25 @@ story.add('Not approved, with link preview', () => { return renderBothDirections(props); }); + +story.add('Custom Color', () => ( + <> + +
+ + +)); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 00b8c59c02b3..bed996b9b0de 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -50,10 +50,15 @@ import { ContactType } from '../../types/Contact'; import { getIncrement } from '../../util/timer'; import { isFileDangerous } from '../../util/isFileDangerous'; import { BodyRangesType, LocalizerType, ThemeType } from '../../types/Util'; -import { ColorType } from '../../types/Colors'; +import { + ContactNameColorType, + ConversationColorType, + CustomColorType, +} from '../../types/Colors'; import { createRefMerger } from '../_util'; import { emojiToData } from '../emoji/lib'; import { SmartReactionPicker } from '../../state/smart/ReactionPicker'; +import { getCustomColorStyle } from '../../util/getCustomColorStyle'; type Trigger = { handleContextClick: (event: React.MouseEvent) => void; @@ -100,6 +105,9 @@ export type AudioAttachmentProps = { export type PropsData = { id: string; + contactNameColor?: ContactNameColorType; + conversationColor: ConversationColorType; + customColor?: CustomColorType; conversationId: string; text?: string; textPending?: boolean; @@ -128,6 +136,8 @@ export type PropsData = { conversationType: ConversationTypesType; attachments?: Array; quote?: { + conversationColor: ConversationColorType; + customColor?: CustomColorType; text: string; rawAttachment?: QuotedAttachmentType; isFromMe: boolean; @@ -137,7 +147,6 @@ export type PropsData = { authorProfileName?: string; authorTitle: string; authorName?: string; - authorColor?: ColorType; bodyRanges?: BodyRangesType; referencedMessageNotFound: boolean; }; @@ -656,6 +665,7 @@ export class Message extends React.Component { const { author, collapseMetadata, + contactNameColor, conversationType, direction, i18n, @@ -687,6 +697,7 @@ export class Message extends React.Component { return (
{ public renderQuote(): JSX.Element | null { const { - author, + conversationColor, conversationType, + customColor, direction, disableScroll, i18n, @@ -1050,8 +1062,6 @@ export class Message extends React.Component { const withContentAbove = conversationType === 'group' && direction === 'incoming'; - const quoteColor = - direction === 'incoming' ? author.color : quote.authorColor; const { referencedMessageNotFound } = quote; const clickHandler = disableScroll @@ -1073,9 +1083,10 @@ export class Message extends React.Component { authorPhoneNumber={quote.authorPhoneNumber} authorProfileName={quote.authorProfileName} authorName={quote.authorName} - authorColor={quoteColor} authorTitle={quote.authorTitle} bodyRanges={quote.bodyRanges} + conversationColor={conversationColor} + customColor={customColor} referencedMessageNotFound={referencedMessageNotFound} isFromMe={quote.isFromMe} withContentAbove={withContentAbove} @@ -2250,7 +2261,8 @@ export class Message extends React.Component { public renderContainer(): JSX.Element { const { attachments, - author, + conversationColor, + customColor, deletedForEveryone, direction, isSticker, @@ -2275,14 +2287,14 @@ export class Message extends React.Component { isTapToView && isTapToViewExpired ? 'module-message__container--with-tap-to-view-expired' : null, - !isSticker && direction === 'incoming' - ? `module-message__container--incoming-${author.color}` + !isSticker && direction === 'outgoing' + ? `module-message__container--outgoing-${conversationColor}` : null, isTapToView && isAttachmentPending && !isTapToViewExpired ? 'module-message__container--with-tap-to-view-pending' : null, isTapToView && isAttachmentPending && !isTapToViewExpired - ? `module-message__container--${direction}-${author.color}-tap-to-view-pending` + ? `module-message__container--${direction}-${conversationColor}-tap-to-view-pending` : null, isTapToViewError ? 'module-message__container--with-tap-to-view-error' @@ -2295,6 +2307,9 @@ export class Message extends React.Component { const containerStyles = { width: isShowingImage ? width : undefined, }; + if (!isSticker && direction === 'outgoing') { + Object.assign(containerStyles, getCustomColorStyle(customColor)); + } return (
diff --git a/ts/components/conversation/MessageDetail.stories.tsx b/ts/components/conversation/MessageDetail.stories.tsx index 78f48082bb88..10e5abbbad0a 100644 --- a/ts/components/conversation/MessageDetail.stories.tsx +++ b/ts/components/conversation/MessageDetail.stories.tsx @@ -25,6 +25,7 @@ const defaultMessage: MessageDataPropsType = { canReply: true, canDeleteForEveryone: true, canDownload: true, + conversationColor: 'crimson', conversationId: 'my-convo', conversationType: 'direct', direction: 'incoming', @@ -41,7 +42,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ contacts: overrideProps.contacts || [ { ...getDefaultConversation({ - color: 'green', + color: 'indigo', title: 'Just Max', }), isOutgoingKeyError: false, @@ -102,7 +103,7 @@ story.add('Message Statuses', () => { contacts: [ { ...getDefaultConversation({ - color: 'green', + color: 'forest', title: 'Max', }), isOutgoingKeyError: false, @@ -124,7 +125,7 @@ story.add('Message Statuses', () => { }, { ...getDefaultConversation({ - color: 'brown', + color: 'burlap', title: 'Terry', }), isOutgoingKeyError: false, @@ -135,7 +136,7 @@ story.add('Message Statuses', () => { }, { ...getDefaultConversation({ - color: 'light_green', + color: 'wintergreen', title: 'Theo', }), isOutgoingKeyError: false, @@ -146,7 +147,7 @@ story.add('Message Statuses', () => { }, { ...getDefaultConversation({ - color: 'blue_grey', + color: 'steel', title: 'Nikki', }), isOutgoingKeyError: false, @@ -205,7 +206,7 @@ story.add('All Errors', () => { contacts: [ { ...getDefaultConversation({ - color: 'green', + color: 'forest', title: 'Max', }), isOutgoingKeyError: true, @@ -233,7 +234,7 @@ story.add('All Errors', () => { }, { ...getDefaultConversation({ - color: 'brown', + color: 'taupe', title: 'Terry', }), isOutgoingKeyError: true, diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 655e5f3e4cd8..94f3f08ac19e 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import { boolean, text } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; -import { Colors } from '../../types/Colors'; +import { ConversationColors } from '../../types/Colors'; import { pngUrl } from '../../storybook/Fixtures'; import { Message, Props as MessagesProps } from './Message'; import { @@ -36,6 +36,7 @@ const defaultMessageProps: MessagesProps = { canDeleteForEveryone: true, canDownload: true, clearSelectedMessage: () => null, + conversationColor: 'crimson', conversationId: 'conversationId', conversationType: 'direct', // override deleteMessage: () => null, @@ -73,11 +74,11 @@ const defaultMessageProps: MessagesProps = { }; const renderInMessage = ({ - authorColor, authorName, authorPhoneNumber, authorProfileName, authorTitle, + conversationColor, isFromMe, rawAttachment, referencedMessageNotFound, @@ -85,14 +86,14 @@ const renderInMessage = ({ }: Props) => { const messageProps = { ...defaultMessageProps, - authorColor, + conversationColor, quote: { authorId: 'an-author', - authorColor, authorName, authorPhoneNumber, authorProfileName, authorTitle, + conversationColor, isFromMe, rawAttachment, referencedMessageNotFound, @@ -111,7 +112,6 @@ const renderInMessage = ({ }; const createProps = (overrideProps: Partial = {}): Props => ({ - authorColor: overrideProps.authorColor || 'green', authorName: text('authorName', overrideProps.authorName || ''), authorPhoneNumber: text( 'authorPhoneNumber', @@ -122,6 +122,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ overrideProps.authorProfileName || '' ), authorTitle: text('authorTitle', overrideProps.authorTitle || ''), + conversationColor: overrideProps.conversationColor || 'forest', i18n, isFromMe: boolean('isFromMe', overrideProps.isFromMe || false), isIncoming: boolean('isIncoming', overrideProps.isIncoming || false), @@ -182,7 +183,9 @@ story.add('Incoming/Outgoing Colors', () => { const props = createProps({}); return ( <> - {Colors.map(color => renderInMessage({ ...props, authorColor: color }))} + {ConversationColors.map(color => + renderInMessage({ ...props, conversationColor: color }) + )} ); }); @@ -440,3 +443,22 @@ story.add('@mention + incoming + me', () => { return ; }); + +story.add('Custom Color', () => ( + <> + + + +)); diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index 209aba4843a0..e5e075cf3677 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -10,16 +10,18 @@ import * as GoogleChrome from '../../util/GoogleChrome'; import { MessageBody } from './MessageBody'; import { BodyRangesType, LocalizerType } from '../../types/Util'; -import { ColorType } from '../../types/Colors'; +import { ConversationColorType, CustomColorType } from '../../types/Colors'; import { ContactName } from './ContactName'; import { getTextWithMentions } from '../../util/getTextWithMentions'; +import { getCustomColorStyle } from '../../util/getCustomColorStyle'; export type Props = { authorTitle: string; authorPhoneNumber?: string; authorProfileName?: string; authorName?: string; - authorColor?: ColorType; + conversationColor: ConversationColorType; + customColor?: CustomColorType; bodyRanges?: BodyRangesType; i18n: LocalizerType; isFromMe: boolean; @@ -361,7 +363,13 @@ export class Quote extends React.Component { } public renderReferenceWarning(): JSX.Element | null { - const { i18n, isIncoming, referencedMessageNotFound } = this.props; + const { + conversationColor, + customColor, + i18n, + isIncoming, + referencedMessageNotFound, + } = this.props; if (!referencedMessageNotFound) { return null; @@ -371,8 +379,11 @@ export class Quote extends React.Component {
{ public render(): JSX.Element | null { const { - authorColor, + conversationColor, + customColor, isIncoming, onClick, referencedMessageNotFound, @@ -424,14 +436,15 @@ export class Quote extends React.Component { 'module-quote', isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing', isIncoming - ? `module-quote--incoming-${authorColor}` - : `module-quote--outgoing-${authorColor}`, + ? `module-quote--incoming-${conversationColor}` + : `module-quote--outgoing-${conversationColor}`, !onClick ? 'module-quote--no-click' : null, withContentAbove ? 'module-quote--with-content-above' : null, referencedMessageNotFound ? 'module-quote--with-reference-warning' : null )} + style={{ ...getCustomColorStyle(customColor, true) }} >
{this.renderAuthor()} diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index b1589c2ddf55..d14c214bf784 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -37,7 +37,7 @@ const items: Record = { timestamp: Date.now(), author: { phoneNumber: '(202) 555-2001', - color: 'green', + color: 'forest', }, text: '🔥', }, @@ -50,7 +50,7 @@ const items: Record = { direction: 'incoming', timestamp: Date.now(), author: { - color: 'green', + color: 'forest', }, text: 'Hello there from the new world! http://somewhere.com', }, @@ -75,7 +75,7 @@ const items: Record = { direction: 'incoming', timestamp: Date.now(), author: { - color: 'red', + color: 'crimson', }, text: 'Hello there from the new world!', }, @@ -161,7 +161,7 @@ const items: Record = { timestamp: Date.now(), status: 'sent', author: { - color: 'pink', + color: 'plum', }, text: '🔥', }, @@ -174,7 +174,7 @@ const items: Record = { timestamp: Date.now(), status: 'read', author: { - color: 'pink', + color: 'plum', }, text: 'Hello there from the new world! http://somewhere.com', }, @@ -336,7 +336,7 @@ const renderLoadingRow = () => ; const renderTypingBubble = () => ( = {}): Props => ({ i18n, color: select( 'color', - Colors.reduce((m, c) => ({ ...m, [c]: c }), {}), - overrideProps.color || 'red' + AvatarColors.reduce((m, c) => ({ ...m, [c]: c }), {}), + overrideProps.color || AvatarColors[0] ), avatarPath: text('avatarPath', overrideProps.avatarPath || ''), title: '', diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx index c10b2e9018a2..83d40d538406 100644 --- a/ts/components/conversation/TypingBubble.tsx +++ b/ts/components/conversation/TypingBubble.tsx @@ -69,7 +69,7 @@ export class TypingBubble extends React.PureComponent { } public render(): JSX.Element { - const { i18n, color, conversationType } = this.props; + const { i18n, conversationType } = this.props; const isGroup = conversationType === 'group'; return ( @@ -85,8 +85,7 @@ export class TypingBubble extends React.PureComponent {
diff --git a/ts/components/conversation/_contactUtil.tsx b/ts/components/conversation/_contactUtil.tsx index fc751132c889..65f481595bc3 100644 --- a/ts/components/conversation/_contactUtil.tsx +++ b/ts/components/conversation/_contactUtil.tsx @@ -48,7 +48,7 @@ export function renderAvatar({ acceptedMessageRequest={false} avatarPath={avatarPath} blur={AvatarBlur.NoBlur} - color="grey" + color="steel" conversationType="direct" i18n={i18n} isMe diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx index b595d0ddea5d..c578cc7e347b 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx @@ -26,6 +26,7 @@ const conversation: ConversationType = getDefaultConversation({ title: 'Some Conversation', type: 'group', sharedGroupNames: [], + conversationColor: 'ultramarine' as const, }); const createProps = (hasGroupLink = false): Props => ({ @@ -55,6 +56,7 @@ const createProps = (hasGroupLink = false): Props => ({ setDisappearingMessages: action('setDisappearingMessages'), showAllMedia: action('showAllMedia'), showContactModal: action('showContactModal'), + showGroupChatColorEditor: action('showGroupChatColorEditor'), showGroupLinkManagement: action('showGroupLinkManagement'), showGroupV2Permissions: action('showGroupV2Permissions'), showPendingInvites: action('showPendingInvites'), diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index 3987ef56a681..28bb8017e1de 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -28,6 +28,7 @@ import { } from './PendingInvites'; import { EditConversationAttributesModal } from './EditConversationAttributesModal'; import { RequestState } from './util'; +import { getCustomColorStyle } from '../../../util/getCustomColorStyle'; enum ModalState { NothingOpen, @@ -50,6 +51,7 @@ export type StateProps = { setDisappearingMessages: (seconds: number) => void; showAllMedia: () => void; showContactModal: (conversationId: string) => void; + showGroupChatColorEditor: () => void; showGroupLinkManagement: () => void; showGroupV2Permissions: () => void; showPendingInvites: () => void; @@ -88,6 +90,7 @@ export const ConversationDetails: React.ComponentType = ({ setDisappearingMessages, showAllMedia, showContactModal, + showGroupChatColorEditor, showGroupLinkManagement, showGroupV2Permissions, showPendingInvites, @@ -224,8 +227,8 @@ export const ConversationDetails: React.ComponentType = ({ }} /> - {canEditGroupInfo ? ( - + + {canEditGroupInfo ? ( = ({
} /> - - ) : null} + ) : null} + + } + label={i18n('showChatColorEditor')} + onClick={showGroupChatColorEditor} + right={ +
+ } + /> + = React.memo( return ( = React.memo( return ( { .map(attachment => this.getPropsForAttachment(attachment)); } + getConversationColor(): ConversationColorType { + const conversation = this.getConversation(); + return conversation?.getConversationColor() || ('ultramarine' as const); + } + // Note: interactionMode is mixed in via selectors/conversations._messageSelector getPropsForMessage(): PropsForMessage { const sourceId = this.getContactId(); @@ -958,6 +963,8 @@ export class MessageModel extends window.Backbone.Model { text: this.createNonBreakingLastSeparator(this.get('body')), textPending: this.get('bodyPending'), id: this.id, + conversationColor: this.getConversationColor(), + customColor: conversation?.get('customColor'), conversationId: this.get('conversationId'), isSticker: Boolean(sticker), direction: this.isIncoming() ? 'incoming' : 'outgoing', @@ -1252,7 +1259,6 @@ export class MessageModel extends window.Backbone.Model { } } - let authorColor: ColorType; let authorId: string; let authorName: undefined | string; let authorPhoneNumber: undefined | string; @@ -1263,7 +1269,6 @@ export class MessageModel extends window.Backbone.Model { if (contact && contact.isPrivate()) { const contactPhoneNumber = contact.get('e164'); - authorColor = contact.getColor(); authorId = contact.id; authorName = contact.get('name'); authorPhoneNumber = contactPhoneNumber @@ -1279,7 +1284,6 @@ export class MessageModel extends window.Backbone.Model { 'getPropsForQuote: contact was missing. This may indicate a bookkeeping error or bad data from another client. Returning a placeholder contact.' ); - authorColor = 'grey'; authorId = 'placeholder-contact'; authorTitle = window.i18n('unknownContact'); isFromMe = false; @@ -1288,13 +1292,14 @@ export class MessageModel extends window.Backbone.Model { const firstAttachment = quote.attachments && quote.attachments[0]; return { - authorColor, authorId, authorName, authorPhoneNumber, authorProfileName, authorTitle, bodyRanges: this.processBodyRanges(bodyRanges), + conversationColor: this.getConversationColor(), + customColor: this.getConversation()?.get('customColor'), isFromMe, rawAttachment: firstAttachment ? this.processQuoteAttachment(firstAttachment) diff --git a/ts/shims/getUserTheme.ts b/ts/shims/getUserTheme.ts new file mode 100644 index 000000000000..9c172df74ded --- /dev/null +++ b/ts/shims/getUserTheme.ts @@ -0,0 +1,9 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { ThemeType } from '../types/Util'; +import { getTheme } from '../state/selectors/user'; + +export function getUserTheme(): ThemeType { + return getTheme(window.reduxStore.getState()); +} diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 0cc234e2c1e2..efcb5b318744 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -29,6 +29,7 @@ import { createBatcher } from '../util/batcher'; import { assert } from '../util/assert'; import { cleanDataForIpc } from './cleanDataForIpc'; import { ReactionType } from '../types/Reactions'; +import { ConversationColorType, CustomColorType } from '../types/Colors'; import { ConversationModelCollectionType, @@ -157,6 +158,7 @@ const dataInterface: ClientInterface = { updateConversation, updateConversations, removeConversation, + updateAllConversationColors, eraseStorageServiceStateFromConversations, getAllConversations, @@ -1549,3 +1551,16 @@ function insertJob(job: Readonly): Promise { function deleteJob(id: string): Promise { return channels.deleteJob(id); } + +async function updateAllConversationColors( + conversationColor?: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } +): Promise { + return channels.updateAllConversationColors( + conversationColor, + customColorData + ); +} diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 5b843de09296..1b833d26417d 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -14,6 +14,7 @@ import { MessageModel } from '../models/messages'; import { ConversationModel } from '../models/conversations'; import { StoredJob } from '../jobs/types'; import { ReactionType } from '../types/Reactions'; +import { ConversationColorType, CustomColorType } from '../types/Colors'; export type AttachmentDownloadJobType = { id: string; @@ -310,6 +311,14 @@ export type DataInterface = { getJobsInQueue(queueType: string): Promise>; insertJob(job: Readonly): Promise; deleteJob(id: string): Promise; + + updateAllConversationColors: ( + conversationColor?: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } + ) => Promise; }; // The reason for client/server divergence is the need to inject Backbone models and diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index df41bdfbe702..b6458b34b537 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -36,6 +36,7 @@ import { combineNames } from '../util/combineNames'; import { getExpiresAt } from '../services/MessageUpdater'; import { isNormalNumber } from '../util/isNormalNumber'; import { isNotNil } from '../util/isNotNil'; +import { ConversationColorType, CustomColorType } from '../types/Colors'; import { AttachmentDownloadJobType, @@ -153,6 +154,7 @@ const dataInterface: ServerInterface = { getAllConversationIds, getAllPrivateConversations, getAllGroupsInvolvingId, + updateAllConversationColors, searchConversations, searchMessages, @@ -5328,3 +5330,26 @@ async function deleteJob(id: string): Promise { db.prepare('DELETE FROM jobs WHERE id = $id').run({ id }); } + +async function updateAllConversationColors( + conversationColor?: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } +): Promise { + const db = getInstance(); + + db.prepare( + ` + UPDATE conversations + SET json = JSON_PATCH(json, $patch); + ` + ).run({ + patch: JSON.stringify({ + conversationColor: conversationColor || null, + customColor: customColorData?.value || null, + customColorId: customColorData?.id || null, + }), + }); +} diff --git a/ts/state/actions.ts b/ts/state/actions.ts index 9b6642be484b..bc64a773145b 100644 --- a/ts/state/actions.ts +++ b/ts/state/actions.ts @@ -6,6 +6,7 @@ import { actions as calling } from './ducks/calling'; import { actions as conversations } from './ducks/conversations'; import { actions as emojis } from './ducks/emojis'; import { actions as expiration } from './ducks/expiration'; +import { actions as globalModals } from './ducks/globalModals'; import { actions as items } from './ducks/items'; import { actions as linkPreviews } from './ducks/linkPreviews'; import { actions as network } from './ducks/network'; @@ -21,6 +22,7 @@ export const mapDispatchToProps = { ...conversations, ...emojis, ...expiration, + ...globalModals, ...items, ...linkPreviews, ...network, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 1bb7f0e45f8f..a45b76899a55 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -22,7 +22,12 @@ import { getOwn } from '../../util/getOwn'; import { assert } from '../../util/assert'; import { trigger } from '../../shims/events'; import { AttachmentType } from '../../types/Attachment'; -import { ColorType } from '../../types/Colors'; +import { + AvatarColorType, + ConversationColorType, + CustomColorType, +} from '../../types/Colors'; +import { ConversationAttributesType } from '../../model-types.d'; import { BodyRangeType } from '../../types/Util'; import { CallMode, CallHistoryDetailsFromDiskType } from '../../types/Calling'; import { MediaItemType } from '../../components/LightboxGallery'; @@ -67,7 +72,10 @@ export type ConversationType = { areWePendingApproval?: boolean; canChangeTimer?: boolean; canEditGroupInfo?: boolean; - color?: ColorType; + color?: AvatarColorType; + conversationColor?: ConversationColorType; + customColor?: CustomColorType; + customColorId?: string; discoveredUnregisteredAt?: number; isArchived?: boolean; isBlocked?: boolean; @@ -117,7 +125,6 @@ export type ConversationType = { isFetchingUUID?: boolean; typingContact?: { avatarPath?: string; - color?: ColorType; name?: string; phoneNumber?: string; profileName?: string; @@ -322,6 +329,9 @@ export const getConversationCallMode = ( // Actions +const COLORS_CHANGED = 'conversations/COLORS_CHANGED'; +const CUSTOM_COLOR_REMOVED = 'conversations/CUSTOM_COLOR_REMOVED'; + type CantAddContactToGroupActionType = { type: 'CANT_ADD_CONTACT_TO_GROUP'; payload: { @@ -344,6 +354,20 @@ type CloseMaximumGroupSizeModalActionType = { type CloseRecommendedGroupSizeModalActionType = { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL'; }; +type ColorsChangedActionType = { + type: typeof COLORS_CHANGED; + payload: { + conversationColor?: ConversationColorType; + customColorData?: { + id: string; + value: CustomColorType; + }; + }; +}; +type CustomColorRemovedActionType = { + type: typeof CUSTOM_COLOR_REMOVED; + payload: string; +}; type SetPreJoinConversationActionType = { type: 'SET_PRE_JOIN_CONVERSATION'; payload: { @@ -584,6 +608,8 @@ export type ConversationActionType = | ConversationChangedActionType | ConversationRemovedActionType | ConversationUnloadedActionType + | ColorsChangedActionType + | CustomColorRemovedActionType | CreateGroupFulfilledActionType | CreateGroupPendingActionType | CreateGroupRejectedActionType @@ -643,11 +669,14 @@ export const actions = { openConversationExternal, openConversationInternal, removeAllConversations, + removeCustomColorOnConversations, repairNewestMessage, repairOldestMessage, + resetAllChatColors, reviewMessageRequestNameCollision, scrollToMessage, selectMessage, + setAllConversationColors, setComposeGroupAvatar, setComposeGroupName, setComposeSearchTerm, @@ -667,6 +696,101 @@ export const actions = { toggleConversationInChooseMembers, }; +function removeCustomColorOnConversations( + colorId: string +): ThunkAction { + return async dispatch => { + const conversationsToUpdate: Array = []; + // We don't want to trigger a model change because we're updating redux + // here manually ourselves. Au revoir Backbone! + window.getConversations().forEach(conversation => { + if (conversation.get('customColorId') === colorId) { + // eslint-disable-next-line no-param-reassign + delete conversation.attributes.conversationColor; + // eslint-disable-next-line no-param-reassign + delete conversation.attributes.customColor; + // eslint-disable-next-line no-param-reassign + delete conversation.attributes.customColorId; + + conversationsToUpdate.push(conversation.attributes); + } + }); + + if (conversationsToUpdate.length) { + await window.Signal.Data.updateConversations(conversationsToUpdate); + } + + dispatch({ + type: CUSTOM_COLOR_REMOVED, + payload: colorId, + }); + }; +} + +function resetAllChatColors(): ThunkAction< + void, + RootStateType, + unknown, + ColorsChangedActionType +> { + return async dispatch => { + // Calling this with no args unsets all the colors in the db + await window.Signal.Data.updateAllConversationColors(); + + // We don't want to trigger a model change because we're updating redux + // here manually ourselves. Au revoir Backbone! + window.getConversations().forEach(conversation => { + // eslint-disable-next-line no-param-reassign + delete conversation.attributes.conversationColor; + // eslint-disable-next-line no-param-reassign + delete conversation.attributes.customColor; + // eslint-disable-next-line no-param-reassign + delete conversation.attributes.customColorId; + }); + + dispatch({ + type: COLORS_CHANGED, + payload: { + conversationColor: undefined, + customColorData: undefined, + }, + }); + }; +} + +function setAllConversationColors( + conversationColor: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } +): ThunkAction { + return async dispatch => { + await window.Signal.Data.updateAllConversationColors( + conversationColor, + customColorData + ); + + // We don't want to trigger a model change because we're updating redux + // here manually ourselves. Au revoir Backbone! + window.getConversations().forEach(conversation => { + Object.assign(conversation.attributes, { + conversationColor, + customColor: customColorData?.value, + customColorId: customColorData?.id, + }); + }); + + dispatch({ + type: COLORS_CHANGED, + payload: { + conversationColor, + customColorData, + }, + }); + }; +} + function cantAddContactToGroup( conversationId: string ): CantAddContactToGroupActionType { @@ -2346,5 +2470,73 @@ export function reducer( }; } + if (action.type === COLORS_CHANGED) { + const { conversationLookup } = state; + const { conversationColor, customColorData } = action.payload; + + const nextState = { + ...state, + }; + + Object.keys(conversationLookup).forEach(id => { + const existing = conversationLookup[id]; + const added = { + ...existing, + conversationColor, + customColor: customColorData?.value, + customColorId: customColorData?.id, + }; + + Object.assign( + nextState, + updateConversationLookups(added, existing, nextState), + { + conversationLookup: { + ...nextState.conversationLookup, + [id]: added, + }, + } + ); + }); + + return nextState; + } + + if (action.type === CUSTOM_COLOR_REMOVED) { + const { conversationLookup } = state; + const colorId = action.payload; + + const nextState = { + ...state, + }; + + Object.keys(conversationLookup).forEach(id => { + const existing = conversationLookup[id]; + + if (existing.customColorId !== colorId) { + return; + } + + const changed = omit(existing, [ + 'conversationColor', + 'customColor', + 'customColorId', + ]); + + Object.assign( + nextState, + updateConversationLookups(changed, existing, nextState), + { + conversationLookup: { + ...nextState.conversationLookup, + [id]: changed, + }, + } + ); + }); + + return nextState; + } + return state; } diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts new file mode 100644 index 000000000000..a24e34cb3ed4 --- /dev/null +++ b/ts/state/ducks/globalModals.ts @@ -0,0 +1,50 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// State + +export type GlobalModalsStateType = { + readonly isChatColorEditorVisible: boolean; +}; + +// Actions + +const TOGGLE_CHAT_COLOR_EDITOR = 'globalModals/TOGGLE_CHAT_COLOR_EDITOR'; + +type ToggleChatColorEditorActionType = { + type: typeof TOGGLE_CHAT_COLOR_EDITOR; +}; + +export type GlobalModalsActionType = ToggleChatColorEditorActionType; + +// Action Creators + +export const actions = { + toggleChatColorEditor, +}; + +function toggleChatColorEditor(): ToggleChatColorEditorActionType { + return { type: TOGGLE_CHAT_COLOR_EDITOR }; +} + +// Reducer + +export function getEmptyState(): GlobalModalsStateType { + return { + isChatColorEditorVisible: false, + }; +} + +export function reducer( + state: Readonly = getEmptyState(), + action: Readonly +): GlobalModalsStateType { + if (action.type === TOGGLE_CHAT_COLOR_EDITOR) { + return { + ...state, + isChatColorEditorVisible: !state.isChatColorEditorVisible, + }; + } + + return state; +} diff --git a/ts/state/ducks/items.ts b/ts/state/ducks/items.ts index 284b798b77df..53ce9c816ec0 100644 --- a/ts/state/ducks/items.ts +++ b/ts/state/ducks/items.ts @@ -2,13 +2,21 @@ // SPDX-License-Identifier: AGPL-3.0-only import { omit } from 'lodash'; +import { v4 as getGuid } from 'uuid'; +import { ThunkAction } from 'redux-thunk'; +import { StateType as RootStateType } from '../reducer'; import * as storageShim from '../../shims/storage'; import { useBoundActions } from '../../util/hooks'; +import { CustomColorType } from '../../types/Colors'; // State export type ItemsStateType = { readonly [key: string]: unknown; + readonly customColors?: { + readonly colors: Record; + readonly version: number; + }; }; // Actions @@ -50,6 +58,9 @@ export type ItemsActionType = // Action Creators export const actions = { + addCustomColor, + editCustomColor, + removeCustomColor, onSetSkinTone, putItem, putItemExternal, @@ -103,6 +114,74 @@ function resetItems(): ItemsResetAction { return { type: 'items/RESET' }; } +function getDefaultCustomColorData() { + return { + colors: {}, + version: 1, + }; +} + +function addCustomColor( + payload: CustomColorType +): ThunkAction { + return (dispatch, getState) => { + const { customColors = getDefaultCustomColorData() } = getState().items; + + let uuid = getGuid(); + while (customColors.colors[uuid]) { + uuid = getGuid(); + } + + const nextCustomColors = { + ...customColors, + colors: { + ...customColors.colors, + [uuid]: payload, + }, + }; + + dispatch(putItem('customColors', nextCustomColors)); + }; +} + +function editCustomColor( + colorId: string, + color: CustomColorType +): ThunkAction { + return (dispatch, getState) => { + const { customColors = getDefaultCustomColorData() } = getState().items; + + if (!customColors.colors[colorId]) { + return; + } + + const nextCustomColors = { + ...customColors, + colors: { + ...customColors.colors, + [colorId]: color, + }, + }; + + dispatch(putItem('customColors', nextCustomColors)); + }; +} + +function removeCustomColor( + payload: string +): ThunkAction { + return (dispatch, getState) => { + const { customColors = getDefaultCustomColorData() } = getState().items; + + const nextCustomColors = { + ...customColors, + colors: omit(customColors.colors, payload), + }; + + dispatch(putItem('customColors', nextCustomColors)); + }; +} + // Reducer function getEmptyState(): ItemsStateType { diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index c8b69f45000a..429e71343bca 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -8,6 +8,7 @@ import { reducer as calling } from './ducks/calling'; import { reducer as conversations } from './ducks/conversations'; import { reducer as emojis } from './ducks/emojis'; import { reducer as expiration } from './ducks/expiration'; +import { reducer as globalModals } from './ducks/globalModals'; import { reducer as items } from './ducks/items'; import { reducer as linkPreviews } from './ducks/linkPreviews'; import { reducer as network } from './ducks/network'; @@ -23,6 +24,7 @@ export const reducer = combineReducers({ conversations, emojis, expiration, + globalModals, items, linkPreviews, network, diff --git a/ts/state/roots/createChatColorPicker.tsx b/ts/state/roots/createChatColorPicker.tsx new file mode 100644 index 000000000000..4457855b40fa --- /dev/null +++ b/ts/state/roots/createChatColorPicker.tsx @@ -0,0 +1,21 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { Provider } from 'react-redux'; + +import { Store } from 'redux'; + +import { + SmartChatColorPicker, + SmartChatColorPickerProps, +} from '../smart/ChatColorPicker'; + +export const createChatColorPicker = ( + store: Store, + props: SmartChatColorPickerProps +): React.ReactElement => ( + + + +); diff --git a/ts/state/roots/createGlobalModalContainer.tsx b/ts/state/roots/createGlobalModalContainer.tsx new file mode 100644 index 000000000000..f48b2b43b2b6 --- /dev/null +++ b/ts/state/roots/createGlobalModalContainer.tsx @@ -0,0 +1,17 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { Provider } from 'react-redux'; + +import { Store } from 'redux'; + +import { SmartGlobalModalContainer } from '../smart/GlobalModalContainer'; + +export const createGlobalModalContainer = ( + store: Store +): React.ReactElement => ( + + + +); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 2178e98a7dc9..b8145b4ef4e3 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -28,6 +28,7 @@ import { TimelineItemType } from '../../components/conversation/TimelineItem'; import { assert } from '../../util/assert'; import { isConversationUnregistered } from '../../util/isConversationUnregistered'; import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations'; +import { ContactNameColors, ContactNameColorType } from '../../types/Colors'; import { getInteractionMode, @@ -850,3 +851,62 @@ export const getInvitedContactsForNewlyCreatedGroup = createSelector( invitedConversationIdsForNewlyCreatedGroup ) ); + +const getCachedConversationMemberColorsSelector = createSelector( + getConversationSelector, + (conversationSelector: GetConversationByIdType) => { + return memoizee( + (conversationId: string) => { + const contactNameColors: Map = new Map(); + const { sortedGroupMembers = [] } = conversationSelector( + conversationId + ); + + [...sortedGroupMembers] + .sort((left, right) => + String(left.uuid) > String(right.uuid) ? 1 : -1 + ) + .forEach((member, i) => { + contactNameColors.set( + member.id, + ContactNameColors[i % ContactNameColors.length] + ); + }); + + return contactNameColors; + }, + { max: 100 } + ); + } +); + +export const getContactNameColorSelector = createSelector( + getCachedConversationMemberColorsSelector, + conversationMemberColorsSelector => { + return ( + conversationId: string, + contactId: string + ): ContactNameColorType => { + const contactNameColors = conversationMemberColorsSelector( + conversationId + ); + const color = contactNameColors.get(contactId); + if (!color) { + assert(false, `No color generated for contact ${contactId}`); + return ContactNameColors[0]; + } + return color; + }; + } +); + +export const getConversationsWithCustomColorSelector = createSelector( + getAllConversations, + conversations => { + return (colorId: string): Array => { + return conversations.filter( + conversation => conversation.customColorId === colorId + ); + }; + } +); diff --git a/ts/state/smart/ChatColorPicker.tsx b/ts/state/smart/ChatColorPicker.tsx new file mode 100644 index 000000000000..e60bbd2fbff9 --- /dev/null +++ b/ts/state/smart/ChatColorPicker.tsx @@ -0,0 +1,60 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { connect } from 'react-redux'; + +import { mapDispatchToProps } from '../actions'; +import { + ChatColorPicker, + PropsDataType, +} from '../../components/ChatColorPicker'; +import { ConversationColorType, CustomColorType } from '../../types/Colors'; +import { StateType } from '../reducer'; +import { + getConversationSelector, + getConversationsWithCustomColorSelector, + getMe, +} from '../selectors/conversations'; +import { getIntl } from '../selectors/user'; + +export type SmartChatColorPickerProps = { + conversationId?: string; + isInModal?: boolean; + onChatColorReset?: () => unknown; + onSelectColor: ( + color: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } + ) => unknown; +}; + +const mapStateToProps = ( + state: StateType, + props: SmartChatColorPickerProps +): PropsDataType => { + const conversation = props.conversationId + ? getConversationSelector(state)(props.conversationId) + : getMe(state); + + const { customColors } = state.items; + + return { + ...props, + customColors: customColors ? customColors.colors : {}, + getConversationsWithCustomColor: getConversationsWithCustomColorSelector( + state + ), + i18n: getIntl(state), + selectedColor: conversation.conversationColor, + selectedCustomColor: { + id: conversation.customColorId, + value: conversation.customColor, + }, + }; +}; + +const smart = connect(mapStateToProps, mapDispatchToProps); + +export const SmartChatColorPicker = smart(ChatColorPicker); diff --git a/ts/state/smart/ContactName.tsx b/ts/state/smart/ContactName.tsx index df3b7886f4d9..90c17531e9f6 100644 --- a/ts/state/smart/ContactName.tsx +++ b/ts/state/smart/ContactName.tsx @@ -30,5 +30,14 @@ export const SmartContactName: React.ComponentType = props => { title: i18n('unknownContact'), }; - return ; + return ( + + ); }; diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx index 17273619c4a7..b21edda03ed9 100644 --- a/ts/state/smart/ConversationDetails.tsx +++ b/ts/state/smart/ConversationDetails.tsx @@ -25,6 +25,7 @@ export type SmartConversationDetailsProps = { setDisappearingMessages: (seconds: number) => void; showAllMedia: () => void; showContactModal: (conversationId: string) => void; + showGroupChatColorEditor: () => void; showGroupLinkManagement: () => void; showGroupV2Permissions: () => void; showPendingInvites: () => void; diff --git a/ts/state/smart/ConversationHeader.tsx b/ts/state/smart/ConversationHeader.tsx index 6066149d7f0e..4bf1792d2d56 100644 --- a/ts/state/smart/ConversationHeader.tsx +++ b/ts/state/smart/ConversationHeader.tsx @@ -34,6 +34,7 @@ export type OwnProps = { onSetMuteNotifications: (seconds: number) => void; onSetPin: (value: boolean) => void; onShowAllMedia: () => void; + onShowChatColorEditor: () => void; onShowContactModal: (contactId: string) => void; onShowGroupMembers: () => void; diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx new file mode 100644 index 000000000000..e5876a72443a --- /dev/null +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -0,0 +1,33 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { connect } from 'react-redux'; +import { mapDispatchToProps } from '../actions'; +import { GlobalModalContainer } from '../../components/GlobalModalContainer'; +import { StateType } from '../reducer'; +import { getIntl } from '../selectors/user'; +import { SmartChatColorPicker } from './ChatColorPicker'; +import { ConversationColorType } from '../../types/Colors'; + +function renderChatColorPicker({ + setAllConversationColors, +}: { + setAllConversationColors: (color: ConversationColorType) => unknown; +}): JSX.Element { + return ( + + ); +} + +const mapStateToProps = (state: StateType) => { + return { + ...state.globalModals, + i18n: getIntl(state), + renderChatColorPicker, + }; +}; + +const smart = connect(mapStateToProps, mapDispatchToProps); + +export const SmartGlobalModalContainer = smart(GlobalModalContainer); diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 947e277811a9..c36a1a05bbec 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -10,6 +10,8 @@ import { StateType } from '../reducer'; import { TimelineItem } from '../../components/conversation/TimelineItem'; import { getIntl, getInteractionMode, getTheme } from '../selectors/user'; import { + getContactNameColorSelector, + getConversationSelector, getMessageSelector, getSelectedMessage, } from '../selectors/conversations'; @@ -37,13 +39,25 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { const messageSelector = getMessageSelector(state); const item = messageSelector(id); + if (item?.type === 'message' && item.data.conversationType === 'group') { + const { author } = item.data; + item.data.contactNameColor = getContactNameColorSelector(state)( + conversationId, + author.id + ); + } + const selectedMessage = getSelectedMessage(state); const isSelected = Boolean(selectedMessage && id === selectedMessage.id); + const conversation = getConversationSelector(state)(conversationId); + return { item, id, conversationId, + conversationColor: conversation?.conversationColor, + customColor: conversation?.customColor, isSelected, renderContact, i18n: getIntl(state), diff --git a/ts/state/types.ts b/ts/state/types.ts index 24815cc332ea..001d3b2d9006 100644 --- a/ts/state/types.ts +++ b/ts/state/types.ts @@ -6,6 +6,7 @@ import { actions as calling } from './ducks/calling'; import { actions as conversations } from './ducks/conversations'; import { actions as emojis } from './ducks/emojis'; import { actions as expiration } from './ducks/expiration'; +import { actions as globalModals } from './ducks/globalModals'; import { actions as items } from './ducks/items'; import { actions as linkPreviews } from './ducks/linkPreviews'; import { actions as network } from './ducks/network'; @@ -21,6 +22,7 @@ export type ReduxActions = { conversations: typeof conversations; emojis: typeof emojis; expiration: typeof expiration; + globalModals: typeof globalModals; items: typeof items; linkPreviews: typeof linkPreviews; network: typeof network; diff --git a/ts/test-both/state/ducks/globalModals_test.ts b/ts/test-both/state/ducks/globalModals_test.ts new file mode 100644 index 000000000000..bc8afe77b2a2 --- /dev/null +++ b/ts/test-both/state/ducks/globalModals_test.ts @@ -0,0 +1,27 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { + actions, + getEmptyState, + reducer, +} from '../../../state/ducks/globalModals'; + +describe('both/state/ducks/globalModals', () => { + describe('toggleChatColorEditor', () => { + const { toggleChatColorEditor } = actions; + + it('toggles isChatColorEditorVisible', () => { + const state = getEmptyState(); + const nextState = reducer(state, toggleChatColorEditor()); + + assert.isTrue(nextState.isChatColorEditorVisible); + + const nextNextState = reducer(nextState, toggleChatColorEditor()); + + assert.isFalse(nextNextState.isChatColorEditorVisible); + }); + }); +}); diff --git a/ts/test-both/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts index bf7d3e0453e9..dfd1b402ef9d 100644 --- a/ts/test-both/state/selectors/conversations_test.ts +++ b/ts/test-both/state/selectors/conversations_test.ts @@ -23,6 +23,7 @@ import { getComposerConversationSearchTerm, getComposerStep, getComposeSelectedContacts, + getContactNameColorSelector, getConversationByIdSelector, getConversationsByTitleSelector, getConversationSelector, @@ -1211,7 +1212,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1236,7 +1236,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1261,7 +1260,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1286,7 +1284,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1311,7 +1308,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1360,7 +1356,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1386,7 +1381,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1412,7 +1406,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1463,7 +1456,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1488,7 +1480,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1513,7 +1504,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1539,7 +1529,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1564,7 +1553,6 @@ describe('both/state/selectors/conversations', () => { isSelected: false, typingContact: { name: 'Someone There', - color: 'blue', phoneNumber: '+18005551111', }, @@ -1842,4 +1830,44 @@ describe('both/state/selectors/conversations', () => { assert.strictEqual(getSelectedConversation(state), conversation); }); }); + + describe('#getContactNameColorSelector', () => { + function makeConversationWithUuid(id: string): ConversationType { + const convo = makeConversation(id); + convo.uuid = id; + return convo; + } + + it('returns the right color order sorted by UUID ASC', () => { + const group = makeConversation('group'); + group.sortedGroupMembers = [ + makeConversationWithUuid('zyx'), + makeConversationWithUuid('vut'), + makeConversationWithUuid('srq'), + makeConversationWithUuid('pon'), + makeConversationWithUuid('mlk'), + makeConversationWithUuid('jih'), + makeConversationWithUuid('gfe'), + ]; + const state = { + ...getEmptyRootState(), + conversations: { + ...getEmptyState(), + conversationLookup: { + group, + }, + }, + }; + + const contactNameColorSelector = getContactNameColorSelector(state); + + assert.equal(contactNameColorSelector('group', 'gfe'), '000'); + assert.equal(contactNameColorSelector('group', 'jih'), '120'); + assert.equal(contactNameColorSelector('group', 'mlk'), '240'); + assert.equal(contactNameColorSelector('group', 'pon'), '040'); + assert.equal(contactNameColorSelector('group', 'srq'), '160'); + assert.equal(contactNameColorSelector('group', 'vut'), '280'); + assert.equal(contactNameColorSelector('group', 'zyx'), '080'); + }); + }); }); diff --git a/ts/test-both/util/getCustomColorStyle.ts b/ts/test-both/util/getCustomColorStyle.ts new file mode 100644 index 000000000000..f82fd178895b --- /dev/null +++ b/ts/test-both/util/getCustomColorStyle.ts @@ -0,0 +1,44 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { getCustomColorStyle } from '../../util/getCustomColorStyle'; + +describe('getCustomColorStyle', () => { + it('returns undefined if no color passed in', () => { + assert.isUndefined(getCustomColorStyle()); + }); + + it('returns backgroundColor for solid colors', () => { + const color = { + start: { + hue: 90, + saturation: 100, + }, + }; + + assert.deepEqual(getCustomColorStyle(color), { + backgroundColor: 'hsl(90, 100%, 30%)', + }); + }); + + it('returns backgroundImage with linear-gradient for gradients', () => { + const color = { + start: { + hue: 90, + saturation: 100, + }, + end: { + hue: 180, + saturation: 50, + }, + deg: 270, + }; + + assert.deepEqual(getCustomColorStyle(color), { + backgroundImage: + 'linear-gradient(0deg, hsl(90, 100%, 30%), hsl(180, 50%, 30%))', + }); + }); +}); diff --git a/ts/test-both/util/getHSL_test.ts b/ts/test-both/util/getHSL_test.ts new file mode 100644 index 000000000000..75e298335027 --- /dev/null +++ b/ts/test-both/util/getHSL_test.ts @@ -0,0 +1,23 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { getHSL } from '../../util/getHSL'; + +describe('getHSL', () => { + it('returns expected lightness values', () => { + const saturation = 100; + assert.equal(getHSL({ hue: 0, saturation }), 'hsl(0, 100%, 45%)'); + assert.equal(getHSL({ hue: 60, saturation }), 'hsl(60, 100%, 30%)'); + assert.equal(getHSL({ hue: 90, saturation }), 'hsl(90, 100%, 30%)'); + assert.equal(getHSL({ hue: 180, saturation }), 'hsl(180, 100%, 30%)'); + assert.equal(getHSL({ hue: 240, saturation }), 'hsl(240, 100%, 50%)'); + assert.equal(getHSL({ hue: 300, saturation }), 'hsl(300, 100%, 40%)'); + assert.equal(getHSL({ hue: 360, saturation }), 'hsl(360, 100%, 45%)'); + }); + + it('calculates lightness between values', () => { + assert.equal(getHSL({ hue: 210, saturation: 100 }), 'hsl(210, 100%, 40%)'); + }); +}); diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index fab484eecbaf..915815ef2f3a 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -40,6 +40,7 @@ const { openConversationInternal, repairNewestMessage, repairOldestMessage, + setAllConversationColors, setComposeGroupAvatar, setComposeGroupName, setComposeSearchTerm, @@ -49,6 +50,7 @@ const { startComposing, showChooseGroupMembers, startSettingGroupMetadata, + resetAllChatColors, reviewMessageRequestNameCollision, toggleConversationInChooseMembers, } = actions; @@ -1977,4 +1979,133 @@ describe('both/state/ducks/conversations', () => { }); }); }); + + describe('COLORS_CHANGED', () => { + const abc = getDefaultConversation({ + id: 'abc', + uuid: 'abc', + conversationColor: 'wintergreen', + }); + const def = getDefaultConversation({ + id: 'def', + uuid: 'def', + conversationColor: 'infrared', + }); + const ghi = getDefaultConversation({ + id: 'ghi', + e164: 'ghi', + conversationColor: 'ember', + }); + const jkl = getDefaultConversation({ + id: 'jkl', + groupId: 'jkl', + conversationColor: 'plum', + }); + const getState = () => ({ + ...getEmptyRootState(), + conversations: { + ...getEmptyState(), + conversationLookup: { + abc, + def, + ghi, + jkl, + }, + conversationsByUuid: { + abc, + def, + }, + conversationsByE164: { + ghi, + }, + conversationsByGroupId: { + jkl, + }, + }, + }); + + it('setAllConversationColors', async () => { + const dispatch = sinon.spy(); + await setAllConversationColors('crimson')(dispatch, getState, null); + + const [action] = dispatch.getCall(0).args; + const nextState = reducer(getState().conversations, action); + + sinon.assert.calledOnce(dispatch); + assert.equal( + nextState.conversationLookup.abc.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationLookup.def.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationLookup.ghi.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationLookup.jkl.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationsByUuid.abc.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationsByUuid.def.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationsByE164.ghi.conversationColor, + 'crimson' + ); + assert.equal( + nextState.conversationsByGroupId.jkl.conversationColor, + 'crimson' + ); + }); + + it('resetAllChatColors', async () => { + const dispatch = sinon.spy(); + await resetAllChatColors()(dispatch, getState, null); + + const [action] = dispatch.getCall(0).args; + const nextState = reducer(getState().conversations, action); + + sinon.assert.calledOnce(dispatch); + assert.isUndefined( + nextState.conversationLookup.abc.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationLookup.def.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationLookup.ghi.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationLookup.jkl.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationsByUuid.abc.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationsByUuid.def.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationsByE164.ghi.conversationColor, + 'crimson' + ); + assert.isUndefined( + nextState.conversationsByGroupId.jkl.conversationColor, + 'crimson' + ); + }); + }); }); diff --git a/ts/types/Colors.ts b/ts/types/Colors.ts index 90bb66d243c5..b186e5067ef5 100644 --- a/ts/types/Colors.ts +++ b/ts/types/Colors.ts @@ -1,21 +1,95 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -export const Colors = [ - 'red', - 'deep_orange', - 'brown', - 'pink', - 'purple', - 'indigo', - 'blue', +export const AvatarColors = [ + 'crimson', + 'vermilion', + 'burlap', + 'forest', + 'wintergreen', 'teal', - 'green', - 'light_green', - 'blue_grey', - 'grey', + 'blue', + 'indigo', + 'violet', + 'plum', + 'taupe', + 'steel', 'ultramarine', - 'signal-blue', ] as const; -export type ColorType = typeof Colors[number]; +export const ConversationColors = [ + 'ultramarine', + 'crimson', + 'vermilion', + 'burlap', + 'forest', + 'wintergreen', + 'teal', + 'blue', + 'indigo', + 'violet', + 'plum', + 'taupe', + 'steel', + 'ember', + 'midnight', + 'infrared', + 'lagoon', + 'fluorescent', + 'basil', + 'sublime', + 'sea', + 'tangerine', +] as const; + +export const ContactNameColors = [ + '000', + '120', + '240', + '040', + '160', + '280', + '080', + '200', + '320', + '020', + '140', + '260', + '060', + '180', + '300', + '100', + '220', + '340', + '010', + '130', + '250', + '050', + '170', + '290', + '090', + '210', + '330', + '030', + '150', + '270', + '070', + '190', + '310', + '110', + '230', + '350', +]; + +export type ContactNameColorType = typeof ContactNameColors[number]; + +export type CustomColorType = { + start: { hue: number; saturation: number }; + end?: { hue: number; saturation: number }; + deg?: number; +}; + +export type AvatarColorType = typeof AvatarColors[number]; +export type ConversationColorType = + | typeof ConversationColors[number] + | 'custom'; diff --git a/ts/util/getCustomColorStyle.ts b/ts/util/getCustomColorStyle.ts new file mode 100644 index 000000000000..0ca18fe56c7e --- /dev/null +++ b/ts/util/getCustomColorStyle.ts @@ -0,0 +1,53 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { CustomColorType } from '../types/Colors'; +import { ThemeType } from '../types/Util'; +import { getHSL } from './getHSL'; +import { getUserTheme } from '../shims/getUserTheme'; + +type ExtraQuotePropsType = { + borderLeftColor?: string; +}; + +type BackgroundPropertyType = + | { backgroundColor: string } + | { backgroundImage: string } + | undefined; + +export function getCustomColorStyle( + color?: CustomColorType, + isQuote = false +): BackgroundPropertyType { + if (!color) { + return undefined; + } + + const extraQuoteProps: ExtraQuotePropsType = {}; + let adjustedLightness = 0; + if (isQuote) { + const theme = getUserTheme(); + if (theme === ThemeType.light) { + adjustedLightness = 0.6; + } + if (theme === ThemeType.dark) { + adjustedLightness = -0.4; + } + extraQuoteProps.borderLeftColor = getHSL(color.start); + } + + if (!color.end) { + return { + ...extraQuoteProps, + backgroundColor: getHSL(color.start, adjustedLightness), + }; + } + + return { + ...extraQuoteProps, + backgroundImage: `linear-gradient(${270 - (color.deg || 0)}deg, ${getHSL( + color.start, + adjustedLightness + )}, ${getHSL(color.end, adjustedLightness)})`, + }; +} diff --git a/ts/util/getHSL.ts b/ts/util/getHSL.ts new file mode 100644 index 000000000000..c6bffa87df5d --- /dev/null +++ b/ts/util/getHSL.ts @@ -0,0 +1,59 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +const LIGHTNESS_TABLE: Record = { + 0: 45, + 60: 30, + 180: 30, + 240: 50, + 300: 40, + 360: 45, +}; + +function getLightnessFromHue(hue: number, min: number, max: number) { + const percentage = ((hue - min) * 100) / (max - min); + const minValue = LIGHTNESS_TABLE[min]; + const maxValue = LIGHTNESS_TABLE[max]; + + return (percentage * (maxValue - minValue)) / 100 + minValue; +} + +function calculateLightness(hue: number): number { + let lightness = 45; + if (hue < 60) { + lightness = getLightnessFromHue(hue, 0, 60); + } else if (hue < 180) { + lightness = 30; + } else if (hue < 240) { + lightness = getLightnessFromHue(hue, 180, 240); + } else if (hue < 300) { + lightness = getLightnessFromHue(hue, 240, 300); + } else { + lightness = getLightnessFromHue(hue, 300, 360); + } + + return lightness; +} + +function adjustLightnessValue( + lightness: number, + percentIncrease: number +): number { + return lightness + lightness * percentIncrease; +} + +export function getHSL( + { + hue, + saturation, + }: { + hue: number; + saturation: number; + }, + adjustedLightness = 0 +): string { + return `hsl(${hue}, ${saturation}%, ${adjustLightnessValue( + calculateLightness(hue), + adjustedLightness + )}%)`; +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 5ff5effd81c2..5311b8a48426 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -13557,6 +13557,13 @@ "updated": "2019-03-09T00:08:44.242Z", "reasonDetail": "Used only to set focus" }, + { + "rule": "React-useRef", + "path": "ts/components/ChatColorPicker.js", + "line": " const menuRef = react_1.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-05-25T18:25:53.896Z" + }, { "rule": "DOM-innerHTML", "path": "ts/components/CompositionArea.js", @@ -13697,6 +13704,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-04-19T18:13:21.664Z" }, + { + "rule": "React-useRef", + "path": "ts/components/GradientDial.js", + "line": " const containerRef = react_1.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-05-25T18:25:53.896Z" + }, { "rule": "React-useRef", "path": "ts/components/GroupCallOverflowArea.js", @@ -13814,6 +13828,27 @@ "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Only used to focus the element." }, + { + "rule": "React-useRef", + "path": "ts/components/Slider.js", + "line": " const diff = react_1.useRef(0);", + "reasonCategory": "usageTrusted", + "updated": "2021-05-25T18:25:53.896Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Slider.js", + "line": " const handleRef = react_1.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-05-25T18:25:53.896Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Slider.js", + "line": " const sliderRef = react_1.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-05-25T18:25:53.896Z" + }, { "rule": "React-useRef", "path": "ts/components/Tooltip.js", @@ -14241,4 +14276,4 @@ "updated": "2021-03-18T21:41:28.361Z", "reasonDetail": "A generic hook. Typically not to be used with non-DOM values." } -] \ No newline at end of file +] diff --git a/ts/util/migrateColor.ts b/ts/util/migrateColor.ts index 150b21ff5d66..b0181edea281 100644 --- a/ts/util/migrateColor.ts +++ b/ts/util/migrateColor.ts @@ -1,42 +1,53 @@ // Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { ColorType } from '../types/Colors'; +import { AvatarColorType } from '../types/Colors'; -export function migrateColor(color?: string): ColorType { +export function migrateColor(color?: string): AvatarColorType { switch (color) { // These colors no longer exist case 'orange': case 'amber': - return 'deep_orange'; + return 'vermilion'; case 'yellow': - return 'brown'; + return 'burlap'; case 'deep_purple': - return 'purple'; + return 'violet'; case 'light_blue': return 'blue'; case 'cyan': return 'teal'; case 'lime': - return 'light_green'; + return 'wintergreen'; + + // Actual color names + case 'red': + return 'crimson'; + case 'deep_orange': + return 'vermilion'; + case 'brown': + return 'burlap'; + case 'pink': + return 'plum'; + case 'purple': + return 'violet'; + case 'green': + return 'forest'; + case 'light_green': + return 'wintergreen'; + case 'blue_grey': + return 'steel'; + case 'grey': + return 'steel'; // These can stay as they are - case 'red': - case 'deep_orange': - case 'brown': - case 'pink': - case 'purple': - case 'indigo': case 'blue': + case 'indigo': case 'teal': - case 'green': - case 'light_green': - case 'blue_grey': - case 'grey': case 'ultramarine': return color; default: - return 'grey'; + return 'steel'; } } diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 85042dff1329..2576661dd8f6 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -4,6 +4,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { AttachmentType } from '../types/Attachment'; +import { ConversationColorType, CustomColorType } from '../types/Colors'; import { ConversationModel } from '../models/conversations'; import { GroupV2PendingMemberType } from '../model-types.d'; import { LinkPreviewType } from '../types/message/LinkPreviews'; @@ -551,6 +552,9 @@ Whisper.ConversationView = Whisper.View.extend({ } }, + onShowChatColorEditor: () => { + this.showChatColorEditor(); + }, onShowConversationDetails: () => { this.showConversationDetails(); }, @@ -3141,6 +3145,44 @@ Whisper.ConversationView = Whisper.View.extend({ view.render(); }, + showChatColorEditor() { + const conversation: ConversationModel = this.model; + + const view = new Whisper.ReactWrapperView({ + className: 'panel', + JSX: window.Signal.State.Roots.createChatColorPicker(window.reduxStore, { + conversationId: conversation.get('id'), + onSelectColor: ( + color: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } + ) => { + conversation.set('conversationColor', color); + if (customColorData) { + conversation.set('customColor', customColorData.value); + conversation.set('customColorId', customColorData.id); + } else { + conversation.unset('customColor'); + conversation.unset('customColorId'); + } + window.Signal.Data.updateConversation(conversation.attributes); + }, + onChatColorReset: () => { + conversation.set('conversationColor', undefined); + conversation.unset('customColor'); + window.Signal.Data.updateConversation(conversation.attributes); + }, + }), + }); + + view.headerTitle = window.i18n('ChatColorPicker__menu-title'); + + this.listenBack(view); + view.render(); + }, + showConversationDetails() { const conversation: ConversationModel = this.model; @@ -3177,6 +3219,7 @@ Whisper.ConversationView = Whisper.View.extend({ setDisappearingMessages: this.setDisappearingMessages.bind(this), showAllMedia: this.showAllMedia.bind(this), showContactModal: this.showContactModal.bind(this), + showGroupChatColorEditor: this.showChatColorEditor.bind(this), showGroupLinkManagement: this.showGroupLinkManagement.bind(this), showGroupV2Permissions: this.showGroupV2Permissions.bind(this), showPendingInvites: this.showPendingInvites.bind(this), diff --git a/ts/window.d.ts b/ts/window.d.ts index 9386b0e3f9c4..0cfb65d214bd 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -36,7 +36,6 @@ import { getEnvironment } from './environment'; import * as zkgroup from './util/zkgroup'; import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util'; import * as Attachment from './types/Attachment'; -import { ColorType } from './types/Colors'; import * as MIME from './types/MIME'; import * as Contact from './types/Contact'; import * as Errors from '../js/modules/types/errors'; @@ -44,11 +43,13 @@ import { ConversationController } from './ConversationController'; import { ReduxActions } from './state/types'; import { createStore } from './state/createStore'; import { createCallManager } from './state/roots/createCallManager'; +import { createChatColorPicker } from './state/roots/createChatColorPicker'; import { createCompositionArea } from './state/roots/createCompositionArea'; import { createContactModal } from './state/roots/createContactModal'; import { createConversationDetails } from './state/roots/createConversationDetails'; import { createConversationHeader } from './state/roots/createConversationHeader'; import { createForwardMessageModal } from './state/roots/createForwardMessageModal'; +import { createGlobalModalContainer } from './state/roots/createGlobalModalContainer'; import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement'; import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal'; import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal'; @@ -89,6 +90,7 @@ import { PropsType as CallingScreenSharingControllerProps, } from './components/CallingScreenSharingController'; import { CaptionEditor } from './components/CaptionEditor'; +import { ChatColorPicker } from './components/ChatColorPicker'; import { ConfirmationDialog } from './components/ConfirmationDialog'; import { ContactDetail } from './components/conversation/ContactDetail'; import { ContactModal } from './components/conversation/ContactModal'; @@ -473,6 +475,7 @@ declare global { Components: { AttachmentList: typeof AttachmentList; CaptionEditor: typeof CaptionEditor; + ChatColorPicker: typeof ChatColorPicker; ConfirmationDialog: typeof ConfirmationDialog; ContactDetail: typeof ContactDetail; ContactModal: typeof ContactModal; @@ -500,11 +503,13 @@ declare global { createStore: typeof createStore; Roots: { createCallManager: typeof createCallManager; + createChatColorPicker: typeof createChatColorPicker; createCompositionArea: typeof createCompositionArea; createContactModal: typeof createContactModal; createConversationDetails: typeof createConversationDetails; createConversationHeader: typeof createConversationHeader; createForwardMessageModal: typeof createForwardMessageModal; + createGlobalModalContainer: typeof createGlobalModalContainer; createGroupLinkManagement: typeof createGroupLinkManagement; createGroupV1MigrationModal: typeof createGroupV1MigrationModal; createGroupV2JoinModal: typeof createGroupV2JoinModal;