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); 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 // Load these images now to ensure that they don't flicker on first use
window.preloadedImages = []; window.preloadedImages = [];
function preload(list) { function preload(list) {
@ -299,10 +354,11 @@
// Stop processing incoming messages // Stop processing incoming messages
if (messageReceiver) { if (messageReceiver) {
await messageReceiver.stopProcessing(); await messageReceiver.stopProcessing();
await window.waitForAllBatchers(); await window.waitForAllBatchers();
messageReceiver.unregisterBatchers(); }
if (messageReceiver) {
messageReceiver.unregisterBatchers();
messageReceiver = null; messageReceiver = null;
} }
@ -522,6 +578,7 @@
ourNumber: textsecure.storage.user.getNumber(), ourNumber: textsecure.storage.user.getNumber(),
platform: window.platform, platform: window.platform,
i18n: window.i18n, i18n: window.i18n,
interactionMode: window.getInteractionMode(),
}, },
}; };
@ -668,6 +725,7 @@
// Navigate by section // Navigate by section
if (ctrlOrCommand && !shiftKey && (key === 't' || key === 'T')) { if (ctrlOrCommand && !shiftKey && (key === 't' || key === 'T')) {
window.enterKeyboardMode();
const focusedElement = document.activeElement; const focusedElement = document.activeElement;
const targets = [ const targets = [

View file

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

View file

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

View file

@ -162,6 +162,13 @@ a {
border: none; border: none;
background: transparent; background: transparent;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
&:before { &:before {
content: ''; content: '';
display: inline-block; display: inline-block;
@ -175,11 +182,6 @@ a {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-25); @include color-svg('../images/icons/v2/plus-24.svg', $color-gray-25);
} }
} }
&:focus,
&:hover {
opacity: 1;
}
} }
input[type='file'] { 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 // Other
@mixin popper-shadow() { @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 { .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 // 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 { .module-message:focus .module-message__container--with-sticker {
box-shadow: none; @include keyboard-mode {
box-shadow: none;
}
} }
.module-message__container--with-sticker { .module-message__container--with-sticker {
@include light-theme {
border: none;
}
@include dark-theme {
border: none;
}
padding-bottom: 0px; padding-bottom: 0px;
} }
@ -603,9 +639,10 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
&:hover, @include keyboard-mode {
&:focus { &:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue; box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
} }
} }
@ -762,9 +799,10 @@
border-top-right-radius: 16px; border-top-right-radius: 16px;
overflow: hidden; overflow: hidden;
&:focus, @include keyboard-mode {
&:hover { &:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue; box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
} }
} }
@ -934,33 +972,35 @@
a { a {
text-decoration: underline; text-decoration: underline;
outline: none;
@include light-theme { @include light-theme {
color: $color-gray-90; color: $color-gray-90;
&:focus, }
&:hover { @include keyboard-mode {
outline: 2px solid $color-gray-90; &:focus {
outline: 1px solid $color-gray-90;
} }
} }
@include dark-theme { @include dark-theme {
color: $color-gray-05; color: $color-gray-05;
&:focus, }
&:hover { @include dark-keyboard-mode {
outline: 2px solid $color-gray-05; &:focus {
outline: 1px solid $color-gray-05;
} }
} }
@include ios-theme { @include ios-theme {
color: $color-white-alpha-90; color: $color-white-alpha-90;
&:focus,
&:hover {
outline: 2px solid $color-white-alpha-90;
}
} }
@include ios-dark-theme { @include ios-dark-theme {
color: $color-white-alpha-90; color: $color-white-alpha-90;
&:focus, }
&:hover { @include ios-keyboard-mode {
outline: 2px solid $color-white-alpha-90; &:focus {
outline: 1px solid $color-white-alpha-90;
} }
} }
} }
@ -982,33 +1022,41 @@
a { a {
text-decoration: underline; text-decoration: underline;
outline: none;
@include light-theme { @include light-theme {
color: $color-white; color: $color-white;
&:focus, }
&:hover { @include keyboard-mode {
outline: 2px solid $color-white; &:focus {
} outline: 1px solid $color-white;
}
@include ios-theme {
color: $color-gray-90;
&:focus,
&:hover {
outline: 2px solid $color-gray-90;
} }
} }
@include dark-theme { @include dark-theme {
color: $color-white-alpha-90; color: $color-white-alpha-90;
&:focus, }
&:hover { @include dark-keyboard-mode {
outline: 2px solid $color-white-alpha-90; &: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 { @include ios-dark-theme {
color: $color-gray-05; color: $color-gray-05;
&:focus, }
&:hover { @include dark-ios-keyboard-mode {
outline: 2px solid $color-gray-05; &:focus {
outline: 1px solid $color-gray-05;
} }
} }
} }
@ -1235,9 +1283,10 @@
border: 1px solid $color-gray-45; border: 1px solid $color-gray-45;
} }
&:focus, @include keyboard-mode {
&:hover { &:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue; 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-width: 4px;
border-left-style: solid; border-left-style: solid;
&:focus, @include keyboard-mode {
&:hover { &:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue; 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; 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 // Note: both of these override all of the specific color classes below
@include ios-theme { @include ios-theme {
border-left-color: $color-white; 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%; border-radius: 50%;
background-color: $color-black-alpha-40; background-color: $color-black-alpha-40;
@include keyboard-mode {
&:focus-within {
background-color: $color-signal-blue;
}
}
} }
.module-quote__close-button { .module-quote__close-button {
@ -1799,9 +1862,18 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
&:focus, @include keyboard-mode {
&:hover { &:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue; 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; outline: none;
padding: 5px; padding: 5px;
&:focus, @include keyboard-mode {
&:hover { &:focus {
@include light-theme { @include light-theme {
background-color: $color-gray-02; background-color: $color-gray-02;
} }
@include dark-theme { @include dark-theme {
background-color: $color-gray-80; 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 { @include light-theme {
background-color: $color-gray-02; background-color: $color-gray-02;
}
&:hover, @include keyboard-mode {
&:focus { &:focus {
background-color: $color-gray-15; background-color: $color-gray-15;
} }
} }
@include dark-theme { @include dark-theme {
background-color: $color-gray-75; background-color: $color-gray-75;
}
&:hover, @include dark-keyboard-mode {
&:focus { &:focus {
background-color: $color-gray-60; 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 { .module-message-detail__delete-button {
@include button-reset; @include button-reset;
@include keyboard-mode {
&:focus {
outline: -webkit-focus-ring-color auto 5px;
}
}
border-radius: 5px; border-radius: 5px;
margin: 1em auto; margin: 1em auto;
padding: 1em; padding: 1em;
@ -2708,6 +2788,19 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
@include dark-theme { @include dark-theme {
background-color: $color-gray-90; 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 { .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 { .module-document-list-item__content {
@include button-reset; @include button-reset;
width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
height: 100%;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
} }
.module-document-list-item__icon { .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-right: 4px;
margin-bottom: 4px; margin-bottom: 4px;
position: relative; position: relative;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue;
}
}
} }
.module-media-grid-item__image { .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; line-height: 0;
border-radius: 50%; border-radius: 50%;
&:focus, @include keyboard-mode {
&:hover { &:focus {
box-shadow: 0px 0px 0px 2px $color-signal-blue; 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 // Overriding some default button styling
border: none; border: none;
padding: 0; padding: 0;
outline: none;
@include light-theme { @include light-theme {
background-color: $color-gray-15; 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 // 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:focus
.module-message__container--with-sticker .module-message__container--with-sticker
.module-image__border-overlay { .module-image__border-overlay {
top: 1px; @include keyboard-mode {
bottom: 1px; top: 1px;
left: 1px; bottom: 1px;
right: 1px; left: 1px;
border-radius: 10px; 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:focus {
button.module-image__border-overlay:hover { @include keyboard-mode {
box-shadow: inset 0px 0px 0px 2px $color-signal-blue; box-shadow: inset 0px 0px 0px 2px $color-signal-blue;
}
} }
.module-image__border-overlay--dark { .module-image__border-overlay--dark {
@ -3697,9 +3822,10 @@ button.module-image__border-overlay:hover {
background-image: url('../images/x-shadow-16.svg'); background-image: url('../images/x-shadow-16.svg');
&:focus, @include keyboard-mode {
&:hover { &:focus {
outline: 2px solid $color-signal-blue; outline: 2px solid $color-signal-blue;
}
} }
} }
@ -3854,8 +3980,10 @@ button.module-image__border-overlay:hover {
z-index: 2; z-index: 2;
@include color-svg('../images/icons/v2/x-24.svg', $color-black); @include color-svg('../images/icons/v2/x-24.svg', $color-black);
&:focus { @include keyboard-mode {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue); &:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
}
} }
} }
@ -4103,18 +4231,21 @@ button.module-image__border-overlay:hover {
&:hover { &:hover {
background: $color-gray-05; background: $color-gray-05;
} }
&:hover, }
@include keyboard-mode {
&:focus { &:focus {
box-shadow: inset 0 0 0 2px $color-signal-blue; box-shadow: inset 0 0 0 2px $color-signal-blue;
} }
} }
@include dark-theme { @include dark-theme {
border: 1px solid $color-gray-60; border: 1px solid $color-gray-60;
&:hover { &:hover {
background: $color-gray-75; background: $color-gray-75;
} }
&:hover, }
@include dark-keyboard-mode {
&:focus { &:focus {
box-shadow: inset 0 0 0 2px $color-signal-blue; box-shadow: inset 0 0 0 2px $color-signal-blue;
} }
@ -4212,12 +4343,17 @@ button.module-image__border-overlay:hover {
@include light-theme { @include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60); @include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
}
@include keyboard-mode {
&:focus { &:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue); @include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
} }
} }
@include dark-theme { @include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25); @include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
}
@include dark-keyboard-mode {
&:focus { &:focus {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue); @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
.module-search-results { .module-search-results {
outline: none;
overflow: hidden; overflow: hidden;
flex-grow: 1; flex-grow: 1;
} }
@ -4381,6 +4518,7 @@ button.module-image__border-overlay:hover {
padding-right: 1em; padding-right: 1em;
width: 100%; width: 100%;
text-align: center; text-align: center;
outline: none;
} }
.module-search-results__contacts-header { .module-search-results__contacts-header {
@ -4575,20 +4713,23 @@ button.module-image__border-overlay:hover {
'../images/icons/v2/chevron-left-24.svg', '../images/icons/v2/chevron-left-24.svg',
$color-gray-60 $color-gray-60
); );
&:focus, }
&:hover { @include keyboard-mode {
&:focus {
@include color-svg( @include color-svg(
'../images/icons/v2/chevron-left-24.svg', '../images/icons/v2/chevron-left-24.svg',
$color-signal-blue $color-signal-blue
); );
} }
} }
@include dark-theme { @include dark-theme {
@include color-svg( @include color-svg(
'../images/icons/v2/chevron-left-24.svg', '../images/icons/v2/chevron-left-24.svg',
$color-gray-25 $color-gray-25
); );
&:focus, }
@include dark-keyboard-mode {
&:hover { &:hover {
@include color-svg( @include color-svg(
'../images/icons/v2/chevron-left-24.svg', '../images/icons/v2/chevron-left-24.svg',
@ -4839,13 +4980,14 @@ button.module-image__border-overlay:hover {
background: none; background: none;
margin-right: 4px; margin-right: 4px;
outline: none;
&:active, &:active,
&:focus { &:focus {
outline: none; @include keyboard-mode {
@include light-theme {
background: $color-gray-05; background: $color-gray-05;
} }
@include dark-theme { @include dark-keyboard-mode {
background: $color-gray-60; background: $color-gray-60;
} }
} }
@ -5040,6 +5182,10 @@ button.module-image__border-overlay:hover {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@include mouse-mode {
outline: none;
}
&__image, &__image,
&__placeholder { &__placeholder {
width: 100%; 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 { &__cover {
width: 48px; width: 48px;
height: 48px; height: 48px;
@ -5280,6 +5432,10 @@ button.module-image__border-overlay:hover {
background: $color-gray-75; background: $color-gray-75;
} }
@include mouse-mode {
outline: none;
}
&--blue { &--blue {
@include light-theme { @include light-theme {
background: $color-signal-blue; background: $color-signal-blue;
@ -5503,10 +5659,13 @@ button.module-image__border-overlay:hover {
align-items: center; align-items: center;
opacity: 0.5; opacity: 0.5;
&:focus,
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
outline: none;
&::after { &::after {
display: block; display: block;
content: ''; content: '';
@ -5716,8 +5875,8 @@ button.module-image__border-overlay:hover {
&__button { &__button {
margin-left: 4px; margin-left: 4px;
border-radius: 14px; border-radius: 17px;
height: 28px; height: 34px;
padding: 5px 12px; padding: 5px 12px;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -5725,6 +5884,10 @@ button.module-image__border-overlay:hover {
@include font-body-1-bold; @include font-body-1-bold;
@include mouse-mode {
outline: none;
}
@include light-theme() { @include light-theme() {
background: $color-white; background: $color-white;
color: $color-gray-60; color: $color-gray-60;
@ -5884,6 +6047,10 @@ button.module-image__border-overlay:hover {
align-items: center; align-items: center;
background: none; background: none;
@include mouse-mode {
outline: none;
}
&--footer { &--footer {
&:not(:first-of-type) { &:not(:first-of-type) {
margin-left: 4px; margin-left: 4px;
@ -6047,10 +6214,13 @@ button.module-image__border-overlay:hover {
align-items: center; align-items: center;
opacity: 0.5; opacity: 0.5;
&:focus,
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
outline: none;
&::after { &::after {
display: block; display: block;
content: ''; content: '';
@ -6795,13 +6965,14 @@ button.module-image__border-overlay:hover {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05); @include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
} }
&:focus, @include keyboard-mode {
&:hover { &:focus {
@include light-theme { @include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue); @include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue);
} }
@include dark-theme { @include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-signal-blue); @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; min-height: 40px;
outline: none; outline: none;
&:focus { @include keyboard-mode {
@include light-theme { &:focus {
background-color: $color-gray-05; @include light-theme {
} background-color: $color-gray-05;
@include dark-theme { }
background-color: $color-gray-90; @include dark-theme {
background-color: $color-gray-90;
}
} }
} }

View file

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

View file

@ -213,6 +213,20 @@ export class SearchResults extends React.Component<PropsType, StateType> {
}; };
public setFocusToFirst = () => { 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(); const scrollContainer = this.getScrollContainer();
if (!scrollContainer) { if (!scrollContainer) {
return; return;
@ -513,9 +527,19 @@ export class SearchResults extends React.Component<PropsType, StateType> {
if (noResults) { if (noResults) {
return ( return (
<div className="module-search-results"> <div
className="module-search-results"
tabIndex={-1}
ref={this.containerRef}
onFocus={this.handleFocus}
>
{!searchConversationName || searchTerm ? ( {!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 ? ( {searchConversationName ? (
<Intl <Intl
id="noSearchResultsInConversation" id="noSearchResultsInConversation"

View file

@ -22,7 +22,7 @@ const contact = {
onSendMessage: () => console.log('onSendMessage'), onSendMessage: () => console.log('onSendMessage'),
signalAccount: '+12025550000', 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"> <div className="module-message-container">
<Message <Message
authorColor="green" authorColor="green"
@ -92,7 +92,7 @@ const contact = {
onSendMessage: () => console.log('onSendMessage'), onSendMessage: () => console.log('onSendMessage'),
signalAccount: '+12025550000', 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"> <div className="module-message-container">
<Message <Message
authorColor="green" 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"> <div className="module-message-container">
<Message <Message
authorColor="green" authorColor="green"
@ -182,7 +182,7 @@ const contact = {
}, },
signalAccount: '+12025550000', 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"> <div className="module-message-container">
<Message <Message
authorColor="green" 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"> <div className="module-message-container">
<Message <Message
authorColor="green" 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"> <div className="module-message-container">
<Message <Message
authorColor="green" authorColor="green"
@ -377,7 +377,7 @@ const contact = {
}, },
signalAccount: '+12025551000', 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"> <div className="module-message-container">
<Message <Message
authorColor="green" 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"> <div className="module-message-container">
<Message <Message
authorColor="green" authorColor="green"
@ -491,7 +491,7 @@ const contact = {
```jsx ```jsx
const contact = {}; 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"> <div className="module-message-container">
<Message <Message
authorColor="green" 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"> <div className="module-message-container">
<Message <Message
text="I want to introduce you to Someone..." text="I want to introduce you to Someone..."

View file

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

View file

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

View file

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

View file

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

View file

@ -35,12 +35,7 @@ interface Props {
} }
export class MessageDetail extends React.Component<Props> { export class MessageDetail extends React.Component<Props> {
private readonly focusRef: React.RefObject<HTMLDivElement>; private readonly focusRef = React.createRef<HTMLDivElement>();
constructor(props: Props) {
super(props);
this.focusRef = React.createRef();
}
public componentDidMount() { public componentDidMount() {
// When this component is created, it's initially not part of the DOM, and then it's // 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 #### Plain text
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -41,7 +41,7 @@
#### Name variations #### Name variations
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -112,7 +112,7 @@
#### With emoji #### With emoji
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -148,7 +148,7 @@
#### Replies to you or yourself #### Replies to you or yourself
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -186,7 +186,12 @@
#### In a group conversation #### In a group conversation
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" 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. messages the color is taken from the contact who wrote the quoted message.
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -610,7 +615,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Referenced message not found #### Referenced message not found
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -687,7 +692,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Long names and context #### Long names and context
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" 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 #### A lot of text in quotation
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" 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 #### A lot of text in quotation, with icon
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" 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 #### A lot of text in quotation, with image
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -885,7 +890,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Image with caption #### Image with caption
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -937,7 +942,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Image #### Image
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -987,7 +992,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Image with no thumbnail #### Image with no thumbnail
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1030,7 +1035,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Pending image download #### Pending image download
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1073,7 +1078,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Video with caption #### Video with caption
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1125,7 +1130,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Video #### Video
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1175,7 +1180,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Video with no thumbnail #### Video with no thumbnail
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1223,7 +1228,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Audio with caption #### Audio with caption
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1267,7 +1272,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Audio #### Audio
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1309,7 +1314,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Voice message #### Voice message
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1355,7 +1360,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Other file type with caption #### Other file type with caption
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1438,7 +1443,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Other file type #### Other file type
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1482,7 +1487,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, image attachment, and caption #### Quote, image attachment, and caption
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1532,7 +1537,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, image attachment #### Quote, image attachment
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1580,7 +1585,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, portrait image attachment #### Quote, portrait image attachment
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1628,7 +1633,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, video attachment #### Quote, video attachment
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1686,7 +1691,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, audio attachment #### Quote, audio attachment
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1730,7 +1735,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Quote, file attachment #### Quote, file attachment
```jsx ```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"> <div className="module-message-container">
<Message <Message
direction="incoming" direction="incoming"
@ -1778,7 +1783,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### Plain text #### Plain text
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 #### With an icon
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 #### With an image
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 #### With attachment and no text
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
authorColor="blue" authorColor="blue"
@ -1867,7 +1872,7 @@ messages the color is taken from the contact who wrote the quoted message.
#### With generic attachment #### With generic attachment
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 #### With a close button
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 #### With a close button and icon
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 #### With a close button and image
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}> <util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="bottom-bar"> <div className="bottom-bar">
<Quote <Quote
text="How many ferrets do you have?" 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 // This is important to ensure that using this quote to navigate to the referenced
// message doesn't also trigger its parent message's keydown. // 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.preventDefault();
event.stopPropagation(); event.stopPropagation();
onClick(); 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 = () => { public handleImageError = () => {
// tslint:disable-next-line no-console // tslint:disable-next-line no-console
console.log('Message: Image failed to load; failing over to placeholder'); 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; return null;
} }
// We don't want the overall click handler for the quote to fire, so we stop const clickHandler = (e: React.MouseEvent): void => {
// propagation before handing control to the caller's callback.
const onClick = (e: React.MouseEvent<{}>): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onClose(); 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. // We need the container to give us the flexibility to implement the iOS design.
return ( 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 // We can't be a button because the overall quote is a button; can't nest them
role="button" role="button"
className="module-quote__close-button" className="module-quote__close-button"
onClick={onClick} onKeyDown={keyDownHandler}
onClick={clickHandler}
/> />
</div> </div>
); );
@ -383,7 +383,6 @@ export class Quote extends React.Component<Props, State> {
<button <button
onClick={onClick} onClick={onClick}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown}
className={classNames( className={classNames(
'module-quote', 'module-quote',
isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing', isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',

View file

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

View file

@ -18,12 +18,6 @@ export type OwnProps = {
export type Props = OwnProps; export type Props = OwnProps;
function focusOnRender(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
export const StickerManager = React.memo( export const StickerManager = React.memo(
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
({ ({
@ -36,6 +30,7 @@ export const StickerManager = React.memo(
uninstallStickerPack, uninstallStickerPack,
i18n, i18n,
}: Props) => { }: Props) => {
const focusRef = React.createRef<HTMLDivElement>();
const [ const [
packToPreview, packToPreview,
setPackToPreview, setPackToPreview,
@ -48,6 +43,14 @@ export const StickerManager = React.memo(
knownPacks.forEach(pack => { knownPacks.forEach(pack => {
downloadStickerPack(pack.id, pack.key); 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( const clearPackToPreview = React.useCallback(
@ -76,11 +79,7 @@ export const StickerManager = React.memo(
uninstallStickerPack={uninstallStickerPack} uninstallStickerPack={uninstallStickerPack}
/> />
) : null} ) : null}
<div <div className="module-sticker-manager" tabIndex={-1} ref={focusRef}>
className="module-sticker-manager"
tabIndex={-1}
ref={focusOnRender}
>
{[ {[
{ {
i18nKey: 'stickers--StickerManager--InstalledPacks', i18nKey: 'stickers--StickerManager--InstalledPacks',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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