Conversation Colors

This commit is contained in:
Josh Perez 2021-05-28 12:15:17 -04:00 committed by GitHub
parent b63d8e908c
commit 28f016ce48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 3997 additions and 1207 deletions

View File

@ -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 */) => {
<div className={styles.container}>
<ClassyProvider themes={['dark']}>
<div
className={classnames(
styles.panel,
firstPaneTheme,
firstPaneDeviceTheme,
firstPaneMode
)}
className={classnames(styles.panel, firstPaneTheme, firstPaneMode)}
>
{contents}
</div>
</ClassyProvider>
{secondPane ? (
<div
className={classnames(
styles.panel,
secondPaneTheme,
secondPaneDeviceTheme,
secondPaneMode
)}
className={classnames(styles.panel, secondPaneTheme, secondPaneMode)}
>
{contents}
</div>

View File

@ -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"
}
}

View File

@ -0,0 +1 @@
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#020202;}</style></defs><path class="cls-1" d="M12,2.5c5.24,0,9.5,3.77,9.5,8.4A5.11,5.11,0,0,1,16.4,16H14.45a2.05,2.05,0,0,0-2,2.05,2.14,2.14,0,0,0,.47,1.3l0,0,0,0a1.34,1.34,0,0,1,.33.85A1.25,1.25,0,0,1,12,21.5a9.5,9.5,0,0,1,0-19M12,1a11,11,0,0,0,0,22,2.75,2.75,0,0,0,2.75-2.75,2.81,2.81,0,0,0-.7-1.84.58.58,0,0,1-.15-.36.54.54,0,0,1,.55-.55H16.4A6.61,6.61,0,0,0,23,10.9C23,5.44,18.06,1,12,1Zm3.5,4.5A1.5,1.5,0,1,0,17,7,1.5,1.5,0,0,0,15.5,5.5ZM18,10a1.5,1.5,0,1,0,1.5,1.5A1.5,1.5,0,0,0,18,10ZM10.5,4.5A1.5,1.5,0,1,0,12,6,1.5,1.5,0,0,0,10.5,4.5ZM6.25,7.75a1.5,1.5,0,1,0,1.5,1.5A1.5,1.5,0,0,0,6.25,7.75ZM6.5,13A1.5,1.5,0,1,0,8,14.5,1.5,1.5,0,0,0,6.5,13Z"/></svg>

After

Width:  |  Height:  |  Size: 757 B

View File

@ -0,0 +1 @@
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#020202;}</style></defs><path class="cls-1" d="M12,1a11,11,0,0,0,0,22,2.75,2.75,0,0,0,2.75-2.75,2.81,2.81,0,0,0-.7-1.84.58.58,0,0,1-.15-.36.55.55,0,0,1,.55-.55H16.4A6.61,6.61,0,0,0,23,10.9C23,5.44,18.06,1,12,1ZM6.25,7.75a1.5,1.5,0,1,1-1.5,1.5A1.5,1.5,0,0,1,6.25,7.75ZM6.5,16A1.5,1.5,0,1,1,8,14.5,1.5,1.5,0,0,1,6.5,16Zm4-8.5A1.5,1.5,0,1,1,12,6,1.5,1.5,0,0,1,10.5,7.5Zm5,1A1.5,1.5,0,1,1,17,7,1.5,1.5,0,0,1,15.5,8.5ZM18,13a1.5,1.5,0,1,1,1.5-1.5A1.5,1.5,0,0,1,18,13Z"/></svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@ -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,

View File

@ -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);

View File

@ -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',
};
})();

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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')
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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
);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -101,7 +101,7 @@
@include keyboard-mode {
&:focus {
border: 1px solid $ultramarine-ui-light;
border: 1px solid $color-ultramarine;
}
}

View File

@ -24,7 +24,7 @@
}
&:focus-within {
border: solid 1px $ultramarine-ui-light;
border: solid 1px $color-ultramarine;
outline: none;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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';

View File

@ -2533,7 +2533,6 @@ export async function startApp(): Promise<void> {
conversation.set({
name: details.name,
color: details.color,
inbox_position: details.inboxPosition,
});
@ -2640,7 +2639,6 @@ export async function startApp(): Promise<void> {
const updates = {
name: details.name,
members,
color: details.color,
type: 'group',
inbox_position: details.inboxPosition,
} as WhatIsThis;

View File

@ -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<string, ColorType> = Colors.reduce(
const colorMap: Record<string, AvatarColorType> = 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 => <Avatar key={color} {...props} color={color} />);
return AvatarColors.map(color => (
<Avatar key={color} {...props} color={color} />
));
});
story.add('Broken Color', () => {
const props = createProps({
color: 'nope' as ColorType,
color: 'nope' as AvatarColorType,
});
return sizes.map(size => <Avatar key={size} {...props} size={size} />);

View File

@ -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;

View File

@ -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<string, ColorType> = Colors.reduce(
const colorMap: Record<string, AvatarColorType> = AvatarColors.reduce(
(m, color) => ({
...m,
[color]: color,
@ -41,6 +41,7 @@ const createProps = (overrideProps: Partial<Props> = {}): 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 || ''),

View File

@ -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')}
</div>
</button>
<button
type="button"
className="module-avatar-popup__item"
onClick={onSetChatColor}
>
<div
className={classNames(
'module-avatar-popup__item__icon',
'module-avatar-popup__item__icon-colors'
)}
/>
<div className="module-avatar-popup__item__text">
{i18n('avatarMenuChatColors')}
</div>
</button>
<button
type="button"
className="module-avatar-popup__item"

View File

@ -3,12 +3,12 @@
import React from 'react';
import classNames from 'classnames';
import { ColorType } from '../types/Colors';
import { AvatarColorType } from '../types/Colors';
export type PropsType = {
avatarPath?: string;
children: React.ReactNode;
color?: ColorType;
color?: AvatarColorType;
};
export const CallBackgroundBlur = ({

View File

@ -15,7 +15,7 @@ import {
GroupCallJoinState,
} from '../types/Calling';
import { ConversationTypeType } from '../state/ducks/conversations';
import { Colors, ColorType } from '../types/Colors';
import { AvatarColors, AvatarColorType } from '../types/Colors';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import { setup as setupI18n } from '../../js/modules/i18n';
@ -28,7 +28,11 @@ const getConversation = () =>
getDefaultConversation({
id: '3051234567',
avatarPath: undefined,
color: select('Callee color', Colors, 'ultramarine' as ColorType),
color: select(
'Callee color',
AvatarColors,
'ultramarine' as AvatarColorType
),
title: text('Callee Title', 'Rick Sanchez'),
name: text('Callee Name', 'Rick Sanchez'),
phoneNumber: '3051234567',
@ -74,7 +78,11 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
keyChangeOk: action('key-change-ok'),
me: {
...getDefaultConversation({
color: select('Caller color', Colors, 'ultramarine' as ColorType),
color: select(
'Caller color',
AvatarColors,
'ultramarine' as AvatarColorType
),
title: text('Caller Title', 'Morty Smith'),
}),
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',

View File

@ -16,7 +16,7 @@ import {
GroupCallRemoteParticipantType,
} from '../types/Calling';
import { ConversationType } from '../state/ducks/conversations';
import { Colors } from '../types/Colors';
import { AvatarColors } from '../types/Colors';
import { CallScreen, PropsType } from './CallScreen';
import { setup as setupI18n } from '../../js/modules/i18n';
import { missingCaseError } from '../util/missingCaseError';
@ -31,7 +31,7 @@ const i18n = setupI18n('en', enMessages);
const conversation = getDefaultConversation({
id: '3051234567',
avatarPath: undefined,
color: Colors[0],
color: AvatarColors[0],
title: 'Rick Sanchez',
name: 'Rick Sanchez',
phoneNumber: '3051234567',
@ -145,7 +145,7 @@ const createProps = (
hangUp: action('hang-up'),
i18n,
me: {
color: Colors[1],
color: AvatarColors[1],
name: 'Morty Smith',
profileName: 'Morty Smith',
title: 'Morty Smith',

View File

@ -24,15 +24,15 @@ import {
PresentedSource,
VideoFrameSource,
} from '../types/Calling';
import { AvatarColorType } from '../types/Colors';
import { CallingToastManager } from './CallingToastManager';
import { ColorType } from '../types/Colors';
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
import { LocalizerType } from '../types/Util';
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
import { isScreenSharingEnabled } from '../util/isScreenSharingEnabled';
import { missingCaseError } from '../util/missingCaseError';
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
export type PropsType = {
activeCall: ActiveCallType;
@ -43,7 +43,7 @@ export type PropsType = {
joinedAt?: number;
me: {
avatarPath?: string;
color?: ColorType;
color?: AvatarColorType;
name?: string;
phoneNumber?: string;
profileName?: string;

View File

@ -7,7 +7,7 @@ import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import { ColorType } from '../types/Colors';
import { AvatarColors } from '../types/Colors';
import { ConversationType } from '../state/ducks/conversations';
import { CallingLobby, PropsType } from './CallingLobby';
import { setup as setupI18n } from '../../js/modules/i18n';
@ -37,7 +37,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
me: overrideProps.me || {
color: 'ultramarine' as ColorType,
color: AvatarColors[0],
uuid: generateUuid(),
},
onCallCanceled: action('on-call-canceled'),
@ -79,7 +79,7 @@ story.add('No Camera, local avatar', () => {
availableCameras: [],
me: {
avatarPath: '/fixtures/kitten-4-112-112.jpg',
color: 'ultramarine' as ColorType,
color: AvatarColors[0],
uuid: generateUuid(),
},
});

View File

@ -14,7 +14,7 @@ import { TooltipPlacement } from './Tooltip';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { CallingHeader } from './CallingHeader';
import { Spinner } from './Spinner';
import { ColorType } from '../types/Colors';
import { AvatarColorType } from '../types/Colors';
import { LocalizerType } from '../types/Util';
import { ConversationType } from '../state/ducks/conversations';
import {
@ -39,7 +39,7 @@ export type PropsType = {
isCallFull?: boolean;
me: {
avatarPath?: string;
color?: ColorType;
color?: AvatarColorType;
uuid: string;
};
onCallCanceled: () => void;

View File

@ -2,12 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { sample } from 'lodash';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
import { Colors } from '../types/Colors';
import { AvatarColors } from '../types/Colors';
import { GroupCallRemoteParticipantType } from '../types/Calling';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { setup as setupI18n } from '../../js/modules/i18n';
@ -18,7 +19,6 @@ const i18n = setupI18n('en', enMessages);
function createParticipant(
participantProps: Partial<GroupCallRemoteParticipantType>
): GroupCallRemoteParticipantType {
const randomColor = Math.floor(Math.random() * Colors.length - 1);
return {
demuxId: 2,
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
@ -28,7 +28,7 @@ function createParticipant(
videoAspectRatio: 1.3,
...getDefaultConversation({
avatarPath: participantProps.avatarPath,
color: Colors[randomColor],
color: sample(AvatarColors),
isBlocked: Boolean(participantProps.isBlocked),
name: participantProps.name,
profileName: participantProps.title,

View File

@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { ColorType } from '../types/Colors';
import { AvatarColors } from '../types/Colors';
import { ConversationType } from '../state/ducks/conversations';
import { CallingPip, PropsType } from './CallingPip';
import {
@ -26,7 +26,7 @@ const i18n = setupI18n('en', enMessages);
const conversation: ConversationType = getDefaultConversation({
id: '3051234567',
avatarPath: undefined,
color: 'ultramarine' as ColorType,
color: AvatarColors[0],
title: 'Rick Sanchez',
name: 'Rick Sanchez',
phoneNumber: '3051234567',

View File

@ -0,0 +1,68 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { select } from '@storybook/addon-knobs';
import enMessages from '../../_locales/en/messages.json';
import { ChatColorPicker, PropsType } from './ChatColorPicker';
import { ConversationColors } from '../types/Colors';
import { setup as setupI18n } from '../../js/modules/i18n';
const story = storiesOf('Components/ChatColorPicker', module);
const i18n = setupI18n('en', enMessages);
const SAMPLE_CUSTOM_COLOR = {
deg: 90,
end: { hue: 197, saturation: 100 },
start: { hue: 315, saturation: 78 },
};
const createProps = (): PropsType => ({
addCustomColor: action('addCustomColor'),
editCustomColor: action('editCustomColor'),
getConversationsWithCustomColor: (_: string) => [],
i18n,
onChatColorReset: action('onChatColorReset'),
onSelectColor: action('onSelectColor'),
removeCustomColor: action('removeCustomColor'),
removeCustomColorOnConversations: action('removeCustomColorOnConversations'),
resetAllChatColors: action('resetAllChatColors'),
selectedColor: select('selectedColor', ConversationColors, 'basil' as const),
selectedCustomColor: {},
});
story.add('Default', () => <ChatColorPicker {...createProps()} />);
const CUSTOM_COLORS = {
abc: {
start: { hue: 32, saturation: 100 },
},
def: {
deg: 90,
start: { hue: 180, saturation: 100 },
end: { hue: 0, saturation: 100 },
},
ghi: SAMPLE_CUSTOM_COLOR,
jkl: {
deg: 90,
start: { hue: 161, saturation: 52 },
end: { hue: 153, saturation: 89 },
},
};
story.add('Custom Colors', () => (
<ChatColorPicker
{...createProps()}
customColors={CUSTOM_COLORS}
selectedColor="custom"
selectedCustomColor={{
id: 'ghi',
value: SAMPLE_CUSTOM_COLOR,
}}
/>
));

View File

@ -0,0 +1,386 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { KeyboardEvent, MouseEvent, useRef, useState } from 'react';
import classNames from 'classnames';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
import { ConfirmationDialog } from './ConfirmationDialog';
import { CustomColorEditor } from './CustomColorEditor';
import { Modal } from './Modal';
import {
ConversationColors,
ConversationColorType,
CustomColorType,
} from '../types/Colors';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
import { SampleMessageBubbles } from './SampleMessageBubbles';
import { PanelRow } from './conversation/conversation-details/PanelRow';
import { getCustomColorStyle } from '../util/getCustomColorStyle';
type CustomColorDataType = {
id?: string;
value?: CustomColorType;
};
export type PropsDataType = {
customColors?: Record<string, CustomColorType>;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
i18n: LocalizerType;
isInModal?: boolean;
onChatColorReset?: () => unknown;
onSelectColor: (
color: ConversationColorType,
customColorData?: {
id: string;
value: CustomColorType;
}
) => unknown;
selectedColor?: ConversationColorType;
selectedCustomColor: CustomColorDataType;
};
type PropsActionType = {
addCustomColor: (color: CustomColorType) => unknown;
editCustomColor: (colorId: string, color: CustomColorType) => unknown;
removeCustomColor: (colorId: string) => unknown;
removeCustomColorOnConversations: (colorId: string) => unknown;
resetAllChatColors: () => unknown;
};
export type PropsType = PropsDataType & PropsActionType;
export const ChatColorPicker = ({
addCustomColor,
customColors = {},
editCustomColor,
getConversationsWithCustomColor,
i18n,
isInModal = false,
onChatColorReset,
onSelectColor,
removeCustomColor,
removeCustomColorOnConversations,
resetAllChatColors,
selectedColor = ConversationColors[0],
selectedCustomColor,
}: PropsType): JSX.Element => {
const [confirmResetAll, setConfirmResetAll] = useState(false);
const [customColorToEdit, setCustomColorToEdit] = useState<
CustomColorDataType | undefined
>(undefined);
const renderCustomColorEditorWrapper = () => (
<CustomColorEditorWrapper
customColorToEdit={customColorToEdit}
i18n={i18n}
isInModal={isInModal}
onClose={() => setCustomColorToEdit(undefined)}
onSave={(color: CustomColorType) => {
if (customColorToEdit?.id) {
editCustomColor(customColorToEdit.id, color);
onSelectColor('custom', {
id: customColorToEdit.id,
value: color,
});
} else {
addCustomColor(color);
}
}}
/>
);
if (isInModal && customColorToEdit) {
return renderCustomColorEditorWrapper();
}
return (
<>
{customColorToEdit ? renderCustomColorEditorWrapper() : null}
{confirmResetAll ? (
<ConfirmationDialog
actions={[
{
action: resetAllChatColors,
style: 'affirmative',
text: i18n('ChatColorPicker__confirm-reset'),
},
]}
i18n={i18n}
onClose={() => {
setConfirmResetAll(false);
}}
title={i18n('ChatColorPicker__resetAll')}
>
{i18n('ChatColorPicker__confirm-reset-message')}
</ConfirmationDialog>
) : null}
<SampleMessageBubbles
backgroundStyle={getCustomColorStyle(selectedCustomColor.value)}
color={selectedColor}
i18n={i18n}
/>
<hr />
<div className="ChatColorPicker__bubbles">
{ConversationColors.map(color => (
<div
aria-label={color}
className={classNames(
`ChatColorPicker__bubble ChatColorPicker__bubble--${color}`,
{
'ChatColorPicker__bubble--selected': color === selectedColor,
}
)}
key={color}
onClick={() => onSelectColor(color)}
onKeyDown={(ev: KeyboardEvent) => {
if (ev.key === 'Enter') {
onSelectColor(color);
}
}}
role="button"
tabIndex={0}
/>
))}
{Object.keys(customColors).map(colorId => {
const colorValues = customColors[colorId];
return (
<CustomColorBubble
color={colorValues}
colorId={colorId}
getConversationsWithCustomColor={getConversationsWithCustomColor}
key={colorId}
i18n={i18n}
isSelected={colorId === selectedCustomColor.id}
onChoose={() => {
onSelectColor('custom', {
id: colorId,
value: colorValues,
});
}}
onDelete={() => {
removeCustomColor(colorId);
removeCustomColorOnConversations(colorId);
}}
onDupe={() => {
addCustomColor(colorValues);
}}
onEdit={() => {
setCustomColorToEdit({ id: colorId, value: colorValues });
}}
/>
);
})}
<div
aria-label={i18n('ChatColorPicker__custom-color--label')}
className="ChatColorPicker__bubble ChatColorPicker__bubble"
onClick={() =>
setCustomColorToEdit({ id: undefined, value: undefined })
}
onKeyDown={(ev: KeyboardEvent) => {
if (ev.key === 'Enter') {
setCustomColorToEdit({ id: undefined, value: undefined });
}
}}
role="button"
tabIndex={0}
>
<i className="ChatColorPicker__add-icon" />
</div>
</div>
<hr />
{onChatColorReset ? (
<PanelRow
label={i18n('ChatColorPicker__reset')}
onClick={onChatColorReset}
/>
) : null}
<PanelRow
label={i18n('ChatColorPicker__resetAll')}
onClick={() => {
setConfirmResetAll(true);
}}
/>
</>
);
};
type CustomColorBubblePropsType = {
color: CustomColorType;
colorId: string;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
i18n: LocalizerType;
isSelected: boolean;
onDelete: () => unknown;
onDupe: () => unknown;
onEdit: () => unknown;
onChoose: () => unknown;
};
const CustomColorBubble = ({
color,
colorId,
getConversationsWithCustomColor,
i18n,
isSelected,
onDelete,
onDupe,
onEdit,
onChoose,
}: CustomColorBubblePropsType): JSX.Element => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const menuRef = useRef<any | null>(null);
const [confirmDeleteCount, setConfirmDeleteCount] = useState<
number | undefined
>(undefined);
const handleClick = (ev: KeyboardEvent | MouseEvent) => {
if (!isSelected) {
onChoose();
return;
}
if (menuRef && menuRef.current) {
menuRef.current.handleContextClick(ev);
}
};
const bubble = (
<div
aria-label={colorId}
className={classNames('ChatColorPicker__bubble', {
'ChatColorPicker__bubble--selected': isSelected,
})}
onClick={handleClick}
onKeyDown={(ev: KeyboardEvent) => {
if (ev.key === 'Enter') {
handleClick(ev);
}
}}
role="button"
tabIndex={0}
style={{
...getCustomColorStyle(color),
}}
/>
);
return (
<>
{confirmDeleteCount ? (
<ConfirmationDialog
actions={[
{
action: onDelete,
style: 'negative',
text: i18n('ChatColorPicker__context--delete'),
},
]}
i18n={i18n}
onClose={() => {
setConfirmDeleteCount(undefined);
}}
title={i18n('ChatColorPicker__delete--title')}
>
{i18n('ChatColorPicker__delete--message', [
String(confirmDeleteCount),
])}
</ConfirmationDialog>
) : null}
{isSelected ? (
<ContextMenuTrigger id={colorId} ref={menuRef}>
{bubble}
</ContextMenuTrigger>
) : (
bubble
)}
<ContextMenu id={colorId}>
<MenuItem
attributes={{
className: 'ChatColorPicker__context--edit',
}}
onClick={(event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onEdit();
}}
>
{i18n('ChatColorPicker__context--edit')}
</MenuItem>
<MenuItem
attributes={{
className: 'ChatColorPicker__context--duplicate',
}}
onClick={(event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onDupe();
}}
>
{i18n('ChatColorPicker__context--duplicate')}
</MenuItem>
<MenuItem
attributes={{
className: 'ChatColorPicker__context--delete',
}}
onClick={(event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
const conversations = getConversationsWithCustomColor(colorId);
if (!conversations.length) {
onDelete();
} else {
setConfirmDeleteCount(conversations.length);
}
}}
>
{i18n('ChatColorPicker__context--delete')}
</MenuItem>
</ContextMenu>
</>
);
};
type CustomColorEditorWrapperPropsType = {
customColorToEdit?: CustomColorDataType;
i18n: LocalizerType;
isInModal: boolean;
onClose: () => unknown;
onSave: (color: CustomColorType) => unknown;
};
const CustomColorEditorWrapper = ({
customColorToEdit,
i18n,
isInModal,
onClose,
onSave,
}: CustomColorEditorWrapperPropsType): JSX.Element => {
const editor = (
<CustomColorEditor
customColor={customColorToEdit?.value}
i18n={i18n}
onClose={onClose}
onSave={onSave}
/>
);
if (!isInModal) {
return (
<Modal
hasXButton
i18n={i18n}
noMouseClose
onClose={onClose}
title={i18n('CustomColorEditor__title')}
>
{editor}
</Modal>
);
}
return editor;
};

View File

@ -22,7 +22,7 @@ type ContactType = Omit<ContactPillPropsType, 'i18n' | 'onClickRemove'>;
const contacts: Array<ContactType> = times(50, index =>
getDefaultConversation({
color: 'red',
color: 'crimson',
id: `contact-${index}`,
name: `Contact ${index}`,
phoneNumber: '(202) 555-0001',
@ -37,7 +37,7 @@ const contactPillProps = (
...(overrideProps ||
getDefaultConversation({
avatarPath: gifUrl,
color: 'red',
color: 'crimson',
firstName: 'John',
id: 'abc123',
isMe: false,

View File

@ -0,0 +1,23 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import enMessages from '../../_locales/en/messages.json';
import { CustomColorEditor, PropsType } from './CustomColorEditor';
import { setup as setupI18n } from '../../js/modules/i18n';
const story = storiesOf('Components/CustomColorEditor', module);
const i18n = setupI18n('en', enMessages);
const createProps = (): PropsType => ({
i18n,
onClose: action('onClose'),
onSave: action('onSave'),
});
story.add('Default', () => <CustomColorEditor {...createProps()} />);

View File

@ -0,0 +1,182 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import { Button, ButtonVariant } from './Button';
import { GradientDial, KnobType } from './GradientDial';
import { SampleMessageBubbles } from './SampleMessageBubbles';
import { Slider } from './Slider';
import { Tabs } from './Tabs';
import { CustomColorType } from '../types/Colors';
import { LocalizerType } from '../types/Util';
import { getHSL } from '../util/getHSL';
import { getCustomColorStyle } from '../util/getCustomColorStyle';
export type PropsType = {
customColor?: CustomColorType;
i18n: LocalizerType;
onClose: () => unknown;
onSave: (color: CustomColorType) => unknown;
};
enum TabViews {
Solid = 'Solid',
Gradient = 'Gradient',
}
function getPercentage(value: number, max: number): number {
return (100 * value) / max;
}
function getValue(percentage: number, max: number): number {
return Math.round((max / 100) * percentage);
}
const MAX_HUE = 360;
const ULTRAMARINE_ISH_VALUES = {
hue: 220,
saturation: 84,
};
const ULTRAMARINE_ISH: CustomColorType = {
start: ULTRAMARINE_ISH_VALUES,
deg: 180,
};
export const CustomColorEditor = ({
customColor = ULTRAMARINE_ISH,
i18n,
onClose,
onSave,
}: PropsType): JSX.Element => {
const [color, setColor] = useState<CustomColorType>(customColor);
const [selectedColorKnob, setSelectedColorKnob] = useState<KnobType>(
KnobType.start
);
const { hue, saturation } =
color[selectedColorKnob] || ULTRAMARINE_ISH_VALUES;
return (
<>
<Tabs
initialSelectedTab={color.end ? TabViews.Gradient : TabViews.Solid}
moduleClassName="CustomColorEditor__tabs"
onTabChange={selectedTab => {
if (selectedTab === TabViews.Gradient && !color.end) {
setColor({
...color,
end: ULTRAMARINE_ISH_VALUES,
});
}
}}
tabs={[
{
id: TabViews.Solid,
label: i18n('CustomColorEditor__solid'),
},
{
id: TabViews.Gradient,
label: i18n('CustomColorEditor__gradient'),
},
]}
>
{({ selectedTab }) => (
<>
<div className="CustomColorEditor__messages">
<SampleMessageBubbles
backgroundStyle={getCustomColorStyle(color)}
i18n={i18n}
includeAnotherBubble
/>
{selectedTab === TabViews.Gradient && (
<>
<GradientDial
deg={color.deg}
knob1Style={{ backgroundColor: getHSL(color.start) }}
knob2Style={{
backgroundColor: getHSL(
color.end || ULTRAMARINE_ISH_VALUES
),
}}
onChange={deg => {
setColor({
...color,
deg,
});
}}
onClick={knob => setSelectedColorKnob(knob)}
selectedKnob={selectedColorKnob}
/>
</>
)}
</div>
<div className="CustomColorEditor__slider-container">
{i18n('CustomColorEditor__hue')}
<Slider
handleStyle={{
backgroundColor: getHSL(
color[selectedColorKnob] || ULTRAMARINE_ISH_VALUES
),
}}
label={i18n('CustomColorEditor__hue')}
moduleClassName="CustomColorEditor__hue-slider"
onChange={(percentage: number) => {
setColor({
...color,
[selectedColorKnob]: {
...ULTRAMARINE_ISH_VALUES,
...color[selectedColorKnob],
hue: getValue(percentage, MAX_HUE),
},
});
}}
value={getPercentage(hue, MAX_HUE)}
/>
</div>
<div className="CustomColorEditor__slider-container">
{i18n('CustomColorEditor__saturation')}
<Slider
containerStyle={getCustomColorStyle({
deg: 180,
start: { hue, saturation: 0 },
end: { hue, saturation: 100 },
})}
handleStyle={{
backgroundColor: getHSL(
color[selectedColorKnob] || ULTRAMARINE_ISH_VALUES
),
}}
label={i18n('CustomColorEditor__saturation')}
moduleClassName="CustomColorEditor__saturation-slider"
onChange={(value: number) => {
setColor({
...color,
[selectedColorKnob]: {
...ULTRAMARINE_ISH_VALUES,
...color[selectedColorKnob],
saturation: value,
},
});
}}
value={saturation}
/>
</div>
<div className="CustomColorEditor__footer">
<Button variant={ButtonVariant.Secondary} onClick={onClose}>
{i18n('cancel')}
</Button>
<Button
onClick={() => {
onSave(color);
onClose();
}}
>
{i18n('save')}
</Button>
</div>
</>
)}
</Tabs>
</>
);
};

View File

@ -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 (
<Modal
hasXButton
i18n={i18n}
noMouseClose
onClose={toggleChatColorEditor}
title={i18n('ChatColorPicker__global-chat-color')}
>
{renderChatColorPicker({
setAllConversationColors,
})}
</Modal>
);
}
return null;
};

View File

@ -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<HTMLDivElement | null>(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 (
<div className="GradientDial__container" ref={containerRef}>
{knobDim.start && (
<div
aria-label="0"
className={classNames('GradientDial__knob', {
'GradientDial__knob--selected': selectedKnob === KnobType.start,
})}
onMouseDown={ev => {
if (selectedKnob === KnobType.start) {
handleMouseDown(ev);
}
}}
onClick={() => {
onClick(KnobType.start);
}}
role="button"
style={{
...knob1Style,
...knobDim.start,
}}
/>
)}
{knobDim.end && (
<div
aria-label="1"
className={classNames('GradientDial__knob', {
'GradientDial__knob--selected': selectedKnob === KnobType.end,
})}
onMouseDown={ev => {
if (selectedKnob === KnobType.end) {
handleMouseDown(ev);
}
}}
onClick={() => {
onClick(KnobType.end);
}}
role="button"
style={{
...knob2Style,
...knobDim.end,
}}
/>
)}
{knobDim.start && knobDim.end && (
<div className="GradientDial__bar--container">
<div
className="GradientDial__bar--node"
style={{
transform: `translate(-50%, -50%) rotate(${90 - deg}deg)`,
}}
/>
</div>
)}
</div>
);
};

View File

@ -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',

View File

@ -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> = {}): PropsType => ({
showArchivedConversations: action('showArchivedConversations'),
startComposing: action('startComposing'),
toggleChatColorEditor: action('toggleChatColorEditor'),
});
story.add('Basic', () => {

View File

@ -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<PropsType, StateType> {
searchConversationName,
searchTerm,
showArchivedConversations,
toggleChatColorEditor,
} = this.props;
const { showingAvatarPopup, popperRoot } = this.state;
@ -408,6 +410,10 @@ export class MainHeader extends React.Component<PropsType, StateType> {
size={28}
// See the comment above about `sharedGroupNames`.
sharedGroupNames={[]}
onSetChatColor={() => {
toggleChatColorEditor();
this.hideAvatarPopup();
}}
onViewPreferences={() => {
showSettings();
this.hideAvatarPopup();

View File

@ -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 (
<ModalHost onClose={onClose} theme={theme}>
<ModalHost noMouseClose={noMouseClose} onClose={onClose} theme={theme}>
<div
className={classNames(
getClassName(''),

View File

@ -7,6 +7,7 @@ import { createPortal } from 'react-dom';
import { Theme, themeClassName } from '../util/theme';
export type PropsType = {
readonly noMouseClose?: boolean;
readonly onEscape?: () => 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<HTMLElement | null>(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}
</div>,

View File

@ -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,

View File

@ -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,

View File

@ -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 => (
<div className={`module-message module-message--${direction}`}>
<div className="module-message__container-outer">
<div
className={`module-message__container module-message__container--${direction} module-message__container--${direction}-${color}`}
style={style}
>
<div
dir="auto"
className={`module-message__text module-message__text--${direction}`}
>
<span>{text}</span>
</div>
<div
className={`module-message__metadata module-message__metadata--${direction}`}
>
<span
className={`module-message__metadata__date module-message__metadata__date--${direction}`}
>
{formatRelativeTime(timestamp, { extended: true, i18n })}
</span>
{direction === 'outgoing' && (
<div
className={`module-message__metadata__status-icon module-message__metadata__status-icon--${status}`}
/>
)}
</div>
</div>
</div>
</div>
);
export const SampleMessageBubbles = ({
backgroundStyle = {},
color,
i18n,
includeAnotherBubble = false,
}: PropsType): JSX.Element => {
const firstBubbleStyle = includeAnotherBubble ? backgroundStyle : undefined;
return (
<>
<SampleMessage
color={color}
direction={includeAnotherBubble ? 'outgoing' : 'incoming'}
i18n={i18n}
text={i18n('ChatColorPicker__sampleBubble1')}
timestamp={Date.now() - A_FEW_DAYS_AGO}
status="read"
style={firstBubbleStyle}
/>
<br />
{includeAnotherBubble ? (
<>
<br style={{ clear: 'both' }} />
<br />
<SampleMessage
direction="incoming"
i18n={i18n}
text={i18n('ChatColorPicker__sampleBubble2')}
timestamp={Date.now() - A_FEW_DAYS_AGO / 2}
status="read"
/>
<br />
<br />
</>
) : null}
<SampleMessage
color={color}
direction="outgoing"
i18n={i18n}
text={i18n('ChatColorPicker__sampleBubble3')}
timestamp={Date.now()}
status="delivered"
style={backgroundStyle}
/>
<br style={{ clear: 'both' }} />
</>
);
};

View File

@ -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', () => <Slider {...createProps()} />);
story.add('Draggable Test', () => {
function StatefulSliderController(props: PropsType): JSX.Element {
const [value, setValue] = useState(30);
return <Slider {...props} onChange={setValue} value={value} />;
}
return <StatefulSliderController {...createProps()} />;
});

126
ts/components/Slider.tsx Normal file
View File

@ -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<number>(0);
const handleRef = useRef<HTMLDivElement | null>(null);
const sliderRef = useRef<HTMLDivElement | null>(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 (
<div
aria-label={label}
className={getClassName('')}
onClick={handleValueChange}
onKeyDown={handleKeyDown}
ref={sliderRef}
role="button"
style={containerStyle}
tabIndex={0}
>
<div
aria-label={label}
aria-valuenow={value}
className={getClassName('__handle')}
onMouseDown={handleMouseDown}
ref={handleRef}
role="slider"
style={{ ...handleStyle, left: `${value}%` }}
tabIndex={-1}
/>
</div>
);
};

68
ts/components/Tabs.tsx Normal file
View File

@ -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<Tab>;
};
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<string>(
initialSelectedTab || tabs[0].id
);
const getClassName = getClassNamesFor('Tabs', moduleClassName);
return (
<>
<div className={getClassName('')}>
{tabs.map(({ id, label }) => (
<div
className={classNames(
getClassName('__tab'),
selectedTab === id && getClassName('__tab--selected')
)}
key={id}
onClick={() => {
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}
</div>
))}
</div>
{children({ selectedTab })}
</>
);
};

View File

@ -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 => (
<div key={color}>
<ContactName
title={`Hello ${color}`}
contactNameColor={color}
i18n={i18n}
phoneNumber="(202) 555-0011"
/>
</div>
));
})
.add('No data provided', () => {
return <ContactName title="unknownContact" i18n={i18n} />;
});

View File

@ -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 (
<span className={prefix} dir="auto">
<span
className={classNames(
getClassName(''),
contactNameColor ? getClassName(`--${contactNameColor}`) : null
)}
dir="auto"
>
<Emojify text={text} />
</span>
);

View File

@ -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<ConversationHeaderStory> = [
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<ConversationHeaderStory> = [
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<ConversationHeaderStory> = [
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<ConversationHeaderStory> = [
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<ConversationHeaderStory> = [
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<ConversationHeaderStory> = [
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<ConversationHeaderStory> = [
title: 'In a forever muted group',
props: {
...commonProps,
color: 'signal-blue',
color: 'ultramarine',
title: 'Way too many messages',
name: 'Way too many messages',
phoneNumber: '',

View File

@ -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<PropsType, StateType> {
onSetDisappearingMessages,
onSetMuteNotifications,
onShowAllMedia,
onShowChatColorEditor,
onShowConversationDetails,
onShowGroupMembers,
onShowSafetyNumber,
@ -456,6 +458,11 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
</MenuItem>
))}
</SubMenu>
{!isGroup ? (
<MenuItem onClick={onShowChatColorEditor}>
{i18n('showChatColorEditor')}
</MenuItem>
) : null}
{hasGV2AdminEnabled ? (
<MenuItem onClick={onShowConversationDetails}>
{i18n('showConversationDetails')}

View File

@ -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> = {}): 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> = {}): 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> = {}): 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 => (
<Message
{...createProps({
author: getDefaultConversation({ color }),
text:
'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
})}
/>
{ConversationColors.map(color => (
<div key={color}>
{renderBothDirections(
createProps({
conversationColor: color,
text: `Here is a preview of the chat color: ${color}. The color is visible to only you.`,
})
)}
</div>
))}
</>
);
@ -1081,3 +1082,25 @@ story.add('Not approved, with link preview', () => {
return renderBothDirections(props);
});
story.add('Custom Color', () => (
<>
<Message
{...createProps({ text: 'Solid.' })}
direction="outgoing"
customColor={{
start: { hue: 82, saturation: 35 },
}}
/>
<br style={{ clear: 'both' }} />
<Message
{...createProps({ text: 'Gradient.' })}
direction="outgoing"
customColor={{
deg: 192,
start: { hue: 304, saturation: 85 },
end: { hue: 231, saturation: 76 },
}}
/>
</>
));

View File

@ -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<HTMLDivElement>) => 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<AttachmentType>;
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<Props, State> {
const {
author,
collapseMetadata,
contactNameColor,
conversationType,
direction,
i18n,
@ -687,6 +697,7 @@ export class Message extends React.Component<Props, State> {
return (
<div className={moduleName}>
<ContactName
contactNameColor={contactNameColor}
title={author.title}
phoneNumber={author.phoneNumber}
name={author.name}
@ -1035,8 +1046,9 @@ export class Message extends React.Component<Props, State> {
public renderQuote(): JSX.Element | null {
const {
author,
conversationColor,
conversationType,
customColor,
direction,
disableScroll,
i18n,
@ -1050,8 +1062,6 @@ export class Message extends React.Component<Props, State> {
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<Props, State> {
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<Props, State> {
public renderContainer(): JSX.Element {
const {
attachments,
author,
conversationColor,
customColor,
deletedForEveryone,
direction,
isSticker,
@ -2275,14 +2287,14 @@ export class Message extends React.Component<Props, State> {
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<Props, State> {
const containerStyles = {
width: isShowingImage ? width : undefined,
};
if (!isSticker && direction === 'outgoing') {
Object.assign(containerStyles, getCustomColorStyle(customColor));
}
return (
<div className="module-message__container-outer">

View File

@ -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> = {}): 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,

View File

@ -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> = {}): Props => ({
authorColor: overrideProps.authorColor || 'green',
authorName: text('authorName', overrideProps.authorName || ''),
authorPhoneNumber: text(
'authorPhoneNumber',
@ -122,6 +122,7 @@ const createProps = (overrideProps: Partial<Props> = {}): 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 <Quote {...props} />;
});
story.add('Custom Color', () => (
<>
<Quote
{...createProps({ isIncoming: true, text: 'Solid + Gradient' })}
customColor={{
start: { hue: 82, saturation: 35 },
}}
/>
<Quote
{...createProps()}
customColor={{
deg: 192,
start: { hue: 304, saturation: 85 },
end: { hue: 231, saturation: 76 },
}}
/>
</>
));

View File

@ -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<Props, State> {
}
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<Props, State> {
<div
className={classNames(
'module-quote__reference-warning',
isIncoming ? 'module-quote__reference-warning--incoming' : null
isIncoming
? `module-quote--incoming-${conversationColor}`
: `module-quote--outgoing-${conversationColor}`
)}
style={{ ...getCustomColorStyle(customColor, true) }}
>
<div
className={classNames(
@ -398,7 +409,8 @@ export class Quote extends React.Component<Props, State> {
public render(): JSX.Element | null {
const {
authorColor,
conversationColor,
customColor,
isIncoming,
onClick,
referencedMessageNotFound,
@ -424,14 +436,15 @@ export class Quote extends React.Component<Props, State> {
'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) }}
>
<div className="module-quote__primary">
{this.renderAuthor()}

View File

@ -37,7 +37,7 @@ const items: Record<string, TimelineItemType> = {
timestamp: Date.now(),
author: {
phoneNumber: '(202) 555-2001',
color: 'green',
color: 'forest',
},
text: '🔥',
},
@ -50,7 +50,7 @@ const items: Record<string, TimelineItemType> = {
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<string, TimelineItemType> = {
direction: 'incoming',
timestamp: Date.now(),
author: {
color: 'red',
color: 'crimson',
},
text: 'Hello there from the new world!',
},
@ -161,7 +161,7 @@ const items: Record<string, TimelineItemType> = {
timestamp: Date.now(),
status: 'sent',
author: {
color: 'pink',
color: 'plum',
},
text: '🔥',
},
@ -174,7 +174,7 @@ const items: Record<string, TimelineItemType> = {
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 = () => <TimelineLoadingRow state="loading" />;
const renderTypingBubble = () => (
<TypingBubble
acceptedMessageRequest
color="red"
color="crimson"
conversationType="direct"
phoneNumber="+18005552222"
i18n={i18n}

View File

@ -86,7 +86,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
timestamp: Date.now(),
author: {
phoneNumber: '(202) 555-2001',
color: 'green',
color: 'forest',
},
text: '🔥',
},

View File

@ -8,7 +8,7 @@ import { select, text } from '@storybook/addon-knobs';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { Props, TypingBubble } from './TypingBubble';
import { Colors } from '../../types/Colors';
import { AvatarColors } from '../../types/Colors';
const i18n = setupI18n('en', enMessages);
@ -20,8 +20,8 @@ const createProps = (overrideProps: Partial<Props> = {}): 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: '',

View File

@ -69,7 +69,7 @@ export class TypingBubble extends React.PureComponent<Props> {
}
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<Props> {
<div
className={classNames(
'module-message__container',
'module-message__container--incoming',
`module-message__container--incoming-${color}`
'module-message__container--incoming'
)}
>
<div className="module-message__typing-container">

View File

@ -48,7 +48,7 @@ export function renderAvatar({
acceptedMessageRequest={false}
avatarPath={avatarPath}
blur={AvatarBlur.NoBlur}
color="grey"
color="steel"
conversationType="direct"
i18n={i18n}
isMe

View File

@ -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'),

View File

@ -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<Props> = ({
setDisappearingMessages,
showAllMedia,
showContactModal,
showGroupChatColorEditor,
showGroupLinkManagement,
showGroupV2Permissions,
showPendingInvites,
@ -224,8 +227,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
}}
/>
{canEditGroupInfo ? (
<PanelSection>
<PanelSection>
{canEditGroupInfo ? (
<PanelRow
icon={
<ConversationDetailsIcon
@ -252,8 +255,26 @@ export const ConversationDetails: React.ComponentType<Props> = ({
</div>
}
/>
</PanelSection>
) : null}
) : null}
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('showChatColorEditor')}
icon="color"
/>
}
label={i18n('showChatColorEditor')}
onClick={showGroupChatColorEditor}
right={
<div
className={`module-conversation-details__chat-color module-conversation-details__chat-color--${conversation.conversationColor}`}
style={{
...getCustomColorStyle(conversation.customColor),
}}
/>
}
/>
</PanelSection>
<ConversationDetailsMembershipList
canAddNewMembers={canEditGroupInfo}

View File

@ -19,7 +19,7 @@ export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
return (
<BaseConversationListItem
acceptedMessageRequest={false}
color="grey"
color="steel"
conversationType="group"
headerName={title}
i18n={i18n}

View File

@ -33,7 +33,7 @@ export const StartNewConversation: FunctionComponent<Props> = React.memo(
return (
<BaseConversationListItem
acceptedMessageRequest={false}
color="grey"
color="steel"
conversationType="direct"
headerName={phoneNumber}
i18n={i18n}

5
ts/model-types.d.ts vendored
View File

@ -6,7 +6,7 @@ import * as Backbone from 'backbone';
import { GroupV2ChangeType } from './groups';
import { LocalizerType, BodyRangeType, BodyRangesType } from './types/Util';
import { CallHistoryDetailsFromDiskType } from './types/Calling';
import { ColorType } from './types/Colors';
import { CustomColorType } from './types/Colors';
import {
ConversationType,
MessageType,
@ -193,6 +193,9 @@ export type ConversationAttributesType = {
addedBy?: string;
capabilities?: CapabilitiesType;
color?: string;
conversationColor?: string;
customColor?: CustomColorType;
customColorId?: string;
discoveredUnregisteredAt?: number;
draftAttachments?: Array<{
path?: string;

View File

@ -4,7 +4,7 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable camelcase */
import { ProfileKeyCredentialRequestContext } from 'zkgroup';
import { compact } from 'lodash';
import { compact, sample } from 'lodash';
import {
MessageModelCollectionType,
WhatIsThis,
@ -20,7 +20,11 @@ import {
SendOptionsType,
} from '../textsecure/SendMessage';
import { ConversationType } from '../state/ducks/conversations';
import { ColorType } from '../types/Colors';
import {
AvatarColorType,
AvatarColors,
ConversationColorType,
} from '../types/Colors';
import { MessageModel } from './messages';
import { isMuted } from '../util/isMuted';
import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
@ -75,21 +79,6 @@ const {
} = window.Signal.Migrations;
const { addStickerPackReference } = window.Signal.Data;
const COLORS = [
'red',
'deep_orange',
'brown',
'pink',
'purple',
'indigo',
'blue',
'teal',
'green',
'light_green',
'blue_grey',
'ultramarine',
];
const THREE_HOURS = 3 * 60 * 60 * 1000;
const FIVE_MINUTES = 1000 * 60 * 5;
@ -105,7 +94,7 @@ type CustomError = Error & {
type CachedIdenticon = {
readonly url: string;
readonly content: string;
readonly color: ColorType;
readonly color: AvatarColorType;
};
export class ConversationModel extends window.Backbone
@ -318,6 +307,12 @@ export class ConversationModel extends window.Backbone
this.fetchSMSOnlyUUID,
FIVE_MINUTES
);
// Ensure each contact has a an avatar color associated with it
if (!this.get('color')) {
this.set('color', sample(AvatarColors));
window.Signal.Data.updateConversation(this.attributes);
}
}
isMe(): boolean {
@ -1452,6 +1447,9 @@ export class ConversationModel extends window.Backbone
avatarPath: this.getAbsoluteAvatarPath(),
unblurredAvatarPath: this.getAbsoluteUnblurredAvatarPath(),
color,
conversationColor: this.getConversationColor(),
customColor: this.get('customColor'),
customColorId: this.get('customColorId'),
discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'),
draftBodyRanges,
draftPreview,
@ -4675,14 +4673,19 @@ export class ConversationModel extends window.Backbone
return this.get('type') === 'private';
}
getColor(): ColorType {
getColor(): AvatarColorType {
if (!this.isPrivate()) {
return 'signal-blue';
return 'ultramarine';
}
return migrateColor(this.get('color'));
}
getConversationColor(): ConversationColorType {
return (this.get('conversationColor') ||
'ultramarine') as ConversationColorType;
}
private getAvatarPath(): undefined | string {
const avatar = this.isMe()
? this.get('profileAvatar') || this.get('avatar')
@ -5187,10 +5190,6 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({
},
});
window.Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(
' '
);
// This is a wrapper model used to display group members in the member list view, within
// the world of backbone, but layering another bit of group-specific data top of base
// conversation data.

View File

@ -29,7 +29,7 @@ import {
import { CallbackResultType } from '../textsecure/SendMessage';
import * as expirationTimer from '../util/expirationTimer';
import { missingCaseError } from '../util/missingCaseError';
import { ColorType } from '../types/Colors';
import { ConversationColorType } from '../types/Colors';
import { CallMode } from '../types/Calling';
import { BodyRangesType } from '../types/Util';
import { ReactionType } from '../types/Reactions';
@ -919,6 +919,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
.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<MessageAttributesType> {
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<MessageAttributesType> {
}
}
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<MessageAttributesType> {
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<MessageAttributesType> {
'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<MessageAttributesType> {
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)

9
ts/shims/getUserTheme.ts Normal file
View File

@ -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());
}

View File

@ -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<StoredJob>): Promise<void> {
function deleteJob(id: string): Promise<void> {
return channels.deleteJob(id);
}
async function updateAllConversationColors(
conversationColor?: ConversationColorType,
customColorData?: {
id: string;
value: CustomColorType;
}
): Promise<void> {
return channels.updateAllConversationColors(
conversationColor,
customColorData
);
}

View File

@ -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<Array<StoredJob>>;
insertJob(job: Readonly<StoredJob>): Promise<void>;
deleteJob(id: string): Promise<void>;
updateAllConversationColors: (
conversationColor?: ConversationColorType,
customColorData?: {
id: string;
value: CustomColorType;
}
) => Promise<void>;
};
// The reason for client/server divergence is the need to inject Backbone models and

Some files were not shown because too many files have changed in this diff Show More