Introduce a new design for the left pane
This commit is contained in:
parent
d60600d6fb
commit
35a54cdc02
63 changed files with 1205 additions and 576 deletions
|
@ -60,26 +60,19 @@
|
|||
<script type="text/x-tmpl-mustache" id="two-column">
|
||||
<div class='module-title-bar-drag-area'></div>
|
||||
|
||||
<div class='inbox-container'>
|
||||
<div class='gutter'>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
|
||||
<div class='conversation-stack'>
|
||||
<div class='no-conversation-open'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p class="whats-new-placeholder"></p>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p class="whats-new-placeholder"></p>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast"></div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
<div id="toast"></div>
|
||||
</div>
|
||||
|
||||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="conversation">
|
||||
|
|
1
images/icons/v2/warning-outline-24.svg
Normal file
1
images/icons/v2/warning-outline-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m10.75 8h2.5l-.5 6.5h-1.5zm2.75 9.5a1.5 1.5 0 1 0 -1.5 1.5 1.5 1.5 0 0 0 1.5-1.5zm-1.5-13.23-9.4 16.23h18.8zm0-2.27a.9.9 0 0 1 .75.53l10.5 18.17c.41.72.07 1.3-.75 1.3h-21c-.83 0-1.16-.58-.75-1.3l10.5-18.13a.9.9 0 0 1 .75-.57z"/></svg>
|
After Width: | Height: | Size: 303 B |
|
@ -14,12 +14,6 @@
|
|||
}
|
||||
|
||||
.conversation {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ body {
|
|||
background-color: $color-gray-95;
|
||||
color: $color-gray-05;
|
||||
}
|
||||
|
||||
&.is-resizing-left-pane {
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
|
|
@ -7,54 +7,27 @@
|
|||
|
||||
.conversation-stack,
|
||||
.inbox,
|
||||
.inbox-container,
|
||||
.gutter {
|
||||
.no-conversation-open {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
@include light-theme {
|
||||
background-color: $color-gray-02;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 2px solid $color-gray-02;
|
||||
}
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-80;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 2px solid $color-gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
float: left;
|
||||
width: $left-pane-width;
|
||||
user-select: none;
|
||||
|
||||
.content {
|
||||
overflow-y: scroll;
|
||||
max-height: calc(100% - 88px);
|
||||
}
|
||||
.no-conversation-open {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.left-pane-placeholder {
|
||||
height: 100%;
|
||||
}
|
||||
.left-pane-wrapper {
|
||||
height: 100%;
|
||||
.conversation-stack {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.conversation.placeholder {
|
||||
|
@ -62,7 +35,6 @@
|
|||
user-select: none;
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
|
|
@ -78,6 +78,17 @@
|
|||
align-items: stretch;
|
||||
|
||||
outline: none;
|
||||
|
||||
max-width: 406px;
|
||||
|
||||
.module-timeline--width-wide &,
|
||||
.module-message-detail & {
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.module-timeline--width-medium & {
|
||||
max-width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-message--expired {
|
||||
|
@ -102,21 +113,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Spec: container < 438px
|
||||
.module-message--incoming {
|
||||
margin-left: 16px;
|
||||
margin-right: 32px;
|
||||
margin-right: auto;
|
||||
}
|
||||
.module-message--outgoing {
|
||||
float: right;
|
||||
margin-right: 16px;
|
||||
margin-left: 32px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.module-message__buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -128,19 +135,31 @@
|
|||
}
|
||||
|
||||
.module-message__buttons--incoming {
|
||||
left: 100%;
|
||||
padding-left: 8px;
|
||||
|
||||
& > * {
|
||||
margin-left: 12px;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__buttons--outgoing {
|
||||
right: 100%;
|
||||
padding-right: 8px;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
& > * {
|
||||
margin-right: 12px;
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__buttons__download {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
|
@ -168,7 +187,6 @@
|
|||
.module-message__buttons__react {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
|
@ -196,17 +214,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.module-message__buttons__download--incoming {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.module-message__buttons__download--outgoing {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.module-message__buttons__reply {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
@include light-theme {
|
||||
|
@ -232,13 +242,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.module-message__buttons__reply--incoming {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.module-message__buttons__reply--outgoing {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.module-message__buttons__menu {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
@ -279,14 +282,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.module-message__buttons__menu--incoming {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.module_message__buttons__menu--outgoing {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.module-message__error-container {
|
||||
min-width: 28px;
|
||||
position: relative;
|
||||
|
@ -3278,8 +3273,8 @@ button.module-conversation-details__action-button {
|
|||
height: calc(#{$header-height} + var(--title-bar-drag-area-height));
|
||||
width: 100%;
|
||||
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-top: var(--title-bar-drag-area-height);
|
||||
|
||||
display: flex;
|
||||
|
@ -3311,6 +3306,10 @@ button.module-conversation-details__action-button {
|
|||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.module-left-pane--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
|
@ -3459,6 +3458,10 @@ button.module-conversation-details__action-button {
|
|||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
||||
}
|
||||
}
|
||||
|
||||
.module-left-pane--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__compose-icon {
|
||||
|
@ -5640,7 +5643,11 @@ button.module-image__border-overlay:focus {
|
|||
// Module: conversation list
|
||||
|
||||
.module-conversation-list {
|
||||
$normal-row-height: 72px;
|
||||
|
||||
@include scrollbar;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
&--scroll-behavior {
|
||||
&-default {
|
||||
|
@ -5654,11 +5661,15 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 64px;
|
||||
justify-content: center;
|
||||
line-height: 64px;
|
||||
border-radius: 10px;
|
||||
height: $normal-row-height;
|
||||
line-height: $normal-row-height;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
display: flex;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
|
@ -5721,20 +5732,42 @@ button.module-image__border-overlay:focus {
|
|||
background-color: $color-gray-75;
|
||||
}
|
||||
}
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
justify-content: flex-start;
|
||||
|
||||
&__icon {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__text,
|
||||
&__archived-count {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--contact-or-conversation {
|
||||
@include button-reset;
|
||||
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
cursor: inherit;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
height: $normal-row-height;
|
||||
margin: 2px 0;
|
||||
padding: 8px 14px;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
&--is-button {
|
||||
cursor: pointer;
|
||||
|
||||
|
@ -5745,10 +5778,10 @@ button.module-image__border-overlay:focus {
|
|||
&:hover:not(:disabled),
|
||||
&:focus:not(:disabled) {
|
||||
@include light-theme {
|
||||
background-color: $color-gray-05;
|
||||
background-color: $color-black-alpha-06;
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-75;
|
||||
background-color: $color-white-alpha-06;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5772,59 +5805,40 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
&--has-unread {
|
||||
padding-left: 12px;
|
||||
|
||||
@include light-theme {
|
||||
border-left: 4px solid $color-ultramarine;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-left: 4px solid $color-ultramarine-light;
|
||||
}
|
||||
}
|
||||
|
||||
&--is-selected {
|
||||
@include light-theme {
|
||||
background-color: $color-gray-15;
|
||||
background-color: $color-black-alpha-12;
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-65;
|
||||
background-color: $color-white-alpha-12;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar-container {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__unread-count {
|
||||
text-align: center;
|
||||
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: 0px;
|
||||
$size: 18px;
|
||||
|
||||
@include font-caption-bold;
|
||||
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 10px;
|
||||
|
||||
color: $color-white;
|
||||
height: $size;
|
||||
line-height: $size;
|
||||
margin-left: 10px;
|
||||
margin-top: 2px;
|
||||
min-width: $size;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
text-align: center;
|
||||
word-break: normal;
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-ultramarine;
|
||||
box-shadow: 0px 0px 0px 1px $color-gray-02;
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-ultramarine-light;
|
||||
box-shadow: 0px 0px 0px 1px $color-gray-90;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5836,6 +5850,10 @@ button.module-image__border-overlay:focus {
|
|||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
@ -5845,6 +5863,10 @@ button.module-image__border-overlay:focus {
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
@ -5867,17 +5889,6 @@ button.module-image__border-overlay:focus {
|
|||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--has-unread {
|
||||
@include font-caption-bold;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
flex-shrink: 0;
|
||||
margin-left: 6px;
|
||||
|
@ -5894,10 +5905,6 @@ button.module-image__border-overlay:focus {
|
|||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
&--with-unread {
|
||||
@include font-caption-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5905,7 +5912,11 @@ button.module-image__border-overlay:focus {
|
|||
&__message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex-grow: 1;
|
||||
|
@ -5913,8 +5924,10 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
@include font-body-2;
|
||||
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
|
||||
|
@ -5925,15 +5938,12 @@ button.module-image__border-overlay:focus {
|
|||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
&--has-unread {
|
||||
@include font-body-2-bold;
|
||||
.module-conversation-list--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
&--always-full-size {
|
||||
height: 36px; // two lines
|
||||
}
|
||||
|
||||
&__muted {
|
||||
|
@ -5977,12 +5987,16 @@ button.module-image__border-overlay:focus {
|
|||
&__status-icon {
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-top: 2px;
|
||||
margin-top: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@mixin normal-status-icon($icon) {
|
||||
@include light-theme {
|
||||
@include color-svg($icon, $color-gray-25);
|
||||
|
@ -6146,14 +6160,38 @@ button.module-image__border-overlay:focus {
|
|||
&--header {
|
||||
@include font-body-1-bold;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
|
||||
.module-conversation-list--width-narrow & {
|
||||
@include rounded-corners;
|
||||
display: block;
|
||||
height: 2px;
|
||||
margin: 19px 0 19px 10px;
|
||||
width: 56px;
|
||||
|
||||
// Hide the text, but keep it for screen readers.
|
||||
color: transparent;
|
||||
overflow: hidden;
|
||||
text-indent: -99999px;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-black-alpha-12;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-white-alpha-12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--spinner {
|
||||
|
@ -6168,12 +6206,36 @@ button.module-image__border-overlay:focus {
|
|||
// Module: Left Pane
|
||||
|
||||
.module-left-pane {
|
||||
border-right-style: solid;
|
||||
border-right-width: 1px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: $left-pane-width;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
@include light-theme {
|
||||
border-color: $color-gray-20;
|
||||
background-color: $color-gray-02;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 2px solid $color-gray-02;
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-color: $color-gray-65;
|
||||
background-color: $color-gray-80;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 2px solid $color-gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&:not(.module-left-pane--is-resizing) {
|
||||
transition: width 200ms cubic-bezier(0.17, 0.17, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-left-pane__empty {
|
||||
|
@ -6184,6 +6246,10 @@ button.module-image__border-overlay:focus {
|
|||
padding: 0 32px;
|
||||
text-align: center;
|
||||
|
||||
.module-left-pane--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--composer_icon {
|
||||
align-items: center;
|
||||
background-color: $color-gray-05;
|
||||
|
@ -6375,6 +6441,16 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
.module-left-pane__resize-grab-area {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
right: -8px;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
// Module: Timeline Loading Row
|
||||
|
||||
.module-timeline-loading-row {
|
||||
|
@ -8103,7 +8179,7 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
.module-avatar-popup {
|
||||
min-width: 240px;
|
||||
max-width: $left-pane-width;
|
||||
max-width: 320px;
|
||||
|
||||
border-radius: 4px;
|
||||
padding-bottom: 4px;
|
||||
|
@ -9124,20 +9200,6 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
// All media query widths have 300px added to account for the left pane
|
||||
// And have been tweaked for smoother transitions
|
||||
|
||||
// To hide in small breakpoints
|
||||
.module-message__buttons__download {
|
||||
display: none;
|
||||
}
|
||||
.module-message__buttons__reply {
|
||||
display: none;
|
||||
}
|
||||
.module-message__buttons__react {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// To limit messages with things forcing them wider, like long attachment names
|
||||
.module-message__container {
|
||||
&--incoming {
|
||||
|
@ -9290,87 +9352,3 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Spec: container < 515px */
|
||||
@media (min-width: 0px) and (max-width: 834px) {
|
||||
.module-message {
|
||||
// Add 2px for 1px border
|
||||
max-width: 302px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spec: container > 514px and container < 606px */
|
||||
@media (min-width: 835px) and (max-width: 925px) {
|
||||
.module-message {
|
||||
// Add 2px for 1px border
|
||||
max-width: 360px;
|
||||
|
||||
&--with-avatar {
|
||||
// Remove 36px from avatar
|
||||
max-width: 324px;
|
||||
}
|
||||
}
|
||||
|
||||
// Spec: container < 438px
|
||||
.module-message--incoming {
|
||||
margin-right: auto;
|
||||
}
|
||||
.module-message--outgoing {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
// To hide in small breakpoints
|
||||
.module-message__buttons__download {
|
||||
display: inline-block;
|
||||
}
|
||||
.module-message__buttons__reply {
|
||||
display: inline-block;
|
||||
}
|
||||
.module-message__buttons__react {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// To hide in larger breakpoints
|
||||
.module-message__context__download {
|
||||
display: none;
|
||||
}
|
||||
.module-message__context__reply {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Spec: container > 593px
|
||||
@media (min-width: 926px) {
|
||||
.module-message {
|
||||
max-width: 66%;
|
||||
}
|
||||
|
||||
.module-message--incoming {
|
||||
margin-right: auto;
|
||||
}
|
||||
.module-message--outgoing {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
// To hide in small breakpoints
|
||||
.module-message__buttons__download {
|
||||
display: inline-block;
|
||||
}
|
||||
.module-message__buttons__reply {
|
||||
display: inline-block;
|
||||
}
|
||||
.module-message__buttons__react {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// To hide in larger breakpoints
|
||||
.module-message__context__download {
|
||||
display: none;
|
||||
}
|
||||
.module-message__context__reply {
|
||||
display: none;
|
||||
}
|
||||
.module-message__context__react {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ $color-gray-90: #1b1b1b;
|
|||
$color-gray-95: #121212;
|
||||
$color-black: #000000;
|
||||
|
||||
$color-white-alpha-06: rgba($color-white, 0.06);
|
||||
$color-white-alpha-12: rgba($color-white, 0.12);
|
||||
$color-white-alpha-20: rgba($color-white, 0.2);
|
||||
$color-white-alpha-40: rgba($color-white, 0.4);
|
||||
$color-white-alpha-60: rgba($color-white, 0.6);
|
||||
|
@ -35,6 +37,8 @@ $color-white-alpha-80: rgba($color-white, 0.8);
|
|||
$color-white-alpha-90: rgba($color-white, 0.9);
|
||||
|
||||
$color-black-alpha-05: rgba($color-black, 0.05);
|
||||
$color-black-alpha-06: rgba($color-black, 0.06);
|
||||
$color-black-alpha-12: rgba($color-black, 0.12);
|
||||
$color-black-alpha-20: rgba($color-black, 0.2);
|
||||
$color-black-alpha-30: rgba($color-black, 0.3);
|
||||
$color-black-alpha-40: rgba($color-black, 0.4);
|
||||
|
@ -226,7 +230,6 @@ $color-deep-red: #ff261f;
|
|||
|
||||
// -- A few layout variables used cross-file
|
||||
|
||||
$left-pane-width: 320px;
|
||||
$header-height: 52px;
|
||||
|
||||
$ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
||||
|
|
8
stylesheets/components/Inbox.scss
Normal file
8
stylesheets/components/Inbox.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.Inbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
}
|
|
@ -11,9 +11,16 @@
|
|||
}
|
||||
|
||||
.LeftPaneDialog {
|
||||
$default-background-color: $color-ultramarine;
|
||||
$default-text-color: $color-white;
|
||||
$error-background-color: $color-accent-red;
|
||||
$error-text-color: $default-text-color;
|
||||
$warning-background-color: $color-accent-yellow;
|
||||
$warning-text-color: $color-black;
|
||||
|
||||
align-items: center;
|
||||
background: $color-ultramarine;
|
||||
color: $color-white;
|
||||
background: $default-background-color;
|
||||
color: $default-text-color;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
|
@ -21,6 +28,10 @@
|
|||
user-select: none;
|
||||
cursor: inherit;
|
||||
|
||||
.module-left-pane--width-narrow & {
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -34,6 +45,10 @@
|
|||
&__container-close {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.module-left-pane--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__spinner-container {
|
||||
|
@ -74,6 +89,11 @@
|
|||
&--update {
|
||||
-webkit-mask: url('../images/icons/v2/refresh-24.svg') no-repeat center;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
-webkit-mask: url('../images/icons/v2/warning-outline-24.svg') no-repeat
|
||||
center;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-text {
|
||||
|
@ -110,6 +130,13 @@
|
|||
&__message {
|
||||
width: 100%;
|
||||
|
||||
.module-left-pane--width-narrow & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__message,
|
||||
&__tooltip {
|
||||
h3 {
|
||||
@include font-body-1-bold;
|
||||
padding: 0px;
|
||||
|
@ -127,12 +154,13 @@
|
|||
}
|
||||
|
||||
&--error {
|
||||
background-color: $color-accent-red;
|
||||
background-color: $error-background-color;
|
||||
color: $error-text-color;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: $color-accent-yellow;
|
||||
color: $color-black;
|
||||
background-color: $warning-background-color;
|
||||
color: $warning-text-color;
|
||||
|
||||
a {
|
||||
color: $color-black;
|
||||
|
@ -174,4 +202,21 @@
|
|||
transition: width 500ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
--tooltip-background-color: #{$default-background-color};
|
||||
--tooltip-text-color: #{$default-text-color};
|
||||
min-width: 280px;
|
||||
text-align: inherit;
|
||||
|
||||
&--error {
|
||||
--tooltip-text-color: #{$error-text-color};
|
||||
--tooltip-background-color: #{$error-background-color};
|
||||
}
|
||||
|
||||
&--warning {
|
||||
--tooltip-text-color: #{$warning-text-color};
|
||||
--tooltip-background-color: #{$warning-background-color};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
padding: 14px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding: 10px 14px;
|
||||
margin: 2px 0;
|
||||
|
||||
&__avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 100%;
|
||||
@include search-results-loading-pulsating-background;
|
||||
}
|
||||
|
@ -23,13 +23,20 @@
|
|||
justify-content: center;
|
||||
margin-left: 12px;
|
||||
|
||||
&__header {
|
||||
@include search-results-loading-box(50%);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&__line {
|
||||
&:nth-child(1) {
|
||||
@include search-results-loading-box(30%);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__message {
|
||||
@include search-results-loading-box(90%);
|
||||
&:nth-child(2) {
|
||||
@include search-results-loading-box(90%);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
@include search-results-loading-box(60%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
@import './components/GroupDescription.scss';
|
||||
@import './components/GroupDialog.scss';
|
||||
@import './components/GroupInput.scss';
|
||||
@import './components/Inbox.scss';
|
||||
@import './components/IncomingCallBar.scss';
|
||||
@import './components/Input.scss';
|
||||
@import './components/LeftPaneDialog.scss';
|
||||
|
|
|
@ -29,25 +29,19 @@
|
|||
<script type="text/x-tmpl-mustache" id="two-column">
|
||||
<div class='module-title-bar-drag-area'></div>
|
||||
|
||||
<div class='inbox-container'>
|
||||
<div class='gutter'>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
|
||||
<div class='conversation-stack'>
|
||||
<div class='no-conversation-open'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p class="whats-new-placeholder"></p>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p class="whats-new-placeholder"></p>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
<div id="toast"></div>
|
||||
</div>
|
||||
|
||||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="conversation">
|
||||
|
|
|
@ -31,6 +31,7 @@ export enum AvatarSize {
|
|||
TWENTY_EIGHT = 28,
|
||||
THIRTY_TWO = 32,
|
||||
THIRTY_SIX = 36,
|
||||
FORTY_EIGHT = 48,
|
||||
FIFTY_TWO = 52,
|
||||
EIGHTY = 80,
|
||||
NINETY_SIX = 96,
|
||||
|
|
|
@ -33,6 +33,11 @@ const defaultConversations: Array<ConversationListItemPropsType> = [
|
|||
isSelected: true,
|
||||
unreadCount: 12,
|
||||
title: 'Marc Barraca',
|
||||
lastMessage: {
|
||||
deletedForEveryone: false,
|
||||
text:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.',
|
||||
},
|
||||
}),
|
||||
getDefaultConversation({
|
||||
id: 'long-name-convo',
|
||||
|
|
|
@ -9,6 +9,7 @@ import { get, pick } from 'lodash';
|
|||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { assert } from '../util/assert';
|
||||
import { LocalizerType, ScrollBehavior } from '../types/Util';
|
||||
import { getConversationListWidthBreakpoint } from './_util';
|
||||
|
||||
import {
|
||||
ConversationListItem,
|
||||
|
@ -134,7 +135,7 @@ export type PropsType = {
|
|||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||
};
|
||||
|
||||
const NORMAL_ROW_HEIGHT = 68;
|
||||
const NORMAL_ROW_HEIGHT = 76;
|
||||
const HEADER_ROW_HEIGHT = 40;
|
||||
|
||||
export const ConversationList: React.FC<PropsType> = ({
|
||||
|
@ -193,6 +194,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
case RowType.ArchiveButton:
|
||||
result = (
|
||||
<button
|
||||
aria-label={i18n('archivedConversations')}
|
||||
className="module-conversation-list__item--archive-button"
|
||||
onClick={onClickArchiveButton}
|
||||
type="button"
|
||||
|
@ -345,11 +347,14 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const widthBreakpoint = getConversationListWidthBreakpoint(width);
|
||||
|
||||
return (
|
||||
<List
|
||||
className={classNames(
|
||||
'module-conversation-list',
|
||||
`module-conversation-list--scroll-behavior-${scrollBehavior}`
|
||||
`module-conversation-list--scroll-behavior-${scrollBehavior}`,
|
||||
`module-conversation-list--width-${widthBreakpoint}`
|
||||
)}
|
||||
height={height}
|
||||
ref={listRef}
|
||||
|
|
|
@ -1,21 +1,38 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
|
||||
import { DialogExpiredBuild } from './DialogExpiredBuild';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { FakeLeftPaneContainer } from '../test-both/helpers/FakeLeftPaneContainer';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
storiesOf('Components/DialogExpiredBuild', module).add(
|
||||
'DialogExpiredBuild',
|
||||
() => {
|
||||
const containerWidthBreakpoint = select(
|
||||
'containerWidthBreakpoint',
|
||||
WidthBreakpoint,
|
||||
WidthBreakpoint.Wide
|
||||
);
|
||||
const hasExpired = boolean('hasExpired', true);
|
||||
|
||||
return <DialogExpiredBuild hasExpired={hasExpired} i18n={i18n} />;
|
||||
return (
|
||||
<FakeLeftPaneContainer
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
>
|
||||
<DialogExpiredBuild
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
hasExpired={hasExpired}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
|
||||
import { LeftPaneDialog } from './LeftPaneDialog';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
|
||||
type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
hasExpired: boolean;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export const DialogExpiredBuild = ({
|
||||
containerWidthBreakpoint,
|
||||
hasExpired,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
|
@ -21,17 +25,16 @@ export const DialogExpiredBuild = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<LeftPaneDialog type="error">
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="error"
|
||||
onClick={() => {
|
||||
openLinkInWebBrowser('https://signal.org/download/');
|
||||
}}
|
||||
clickLabel={i18n('upgrade')}
|
||||
hasAction
|
||||
>
|
||||
{i18n('expiredWarning')}{' '}
|
||||
<a
|
||||
className="LeftPaneDialog__action-text"
|
||||
href="https://signal.org/download/"
|
||||
rel="noreferrer"
|
||||
tabIndex={-1}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n('upgrade')}
|
||||
</a>
|
||||
</LeftPaneDialog>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -10,10 +10,13 @@ import { DialogNetworkStatus } from './DialogNetworkStatus';
|
|||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { FakeLeftPaneContainer } from '../test-both/helpers/FakeLeftPaneContainer';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
|
@ -26,6 +29,11 @@ const defaultProps = {
|
|||
const story = storiesOf('Components/DialogNetworkStatus', module);
|
||||
|
||||
story.add('Knobs Playground', () => {
|
||||
const containerWidthBreakpoint = select(
|
||||
'containerWidthBreakpoint',
|
||||
WidthBreakpoint,
|
||||
WidthBreakpoint.Wide
|
||||
);
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', true);
|
||||
const isOnline = boolean('isOnline', true);
|
||||
const socketStatus = select(
|
||||
|
@ -40,30 +48,57 @@ story.add('Knobs Playground', () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Connecting', () => (
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
socketStatus={SocketStatus.CONNECTING}
|
||||
/>
|
||||
));
|
||||
([
|
||||
['wide', WidthBreakpoint.Wide],
|
||||
['narrow', WidthBreakpoint.Narrow],
|
||||
] as const).forEach(([name, containerWidthBreakpoint]) => {
|
||||
const defaultPropsForBreakpoint = {
|
||||
...defaultProps,
|
||||
containerWidthBreakpoint,
|
||||
};
|
||||
|
||||
story.add('Closing', () => (
|
||||
<DialogNetworkStatus {...defaultProps} socketStatus={SocketStatus.CLOSING} />
|
||||
));
|
||||
story.add(`Connecting (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogNetworkStatus
|
||||
{...defaultPropsForBreakpoint}
|
||||
socketStatus={SocketStatus.CONNECTING}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add('Closed', () => (
|
||||
<DialogNetworkStatus {...defaultProps} socketStatus={SocketStatus.CLOSED} />
|
||||
));
|
||||
story.add(`Closing (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogNetworkStatus
|
||||
{...defaultPropsForBreakpoint}
|
||||
socketStatus={SocketStatus.CLOSING}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add('Offline', () => (
|
||||
<DialogNetworkStatus {...defaultProps} isOnline={false} />
|
||||
));
|
||||
story.add(`Closed (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogNetworkStatus
|
||||
{...defaultPropsForBreakpoint}
|
||||
socketStatus={SocketStatus.CLOSED}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add(`Offline (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogNetworkStatus {...defaultPropsForBreakpoint} isOnline={false} />
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
});
|
||||
|
|
|
@ -8,16 +8,19 @@ import { Spinner } from './Spinner';
|
|||
import { LocalizerType } from '../types/Util';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { NetworkStateType } from '../state/ducks/network';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
|
||||
const FIVE_SECONDS = 5 * 1000;
|
||||
|
||||
export type PropsType = NetworkStateType & {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
manualReconnect: () => void;
|
||||
};
|
||||
|
||||
export const DialogNetworkStatus = ({
|
||||
containerWidthBreakpoint,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
isOnline,
|
||||
|
@ -70,6 +73,7 @@ export const DialogNetworkStatus = ({
|
|||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
icon={spinner}
|
||||
title={i18n('connecting')}
|
||||
|
@ -80,6 +84,7 @@ export const DialogNetworkStatus = ({
|
|||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
icon="network"
|
||||
title={isOnline ? i18n('disconnected') : i18n('offline')}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -9,10 +9,13 @@ import { action } from '@storybook/addon-actions';
|
|||
import { DialogRelink } from './DialogRelink';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { FakeLeftPaneContainer } from '../test-both/helpers/FakeLeftPaneContainer';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
i18n,
|
||||
isRegistrationDone: true,
|
||||
relinkDevice: action('relink-device'),
|
||||
|
@ -20,8 +23,16 @@ const defaultProps = {
|
|||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Unlinked',
|
||||
title: 'Unlinked (wide container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
isRegistrationDone: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Unlinked (narrow container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Narrow,
|
||||
isRegistrationDone: false,
|
||||
},
|
||||
},
|
||||
|
@ -39,7 +50,11 @@ storiesOf('Components/DialogRelink', module)
|
|||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<DialogRelink {...defaultProps} {...props} />
|
||||
<FakeLeftPaneContainer
|
||||
containerWidthBreakpoint={props.containerWidthBreakpoint}
|
||||
>
|
||||
<DialogRelink {...defaultProps} {...props} />
|
||||
</FakeLeftPaneContainer>
|
||||
</>
|
||||
));
|
||||
});
|
||||
|
|
|
@ -4,16 +4,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
|
||||
import { LeftPaneDialog } from './LeftPaneDialog';
|
||||
|
||||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
isRegistrationDone: boolean;
|
||||
relinkDevice: () => void;
|
||||
};
|
||||
|
||||
export const DialogRelink = ({
|
||||
containerWidthBreakpoint,
|
||||
i18n,
|
||||
isRegistrationDone,
|
||||
relinkDevice,
|
||||
|
@ -24,6 +27,7 @@ export const DialogRelink = ({
|
|||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
icon="relink"
|
||||
clickLabel={i18n('unlinkedWarning')}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -7,6 +7,8 @@ import { boolean, select } from '@storybook/addon-knobs';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { FakeLeftPaneContainer } from '../test-both/helpers/FakeLeftPaneContainer';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
@ -14,6 +16,7 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
dismissDialog: action('dismiss-dialog'),
|
||||
downloadSize: 116504357,
|
||||
downloadedSize: 61003110,
|
||||
|
@ -29,36 +32,79 @@ const defaultProps = {
|
|||
const story = storiesOf('Components/DialogUpdate', module);
|
||||
|
||||
story.add('Knobs Playground', () => {
|
||||
const containerWidthBreakpoint = select(
|
||||
'containerWidthBreakpoint',
|
||||
WidthBreakpoint,
|
||||
WidthBreakpoint.Wide
|
||||
);
|
||||
const dialogType = select('dialogType', DialogType, DialogType.Update);
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', false);
|
||||
const didSnooze = boolean('didSnooze', false);
|
||||
|
||||
return (
|
||||
<DialogUpdate
|
||||
{...defaultProps}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogUpdate
|
||||
{...defaultProps}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Update', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Update} />
|
||||
));
|
||||
([
|
||||
['wide', WidthBreakpoint.Wide],
|
||||
['narrow', WidthBreakpoint.Narrow],
|
||||
] as const).forEach(([name, containerWidthBreakpoint]) => {
|
||||
const defaultPropsForBreakpoint = {
|
||||
...defaultProps,
|
||||
containerWidthBreakpoint,
|
||||
};
|
||||
|
||||
story.add('Download Ready', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.DownloadReady} />
|
||||
));
|
||||
story.add(`Update (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogUpdate
|
||||
{...defaultPropsForBreakpoint}
|
||||
dialogType={DialogType.Update}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add('Downloading', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Downloading} />
|
||||
));
|
||||
story.add(`Download Ready (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogUpdate
|
||||
{...defaultPropsForBreakpoint}
|
||||
dialogType={DialogType.DownloadReady}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add('Cannot Update', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Cannot_Update} />
|
||||
));
|
||||
story.add(`Downloading (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogUpdate
|
||||
{...defaultPropsForBreakpoint}
|
||||
dialogType={DialogType.Downloading}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add('macOS RO Error', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.MacOS_Read_Only} />
|
||||
));
|
||||
story.add(`Cannot Update (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogUpdate
|
||||
{...defaultPropsForBreakpoint}
|
||||
dialogType={DialogType.Cannot_Update}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
|
||||
story.add(`macOS RO Error (${name} container)`, () => (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogUpdate
|
||||
{...defaultPropsForBreakpoint}
|
||||
dialogType={DialogType.MacOS_Read_Only}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
));
|
||||
});
|
||||
|
|
|
@ -8,8 +8,10 @@ import { DialogType } from '../types/Dialogs';
|
|||
import { LocalizerType } from '../types/Util';
|
||||
import { Intl } from './Intl';
|
||||
import { LeftPaneDialog } from './LeftPaneDialog';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
|
||||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
dialogType: DialogType;
|
||||
didSnooze: boolean;
|
||||
dismissDialog: () => void;
|
||||
|
@ -24,6 +26,7 @@ export type PropsType = {
|
|||
};
|
||||
|
||||
export const DialogUpdate = ({
|
||||
containerWidthBreakpoint,
|
||||
dialogType,
|
||||
didSnooze,
|
||||
dismissDialog,
|
||||
|
@ -49,7 +52,11 @@ export const DialogUpdate = ({
|
|||
|
||||
if (dialogType === DialogType.Cannot_Update) {
|
||||
return (
|
||||
<LeftPaneDialog type="warning" title={i18n('cannotUpdate')}>
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
title={i18n('cannotUpdate')}
|
||||
>
|
||||
<span>
|
||||
<Intl
|
||||
components={[
|
||||
|
@ -73,6 +80,7 @@ export const DialogUpdate = ({
|
|||
if (dialogType === DialogType.MacOS_Read_Only) {
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
title={i18n('cannotUpdate')}
|
||||
hasXButton
|
||||
|
@ -113,7 +121,12 @@ export const DialogUpdate = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<LeftPaneDialog icon="update" title={title} hoverText={versionTitle}>
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
icon="update"
|
||||
title={title}
|
||||
hoverText={versionTitle}
|
||||
>
|
||||
<div className="LeftPaneDialog__progress--container">
|
||||
<div
|
||||
className="LeftPaneDialog__progress--bar"
|
||||
|
@ -133,6 +146,7 @@ export const DialogUpdate = ({
|
|||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
icon="update"
|
||||
title={title}
|
||||
hoverText={versionTitle}
|
||||
|
|
|
@ -96,7 +96,7 @@ export const Inbox = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="inbox index" ref={hostRef} />
|
||||
<div className="Inbox" ref={hostRef} />
|
||||
{activeModal}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -72,12 +72,15 @@ const defaultModeSpecificProps = {
|
|||
pinnedConversations,
|
||||
conversations: defaultConversations,
|
||||
archivedConversations: defaultArchivedConversations,
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
};
|
||||
|
||||
const emptySearchResultsGroup = { isLoading: false, results: [] };
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
cantAddContactToGroup: action('cantAddContactToGroup'),
|
||||
canResizeLeftPane: true,
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
closeCantAddContactToGroupModal: action('closeCantAddContactToGroupModal'),
|
||||
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
||||
|
@ -88,6 +91,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
createGroup: action('createGroup'),
|
||||
i18n,
|
||||
modeSpecificProps: defaultModeSpecificProps,
|
||||
preferredWidthFromStorage: 320,
|
||||
openConversationInternal: action('openConversationInternal'),
|
||||
regionCode: 'US',
|
||||
challengeStatus: select(
|
||||
|
@ -125,6 +129,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
),
|
||||
selectedConversationId: undefined,
|
||||
selectedMessageId: undefined,
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
||||
setComposeGroupName: action('setComposeGroupName'),
|
||||
|
@ -155,6 +160,8 @@ story.add('Inbox: no conversations', () => (
|
|||
pinnedConversations: [],
|
||||
conversations: [],
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -168,6 +175,8 @@ story.add('Inbox: only pinned conversations', () => (
|
|||
pinnedConversations,
|
||||
conversations: [],
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -181,6 +190,8 @@ story.add('Inbox: only non-pinned conversations', () => (
|
|||
pinnedConversations: [],
|
||||
conversations: defaultConversations,
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -194,6 +205,8 @@ story.add('Inbox: only archived conversations', () => (
|
|||
pinnedConversations: [],
|
||||
conversations: [],
|
||||
archivedConversations: defaultArchivedConversations,
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -207,6 +220,8 @@ story.add('Inbox: pinned and archived conversations', () => (
|
|||
pinnedConversations,
|
||||
conversations: [],
|
||||
archivedConversations: defaultArchivedConversations,
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -220,6 +235,8 @@ story.add('Inbox: non-pinned and archived conversations', () => (
|
|||
pinnedConversations: [],
|
||||
conversations: defaultConversations,
|
||||
archivedConversations: defaultArchivedConversations,
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -233,6 +250,8 @@ story.add('Inbox: pinned and non-pinned conversations', () => (
|
|||
pinnedConversations,
|
||||
conversations: defaultConversations,
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -499,6 +518,8 @@ story.add('Captcha dialog: required', () => (
|
|||
pinnedConversations,
|
||||
conversations: defaultConversations,
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
challengeStatus: 'required',
|
||||
})}
|
||||
|
@ -513,6 +534,8 @@ story.add('Captcha dialog: pending', () => (
|
|||
pinnedConversations,
|
||||
conversations: defaultConversations,
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
},
|
||||
challengeStatus: 'pending',
|
||||
})}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useEffect, useCallback, useMemo, useState } from 'react';
|
||||
import Measure, { MeasuredComponentProps } from 'react-measure';
|
||||
import { isNumber } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { clamp, isNumber, noop } from 'lodash';
|
||||
|
||||
import {
|
||||
LeftPaneHelper,
|
||||
|
@ -39,6 +40,7 @@ import * as OS from '../OS';
|
|||
import { LocalizerType, ScrollBehavior } from '../types/Util';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { getConversationListWidthBreakpoint, WidthBreakpoint } from './_util';
|
||||
|
||||
import { ConversationList } from './ConversationList';
|
||||
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
||||
|
@ -49,6 +51,11 @@ import {
|
|||
SaveAvatarToDiskActionType,
|
||||
} from '../types/Avatar';
|
||||
|
||||
const MIN_WIDTH = 119;
|
||||
const MIN_SNAP_WIDTH = 280;
|
||||
const MIN_FULL_WIDTH = 320;
|
||||
const MAX_WIDTH = 380;
|
||||
|
||||
export enum LeftPaneMode {
|
||||
Inbox,
|
||||
Search,
|
||||
|
@ -82,9 +89,11 @@ export type PropsType = {
|
|||
mode: LeftPaneMode.SetGroupMetadata;
|
||||
} & LeftPaneSetGroupMetadataPropsType);
|
||||
i18n: LocalizerType;
|
||||
preferredWidthFromStorage: number;
|
||||
selectedConversationId: undefined | string;
|
||||
selectedMessageId: undefined | string;
|
||||
regionCode: string;
|
||||
canResizeLeftPane: boolean;
|
||||
challengeStatus: 'idle' | 'required' | 'pending';
|
||||
setChallengeStatus: (status: 'idle') => void;
|
||||
|
||||
|
@ -101,6 +110,7 @@ export type PropsType = {
|
|||
messageId?: string;
|
||||
switchToAssociatedView?: boolean;
|
||||
}) => void;
|
||||
savePreferredLeftPaneWidth: (_: number) => void;
|
||||
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
||||
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
|
||||
setComposeGroupName: (_: string) => void;
|
||||
|
@ -117,17 +127,26 @@ export type PropsType = {
|
|||
toggleComposeEditingAvatar: () => unknown;
|
||||
|
||||
// Render Props
|
||||
renderExpiredBuildDialog: () => JSX.Element;
|
||||
renderExpiredBuildDialog: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderMainHeader: () => JSX.Element;
|
||||
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||
renderNetworkStatus: () => JSX.Element;
|
||||
renderRelinkDialog: () => JSX.Element;
|
||||
renderUpdateDialog: () => JSX.Element;
|
||||
renderNetworkStatus: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderRelinkDialog: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderUpdateDialog: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||
};
|
||||
|
||||
export const LeftPane: React.FC<PropsType> = ({
|
||||
cantAddContactToGroup,
|
||||
canResizeLeftPane,
|
||||
challengeStatus,
|
||||
clearGroupCreationError,
|
||||
closeCantAddContactToGroupModal,
|
||||
|
@ -140,6 +159,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
i18n,
|
||||
modeSpecificProps,
|
||||
openConversationInternal,
|
||||
preferredWidthFromStorage,
|
||||
renderCaptchaDialog,
|
||||
renderExpiredBuildDialog,
|
||||
renderMainHeader,
|
||||
|
@ -147,6 +167,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
renderNetworkStatus,
|
||||
renderRelinkDialog,
|
||||
renderUpdateDialog,
|
||||
savePreferredLeftPaneWidth,
|
||||
selectedConversationId,
|
||||
selectedMessageId,
|
||||
setChallengeStatus,
|
||||
|
@ -163,6 +184,12 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
toggleComposeEditingAvatar,
|
||||
toggleConversationInChooseMembers,
|
||||
}) => {
|
||||
const [preferredWidth, setPreferredWidth] = useState(
|
||||
// This clamp is present just in case we get a bogus value from storage.
|
||||
clamp(preferredWidthFromStorage, MIN_WIDTH, MAX_WIDTH)
|
||||
);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
|
||||
const previousModeSpecificProps = usePrevious(
|
||||
modeSpecificProps,
|
||||
modeSpecificProps
|
||||
|
@ -349,6 +376,70 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
startComposing,
|
||||
]);
|
||||
|
||||
const requiresFullWidth = helper.requiresFullWidth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResizing) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
let width: number;
|
||||
if (requiresFullWidth) {
|
||||
width = Math.max(event.clientX, MIN_FULL_WIDTH);
|
||||
} else if (event.clientX < MIN_SNAP_WIDTH) {
|
||||
width = MIN_WIDTH;
|
||||
} else {
|
||||
width = Math.max(event.clientX, MIN_WIDTH);
|
||||
}
|
||||
setPreferredWidth(Math.min(width, MAX_WIDTH));
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
document.body.addEventListener('mousemove', onMouseMove);
|
||||
document.body.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('mousemove', onMouseMove);
|
||||
document.body.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}, [isResizing, requiresFullWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResizing) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
document.body.classList.add('is-resizing-left-pane');
|
||||
return () => {
|
||||
document.body.classList.remove('is-resizing-left-pane');
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizing || preferredWidth === preferredWidthFromStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
savePreferredLeftPaneWidth(preferredWidth);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [
|
||||
isResizing,
|
||||
preferredWidth,
|
||||
preferredWidthFromStorage,
|
||||
savePreferredLeftPaneWidth,
|
||||
]);
|
||||
|
||||
const preRowsNode = helper.getPreRowsNode({
|
||||
clearGroupCreationError,
|
||||
closeCantAddContactToGroupModal,
|
||||
|
@ -392,6 +483,15 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
selectedConversationId
|
||||
);
|
||||
|
||||
let width: number;
|
||||
if (requiresFullWidth) {
|
||||
width = Math.max(preferredWidth, MIN_FULL_WIDTH);
|
||||
} else if (preferredWidth < MIN_SNAP_WIDTH) {
|
||||
width = MIN_WIDTH;
|
||||
} else {
|
||||
width = preferredWidth;
|
||||
}
|
||||
|
||||
const isScrollable = helper.isScrollable();
|
||||
|
||||
let rowIndexToScrollTo: undefined | number;
|
||||
|
@ -413,13 +513,22 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
// It also ensures that we scroll to the top when switching views.
|
||||
const listKey = preRowsNode ? 1 : 0;
|
||||
|
||||
const widthBreakpoint = getConversationListWidthBreakpoint(width);
|
||||
|
||||
// We disable this lint rule because we're trying to capture bubbled events. See [the
|
||||
// lint rule's docs][0].
|
||||
//
|
||||
// [0]: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/645900a0e296ca7053dbf6cd9e12cc85849de2d5/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
return (
|
||||
<div className="module-left-pane">
|
||||
<div
|
||||
className={classNames(
|
||||
'module-left-pane',
|
||||
isResizing && 'module-left-pane--is-resizing',
|
||||
`module-left-pane--width-${widthBreakpoint}`
|
||||
)}
|
||||
style={{ width }}
|
||||
>
|
||||
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
|
||||
<div className="module-left-pane__header">
|
||||
{helper.getHeaderContents({
|
||||
|
@ -429,10 +538,10 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
showChooseGroupMembers,
|
||||
}) || renderMainHeader()}
|
||||
</div>
|
||||
{renderExpiredBuildDialog()}
|
||||
{renderRelinkDialog()}
|
||||
{renderNetworkStatus()}
|
||||
{renderUpdateDialog()}
|
||||
{renderExpiredBuildDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{renderRelinkDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{renderNetworkStatus({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{renderUpdateDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{preRowsNode && <React.Fragment key={0}>{preRowsNode}</React.Fragment>}
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }: MeasuredComponentProps) => (
|
||||
|
@ -489,6 +598,17 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
{footerContents && (
|
||||
<div className="module-left-pane__footer">{footerContents}</div>
|
||||
)}
|
||||
{canResizeLeftPane && (
|
||||
<>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className="module-left-pane__resize-grab-area"
|
||||
onMouseDown={() => {
|
||||
setIsResizing(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{challengeStatus !== 'idle' &&
|
||||
renderCaptchaDialog({
|
||||
onSkip() {
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import React, { ReactChild, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
|
||||
const BASE_CLASS_NAME = 'LeftPaneDialog';
|
||||
const TOOLTIP_CLASS_NAME = `${BASE_CLASS_NAME}__tooltip`;
|
||||
|
||||
export type PropsType = {
|
||||
type?: 'warning' | 'error';
|
||||
icon?: 'update' | 'relink' | 'network' | ReactNode;
|
||||
icon?: 'update' | 'relink' | 'network' | 'warning' | ReactChild;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children?: ReactNode;
|
||||
hoverText?: string;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
} & (
|
||||
| {
|
||||
onClick?: undefined;
|
||||
|
@ -38,7 +43,7 @@ export type PropsType = {
|
|||
);
|
||||
|
||||
export const LeftPaneDialog: React.FC<PropsType> = ({
|
||||
icon,
|
||||
icon = 'warning',
|
||||
type,
|
||||
onClick,
|
||||
clickLabel,
|
||||
|
@ -48,6 +53,7 @@ export const LeftPaneDialog: React.FC<PropsType> = ({
|
|||
hoverText,
|
||||
hasAction,
|
||||
|
||||
containerWidthBreakpoint,
|
||||
hasXButton,
|
||||
onClose,
|
||||
closeLabel,
|
||||
|
@ -123,23 +129,28 @@ export const LeftPaneDialog: React.FC<PropsType> = ({
|
|||
onClick === undefined ? undefined : `${BASE_CLASS_NAME}--clickable`,
|
||||
]);
|
||||
|
||||
const message = (
|
||||
<>
|
||||
{title === undefined ? undefined : <h3>{title}</h3>}
|
||||
{subtitle === undefined ? undefined : <div>{subtitle}</div>}
|
||||
{children}
|
||||
{action}
|
||||
</>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className={`${BASE_CLASS_NAME}__container`}>
|
||||
{typeof icon === 'string' ? <div className={iconClassName} /> : icon}
|
||||
<div className={`${BASE_CLASS_NAME}__message`}>
|
||||
{title === undefined ? undefined : <h3>{title}</h3>}
|
||||
{subtitle === undefined ? undefined : <div>{subtitle}</div>}
|
||||
{children}
|
||||
{action}
|
||||
</div>
|
||||
<div className={`${BASE_CLASS_NAME}__message`}>{message}</div>
|
||||
</div>
|
||||
{xButton}
|
||||
</>
|
||||
);
|
||||
|
||||
let dialogNode: ReactChild;
|
||||
if (onClick) {
|
||||
return (
|
||||
dialogNode = (
|
||||
<div
|
||||
className={className}
|
||||
role="button"
|
||||
|
@ -152,11 +163,28 @@ export const LeftPaneDialog: React.FC<PropsType> = ({
|
|||
{content}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
dialogNode = (
|
||||
<div className={className} title={hoverText}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} title={hoverText}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
if (containerWidthBreakpoint === WidthBreakpoint.Narrow) {
|
||||
return (
|
||||
<Tooltip
|
||||
content={message}
|
||||
direction={TooltipPlacement.Right}
|
||||
className={classNames(
|
||||
TOOLTIP_CLASS_NAME,
|
||||
type && `${TOOLTIP_CLASS_NAME}--${type}`
|
||||
)}
|
||||
>
|
||||
{dialogNode}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return dialogNode;
|
||||
};
|
||||
|
|
|
@ -230,6 +230,13 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
this.setFocus();
|
||||
};
|
||||
|
||||
private handleInputBlur = (): void => {
|
||||
const { clearSearch, searchConversationId, searchTerm } = this.props;
|
||||
if (!searchConversationId && !searchTerm) {
|
||||
clearSearch();
|
||||
}
|
||||
};
|
||||
|
||||
public handleInputKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
): void => {
|
||||
|
@ -478,6 +485,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
)}
|
||||
placeholder={placeholder}
|
||||
dir="auto"
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleInputKeyDown}
|
||||
value={searchTerm}
|
||||
onChange={this.updateSearch}
|
||||
|
|
|
@ -68,6 +68,7 @@ export enum TooltipPlacement {
|
|||
|
||||
export type PropsType = {
|
||||
content: string | JSX.Element;
|
||||
className?: string;
|
||||
direction?: TooltipPlacement;
|
||||
sticky?: boolean;
|
||||
theme?: Theme;
|
||||
|
@ -75,6 +76,7 @@ export type PropsType = {
|
|||
|
||||
export const Tooltip: React.FC<PropsType> = ({
|
||||
children,
|
||||
className,
|
||||
content,
|
||||
direction,
|
||||
sticky,
|
||||
|
@ -101,7 +103,11 @@ export const Tooltip: React.FC<PropsType> = ({
|
|||
{({ arrowProps, placement, ref, style }) =>
|
||||
showTooltip && (
|
||||
<div
|
||||
className={classNames('module-tooltip', tooltipThemeClassName)}
|
||||
className={classNames(
|
||||
'module-tooltip',
|
||||
tooltipThemeClassName,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
style={style}
|
||||
data-placement={placement}
|
||||
|
|
|
@ -4,3 +4,14 @@
|
|||
export function cleanId(id: string): string {
|
||||
return id.replace(/[^\u0020-\u007e\u00a0-\u00ff]/g, '_');
|
||||
}
|
||||
|
||||
export enum WidthBreakpoint {
|
||||
Wide = 'wide',
|
||||
Medium = 'medium',
|
||||
Narrow = 'narrow',
|
||||
}
|
||||
|
||||
export const getConversationListWidthBreakpoint = (
|
||||
width: number
|
||||
): WidthBreakpoint =>
|
||||
width >= 150 ? WidthBreakpoint.Wide : WidthBreakpoint.Narrow;
|
||||
|
|
|
@ -27,6 +27,7 @@ import { setupI18n } from '../../util/setupI18n';
|
|||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { pngUrl } from '../../storybook/Fixtures';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
||||
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
|
||||
|
||||
|
@ -102,6 +103,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
collapseMetadata: overrideProps.collapseMetadata,
|
||||
containerElementRef: React.createRef<HTMLElement>(),
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
conversationColor:
|
||||
overrideProps.conversationColor ||
|
||||
select('conversationColor', ConversationColors, ConversationColors[0]),
|
||||
|
|
|
@ -34,6 +34,7 @@ import { Emoji } from '../emoji/Emoji';
|
|||
import { LinkPreviewDate } from './LinkPreviewDate';
|
||||
import { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||
import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseFullSizeLinkPreviewImage';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
import {
|
||||
|
@ -194,6 +195,7 @@ export type PropsData = {
|
|||
|
||||
export type PropsHousekeeping = {
|
||||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
interactionMode: InteractionModeType;
|
||||
theme?: ThemeType;
|
||||
|
@ -1450,9 +1452,13 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
`module-message__buttons--${direction}`
|
||||
)}
|
||||
>
|
||||
{canReply ? reactButton : null}
|
||||
{canDownload ? downloadButton : null}
|
||||
{canReply ? replyButton : null}
|
||||
{this.shouldShowAdditionalMenuButtons() && (
|
||||
<>
|
||||
{canReply ? reactButton : null}
|
||||
{canDownload ? downloadButton : null}
|
||||
{canReply ? replyButton : null}
|
||||
</>
|
||||
)}
|
||||
{menuButton}
|
||||
</div>
|
||||
{reactionPickerRoot &&
|
||||
|
@ -1523,6 +1529,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const menu = (
|
||||
<ContextMenu id={triggerId}>
|
||||
{canDownload &&
|
||||
!this.shouldShowAdditionalMenuButtons() &&
|
||||
!isSticker &&
|
||||
!multipleAttachments &&
|
||||
!isTapToView &&
|
||||
|
@ -1538,7 +1545,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
{i18n('downloadAttachment')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{canReply ? (
|
||||
{canReply && !this.shouldShowAdditionalMenuButtons() ? (
|
||||
<>
|
||||
<MenuItem
|
||||
attributes={{
|
||||
|
@ -1652,6 +1659,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return ReactDOM.createPortal(menu, document.body);
|
||||
}
|
||||
|
||||
private shouldShowAdditionalMenuButtons(): boolean {
|
||||
const { containerWidthBreakpoint } = this.props;
|
||||
return containerWidthBreakpoint !== WidthBreakpoint.Narrow;
|
||||
}
|
||||
|
||||
public getWidth(): number | undefined {
|
||||
const { attachments, isSticker, previews } = this.props;
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { ConversationType } from '../../state/ducks/conversations';
|
|||
import { groupBy } from '../../util/mapUtil';
|
||||
import { ContactNameColorType } from '../../types/Colors';
|
||||
import { SendStatus } from '../../messages/MessageSendState';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
import * as log from '../../logging/log';
|
||||
import { Timestamp } from './Timestamp';
|
||||
|
||||
|
@ -301,6 +302,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
clearSelectedMessage={clearSelectedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
containerElementRef={this.messageContainerRef}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
||||
deleteMessage={() =>
|
||||
log.warn('MessageDetail: deleteMessage called!')
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { ReadStatus } from '../../messages/MessageReadStatus';
|
|||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -39,6 +40,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('default--clearSelectedMessage'),
|
||||
containerElementRef: React.createRef<HTMLElement>(),
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversationId',
|
||||
conversationType: 'direct', // override
|
||||
|
|
|
@ -21,6 +21,7 @@ import { TimelineLoadingRow } from './TimelineLoadingRow';
|
|||
import { TypingBubble } from './TypingBubble';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -369,9 +370,11 @@ const actions = () => ({
|
|||
const renderItem = ({
|
||||
messageId,
|
||||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
}: {
|
||||
messageId: string;
|
||||
containerElementRef: React.RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => (
|
||||
<TimelineItem
|
||||
id=""
|
||||
|
@ -384,6 +387,7 @@ const renderItem = ({
|
|||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
containerElementRef={containerElementRef}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
conversationId=""
|
||||
renderContact={() => '*ContactName*'}
|
||||
renderUniversalTimerNotification={() => (
|
||||
|
|
|
@ -20,6 +20,8 @@ import { AssertProps, LocalizerType } from '../../types/Util';
|
|||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { assert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { createRefMerger } from '../../util/refMerger';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
||||
import { PropsActions as MessageActionsType } from './Message';
|
||||
import { PropsActions as UnsupportedMessageActionsType } from './UnsupportedMessage';
|
||||
|
@ -106,6 +108,7 @@ type PropsHousekeepingType = {
|
|||
renderItem: (props: {
|
||||
actionProps: PropsActionsType;
|
||||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
|
@ -207,6 +210,8 @@ type StateType = {
|
|||
atTop: boolean;
|
||||
oneTimeScrollRow?: number;
|
||||
|
||||
widthBreakpoint: WidthBreakpoint;
|
||||
|
||||
prevPropScrollToIndex?: number;
|
||||
prevPropScrollToIndexCounter?: number;
|
||||
propScrollToIndex?: number;
|
||||
|
@ -307,6 +312,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
|
||||
public loadCountdownTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
private containerRefMerger = createRefMerger();
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
|
@ -328,6 +335,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
areUnreadBelowCurrentPosition: false,
|
||||
hasDismissedDirectContactSpoofingWarning: false,
|
||||
lastMeasuredWarningHeight: 0,
|
||||
// This may be swiftly overridden.
|
||||
widthBreakpoint: WidthBreakpoint.Wide,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -740,7 +749,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
unblurAvatar,
|
||||
updateSharedGroups,
|
||||
} = this.props;
|
||||
const { lastMeasuredWarningHeight } = this.state;
|
||||
const { lastMeasuredWarningHeight, widthBreakpoint } = this.state;
|
||||
|
||||
const styleWithWidth = {
|
||||
...style,
|
||||
|
@ -813,6 +822,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
{renderItem({
|
||||
actionProps,
|
||||
containerElementRef: this.containerRef,
|
||||
containerWidthBreakpoint: widthBreakpoint,
|
||||
conversationId: id,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
|
@ -1324,6 +1334,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
const {
|
||||
shouldShowScrollDownButton,
|
||||
areUnreadBelowCurrentPosition,
|
||||
widthBreakpoint,
|
||||
} = this.state;
|
||||
|
||||
const rowCount = this.getRowCount();
|
||||
|
@ -1515,30 +1526,42 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-timeline',
|
||||
isGroupV1AndDisabled ? 'module-timeline--disabled' : null
|
||||
)}
|
||||
role="presentation"
|
||||
tabIndex={-1}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.containerRef}
|
||||
<Measure
|
||||
bounds
|
||||
onResize={({ bounds }) => {
|
||||
this.setState({
|
||||
widthBreakpoint: getWidthBreakpoint(bounds?.width || 0),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{timelineWarning}
|
||||
{({ measureRef }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-timeline',
|
||||
isGroupV1AndDisabled ? 'module-timeline--disabled' : null,
|
||||
`module-timeline--width-${widthBreakpoint}`
|
||||
)}
|
||||
role="presentation"
|
||||
tabIndex={-1}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.containerRefMerger(measureRef)}
|
||||
>
|
||||
{timelineWarning}
|
||||
|
||||
{autoSizer}
|
||||
{autoSizer}
|
||||
|
||||
{shouldShowScrollDownButton ? (
|
||||
<ScrollDownButton
|
||||
conversationId={id}
|
||||
withNewMessages={areUnreadBelowCurrentPosition}
|
||||
scrollDown={this.onClickScrollDownButton}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{shouldShowScrollDownButton ? (
|
||||
<ScrollDownButton
|
||||
conversationId={id}
|
||||
withNewMessages={areUnreadBelowCurrentPosition}
|
||||
scrollDown={this.onClickScrollDownButton}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
|
||||
{Boolean(invitedContactsForNewlyCreatedGroup.length) && (
|
||||
<NewlyCreatedGroupInvitedContactsDialog
|
||||
|
@ -1578,3 +1601,13 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWidthBreakpoint(width: number): WidthBreakpoint {
|
||||
if (width > 606) {
|
||||
return WidthBreakpoint.Wide;
|
||||
}
|
||||
if (width > 514) {
|
||||
return WidthBreakpoint.Medium;
|
||||
}
|
||||
return WidthBreakpoint.Narrow;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { UniversalTimerNotification } from './UniversalTimerNotification';
|
|||
import { CallMode } from '../../types/Calling';
|
||||
import { AvatarColors } from '../../types/Colors';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -46,6 +47,7 @@ const renderUniversalTimerNotification = () => (
|
|||
|
||||
const getDefaultProps = () => ({
|
||||
containerElementRef: React.createRef<HTMLElement>(),
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
conversationId: 'conversation-id',
|
||||
id: 'asdf',
|
||||
isSelected: false,
|
||||
|
|
|
@ -181,7 +181,10 @@ export type PropsType = PropsLocalType &
|
|||
PropsActionsType &
|
||||
Pick<
|
||||
AllMessageProps,
|
||||
'renderEmojiPicker' | 'renderAudioAttachment' | 'renderReactionPicker'
|
||||
| 'containerWidthBreakpoint'
|
||||
| 'renderEmojiPicker'
|
||||
| 'renderAudioAttachment'
|
||||
| 'renderReactionPicker'
|
||||
>;
|
||||
|
||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
@ -16,7 +16,6 @@ export type Props = {
|
|||
withImageNoCaption?: boolean;
|
||||
withSticker?: boolean;
|
||||
withTapToViewExpired?: boolean;
|
||||
withUnread?: boolean;
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
@ -58,7 +57,6 @@ export class Timestamp extends React.Component<Props> {
|
|||
withImageNoCaption,
|
||||
withSticker,
|
||||
withTapToViewExpired,
|
||||
withUnread,
|
||||
extended,
|
||||
} = this.props;
|
||||
const moduleName = module || 'module-timestamp';
|
||||
|
@ -76,8 +74,7 @@ export class Timestamp extends React.Component<Props> {
|
|||
? `${moduleName}--${direction}-with-tap-to-view-expired`
|
||||
: null,
|
||||
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
|
||||
withSticker ? `${moduleName}--with-sticker` : null,
|
||||
withUnread ? `${moduleName}--with-unread` : null
|
||||
withSticker ? `${moduleName}--with-sticker` : null
|
||||
)}
|
||||
title={moment(timestamp).format('llll')}
|
||||
>
|
||||
|
|
|
@ -36,6 +36,7 @@ type PropsType = {
|
|||
messageId?: string;
|
||||
messageStatusIcon?: ReactNode;
|
||||
messageText?: ReactNode;
|
||||
messageTextIsAlwaysFullSize?: boolean;
|
||||
onClick?: () => void;
|
||||
unreadCount?: number;
|
||||
} & Pick<
|
||||
|
@ -71,6 +72,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
markedUnread,
|
||||
messageStatusIcon,
|
||||
messageText,
|
||||
messageTextIsAlwaysFullSize,
|
||||
name,
|
||||
onClick,
|
||||
phoneNumber,
|
||||
|
@ -119,29 +121,22 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
|
||||
const contents = (
|
||||
<>
|
||||
<div className={`${BASE_CLASS_NAME}__avatar-container`}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={conversationType}
|
||||
noteToSelf={isAvatarNoteToSelf}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FIFTY_TWO}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
{isUnread && (
|
||||
<div className={`${BASE_CLASS_NAME}__unread-count`}>
|
||||
{formatUnreadCount(unreadCount)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={conversationType}
|
||||
noteToSelf={isAvatarNoteToSelf}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
CONTENT_CLASS_NAME,
|
||||
|
@ -151,32 +146,36 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
<div className={HEADER_CLASS_NAME}>
|
||||
<div className={`${HEADER_CLASS_NAME}__name`}>{headerName}</div>
|
||||
{isNumber(headerDate) && (
|
||||
<div
|
||||
className={classNames(DATE_CLASS_NAME, {
|
||||
[`${DATE_CLASS_NAME}--has-unread`]: isUnread,
|
||||
})}
|
||||
>
|
||||
<div className={DATE_CLASS_NAME}>
|
||||
<Timestamp
|
||||
timestamp={headerDate}
|
||||
extended={false}
|
||||
module={TIMESTAMP_CLASS_NAME}
|
||||
withUnread={isUnread}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{messageText ? (
|
||||
{messageText || isUnread ? (
|
||||
<div className={MESSAGE_CLASS_NAME}>
|
||||
<div
|
||||
dir="auto"
|
||||
className={classNames(MESSAGE_TEXT_CLASS_NAME, {
|
||||
[`${MESSAGE_TEXT_CLASS_NAME}--has-unread`]: isUnread,
|
||||
})}
|
||||
>
|
||||
{messageText}
|
||||
</div>
|
||||
{Boolean(messageText) && (
|
||||
<div
|
||||
dir="auto"
|
||||
className={classNames(
|
||||
MESSAGE_TEXT_CLASS_NAME,
|
||||
messageTextIsAlwaysFullSize &&
|
||||
`${MESSAGE_TEXT_CLASS_NAME}--always-full-size`
|
||||
)}
|
||||
>
|
||||
{messageText}
|
||||
</div>
|
||||
)}
|
||||
{messageStatusIcon}
|
||||
{isUnread && (
|
||||
<div className={`${BASE_CLASS_NAME}__unread-count`}>
|
||||
{formatUnreadCount(unreadCount)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -185,7 +184,6 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
);
|
||||
|
||||
const commonClassNames = classNames(BASE_CLASS_NAME, {
|
||||
[`${BASE_CLASS_NAME}--has-unread`]: isUnread,
|
||||
[`${BASE_CLASS_NAME}--is-selected`]: isSelected,
|
||||
});
|
||||
|
||||
|
|
|
@ -173,6 +173,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
markedUnread={markedUnread}
|
||||
messageStatusIcon={messageStatusIcon}
|
||||
messageText={messageText}
|
||||
messageTextIsAlwaysFullSize
|
||||
name={name}
|
||||
onClick={onClickItem}
|
||||
phoneNumber={phoneNumber}
|
||||
|
@ -193,5 +194,5 @@ function truncateMessageText(text: unknown): string {
|
|||
if (typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return text.split('\n', 1)[0];
|
||||
return text.replace(/(?:\r?\n)+/g, ' ');
|
||||
}
|
||||
|
|
|
@ -9,8 +9,9 @@ export const SearchResultsLoadingFakeRow: FunctionComponent<PropsType> = () => (
|
|||
<div className="module-SearchResultsLoadingFakeRow">
|
||||
<div className="module-SearchResultsLoadingFakeRow__avatar" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content">
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__header" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__message" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__line" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__line" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__line" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -93,6 +93,10 @@ export abstract class LeftPaneHelper<T> {
|
|||
return true;
|
||||
}
|
||||
|
||||
requiresFullWidth(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string; messageId?: string };
|
||||
|
|
|
@ -15,6 +15,8 @@ export type LeftPaneInboxPropsType = {
|
|||
conversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
pinnedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
isAboutToSearchInAConversation: boolean;
|
||||
startSearchCounter: number;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
@ -26,16 +28,24 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
|||
|
||||
private readonly pinnedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
|
||||
private readonly isAboutToSearchInAConversation: boolean;
|
||||
|
||||
private readonly startSearchCounter: number;
|
||||
|
||||
constructor({
|
||||
conversations,
|
||||
archivedConversations,
|
||||
pinnedConversations,
|
||||
isAboutToSearchInAConversation,
|
||||
startSearchCounter,
|
||||
}: Readonly<LeftPaneInboxPropsType>) {
|
||||
super();
|
||||
|
||||
this.conversations = conversations;
|
||||
this.archivedConversations = archivedConversations;
|
||||
this.pinnedConversations = pinnedConversations;
|
||||
this.isAboutToSearchInAConversation = isAboutToSearchInAConversation;
|
||||
this.startSearchCounter = startSearchCounter;
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
|
@ -176,6 +186,18 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
|||
return undefined;
|
||||
}
|
||||
|
||||
requiresFullWidth(): boolean {
|
||||
const hasNoConversations =
|
||||
!this.conversations.length &&
|
||||
!this.pinnedConversations.length &&
|
||||
!this.archivedConversations.length;
|
||||
return (
|
||||
hasNoConversations ||
|
||||
this.isAboutToSearchInAConversation ||
|
||||
Boolean(this.startSearchCounter)
|
||||
);
|
||||
}
|
||||
|
||||
shouldRecomputeRowHeights(old: Readonly<LeftPaneInboxPropsType>): boolean {
|
||||
return old.pinnedConversations.length !== this.pinnedConversations.length;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { assert } from '../../util/assert';
|
|||
|
||||
// The "correct" thing to do is to measure the size of the left pane and render enough
|
||||
// search results for the container height. But (1) that's slow (2) the list is
|
||||
// virtualized (3) 99 rows is over 6000px tall, taller than most monitors (4) it's fine
|
||||
// virtualized (3) 99 rows is over 7500px tall, taller than most monitors (4) it's fine
|
||||
// if, in some extremely tall window, we have some empty space. So we just hard-code a
|
||||
// fairly big number.
|
||||
const SEARCH_RESULTS_FAKE_ROW_COUNT = 99;
|
||||
|
|
|
@ -30,6 +30,8 @@ export type ItemsStateType = {
|
|||
|
||||
readonly customColors?: CustomColorsItemType;
|
||||
|
||||
readonly preferredLeftPaneWidth?: number;
|
||||
|
||||
readonly preferredReactionEmoji?: Array<string>;
|
||||
};
|
||||
|
||||
|
@ -76,6 +78,7 @@ export const actions = {
|
|||
editCustomColor,
|
||||
removeCustomColor,
|
||||
resetDefaultChatColor,
|
||||
savePreferredLeftPaneWidth,
|
||||
setGlobalDefaultConversationColor,
|
||||
onSetSkinTone,
|
||||
putItem,
|
||||
|
@ -256,6 +259,14 @@ function setGlobalDefaultConversationColor(
|
|||
};
|
||||
}
|
||||
|
||||
function savePreferredLeftPaneWidth(
|
||||
preferredWidth: number
|
||||
): ThunkAction<void, RootStateType, unknown, ItemPutAction> {
|
||||
return dispatch => {
|
||||
dispatch(putItem('preferredLeftPaneWidth', preferredWidth));
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
function getEmptyState(): ItemsStateType {
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
} from '../../types/Colors';
|
||||
import { getPreferredReactionEmoji as getPreferredReactionEmojiFromStoredValue } from '../../reactions/preferredReactionEmoji';
|
||||
|
||||
const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
||||
|
||||
export const getItems = (state: StateType): ItemsStateType => state.items;
|
||||
|
||||
export const getUserAgent = createSelector(
|
||||
|
@ -63,6 +65,15 @@ export const getEmojiSkinTone = createSelector(
|
|||
: 0
|
||||
);
|
||||
|
||||
export const getPreferredLeftPaneWidth = createSelector(
|
||||
getItems,
|
||||
({ preferredLeftPaneWidth }: Readonly<ItemsStateType>): number =>
|
||||
typeof preferredLeftPaneWidth === 'number' &&
|
||||
Number.isInteger(preferredLeftPaneWidth)
|
||||
? preferredLeftPaneWidth
|
||||
: DEFAULT_PREFERRED_LEFT_PANE_WIDTH
|
||||
);
|
||||
|
||||
export const getPreferredReactionEmoji = createSelector(
|
||||
getItems,
|
||||
getEmojiSkinTone,
|
||||
|
|
|
@ -48,6 +48,11 @@ export const getSearchConversationId = createSelector(
|
|||
(state: SearchStateType): string | undefined => state.searchConversationId
|
||||
);
|
||||
|
||||
export const getIsSearchingInAConversation = createSelector(
|
||||
getSearchConversationId,
|
||||
Boolean
|
||||
);
|
||||
|
||||
export const getSearchConversationName = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType): string | undefined => state.searchConversationName
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -6,11 +6,15 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
|
||||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
||||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
hasExpired: state.expiration.hasExpired,
|
||||
i18n: getIntl(state),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,8 +14,14 @@ import { StateType } from '../reducer';
|
|||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
import { ComposerStep, OneTimeModalState } from '../ducks/conversations';
|
||||
import { getSearchResults, isSearching } from '../selectors/search';
|
||||
import {
|
||||
getIsSearchingInAConversation,
|
||||
getSearchResults,
|
||||
getStartSearchCounter,
|
||||
isSearching,
|
||||
} from '../selectors/search';
|
||||
import { getIntl, getRegionCode } from '../selectors/user';
|
||||
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
||||
import {
|
||||
getCantAddContactForModal,
|
||||
getComposeAvatarData,
|
||||
|
@ -38,6 +44,7 @@ import {
|
|||
isCreatingGroup,
|
||||
isEditingAvatar,
|
||||
} from '../selectors/conversations';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
import { SmartExpiredBuildDialog } from './ExpiredBuildDialog';
|
||||
import { SmartMainHeader } from './MainHeader';
|
||||
|
@ -49,8 +56,10 @@ import { SmartCaptchaDialog } from './CaptchaDialog';
|
|||
|
||||
const FilteredSmartMessageSearchResult = SmartMessageSearchResult;
|
||||
|
||||
function renderExpiredBuildDialog(): JSX.Element {
|
||||
return <SmartExpiredBuildDialog />;
|
||||
function renderExpiredBuildDialog(
|
||||
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
): JSX.Element {
|
||||
return <SmartExpiredBuildDialog {...props} />;
|
||||
}
|
||||
function renderMainHeader(): JSX.Element {
|
||||
return <SmartMainHeader />;
|
||||
|
@ -58,14 +67,20 @@ function renderMainHeader(): JSX.Element {
|
|||
function renderMessageSearchResult(id: string): JSX.Element {
|
||||
return <FilteredSmartMessageSearchResult id={id} />;
|
||||
}
|
||||
function renderNetworkStatus(): JSX.Element {
|
||||
return <SmartNetworkStatus />;
|
||||
function renderNetworkStatus(
|
||||
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
): JSX.Element {
|
||||
return <SmartNetworkStatus {...props} />;
|
||||
}
|
||||
function renderRelinkDialog(): JSX.Element {
|
||||
return <SmartRelinkDialog />;
|
||||
function renderRelinkDialog(
|
||||
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
): JSX.Element {
|
||||
return <SmartRelinkDialog {...props} />;
|
||||
}
|
||||
function renderUpdateDialog(): JSX.Element {
|
||||
return <SmartUpdateDialog />;
|
||||
function renderUpdateDialog(
|
||||
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
): JSX.Element {
|
||||
return <SmartUpdateDialog {...props} />;
|
||||
}
|
||||
function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
||||
return <SmartCaptchaDialog onSkip={onSkip} />;
|
||||
|
@ -97,6 +112,8 @@ const getModeSpecificProps = (
|
|||
}
|
||||
return {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
isAboutToSearchInAConversation: getIsSearchingInAConversation(state),
|
||||
startSearchCounter: getStartSearchCounter(state),
|
||||
...getLeftPaneLists(state),
|
||||
};
|
||||
case ComposerStep.StartDirectConversation:
|
||||
|
@ -140,6 +157,10 @@ const getModeSpecificProps = (
|
|||
const mapStateToProps = (state: StateType) => {
|
||||
return {
|
||||
modeSpecificProps: getModeSpecificProps(state),
|
||||
canResizeLeftPane: window.Signal.RemoteConfig.isEnabled(
|
||||
'desktop.internalUser'
|
||||
),
|
||||
preferredWidthFromStorage: getPreferredLeftPaneWidth(state),
|
||||
selectedConversationId: getSelectedConversationId(state),
|
||||
selectedMessageId: getSelectedMessage(state)?.id,
|
||||
showArchived: getShowArchived(state),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -7,12 +7,16 @@ import { DialogNetworkStatus } from '../../components/DialogNetworkStatus';
|
|||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
||||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
...state.network,
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
i18n: getIntl(state),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -7,11 +7,15 @@ import { DialogRelink } from '../../components/DialogRelink';
|
|||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { isDone } from '../../util/registration';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
||||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
i18n: getIntl(state),
|
||||
isRegistrationDone: isDone(),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
invertIdsByTitle,
|
||||
} from '../../util/groupMemberNameCollisions';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
|
@ -109,6 +110,7 @@ const createBoundOnHeightChange = memoizee(
|
|||
function renderItem({
|
||||
actionProps,
|
||||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
conversationId,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
|
@ -117,6 +119,7 @@ function renderItem({
|
|||
}: {
|
||||
actionProps: TimelineActionsType;
|
||||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
|
@ -127,6 +130,7 @@ function renderItem({
|
|||
<SmartTimelineItem
|
||||
{...actionProps}
|
||||
containerElementRef={containerElementRef}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
conversationId={conversationId}
|
||||
messageId={messageId}
|
||||
previousMessageId={previousMessageId}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -7,12 +7,16 @@ import { DialogUpdate } from '../../components/DialogUpdate';
|
|||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
||||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
...state.updates,
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
i18n: getIntl(state),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
29
ts/test-both/helpers/FakeLeftPaneContainer.tsx
Normal file
29
ts/test-both/helpers/FakeLeftPaneContainer.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
};
|
||||
|
||||
const WIDTHS = {
|
||||
[WidthBreakpoint.Wide]: 350,
|
||||
[WidthBreakpoint.Medium]: 280,
|
||||
[WidthBreakpoint.Narrow]: 130,
|
||||
};
|
||||
|
||||
export const FakeLeftPaneContainer: FunctionComponent<PropsType> = ({
|
||||
children,
|
||||
containerWidthBreakpoint,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`module-left-pane--width-${containerWidthBreakpoint}`}
|
||||
style={{ width: WIDTHS[containerWidthBreakpoint] }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -5,6 +5,7 @@ import { assert } from 'chai';
|
|||
import {
|
||||
getEmojiSkinTone,
|
||||
getPinnedConversationIds,
|
||||
getPreferredLeftPaneWidth,
|
||||
getPreferredReactionEmoji,
|
||||
} from '../../../state/selectors/items';
|
||||
import type { StateType } from '../../../state/reducer';
|
||||
|
@ -50,6 +51,32 @@ describe('both/state/selectors/items', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getPreferredLeftPaneWidth', () => {
|
||||
it('returns a default if no value is present', () => {
|
||||
const state = getRootState({});
|
||||
assert.strictEqual(getPreferredLeftPaneWidth(state), 320);
|
||||
});
|
||||
|
||||
it('returns a default value if passed something invalid', () => {
|
||||
[undefined, null, '250', [250], 250.123].forEach(
|
||||
preferredLeftPaneWidth => {
|
||||
const state = getRootState({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
preferredLeftPaneWidth: preferredLeftPaneWidth as any,
|
||||
});
|
||||
assert.strictEqual(getPreferredLeftPaneWidth(state), 320);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the value in storage if it is valid', () => {
|
||||
const state = getRootState({
|
||||
preferredLeftPaneWidth: 345,
|
||||
});
|
||||
assert.strictEqual(getPreferredLeftPaneWidth(state), 345);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getPinnedConversationIds', () => {
|
||||
it('returns pinnedConversationIds key from items', () => {
|
||||
const expected = ['one', 'two'];
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../../../state/ducks/search';
|
||||
import { getEmptyState as getEmptyUserState } from '../../../state/ducks/user';
|
||||
import {
|
||||
getIsSearchingInAConversation,
|
||||
getMessageSearchResultSelector,
|
||||
getSearchResults,
|
||||
} from '../../../state/selectors/search';
|
||||
|
@ -68,6 +69,27 @@ describe('both/state/selectors/search', () => {
|
|||
};
|
||||
}
|
||||
|
||||
describe('#getIsSearchingInAConversation', () => {
|
||||
it('returns false if not searching in a conversation', () => {
|
||||
const state = getEmptyRootState();
|
||||
|
||||
assert.isFalse(getIsSearchingInAConversation(state));
|
||||
});
|
||||
|
||||
it('returns true if searching in a conversation', () => {
|
||||
const state = {
|
||||
...getEmptyRootState(),
|
||||
search: {
|
||||
...getEmptySearchState(),
|
||||
searchConversationId: 'abc123',
|
||||
searchConversationName: 'Test Conversation',
|
||||
},
|
||||
};
|
||||
|
||||
assert.isTrue(getIsSearchingInAConversation(state));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMessageSearchResultSelector', () => {
|
||||
it('returns undefined if message not found in lookup', () => {
|
||||
const state = getEmptyRootState();
|
||||
|
|
|
@ -7,16 +7,23 @@ import { RowType } from '../../../components/ConversationList';
|
|||
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
import { LeftPaneInboxHelper } from '../../../components/leftPane/LeftPaneInboxHelper';
|
||||
import {
|
||||
LeftPaneInboxHelper,
|
||||
LeftPaneInboxPropsType,
|
||||
} from '../../../components/leftPane/LeftPaneInboxHelper';
|
||||
|
||||
describe('LeftPaneInboxHelper', () => {
|
||||
const defaultProps: LeftPaneInboxPropsType = {
|
||||
conversations: [],
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
isAboutToSearchInAConversation: false,
|
||||
startSearchCounter: 0,
|
||||
};
|
||||
|
||||
describe('getBackAction', () => {
|
||||
it("returns undefined; you can't go back from the main inbox", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
});
|
||||
const helper = new LeftPaneInboxHelper(defaultProps);
|
||||
|
||||
assert.isUndefined(
|
||||
helper.getBackAction({
|
||||
|
@ -30,19 +37,14 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
describe('getRowCount', () => {
|
||||
it('returns 0 if there are no conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
});
|
||||
const helper = new LeftPaneInboxHelper(defaultProps);
|
||||
|
||||
assert.strictEqual(helper.getRowCount(), 0);
|
||||
});
|
||||
|
||||
it('returns 1 if there are only archived conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
pinnedConversations: [],
|
||||
...defaultProps,
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
});
|
||||
|
||||
|
@ -51,13 +53,12 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it("returns the number of non-pinned conversations if that's all there is", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(helper.getRowCount(), 3);
|
||||
|
@ -65,13 +66,12 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it("returns the number of pinned conversations if that's all there is", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
...defaultProps,
|
||||
pinnedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(helper.getRowCount(), 3);
|
||||
|
@ -79,13 +79,13 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it('adds 2 rows for each header if there are pinned and non-pinned conversations,', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
pinnedConversations: [getDefaultConversation()],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(helper.getRowCount(), 6);
|
||||
|
@ -93,12 +93,12 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it('adds 1 row for the archive button if there are any archived conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
});
|
||||
|
||||
|
@ -109,9 +109,9 @@ describe('LeftPaneInboxHelper', () => {
|
|||
describe('getRowIndexToScrollTo', () => {
|
||||
it('returns undefined if no conversation is selected', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation(), getDefaultConversation()],
|
||||
pinnedConversations: [getDefaultConversation()],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.isUndefined(helper.getRowIndexToScrollTo(undefined));
|
||||
|
@ -120,6 +120,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
it('returns undefined if the selected conversation is not pinned or non-pinned', () => {
|
||||
const archivedConversations = [getDefaultConversation()];
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation(), getDefaultConversation()],
|
||||
pinnedConversations: [getDefaultConversation()],
|
||||
archivedConversations,
|
||||
|
@ -136,9 +137,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
...defaultProps,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -157,9 +157,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(helper.getRowIndexToScrollTo(conversations[0].id), 0);
|
||||
|
@ -172,9 +171,9 @@ describe('LeftPaneInboxHelper', () => {
|
|||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -193,13 +192,13 @@ describe('LeftPaneInboxHelper', () => {
|
|||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(helper.getRowIndexToScrollTo(conversations[0].id), 5);
|
||||
|
@ -210,8 +209,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
describe('getRow', () => {
|
||||
it('returns the archive button if there are only archived conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
pinnedConversations: [],
|
||||
...defaultProps,
|
||||
archivedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -232,9 +230,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
...defaultProps,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(helper.getRow(0), {
|
||||
|
@ -255,7 +252,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
...defaultProps,
|
||||
pinnedConversations,
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
});
|
||||
|
@ -282,9 +279,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(helper.getRow(0), {
|
||||
|
@ -305,8 +301,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
});
|
||||
|
||||
|
@ -337,9 +333,9 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(helper.getRow(0), {
|
||||
|
@ -385,6 +381,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations,
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
|
@ -439,9 +436,9 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -473,9 +470,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
...defaultProps,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -501,9 +497,8 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -533,9 +528,9 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -556,8 +551,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it('returns undefined if there are no conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
conversations: [],
|
||||
pinnedConversations: [],
|
||||
...defaultProps,
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
});
|
||||
|
||||
|
@ -575,9 +569,9 @@ describe('LeftPaneInboxHelper', () => {
|
|||
];
|
||||
const conversations = [getDefaultConversation()];
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations,
|
||||
pinnedConversations,
|
||||
archivedConversations: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
|
@ -593,9 +587,45 @@ describe('LeftPaneInboxHelper', () => {
|
|||
// Additional tests are found with `getConversationInDirection`.
|
||||
});
|
||||
|
||||
describe('requiresFullWidth', () => {
|
||||
it("returns false if we're not about to search in a conversation and there's at least one", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
});
|
||||
|
||||
assert.isFalse(helper.requiresFullWidth());
|
||||
});
|
||||
|
||||
it('returns true if there are no conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper(defaultProps);
|
||||
|
||||
assert.isTrue(helper.requiresFullWidth());
|
||||
});
|
||||
|
||||
it("returns true if we're about to search", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
startSearchCounter: 1,
|
||||
});
|
||||
|
||||
assert.isTrue(helper.requiresFullWidth());
|
||||
});
|
||||
|
||||
it("returns true if we're about to search in a conversation", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
isAboutToSearchInAConversation: true,
|
||||
});
|
||||
|
||||
assert.isTrue(helper.requiresFullWidth());
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRecomputeRowHeights', () => {
|
||||
it("returns false if the number of conversations in each section doesn't change", () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -610,6 +640,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
assert.isFalse(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -629,6 +660,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it('returns false if the only thing changed is whether conversations are archived', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -643,6 +675,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
assert.isFalse(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -652,13 +685,13 @@ describe('LeftPaneInboxHelper', () => {
|
|||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
archivedConversations: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false if the only thing changed is the number of non-pinned conversations', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -673,6 +706,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
assert.isFalse(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
pinnedConversations: [
|
||||
getDefaultConversation(),
|
||||
|
@ -688,6 +722,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
it('returns true if the number of pinned conversations changes', () => {
|
||||
const helper = new LeftPaneInboxHelper({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
pinnedConversations: [
|
||||
getDefaultConversation(),
|
||||
|
@ -698,6 +733,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
|
||||
assert.isTrue(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
pinnedConversations: [
|
||||
getDefaultConversation(),
|
||||
|
@ -709,6 +745,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
);
|
||||
assert.isTrue(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
pinnedConversations: [getDefaultConversation()],
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
|
@ -716,6 +753,7 @@ describe('LeftPaneInboxHelper', () => {
|
|||
);
|
||||
assert.isTrue(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaultProps,
|
||||
conversations: [getDefaultConversation()],
|
||||
pinnedConversations: [],
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -132,6 +132,7 @@ export type StorageAccessType = {
|
|||
senderCertificateNoE164: SerializedCertificateType;
|
||||
paymentAddress: string;
|
||||
zoomFactor: ZoomFactorType;
|
||||
preferredLeftPaneWidth: number;
|
||||
|
||||
// Deprecated
|
||||
senderCertificateWithUuid: never;
|
||||
|
|
|
@ -25,6 +25,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
|
|||
'preferred-video-input-device',
|
||||
'preferred-audio-input-device',
|
||||
'preferred-audio-output-device',
|
||||
'preferredLeftPaneWidth',
|
||||
'preferredReactionEmoji',
|
||||
'previousAudioDeviceModule',
|
||||
'skinTone',
|
||||
|
|
|
@ -13323,9 +13323,16 @@
|
|||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"line": " this.$('.left-pane-placeholder').replaceWith(this.leftPaneView.el);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
"updated": "2021-10-08T17:39:41.541Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/inbox_view.js",
|
||||
"line": " this.$('.no-conversation-open').toggle(!isAnyConversationOpen);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-10-08T17:40:22.770Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
|
@ -13334,13 +13341,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "ts/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "ts/views/inbox_view.js",
|
||||
|
@ -13407,9 +13407,16 @@
|
|||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/inbox_view.ts",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"line": " this.$('.no-conversation-open').toggle(!isAnyConversationOpen);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
"updated": "2021-10-08T17:40:22.770Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/inbox_view.ts",
|
||||
"line": " this.$('.left-pane-placeholder').replaceWith(this.leftPaneView.el);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-10-08T17:40:22.770Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
|
@ -13418,13 +13425,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "ts/views/inbox_view.ts",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "ts/views/inbox_view.ts",
|
||||
|
@ -14083,4 +14083,4 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-17T21:02:59.414Z"
|
||||
}
|
||||
]
|
||||
]
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2014-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as Backbone from 'backbone';
|
||||
import * as log from '../logging/log';
|
||||
import { ConversationModel } from '../models/conversations';
|
||||
import { showToast } from '../util/showToast';
|
||||
|
@ -9,12 +10,19 @@ import { ToastStickerPackInstallFailed } from '../components/ToastStickerPackIns
|
|||
window.Whisper = window.Whisper || {};
|
||||
const { Whisper } = window;
|
||||
|
||||
const ConversationStack = Whisper.View.extend({
|
||||
className: 'conversation-stack',
|
||||
lastConversation: null,
|
||||
open(conversation: ConversationModel, messageId: string) {
|
||||
const id = `conversation-${conversation.cid}`;
|
||||
if (id !== this.el.lastChild.id) {
|
||||
class ConversationStack extends Backbone.View {
|
||||
public className = 'conversation-stack';
|
||||
|
||||
private conversationStack: Array<ConversationModel> = [];
|
||||
|
||||
private getTopConversation(): undefined | ConversationModel {
|
||||
return this.conversationStack[this.conversationStack.length - 1];
|
||||
}
|
||||
|
||||
public open(conversation: ConversationModel, messageId: string): void {
|
||||
const topConversation = this.getTopConversation();
|
||||
|
||||
if (!topConversation || topConversation.id !== conversation.id) {
|
||||
const view = new Whisper.ConversationView({
|
||||
model: conversation,
|
||||
});
|
||||
|
@ -24,36 +32,43 @@ const ConversationStack = Whisper.View.extend({
|
|||
);
|
||||
view.$el.appendTo(this.el);
|
||||
|
||||
if (this.lastConversation && this.lastConversation !== conversation) {
|
||||
this.lastConversation.trigger('unload', 'opened another conversation');
|
||||
this.stopListening(this.lastConversation);
|
||||
this.lastConversation = null;
|
||||
if (topConversation) {
|
||||
topConversation.trigger('unload', 'opened another conversation');
|
||||
}
|
||||
|
||||
this.lastConversation = conversation;
|
||||
this.conversationStack.push(conversation);
|
||||
|
||||
conversation.trigger('opened', messageId);
|
||||
} else if (messageId) {
|
||||
conversation.trigger('scroll-to-message', messageId);
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
this.getTopConversation()?.trigger('unload', 'force unload requested');
|
||||
}
|
||||
|
||||
private onUnload(conversation: ConversationModel) {
|
||||
this.stopListening(conversation);
|
||||
this.conversationStack = this.conversationStack.filter(
|
||||
(c: ConversationModel) => c !== conversation
|
||||
);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
public render(): ConversationStack {
|
||||
const isAnyConversationOpen = Boolean(this.conversationStack.length);
|
||||
this.$('.no-conversation-open').toggle(!isAnyConversationOpen);
|
||||
|
||||
// Make sure poppers are positioned properly
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
unload() {
|
||||
const { lastConversation } = this;
|
||||
if (!lastConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastConversation.trigger('unload', 'force unload requested');
|
||||
},
|
||||
onUnload(conversation: ConversationModel) {
|
||||
if (this.lastConversation === conversation) {
|
||||
this.stopListening(this.lastConversation);
|
||||
this.lastConversation = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const AppLoadingScreen = Whisper.View.extend({
|
||||
template: () => $('#app-loading-screen').html(),
|
||||
|
@ -71,7 +86,7 @@ const AppLoadingScreen = Whisper.View.extend({
|
|||
|
||||
Whisper.InboxView = Whisper.View.extend({
|
||||
template: () => $('#two-column').html(),
|
||||
className: 'inbox index',
|
||||
className: 'Inbox',
|
||||
initialize(
|
||||
options: {
|
||||
initialLoadComplete?: boolean;
|
||||
|
@ -83,7 +98,6 @@ Whisper.InboxView = Whisper.View.extend({
|
|||
|
||||
this.conversation_stack = new ConversationStack({
|
||||
el: this.$('.conversation-stack'),
|
||||
model: { window: options.window },
|
||||
});
|
||||
|
||||
this.renderWhatsNew();
|
||||
|
@ -166,7 +180,7 @@ Whisper.InboxView = Whisper.View.extend({
|
|||
JSX: window.Signal.State.Roots.createLeftPane(window.reduxStore),
|
||||
});
|
||||
|
||||
this.$('.left-pane-placeholder').append(this.leftPaneView.el);
|
||||
this.$('.left-pane-placeholder').replaceWith(this.leftPaneView.el);
|
||||
},
|
||||
startConnectionListener() {
|
||||
this.interval = setInterval(() => {
|
||||
|
|
Loading…
Reference in a new issue