Conversation Colors
This commit is contained in:
parent
b63d8e908c
commit
28f016ce48
128 changed files with 3997 additions and 1207 deletions
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
1
images/icons/v2/color-outline-24.svg
Normal file
1
images/icons/v2/color-outline-24.svg
Normal 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 |
1
images/icons/v2/color-solid-24.svg
Normal file
1
images/icons/v2/color-solid-24.svg
Normal 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 |
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
58
stylesheets/components/ChatColorPicker.scss
Normal file
58
stylesheets/components/ChatColorPicker.scss
Normal 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;
|
||||
}
|
||||
}
|
256
stylesheets/components/ContactName.scss
Normal file
256
stylesheets/components/ContactName.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
58
stylesheets/components/CustomColorEditor.scss
Normal file
58
stylesheets/components/CustomColorEditor.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
52
stylesheets/components/GradientDial.scss
Normal file
52
stylesheets/components/GradientDial.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
border: 1px solid $ultramarine-ui-light;
|
||||
border: 1px solid $color-ultramarine;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
&:focus-within {
|
||||
border: solid 1px $ultramarine-ui-light;
|
||||
border: solid 1px $color-ultramarine;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
|
22
stylesheets/components/Slider.scss
Normal file
22
stylesheets/components/Slider.scss
Normal 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;
|
||||
}
|
||||
}
|
26
stylesheets/components/Tabs.scss
Normal file
26
stylesheets/components/Tabs.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 || ''),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 = ({
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
68
ts/components/ChatColorPicker.stories.tsx
Normal file
68
ts/components/ChatColorPicker.stories.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
||||
));
|
386
ts/components/ChatColorPicker.tsx
Normal file
386
ts/components/ChatColorPicker.tsx
Normal 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;
|
||||
};
|
|
@ -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,
|
||||
|
|
23
ts/components/CustomColorEditor.stories.tsx
Normal file
23
ts/components/CustomColorEditor.stories.tsx
Normal 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()} />);
|
182
ts/components/CustomColorEditor.tsx
Normal file
182
ts/components/CustomColorEditor.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
43
ts/components/GlobalModalContainer.tsx
Normal file
43
ts/components/GlobalModalContainer.tsx
Normal 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;
|
||||
};
|
309
ts/components/GradientDial.tsx
Normal file
309
ts/components/GradientDial.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(''),
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
112
ts/components/SampleMessageBubbles.tsx
Normal file
112
ts/components/SampleMessageBubbles.tsx
Normal 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' }} />
|
||||
</>
|
||||
);
|
||||
};
|
29
ts/components/Slider.stories.tsx
Normal file
29
ts/components/Slider.stories.tsx
Normal 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
126
ts/components/Slider.tsx
Normal 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
68
ts/components/Tabs.tsx
Normal 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 })}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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} />;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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 },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
));
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
));
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -86,7 +86,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
|||
timestamp: Date.now(),
|
||||
author: {
|
||||
phoneNumber: '(202) 555-2001',
|
||||
color: 'green',
|
||||
color: 'forest',
|
||||
},
|
||||
text: '🔥',
|
||||
},
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -48,7 +48,7 @@ export function renderAvatar({
|
|||
acceptedMessageRequest={false}
|
||||
avatarPath={avatarPath}
|
||||
blur={AvatarBlur.NoBlur}
|
||||
color="grey"
|
||||
color="steel"
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
5
ts/model-types.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
9
ts/shims/getUserTheme.ts
Normal 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());
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue