Keyboard/mouse mode and keyboard support bugfixes

This commit is contained in:
Scott Nonnenberg 2019-11-21 11:16:06 -08:00 committed by Ken Powers
parent ed55006f20
commit 2a0a73cfc1
25 changed files with 686 additions and 274 deletions

View File

@ -109,6 +109,61 @@
activeHandlers = activeHandlers.filter(item => item !== handler);
};
// Keyboard/mouse mode
let interactionMode = 'mouse';
$(document.body).addClass('mouse-mode');
window.enterKeyboardMode = () => {
if (interactionMode === 'keyboard') {
return;
}
interactionMode = 'keyboard';
$(document.body)
.addClass('keyboard-mode')
.removeClass('mouse-mode');
const { userChanged } = window.reduxActions.user;
const { clearSelectedMessage } = window.reduxActions.conversations;
if (clearSelectedMessage) {
clearSelectedMessage();
}
if (userChanged) {
userChanged({ interactionMode });
}
};
window.enterMouseMode = () => {
if (interactionMode === 'mouse') {
return;
}
interactionMode = 'mouse';
$(document.body)
.addClass('mouse-mode')
.removeClass('keyboard-mode');
const { userChanged } = window.reduxActions.user;
const { clearSelectedMessage } = window.reduxActions.conversations;
if (clearSelectedMessage) {
clearSelectedMessage();
}
if (userChanged) {
userChanged({ interactionMode });
}
};
document.addEventListener(
'keydown',
event => {
if (event.key === 'Tab') {
window.enterKeyboardMode();
}
},
true
);
document.addEventListener('wheel', window.enterMouseMode, true);
document.addEventListener('mousedown', window.enterMouseMode, true);
window.getInteractionMode = () => interactionMode;
// Load these images now to ensure that they don't flicker on first use
window.preloadedImages = [];
function preload(list) {
@ -299,10 +354,11 @@
// Stop processing incoming messages
if (messageReceiver) {
await messageReceiver.stopProcessing();
await window.waitForAllBatchers();
messageReceiver.unregisterBatchers();
}
if (messageReceiver) {
messageReceiver.unregisterBatchers();
messageReceiver = null;
}
@ -522,6 +578,7 @@
ourNumber: textsecure.storage.user.getNumber(),
platform: window.platform,
i18n: window.i18n,
interactionMode: window.getInteractionMode(),
},
};
@ -668,6 +725,7 @@
// Navigate by section
if (ctrlOrCommand && !shiftKey && (key === 't' || key === 'T')) {
window.enterKeyboardMode();
const focusedElement = document.activeElement;
const targets = [

View File

@ -80,7 +80,6 @@
initialize(options = {}) {
this.ready = false;
this.render();
this.$el.attr('tabindex', '1');
this.conversation_stack = new Whisper.ConversationStack({
el: this.$('.conversation-stack'),

View File

@ -408,6 +408,13 @@
padding: 5px 8px;
border-radius: 5px;
outline: none;
@include keyboard-mode {
&:focus {
outline: -webkit-focus-ring-color auto 5px;
}
}
@include light-theme {
background-color: $color-gray-02;
border: 1px solid $color-gray-15;

View File

@ -162,6 +162,13 @@ a {
border: none;
background: transparent;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
&:before {
content: '';
display: inline-block;
@ -175,11 +182,6 @@ a {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-25);
}
}
&:focus,
&:hover {
opacity: 1;
}
}
input[type='file'] {

View File

@ -128,6 +128,36 @@
}
}
// Keyboard
@mixin keyboard-mode() {
.keyboard-mode & {
@content;
}
}
@mixin mouse-mode() {
.mouse-mode & {
@content;
}
}
@mixin dark-keyboard-mode() {
.dark-theme.keyboard-mode & {
@content;
}
}
@mixin ios-keyboard-mode() {
.ios-theme.keyboard-mode & {
@content;
}
}
@mixin dark-ios-keyboard-mode() {
.dark-theme.ios-theme.keyboard-mode & {
@content;
}
}
// Other
@mixin popper-shadow() {

View File

@ -252,17 +252,53 @@
}
}
// This is the component we put the outline around when the whole message is focused
// This is the component we put the outline around when the whole message is selected
.module-message--selected .module-message__container {
@include mouse-mode {
animation: message--mouse-selected 1s cubic-bezier(0.19, 1, 0.22, 1);
}
}
.module-message:focus .module-message__container {
box-shadow: 0 0 0 5px $color-signal-blue;
@include keyboard-mode {
box-shadow: 0 0 0 3px $color-signal-blue;
}
}
@keyframes message--mouse-selected {
0% {
box-shadow: 0 0 0 5px transparent;
}
10% {
box-shadow: 0 0 0 5px $color-signal-blue;
}
70% {
box-shadow: 0 0 0 5px $color-signal-blue;
}
100% {
box-shadow: 0 0 0 5px transparent;
}
}
// We disable this highlight for messages with stickers, instead highlighting the sticker
.module-message--selected .module-message__container--with-sticker {
@include mouse-mode {
animation: none;
}
}
.module-message:focus .module-message__container--with-sticker {
box-shadow: none;
@include keyboard-mode {
box-shadow: none;
}
}
.module-message__container--with-sticker {
@include light-theme {
border: none;
}
@include dark-theme {
border: none;
}
padding-bottom: 0px;
}
@ -603,9 +639,10 @@
flex-direction: row;
align-items: center;
&:hover,
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
@ -762,9 +799,10 @@
border-top-right-radius: 16px;
overflow: hidden;
&:focus,
&:hover {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
@ -934,33 +972,35 @@
a {
text-decoration: underline;
outline: none;
@include light-theme {
color: $color-gray-90;
&:focus,
&:hover {
outline: 2px solid $color-gray-90;
}
@include keyboard-mode {
&:focus {
outline: 1px solid $color-gray-90;
}
}
@include dark-theme {
color: $color-gray-05;
&:focus,
&:hover {
outline: 2px solid $color-gray-05;
}
@include dark-keyboard-mode {
&:focus {
outline: 1px solid $color-gray-05;
}
}
@include ios-theme {
color: $color-white-alpha-90;
&:focus,
&:hover {
outline: 2px solid $color-white-alpha-90;
}
}
@include ios-dark-theme {
color: $color-white-alpha-90;
&:focus,
&:hover {
outline: 2px solid $color-white-alpha-90;
}
@include ios-keyboard-mode {
&:focus {
outline: 1px solid $color-white-alpha-90;
}
}
}
@ -982,33 +1022,41 @@
a {
text-decoration: underline;
outline: none;
@include light-theme {
color: $color-white;
&:focus,
&:hover {
outline: 2px solid $color-white;
}
}
@include ios-theme {
color: $color-gray-90;
&:focus,
&:hover {
outline: 2px solid $color-gray-90;
}
@include keyboard-mode {
&:focus {
outline: 1px solid $color-white;
}
}
@include dark-theme {
color: $color-white-alpha-90;
&:focus,
&:hover {
outline: 2px solid $color-white-alpha-90;
}
@include dark-keyboard-mode {
&:focus {
outline: 1px solid $color-white-alpha-90;
}
}
@include ios-theme {
color: $color-gray-90;
}
@include ios-keyboard-mode {
&:focus {
outline: 1px solid $color-gray-90;
}
}
@include ios-dark-theme {
color: $color-gray-05;
&:focus,
&:hover {
outline: 2px solid $color-gray-05;
}
@include dark-ios-keyboard-mode {
&:focus {
outline: 1px solid $color-gray-05;
}
}
}
@ -1235,9 +1283,10 @@
border: 1px solid $color-gray-45;
}
&:focus,
&:hover {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
@ -1375,9 +1424,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
border-left-width: 4px;
border-left-style: solid;
&:focus,
&:hover {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
@ -1404,6 +1454,13 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
background-color: $color-conversation-grey-shade;
}
// To preserve contrast
@include ios-keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-white;
}
}
// Note: both of these override all of the specific color classes below
@include ios-theme {
border-left-color: $color-white;
@ -1592,6 +1649,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
border-radius: 50%;
background-color: $color-black-alpha-40;
@include keyboard-mode {
&:focus-within {
background-color: $color-signal-blue;
}
}
}
.module-quote__close-button {
@ -1799,9 +1862,18 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
flex-direction: row;
align-items: center;
&:focus,
&:hover {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
.module-embedded-contact--outgoing {
@include ios-keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-white;
}
}
}
@ -1977,13 +2049,14 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
outline: none;
padding: 5px;
&:focus,
&:hover {
@include light-theme {
background-color: $color-gray-02;
}
@include dark-theme {
background-color: $color-gray-80;
@include keyboard-mode {
&:focus {
@include light-theme {
background-color: $color-gray-02;
}
@include dark-theme {
background-color: $color-gray-80;
}
}
}
}
@ -2081,16 +2154,17 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
@include light-theme {
background-color: $color-gray-02;
&:hover,
}
@include keyboard-mode {
&:focus {
background-color: $color-gray-15;
}
}
@include dark-theme {
background-color: $color-gray-75;
&:hover,
}
@include dark-keyboard-mode {
&:focus {
background-color: $color-gray-60;
}
@ -2526,6 +2600,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
.module-message-detail__delete-button {
@include button-reset;
@include keyboard-mode {
&:focus {
outline: -webkit-focus-ring-color auto 5px;
}
}
border-radius: 5px;
margin: 1em auto;
padding: 1em;
@ -2708,6 +2788,19 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
@include dark-theme {
background-color: $color-gray-90;
}
outline: none;
@include keyboard-mode {
&:focus {
@include light-theme {
background-color: $color-gray-15;
}
@include dark-theme {
background-color: $color-gray-75;
}
}
}
}
.module-media-gallery__tab--active {
@ -2763,12 +2856,19 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
.module-document-list-item__content {
@include button-reset;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
height: 100%;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
.module-document-list-item__icon {
@ -2810,6 +2910,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
margin-right: 4px;
margin-bottom: 4px;
position: relative;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
.module-media-grid-item__image {
@ -3156,9 +3262,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
line-height: 0;
border-radius: 50%;
&:focus,
&:hover {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
}
@ -3440,6 +3547,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
// Overriding some default button styling
border: none;
padding: 0;
outline: none;
@include light-theme {
background-color: $color-gray-15;
@ -3574,21 +3682,38 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
}
// Only if it's a sticker do we put the outline inside it
.module-message--selected
.module-message__container--with-sticker
.module-image__border-overlay {
@include mouse-mode {
top: 1px;
bottom: 1px;
left: 1px;
right: 1px;
border-radius: 10px;
animation: message--mouse-selected 1s cubic-bezier(0.19, 1, 0.22, 1);
}
}
.module-message:focus
.module-message__container--with-sticker
.module-image__border-overlay {
top: 1px;
bottom: 1px;
left: 1px;
right: 1px;
border-radius: 10px;
@include keyboard-mode {
top: 1px;
bottom: 1px;
left: 1px;
right: 1px;
border-radius: 10px;
box-shadow: 0 0 0 5px $color-signal-blue;
box-shadow: 0 0 0 3px $color-signal-blue;
}
}
button.module-image__border-overlay:focus,
button.module-image__border-overlay:hover {
box-shadow: inset 0px 0px 0px 2px $color-signal-blue;
button.module-image__border-overlay:focus {
@include keyboard-mode {
box-shadow: inset 0px 0px 0px 2px $color-signal-blue;
}
}
.module-image__border-overlay--dark {
@ -3697,9 +3822,10 @@ button.module-image__border-overlay:hover {
background-image: url('../images/x-shadow-16.svg');
&:focus,
&:hover {
outline: 2px solid $color-signal-blue;
@include keyboard-mode {
&:focus {
outline: 2px solid $color-signal-blue;
}
}
}
@ -3854,8 +3980,10 @@ button.module-image__border-overlay:hover {
z-index: 2;
@include color-svg('../images/icons/v2/x-24.svg', $color-black);
&:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
@include keyboard-mode {
&:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
}
}
@ -4103,18 +4231,21 @@ button.module-image__border-overlay:hover {
&:hover {
background: $color-gray-05;
}
&:hover,
}
@include keyboard-mode {
&:focus {
box-shadow: inset 0 0 0 2px $color-signal-blue;
}
}
@include dark-theme {
border: 1px solid $color-gray-60;
&:hover {
background: $color-gray-75;
}
&:hover,
}
@include dark-keyboard-mode {
&:focus {
box-shadow: inset 0 0 0 2px $color-signal-blue;
}
@ -4212,12 +4343,17 @@ button.module-image__border-overlay:hover {
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
}
@include keyboard-mode {
&:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
}
@include dark-keyboard-mode {
&:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
@ -4360,6 +4496,7 @@ button.module-image__border-overlay:hover {
// Module: Search Results
.module-search-results {
outline: none;
overflow: hidden;
flex-grow: 1;
}
@ -4381,6 +4518,7 @@ button.module-image__border-overlay:hover {
padding-right: 1em;
width: 100%;
text-align: center;
outline: none;
}
.module-search-results__contacts-header {
@ -4575,20 +4713,23 @@ button.module-image__border-overlay:hover {
'../images/icons/v2/chevron-left-24.svg',
$color-gray-60
);
&:focus,
&:hover {
}
@include keyboard-mode {
&:focus {
@include color-svg(
'../images/icons/v2/chevron-left-24.svg',
$color-signal-blue
);
}
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/chevron-left-24.svg',
$color-gray-25
);
&:focus,
}
@include dark-keyboard-mode {
&:hover {
@include color-svg(
'../images/icons/v2/chevron-left-24.svg',
@ -4839,13 +4980,14 @@ button.module-image__border-overlay:hover {
background: none;
margin-right: 4px;
outline: none;
&:active,
&:focus {
outline: none;
@include light-theme {
@include keyboard-mode {
background: $color-gray-05;
}
@include dark-theme {
@include dark-keyboard-mode {
background: $color-gray-60;
}
}
@ -5040,6 +5182,10 @@ button.module-image__border-overlay:hover {
justify-content: center;
align-items: center;
@include mouse-mode {
outline: none;
}
&__image,
&__placeholder {
width: 100%;
@ -5182,6 +5328,12 @@ button.module-image__border-overlay:hover {
}
}
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
&__cover {
width: 48px;
height: 48px;
@ -5280,6 +5432,10 @@ button.module-image__border-overlay:hover {
background: $color-gray-75;
}
@include mouse-mode {
outline: none;
}
&--blue {
@include light-theme {
background: $color-signal-blue;
@ -5503,10 +5659,13 @@ button.module-image__border-overlay:hover {
align-items: center;
opacity: 0.5;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
&::after {
display: block;
content: '';
@ -5716,8 +5875,8 @@ button.module-image__border-overlay:hover {
&__button {
margin-left: 4px;
border-radius: 14px;
height: 28px;
border-radius: 17px;
height: 34px;
padding: 5px 12px;
display: flex;
justify-content: center;
@ -5725,6 +5884,10 @@ button.module-image__border-overlay:hover {
@include font-body-1-bold;
@include mouse-mode {
outline: none;
}
@include light-theme() {
background: $color-white;
color: $color-gray-60;
@ -5884,6 +6047,10 @@ button.module-image__border-overlay:hover {
align-items: center;
background: none;
@include mouse-mode {
outline: none;
}
&--footer {
&:not(:first-of-type) {
margin-left: 4px;
@ -6047,10 +6214,13 @@ button.module-image__border-overlay:hover {
align-items: center;
opacity: 0.5;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
&::after {
display: block;
content: '';
@ -6795,13 +6965,14 @@ button.module-image__border-overlay:hover {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
}
&:focus,
&:hover {
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
@include keyboard-mode {
&:focus {
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
}
}
}
@ -6841,12 +7012,14 @@ button.module-image__border-overlay:hover {
min-height: 40px;
outline: none;
&:focus {
@include light-theme {
background-color: $color-gray-05;
}
@include dark-theme {
background-color: $color-gray-90;
@include keyboard-mode {
&:focus {
@include light-theme {
background-color: $color-gray-05;
}
@include dark-theme {
background-color: $color-gray-90;
}
}
}

View File

@ -15,6 +15,8 @@
opacity: 1;
}
outline: none;
&:before {
content: '';
display: inline-block;
@ -43,7 +45,7 @@
height: 32px;
border-radius: 32px;
margin-left: 5px;
opacity: 0.5;
opacity: 0.3;
text-align: center;
padding: 0;
@ -52,6 +54,8 @@
opacity: 1;
}
outline: none;
.icon {
display: inline-block;
width: 24px;

View File

@ -213,6 +213,20 @@ export class SearchResults extends React.Component<PropsType, StateType> {
};
public setFocusToFirst = () => {
const { current: container } = this.containerRef;
if (container) {
// tslint:disable-next-line no-unnecessary-type-assertion
const noResultsItem = container.querySelector(
'.module-search-results__no-results'
) as any;
if (noResultsItem && noResultsItem.focus) {
noResultsItem.focus();
return;
}
}
const scrollContainer = this.getScrollContainer();
if (!scrollContainer) {
return;
@ -513,9 +527,19 @@ export class SearchResults extends React.Component<PropsType, StateType> {
if (noResults) {
return (
<div className="module-search-results">
<div
className="module-search-results"
tabIndex={-1}
ref={this.containerRef}
onFocus={this.handleFocus}
>
{!searchConversationName || searchTerm ? (
<div className="module-search-results__no-results" key={searchTerm}>
<div
// We need this for Ctrl-T shortcut cycling through parts of app
tabIndex={-1}
className="module-search-results__no-results"
key={searchTerm}
>
{searchConversationName ? (
<Intl
id="noSearchResultsInConversation"

View File

@ -22,7 +22,7 @@ const contact = {
onSendMessage: () => console.log('onSendMessage'),
signalAccount: '+12025550000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -92,7 +92,7 @@ const contact = {
onSendMessage: () => console.log('onSendMessage'),
signalAccount: '+12025550000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -137,7 +137,7 @@ const contact = {
},
},
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -182,7 +182,7 @@ const contact = {
},
signalAccount: '+12025550000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -244,7 +244,7 @@ const contact = {
},
},
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -309,7 +309,7 @@ const contact = {
},
},
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -377,7 +377,7 @@ const contact = {
},
signalAccount: '+12025551000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -439,7 +439,7 @@ const contact = {
},
],
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -491,7 +491,7 @@ const contact = {
```jsx
const contact = {};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -575,7 +575,7 @@ const contactWithoutAccount = {
},
},
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
text="I want to introduce you to Someone..."

View File

@ -38,6 +38,7 @@ export class EmbeddedContact extends React.Component<Props> {
<button
className={classNames(
'module-embedded-contact',
`module-embedded-contact--${direction}`,
withContentAbove
? 'module-embedded-contact--with-content-above'
: null,

View File

@ -4,7 +4,7 @@ export type PropsType = {
id: string;
conversationId: string;
isSelected: boolean;
selectMessage: (messageId: string, conversationId: string) => unknown;
selectMessage?: (messageId: string, conversationId: string) => unknown;
};
export class InlineNotificationWrapper extends React.Component<PropsType> {
@ -18,10 +18,19 @@ export class InlineNotificationWrapper extends React.Component<PropsType> {
}
};
public handleFocus = () => {
// @ts-ignore
if (window.getInteractionMode() === 'keyboard') {
this.setSelected();
}
};
public setSelected = () => {
const { id, conversationId, selectMessage } = this.props;
selectMessage(id, conversationId);
if (selectMessage) {
selectMessage(id, conversationId);
}
};
public componentDidMount() {
@ -45,7 +54,7 @@ export class InlineNotificationWrapper extends React.Component<PropsType> {
className="module-inline-notification-wrapper"
tabIndex={0}
ref={this.focusRef}
onFocus={this.setSelected}
onFocus={this.handleFocus}
>
{children}
</div>

View File

@ -3,7 +3,7 @@
Note that timestamp and status can be hidden with the `collapseMetadata` boolean property.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -148,7 +148,7 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
### Status
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="outgoing"
@ -323,7 +323,7 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
### All colors
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -450,7 +450,7 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
### Long data
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="purple"
@ -515,7 +515,7 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
### Pending long message download
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="purple"
@ -553,7 +553,7 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
#### Image with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="blue"
@ -645,7 +645,7 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
First, showing the metadata overlay on dark and light images, then a message with `collapseMetadata` set.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -765,7 +765,7 @@ First, showing the metadata overlay on dark and light images, then a message wit
Stickers have no background, but they have all the standard message bubble features.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -912,7 +912,7 @@ Stickers have no background, but they have all the standard message bubble featu
First set is in a 1:1 conversation, second set is in a group.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1002,7 +1002,7 @@ First set is in a 1:1 conversation, second set is in a group.
A sticker with no attachments (what our selectors produce for a pending sticker) is not displayed at all.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1062,7 +1062,7 @@ A sticker with no attachments (what our selectors produce for a pending sticker)
#### Multiple images
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1244,7 +1244,7 @@ A sticker with no attachments (what our selectors produce for a pending sticker)
#### Multiple images with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1433,7 +1433,7 @@ A sticker with no attachments (what our selectors produce for a pending sticker)
Note that the delivered indicator is always Signal Blue, not the conversation color.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="outgoing"
@ -1512,7 +1512,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Pending images
```
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1617,7 +1617,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Image with portrait aspect ratio
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="purple"
@ -1695,7 +1695,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Image with portrait aspect ratio and caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1819,7 +1819,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Image with landscape aspect ratio
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1897,7 +1897,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Image with landscape aspect ratio and caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -1979,7 +1979,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Video with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2073,7 +2073,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Video
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2170,7 +2170,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Missing images and videos
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2349,7 +2349,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Broken source URL images and videos
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2441,7 +2441,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Image/video which is too big
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2532,7 +2532,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Image/video missing height/width
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2619,7 +2619,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Audio with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2693,7 +2693,7 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
#### Audio
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2767,7 +2767,7 @@ Voice notes are not shown any differently from audio attachments.
#### Other file type with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2906,7 +2906,7 @@ Voice notes are not shown any differently from audio attachments.
#### Other file type
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -2984,7 +2984,7 @@ Voice notes are not shown any differently from audio attachments.
#### Other file type pending
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3062,7 +3062,7 @@ Voice notes are not shown any differently from audio attachments.
#### Dangerous file type
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3108,7 +3108,7 @@ Voice notes are not shown any differently from audio attachments.
#### Link previews, full-size image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3219,7 +3219,7 @@ Voice notes are not shown any differently from audio attachments.
Sticker link previews are forced to use the small link preview form, no matter the image size.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3273,7 +3273,7 @@ Sticker link previews are forced to use the small link preview form, no matter t
#### Link previews, small image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3384,7 +3384,7 @@ Sticker link previews are forced to use the small link preview form, no matter t
#### Link previews with pending image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3481,7 +3481,7 @@ Sticker link previews are forced to use the small link preview form, no matter t
#### Link previews, no image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
authorColor="green"
@ -3568,7 +3568,7 @@ Sticker link previews are forced to use the small link preview form, no matter t
### Tap to view
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -4004,7 +4004,7 @@ Sticker link previews are forced to use the small link preview form, no matter t
Note that the author avatar goes away if `collapseMetadata` is set.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"

View File

@ -40,6 +40,7 @@ interface Trigger {
// Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
const STICKER_SIZE = 128;
const SELECTED_TIMEOUT = 1000;
interface LinkPreviewType {
title: string;
@ -56,6 +57,8 @@ export type PropsData = {
textPending?: boolean;
isSticker: boolean;
isSelected: boolean;
isSelectedCounter: number;
interactionMode: 'mouse' | 'keyboard';
direction: 'incoming' | 'outgoing';
timestamp: number;
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
@ -130,7 +133,7 @@ export type PropsActions = {
sentAt: number;
}
) => void;
selectMessage: (messageId: string, conversationId: string) => unknown;
selectMessage?: (messageId: string, conversationId: string) => unknown;
};
export type Props = PropsData & PropsHousekeeping & PropsActions;
@ -139,6 +142,9 @@ interface State {
expiring: boolean;
expired: boolean;
imageBroken: boolean;
isSelected: boolean;
prevSelectedCounter: number;
}
const EXPIRATION_CHECK_MINIMUM = 2000;
@ -149,16 +155,46 @@ export class Message extends React.PureComponent<Props, State> {
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();
public state = {
expiring: false,
expired: false,
imageBroken: false,
};
public expirationCheckInterval: any;
public expiredTimeout: any;
public selectedTimeout: any;
public constructor(props: Props) {
super(props);
this.state = {
expiring: false,
expired: false,
imageBroken: false,
isSelected: props.isSelected,
prevSelectedCounter: props.isSelectedCounter,
};
}
public static getDerivedStateFromProps(props: Props, state: State): State {
if (!props.isSelected) {
return {
...state,
isSelected: false,
prevSelectedCounter: 0,
};
}
if (
props.isSelected &&
props.isSelectedCounter !== state.prevSelectedCounter
) {
return {
...state,
isSelected: props.isSelected,
prevSelectedCounter: props.isSelectedCounter,
};
}
return state;
}
public captureMenuTrigger = (triggerRef: Trigger) => {
this.menuTriggerRef = triggerRef;
};
@ -180,10 +216,20 @@ export class Message extends React.PureComponent<Props, State> {
});
};
public handleFocus = () => {
const { interactionMode } = this.props;
if (interactionMode === 'keyboard') {
this.setSelected();
}
};
public setSelected = () => {
const { id, conversationId, selectMessage } = this.props;
selectMessage(id, conversationId);
if (selectMessage) {
selectMessage(id, conversationId);
}
};
public setFocus = () => {
@ -195,6 +241,8 @@ export class Message extends React.PureComponent<Props, State> {
};
public componentDidMount() {
this.startSelectedTimer();
const { isSelected } = this.props;
if (isSelected) {
this.setFocus();
@ -228,6 +276,8 @@ export class Message extends React.PureComponent<Props, State> {
}
public componentDidUpdate(prevProps: Props) {
this.startSelectedTimer();
if (!prevProps.isSelected && this.props.isSelected) {
this.setFocus();
}
@ -235,6 +285,23 @@ export class Message extends React.PureComponent<Props, State> {
this.checkExpired();
}
public startSelectedTimer() {
const { interactionMode } = this.props;
const { isSelected } = this.state;
if (interactionMode === 'keyboard' || !isSelected) {
return;
}
if (!this.selectedTimeout) {
this.selectedTimeout = setTimeout(() => {
this.selectedTimeout = undefined;
this.setState({ isSelected: false });
this.props.clearSelectedMessage();
}, SELECTED_TIMEOUT);
}
}
public checkExpired() {
const now = Date.now();
const { isExpired, expirationTimestamp, expirationLength } = this.props;
@ -598,6 +665,7 @@ export class Message extends React.PureComponent<Props, State> {
<button
className={classNames(
'module-message__link-preview',
`module-message__link-preview--${direction}`,
withContentAbove
? 'module-message__link-preview--with-content-above'
: null
@ -1389,12 +1457,12 @@ export class Message extends React.PureComponent<Props, State> {
const {
authorColor,
direction,
isSelected,
isSticker,
isTapToView,
isTapToViewExpired,
isTapToViewError,
} = this.props;
const { isSelected } = this.state;
const isAttachmentPending = this.isAttachmentPending();
@ -1447,7 +1515,7 @@ export class Message extends React.PureComponent<Props, State> {
isSticker,
timestamp,
} = this.props;
const { expired, expiring, imageBroken } = this.state;
const { expired, expiring, imageBroken, isSelected } = this.state;
// This id is what connects our triple-dot click with our associated pop-up menu.
// It needs to be unique.
@ -1466,6 +1534,7 @@ export class Message extends React.PureComponent<Props, State> {
className={classNames(
'module-message',
`module-message--${direction}`,
isSelected ? 'module-message--selected' : null,
expiring ? 'module-message--expired' : null,
conversationType === 'group' ? 'module-message--group' : null
)}
@ -1475,7 +1544,7 @@ export class Message extends React.PureComponent<Props, State> {
role="button"
onKeyDown={this.handleKeyDown}
onClick={this.handleClick}
onFocus={this.setSelected}
onFocus={this.handleFocus}
ref={this.focusRef}
>
{this.renderError(direction === 'incoming')}

View File

@ -35,12 +35,7 @@ interface Props {
}
export class MessageDetail extends React.Component<Props> {
private readonly focusRef: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
super(props);
this.focusRef = React.createRef();
}
private readonly focusRef = React.createRef<HTMLDivElement>();
public componentDidMount() {
// When this component is created, it's initially not part of the DOM, and then it's

View File

@ -3,7 +3,7 @@
#### Plain text
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -41,7 +41,7 @@
#### Name variations
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -112,7 +112,7 @@
#### With emoji
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -148,7 +148,7 @@
#### Replies to you or yourself
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -186,7 +186,12 @@
#### In a group conversation
```jsx
<util.ConversationContext theme={util.theme} type="group" ios={util.ios}>
<util.ConversationContext
theme={util.theme}
type="group"
ios={util.ios}
mode={util.mode}
>
<div className="module-message-container">
<Message
direction="incoming"
@ -231,7 +236,7 @@ Note: for incoming messages, quote color is taken from the parent message. For o
messages the color is taken from the contact who wrote the quoted message.
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -610,7 +615,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Referenced message not found
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -687,7 +692,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Long names and context
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -729,7 +734,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### A lot of text in quotation
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -773,7 +778,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### A lot of text in quotation, with icon
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -825,7 +830,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### A lot of text in quotation, with image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -885,7 +890,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Image with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -937,7 +942,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -987,7 +992,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Image with no thumbnail
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1030,7 +1035,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Pending image download
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1073,7 +1078,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Video with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1125,7 +1130,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Video
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1175,7 +1180,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Video with no thumbnail
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1223,7 +1228,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Audio with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1267,7 +1272,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Audio
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1309,7 +1314,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Voice message
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1355,7 +1360,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Other file type with caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1438,7 +1443,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Other file type
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1482,7 +1487,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, image attachment, and caption
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1532,7 +1537,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, image attachment
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1580,7 +1585,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, portrait image attachment
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1628,7 +1633,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, video attachment
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1686,7 +1691,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, audio attachment
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1730,7 +1735,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, file attachment
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
@ -1778,7 +1783,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Plain text
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"
@ -1796,7 +1801,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With an icon
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"
@ -1818,7 +1823,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With an image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"
@ -1843,7 +1848,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With attachment and no text
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
authorColor="blue"
@ -1867,7 +1872,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With generic attachment
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"
@ -1889,7 +1894,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With a close button
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"
@ -1908,7 +1913,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With a close button and icon
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"
@ -1931,7 +1936,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With a close button and image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar">
<Quote
text="How many ferrets do you have?"

View File

@ -99,20 +99,13 @@ export class Quote extends React.Component<Props, State> {
// This is important to ensure that using this quote to navigate to the referenced
// message doesn't also trigger its parent message's keydown.
if (onClick && (event.key === 'Enter' || event.key === 'Space')) {
if (onClick && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
event.stopPropagation();
onClick();
}
};
// We prevent this from bubbling to prevent the focus flash around a message when
// you click a quote.
public handleMouseDown = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
};
public handleImageError = () => {
// tslint:disable-next-line no-console
console.log('Message: Image failed to load; failing over to placeholder');
@ -271,14 +264,20 @@ export class Quote extends React.Component<Props, State> {
return null;
}
// We don't want the overall click handler for the quote to fire, so we stop
// propagation before handing control to the caller's callback.
const onClick = (e: React.MouseEvent<{}>): void => {
const clickHandler = (e: React.MouseEvent): void => {
e.stopPropagation();
e.preventDefault();
onClose();
};
const keyDownHandler = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
e.preventDefault();
onClose();
}
};
// We need the container to give us the flexibility to implement the iOS design.
return (
@ -288,7 +287,8 @@ export class Quote extends React.Component<Props, State> {
// We can't be a button because the overall quote is a button; can't nest them
role="button"
className="module-quote__close-button"
onClick={onClick}
onKeyDown={keyDownHandler}
onClick={clickHandler}
/>
</div>
);
@ -383,7 +383,6 @@ export class Quote extends React.Component<Props, State> {
<button
onClick={onClick}
onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown}
className={classNames(
'module-quote',
isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',

View File

@ -55,6 +55,7 @@ const Tab = ({
)}
onClick={handleClick}
role="tab"
tabIndex={0}
>
{label}
</div>
@ -81,7 +82,7 @@ export class MediaGallery extends React.Component<Props, State> {
const { selectedTab } = this.state;
return (
<div className="module-media-gallery" tabIndex={0} ref={this.focusRef}>
<div className="module-media-gallery" tabIndex={-1} ref={this.focusRef}>
<div className="module-media-gallery__tab-container">
<Tab
label="Media"

View File

@ -18,12 +18,6 @@ export type OwnProps = {
export type Props = OwnProps;
function focusOnRender(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
export const StickerManager = React.memo(
// tslint:disable-next-line max-func-body-length
({
@ -36,6 +30,7 @@ export const StickerManager = React.memo(
uninstallStickerPack,
i18n,
}: Props) => {
const focusRef = React.createRef<HTMLDivElement>();
const [
packToPreview,
setPackToPreview,
@ -48,6 +43,14 @@ export const StickerManager = React.memo(
knownPacks.forEach(pack => {
downloadStickerPack(pack.id, pack.key);
});
// When this component is created, it's initially not part of the DOM, and then it's
// added off-screen and animated in. This ensures that the focus takes.
setTimeout(() => {
if (focusRef.current) {
focusRef.current.focus();
}
});
}, []);
const clearPackToPreview = React.useCallback(
@ -76,11 +79,7 @@ export const StickerManager = React.memo(
uninstallStickerPack={uninstallStickerPack}
/>
) : null}
<div
className="module-sticker-manager"
tabIndex={-1}
ref={focusOnRender}
>
<div className="module-sticker-manager" tabIndex={-1} ref={focusRef}>
{[
{
i18nKey: 'stickers--StickerManager--InstalledPacks',

View File

@ -81,6 +81,10 @@ export const StickerPreviewModal = React.memo(
// Restore focus on teardown
React.useEffect(
() => {
if (!root) {
return;
}
const lastFocused = document.activeElement as any;
if (focusRef.current) {
focusRef.current.focus();

View File

@ -1,4 +1,3 @@
import { AnyAction } from 'redux';
import { LocalizerType } from '../../types/Util';
// State
@ -11,6 +10,7 @@ export type UserStateType = {
platform: string;
regionCode: string;
i18n: LocalizerType;
interactionMode: 'mouse' | 'keyboard';
};
// Actions
@ -18,12 +18,13 @@ export type UserStateType = {
type UserChangedActionType = {
type: 'USER_CHANGED';
payload: {
ourNumber: string;
regionCode: string;
ourNumber?: string;
regionCode?: string;
interactionMode?: 'mouse' | 'keyboard';
};
};
export type UserActionType = AnyAction | UserChangedActionType;
export type UserActionType = UserChangedActionType;
// Action Creators
@ -51,6 +52,7 @@ function getEmptyState(): UserStateType {
ourNumber: 'missing',
regionCode: 'missing',
platform: 'missing',
interactionMode: 'mouse',
i18n: () => 'missing',
};
}

View File

@ -18,7 +18,12 @@ import { getBubbleProps } from '../../shims/Whisper';
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
import { TimelineItemType } from '../../components/conversation/TimelineItem';
import { getIntl, getRegionCode, getUserNumber } from './user';
import {
getInteractionMode,
getIntl,
getRegionCode,
getUserNumber,
} from './user';
export const getConversations = (state: StateType): ConversationsStateType =>
state.conversations;
@ -245,6 +250,7 @@ export function _messageSelector(
ourNumber: string,
// @ts-ignore
regionCode: string,
interactionMode: 'mouse' | 'keyboard',
// @ts-ignore
conversation?: ConversationType,
// @ts-ignore
@ -263,13 +269,20 @@ export function _messageSelector(
...props,
data: {
...props.data,
interactionMode,
isSelected: true,
isSelectedCounter: selectedMessageCounter,
},
};
}
return props;
return {
...props,
data: {
...props.data,
interactionMode,
},
};
}
// A little optimization to reset our selector cache whenever high-level application data
@ -278,6 +291,7 @@ type CachedMessageSelectorType = (
message: MessageType,
ourNumber: string,
regionCode: string,
interactionMode: 'mouse' | 'keyboard',
conversation?: ConversationType,
author?: ConversationType,
quoted?: ConversationType,
@ -302,13 +316,15 @@ export const getMessageSelector = createSelector(
getConversationSelector,
getRegionCode,
getUserNumber,
getInteractionMode,
(
messageSelector: CachedMessageSelectorType,
messageLookup: MessageLookupType,
selectedMessage: SelectedMessageType | undefined,
conversationSelector: GetConversationByIdType,
regionCode: string,
ourNumber: string
ourNumber: string,
interactionMode: 'keyboard' | 'mouse'
): GetMessageByIdType => {
return (id: string) => {
const message = messageLookup[id];
@ -335,6 +351,7 @@ export const getMessageSelector = createSelector(
message,
ourNumber,
regionCode,
interactionMode,
conversation,
author,
quoted,

View File

@ -22,6 +22,11 @@ export const getIntl = createSelector(
(state: UserStateType): LocalizerType => state.i18n
);
export const getInteractionMode = createSelector(
getUser,
(state: UserStateType) => state.interactionMode
);
export const getAttachmentsPath = createSelector(
getUser,
(state: UserStateType): string => state.attachmentsPath

View File

@ -7,6 +7,7 @@ interface Props {
*/
ios: boolean;
theme: 'light-theme' | 'dark-theme';
mode: 'mouse-mode' | 'keyboard-mode';
}
/**
@ -15,11 +16,15 @@ interface Props {
*/
export class ConversationContext extends React.Component<Props> {
public render() {
const { ios, theme } = this.props;
const { ios, theme, mode } = this.props;
return (
<div
className={classNames(theme || 'light-theme', ios ? 'ios-theme' : null)}
className={classNames(
theme || 'light-theme',
ios ? 'ios-theme' : null,
mode
)}
style={{
backgroundColor: theme === 'dark-theme' ? 'black' : undefined,
}}

View File

@ -119,6 +119,7 @@ const urlOptions = QueryString.parse(query);
const theme = urlOptions.theme || 'light-theme';
const ios = urlOptions.ios || false;
const locale = urlOptions.locale || 'en';
const mode = urlOptions.mode || 'mouse-mode';
// @ts-ignore
import localeMessages from '../../_locales/en/messages.json';
@ -127,7 +128,10 @@ import localeMessages from '../../_locales/en/messages.json';
import { setup } from '../../js/modules/i18n';
const i18n = setup(locale, localeMessages);
export { theme, ios, locale, i18n };
export { theme, ios, locale, mode, i18n };
// @ts-ignore
window.getInteractionMode = () => mode;
// Telling Lodash to relinquish _ for use by underscore
// @ts-ignore

View File

@ -479,7 +479,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " el: this.$('.conversation-stack'),",
"lineNumber": 86,
"lineNumber": 85,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Hardcoded selector"
@ -488,7 +488,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
"lineNumber": 93,
"lineNumber": 92,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -497,7 +497,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " .append(this.networkStatusView.render().el);",
"lineNumber": 110,
"lineNumber": 109,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -506,7 +506,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " banner.$el.prependTo(this.$el);",
"lineNumber": 114,
"lineNumber": 113,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -515,7 +515,7 @@
"rule": "jQuery-appendTo(",
"path": "js/views/inbox_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 120,
"lineNumber": 119,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -524,7 +524,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 140,
"lineNumber": 139,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -533,7 +533,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 140,
"lineNumber": 139,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -542,7 +542,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
"lineNumber": 190,
"lineNumber": 189,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -551,7 +551,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('#header, .gutter').addClass('inactive');",
"lineNumber": 194,
"lineNumber": 193,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Hardcoded selector"
@ -560,7 +560,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation-stack').addClass('inactive');",
"lineNumber": 198,
"lineNumber": 197,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Hardcoded selector"
@ -569,7 +569,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .menu').trigger('close');",
"lineNumber": 200,
"lineNumber": 199,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Hardcoded selector"
@ -578,7 +578,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"lineNumber": 220,
"lineNumber": 219,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -587,7 +587,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .recorder').trigger('close');",
"lineNumber": 223,
"lineNumber": 222,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Hardcoded selector"
@ -7623,7 +7623,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.js",
"line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 31,
"lineNumber": 32,
"reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
@ -7632,7 +7632,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 149,
"lineNumber": 155,
"reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
@ -7646,15 +7646,6 @@
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/MessageDetail.tsx",
"line": " this.focusRef = React.createRef();",
"lineNumber": 42,
"reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/Timeline.js",
@ -7677,11 +7668,20 @@
"rule": "React-createRef",
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 65,
"lineNumber": 66,
"reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
},
{
"rule": "React-createRef",
"path": "ts/components/stickers/StickerManager.js",
"line": " const focusRef = React.createRef();",
"lineNumber": 20,
"reasonCategory": "usageTrusted",
"updated": "2019-11-21T06:13:49.384Z",
"reasonDetail": "Used for setting focus only"
},
{
"rule": "jQuery-wrap(",
"path": "ts/shims/textsecure.js",