Show story replies in the timeline
This commit is contained in:
parent
55716c5db6
commit
3620309f22
17 changed files with 705 additions and 461 deletions
|
@ -1334,401 +1334,6 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
}
|
||||
}
|
||||
|
||||
// Module: Quoted Reply
|
||||
|
||||
.module-quote-container {
|
||||
margin: {
|
||||
left: -6px;
|
||||
right: -6px;
|
||||
top: 3px;
|
||||
bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote {
|
||||
@include button-reset;
|
||||
width: 100%;
|
||||
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 2px $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--no-click {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.module-quote--with-reference-warning {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.module-quote--outgoing {
|
||||
border-left-color: $color-steel;
|
||||
background-color: $color-steel;
|
||||
margin-top: -4px;
|
||||
|
||||
// To preserve contrast
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 2px $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $conversation-colors {
|
||||
.module-quote--incoming-#{$color} {
|
||||
background-color: scale-color($value, $lightness: 60%);
|
||||
border-left-color: $value;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: scale-color($value, $lightness: -40%);
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--outgoing-#{$color} {
|
||||
background-color: scale-color($value, $lightness: 60%);
|
||||
border-left-color: $color-white;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: scale-color($value, $lightness: -40%);
|
||||
border-left-color: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--incoming-custom,
|
||||
.module-quote--outgoing-custom {
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
@each $color, $value in $conversation-colors-gradient {
|
||||
.module-quote--incoming-#{$color} {
|
||||
border-left-color: map-get($value, 'start');
|
||||
}
|
||||
.module-quote--incoming-#{$color},
|
||||
.module-quote--outgoing-#{$color} {
|
||||
background-attachment: fixed;
|
||||
@include light-theme {
|
||||
background-image: linear-gradient(
|
||||
map-get($value, 'deg'),
|
||||
scale-color(map-get($value, 'start'), $lightness: 60%),
|
||||
scale-color(map-get($value, 'end'), $lightness: 60%)
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
background-image: linear-gradient(
|
||||
map-get($value, 'deg'),
|
||||
scale-color(map-get($value, 'start'), $lightness: -40%),
|
||||
scale-color(map-get($value, 'end'), $lightness: -40%)
|
||||
);
|
||||
}
|
||||
}
|
||||
.module-quote--outgoing-#{$color} {
|
||||
border-left-color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--curve-top-left {
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
|
||||
.module-quote--curve-top-right {
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
|
||||
.module-quote__primary {
|
||||
flex-grow: 1;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
|
||||
// To leave room for image thumbnail
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.module-quote__primary__author {
|
||||
@include font-body-2-bold;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__author--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__text {
|
||||
@include font-body-1;
|
||||
|
||||
text-align: start;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
a {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
a {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.module-quote__primary__text--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
a {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__type-label {
|
||||
@include font-body-2-italic;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__type-label--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__filename-label {
|
||||
@include font-body-2;
|
||||
}
|
||||
|
||||
.module-quote__close-container {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background-color: $color-black-alpha-40;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus-within {
|
||||
background-color: $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__close-button {
|
||||
@include button-reset;
|
||||
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
@include color-svg('../images/icons/v2/x-24.svg', $color-white);
|
||||
}
|
||||
|
||||
.module-quote__icon-container {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
flex: 0 0 54px;
|
||||
position: relative;
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__circle-background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__icon--file {
|
||||
@include color-svg('../images/file.svg', $color-ultramarine);
|
||||
}
|
||||
.module-quote__icon-container__icon--image {
|
||||
@include color-svg('../images/image.svg', $color-ultramarine);
|
||||
}
|
||||
.module-quote__icon-container__icon--microphone {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/mic-outline-24.svg',
|
||||
$color-ultramarine
|
||||
);
|
||||
}
|
||||
.module-quote__icon-container__icon--play {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/play-solid-24.svg',
|
||||
$color-ultramarine
|
||||
);
|
||||
}
|
||||
.module-quote__icon-container__icon--movie {
|
||||
@include color-svg('../images/movie.svg', $color-ultramarine);
|
||||
}
|
||||
.module-quote__icon-container__icon--view-once {
|
||||
@include color-svg('../images/icons/v2/view-once-24.svg', $color-ultramarine);
|
||||
}
|
||||
|
||||
.module-quote__generic-file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.module-quote__generic-file__icon {
|
||||
background: url('../images/file-gradient.svg');
|
||||
background-size: 75%;
|
||||
background-repeat: no-repeat;
|
||||
height: 28px;
|
||||
width: 36px;
|
||||
margin-left: -4px;
|
||||
margin-right: -6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.module-quote__generic-file__text {
|
||||
@include font-body-2;
|
||||
|
||||
max-width: calc(100% - 26px);
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__generic-file__text--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning {
|
||||
color: $color-gray-90;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-left-style: solid;
|
||||
border-left-width: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-05);
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__icon--incoming {
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-05);
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__text {
|
||||
@include font-caption;
|
||||
|
||||
margin-left: 6px;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__text--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-about {
|
||||
&__container {
|
||||
margin-left: auto;
|
||||
|
|
395
stylesheets/components/Quote.scss
Normal file
395
stylesheets/components/Quote.scss
Normal file
|
@ -0,0 +1,395 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-quote {
|
||||
&__container {
|
||||
margin: {
|
||||
left: -6px;
|
||||
right: -6px;
|
||||
top: 3px;
|
||||
bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@include button-reset;
|
||||
width: 100%;
|
||||
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 2px $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--no-click {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.module-quote--with-reference-warning {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.module-quote--outgoing {
|
||||
border-left-color: $color-steel;
|
||||
background-color: $color-steel;
|
||||
margin-top: -4px;
|
||||
|
||||
// To preserve contrast
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 2px $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $conversation-colors {
|
||||
.module-quote--incoming-#{$color} {
|
||||
background-color: scale-color($value, $lightness: 60%);
|
||||
border-left-color: $value;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: scale-color($value, $lightness: -40%);
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--outgoing-#{$color} {
|
||||
background-color: scale-color($value, $lightness: 60%);
|
||||
border-left-color: $color-white;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: scale-color($value, $lightness: -40%);
|
||||
border-left-color: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--incoming-custom,
|
||||
.module-quote--outgoing-custom {
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
@each $color, $value in $conversation-colors-gradient {
|
||||
.module-quote--incoming-#{$color} {
|
||||
border-left-color: map-get($value, 'start');
|
||||
}
|
||||
.module-quote--incoming-#{$color},
|
||||
.module-quote--outgoing-#{$color} {
|
||||
background-attachment: fixed;
|
||||
@include light-theme {
|
||||
background-image: linear-gradient(
|
||||
map-get($value, 'deg'),
|
||||
scale-color(map-get($value, 'start'), $lightness: 60%),
|
||||
scale-color(map-get($value, 'end'), $lightness: 60%)
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
background-image: linear-gradient(
|
||||
map-get($value, 'deg'),
|
||||
scale-color(map-get($value, 'start'), $lightness: -40%),
|
||||
scale-color(map-get($value, 'end'), $lightness: -40%)
|
||||
);
|
||||
}
|
||||
}
|
||||
.module-quote--outgoing-#{$color} {
|
||||
border-left-color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote--curve-top-left {
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
|
||||
.module-quote--curve-top-right {
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
|
||||
.module-quote__primary {
|
||||
flex-grow: 1;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
|
||||
// To leave room for image thumbnail
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.module-quote__primary__author {
|
||||
@include font-body-2-bold;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__author--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__text {
|
||||
@include font-body-1;
|
||||
|
||||
text-align: start;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
a {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
a {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.module-quote__primary__text--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
a {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__type-label {
|
||||
@include font-body-2-italic;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__type-label--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__primary__filename-label {
|
||||
@include font-body-2;
|
||||
}
|
||||
|
||||
.module-quote__close-container {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background-color: $color-black-alpha-40;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus-within {
|
||||
background-color: $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__close-button {
|
||||
@include button-reset;
|
||||
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
@include color-svg('../images/icons/v2/x-24.svg', $color-white);
|
||||
}
|
||||
|
||||
.module-quote__icon-container {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
flex: 0 0 54px;
|
||||
position: relative;
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__circle-background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.module-quote__icon-container__icon--file {
|
||||
@include color-svg('../images/file.svg', $color-ultramarine);
|
||||
}
|
||||
.module-quote__icon-container__icon--image {
|
||||
@include color-svg('../images/image.svg', $color-ultramarine);
|
||||
}
|
||||
.module-quote__icon-container__icon--microphone {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/mic-outline-24.svg',
|
||||
$color-ultramarine
|
||||
);
|
||||
}
|
||||
.module-quote__icon-container__icon--play {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/play-solid-24.svg',
|
||||
$color-ultramarine
|
||||
);
|
||||
}
|
||||
.module-quote__icon-container__icon--movie {
|
||||
@include color-svg('../images/movie.svg', $color-ultramarine);
|
||||
}
|
||||
.module-quote__icon-container__icon--view-once {
|
||||
@include color-svg('../images/icons/v2/view-once-24.svg', $color-ultramarine);
|
||||
}
|
||||
|
||||
.module-quote__generic-file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.module-quote__generic-file__icon {
|
||||
background: url('../images/file-gradient.svg');
|
||||
background-size: 75%;
|
||||
background-repeat: no-repeat;
|
||||
height: 28px;
|
||||
width: 36px;
|
||||
margin-left: -4px;
|
||||
margin-right: -6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.module-quote__generic-file__text {
|
||||
@include font-body-2;
|
||||
|
||||
max-width: calc(100% - 26px);
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__generic-file__text--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning {
|
||||
color: $color-gray-90;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-left-style: solid;
|
||||
border-left-width: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-05);
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__icon--incoming {
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-90);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/link-broken-16.svg', $color-gray-05);
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__text {
|
||||
@include font-caption;
|
||||
|
||||
margin-left: 6px;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote__reference-warning__text--incoming {
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
14
stylesheets/components/StoryReplyQuote.scss
Normal file
14
stylesheets/components/StoryReplyQuote.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.StoryReplyQuote {
|
||||
&__primary {
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
&__icon-container {
|
||||
flex: 0 0 40px;
|
||||
height: 64px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
|
@ -13,11 +13,6 @@
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.module-quote-container {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__compose-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -131,6 +126,37 @@
|
|||
margin-left: 8px;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
&__quote {
|
||||
&__container {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&--outgoing-ultramarine {
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-60;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__primary {
|
||||
min-height: 64px;
|
||||
|
||||
color: $color-gray-05;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
&__author,
|
||||
&__text {
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-container {
|
||||
flex: 0 0 40px;
|
||||
height: 64px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Tabs.StoryViewsNRepliesModal__tabs {
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
@import './components/PermissionsPopup.scss';
|
||||
@import './components/Preferences.scss';
|
||||
@import './components/ProfileEditor.scss';
|
||||
@import './components/Quote.scss';
|
||||
@import './components/ReactionPickerPicker.scss';
|
||||
@import './components/SafetyNumberChangeDialog.scss';
|
||||
@import './components/SafetyNumberViewer.scss';
|
||||
|
@ -100,6 +101,7 @@
|
|||
@import './components/Slider.scss';
|
||||
@import './components/Stories.scss';
|
||||
@import './components/StoryListItem.scss';
|
||||
@import './components/StoryReplyQuote.scss';
|
||||
@import './components/StoryViewsNRepliesModal.scss';
|
||||
@import './components/StoryViewer.scss';
|
||||
@import './components/SystemMessage.scss';
|
||||
|
|
|
@ -146,10 +146,11 @@ export const StoryViewsNRepliesModal = ({
|
|||
{!replies.length && (
|
||||
<Quote
|
||||
authorTitle={authorTitle}
|
||||
conversationColor="steel"
|
||||
conversationColor="ultramarine"
|
||||
i18n={i18n}
|
||||
isFromMe={false}
|
||||
isViewOnce={false}
|
||||
moduleClassName="StoryViewsNRepliesModal__quote"
|
||||
rawAttachment={storyPreviewAttachment}
|
||||
referencedMessageNotFound={false}
|
||||
text={i18n('message--getNotificationText--text-with-emoji', {
|
||||
|
|
|
@ -31,7 +31,10 @@ import { getDefaultConversation } from '../../test-both/helpers/getDefaultConver
|
|||
import { WidthBreakpoint } from '../_util';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
|
||||
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
|
||||
import {
|
||||
fakeAttachment,
|
||||
fakeThumbnail,
|
||||
} from '../../test-both/helpers/fakeAttachment';
|
||||
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
|
||||
import { ThemeType } from '../../types/Util';
|
||||
|
||||
|
@ -1463,3 +1466,22 @@ story.add('Collapsing text-only group messages', () => {
|
|||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
story.add('Story reply', () => {
|
||||
const conversation = getDefaultConversation();
|
||||
|
||||
return (
|
||||
<Message
|
||||
{...createProps({ text: 'Wow!' })}
|
||||
storyReplyContext={{
|
||||
authorTitle: conversation.title,
|
||||
conversationColor: ConversationColors[0],
|
||||
isFromMe: false,
|
||||
rawAttachment: fakeAttachment({
|
||||
url: '/fixtures/snow.jpg',
|
||||
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -217,6 +217,13 @@ export type PropsData = {
|
|||
referencedMessageNotFound: boolean;
|
||||
isViewOnce: boolean;
|
||||
};
|
||||
storyReplyContext?: {
|
||||
authorTitle: string;
|
||||
conversationColor: ConversationColorType;
|
||||
customColor?: CustomColorType;
|
||||
isFromMe: boolean;
|
||||
rawAttachment?: QuotedAttachmentType;
|
||||
};
|
||||
previews: Array<LinkPreviewType>;
|
||||
|
||||
isTapToView?: boolean;
|
||||
|
@ -1255,6 +1262,59 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderStoryReplyContext(): JSX.Element | null {
|
||||
const {
|
||||
conversationColor,
|
||||
customColor,
|
||||
direction,
|
||||
i18n,
|
||||
storyReplyContext,
|
||||
} = this.props;
|
||||
|
||||
if (!storyReplyContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isIncoming = direction === 'incoming';
|
||||
|
||||
let curveTopLeft: boolean;
|
||||
let curveTopRight: boolean;
|
||||
if (this.shouldRenderAuthor()) {
|
||||
curveTopLeft = false;
|
||||
curveTopRight = false;
|
||||
} else if (isIncoming) {
|
||||
curveTopLeft = !this.isCollapsedAbove();
|
||||
curveTopRight = true;
|
||||
} else {
|
||||
curveTopLeft = true;
|
||||
curveTopRight = !this.isCollapsedAbove();
|
||||
}
|
||||
|
||||
return (
|
||||
<Quote
|
||||
authorTitle={storyReplyContext.authorTitle}
|
||||
conversationColor={conversationColor}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveTopRight={curveTopRight}
|
||||
customColor={customColor}
|
||||
i18n={i18n}
|
||||
isFromMe={storyReplyContext.isFromMe}
|
||||
isIncoming={isIncoming}
|
||||
isViewOnce={false}
|
||||
moduleClassName="StoryReplyQuote"
|
||||
onClick={() => {
|
||||
// TODO DESKTOP-3255
|
||||
}}
|
||||
rawAttachment={storyReplyContext.rawAttachment}
|
||||
referencedMessageNotFound={false}
|
||||
text={i18n('message--getNotificationText--text-with-emoji', {
|
||||
text: i18n('message--getNotificationText--photo'),
|
||||
emoji: '📷',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderEmbeddedContact(): JSX.Element | null {
|
||||
const {
|
||||
contact,
|
||||
|
@ -2284,6 +2344,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<>
|
||||
{this.renderQuote()}
|
||||
{this.renderStoryReplyContext()}
|
||||
{this.renderAttachment()}
|
||||
{this.renderPreview()}
|
||||
{this.renderEmbeddedContact()}
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
} from '../../types/Colors';
|
||||
import { ContactName } from './ContactName';
|
||||
import { getTextWithMentions } from '../../util/getTextWithMentions';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||
import { getCustomColorStyle } from '../../util/getCustomColorStyle';
|
||||
|
||||
export type Props = {
|
||||
|
@ -30,6 +31,7 @@ export type Props = {
|
|||
i18n: LocalizerType;
|
||||
isFromMe: boolean;
|
||||
isIncoming?: boolean;
|
||||
moduleClassName?: string;
|
||||
onClick?: () => void;
|
||||
onClose?: () => void;
|
||||
text: string;
|
||||
|
@ -113,11 +115,14 @@ function getTypeLabel({
|
|||
}
|
||||
|
||||
export class Quote extends React.Component<Props, State> {
|
||||
private getClassName: (modifier?: string) => string;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
imageBroken: false,
|
||||
};
|
||||
this.getClassName = getClassNamesFor('module-quote', props.moduleClassName);
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
|
@ -164,12 +169,14 @@ export class Quote extends React.Component<Props, State> {
|
|||
|
||||
public renderImage(url: string, icon?: string): JSX.Element {
|
||||
const iconElement = icon ? (
|
||||
<div className="module-quote__icon-container__inner">
|
||||
<div className="module-quote__icon-container__circle-background">
|
||||
<div className={this.getClassName('__icon-container__inner')}>
|
||||
<div
|
||||
className={this.getClassName('__icon-container__circle-background')}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__icon-container__icon',
|
||||
`module-quote__icon-container__icon--${icon}`
|
||||
this.getClassName('__icon-container__icon'),
|
||||
this.getClassName(`__icon-container__icon--${icon}`)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -177,7 +184,11 @@ export class Quote extends React.Component<Props, State> {
|
|||
) : null;
|
||||
|
||||
return (
|
||||
<ThumbnailImage src={url} onError={this.handleImageError}>
|
||||
<ThumbnailImage
|
||||
className={this.getClassName('__icon-container')}
|
||||
src={url}
|
||||
onError={this.handleImageError}
|
||||
>
|
||||
{iconElement}
|
||||
</ThumbnailImage>
|
||||
);
|
||||
|
@ -185,13 +196,15 @@ export class Quote extends React.Component<Props, State> {
|
|||
|
||||
public renderIcon(icon: string): JSX.Element {
|
||||
return (
|
||||
<div className="module-quote__icon-container">
|
||||
<div className="module-quote__icon-container__inner">
|
||||
<div className="module-quote__icon-container__circle-background">
|
||||
<div className={this.getClassName('__icon-container')}>
|
||||
<div className={this.getClassName('__icon-container__inner')}>
|
||||
<div
|
||||
className={this.getClassName('__icon-container__circle-background')}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__icon-container__icon',
|
||||
`module-quote__icon-container__icon--${icon}`
|
||||
this.getClassName('__icon-container__icon'),
|
||||
this.getClassName(`__icon-container__icon--${icon}`)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -219,12 +232,14 @@ export class Quote extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="module-quote__generic-file">
|
||||
<div className="module-quote__generic-file__icon" />
|
||||
<div className={this.getClassName('__generic-file')}>
|
||||
<div className={this.getClassName('__generic-file__icon')} />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__generic-file__text',
|
||||
isIncoming ? 'module-quote__generic-file__text--incoming' : null
|
||||
this.getClassName('__generic-file__text'),
|
||||
isIncoming
|
||||
? this.getClassName('__generic-file__text--incoming')
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{fileName}
|
||||
|
@ -279,8 +294,8 @@ export class Quote extends React.Component<Props, State> {
|
|||
<div
|
||||
dir="auto"
|
||||
className={classNames(
|
||||
'module-quote__primary__text',
|
||||
isIncoming ? 'module-quote__primary__text--incoming' : null
|
||||
this.getClassName('__primary__text'),
|
||||
isIncoming ? this.getClassName('__primary__text--incoming') : null
|
||||
)}
|
||||
>
|
||||
<MessageBody
|
||||
|
@ -311,8 +326,10 @@ export class Quote extends React.Component<Props, State> {
|
|||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__primary__type-label',
|
||||
isIncoming ? 'module-quote__primary__type-label--incoming' : null
|
||||
this.getClassName('__primary__type-label'),
|
||||
isIncoming
|
||||
? this.getClassName('__primary__type-label--incoming')
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{typeLabel}
|
||||
|
@ -347,12 +364,12 @@ export class Quote extends React.Component<Props, State> {
|
|||
|
||||
// We need the container to give us the flexibility to implement the iOS design.
|
||||
return (
|
||||
<div className="module-quote__close-container">
|
||||
<div className={this.getClassName('__close-container')}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
// We can't be a button because the overall quote is a button; can't nest them
|
||||
role="button"
|
||||
className="module-quote__close-button"
|
||||
className={this.getClassName('__close-button')}
|
||||
aria-label={i18n('close')}
|
||||
onKeyDown={keyDownHandler}
|
||||
onClick={clickHandler}
|
||||
|
@ -367,8 +384,8 @@ export class Quote extends React.Component<Props, State> {
|
|||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__primary__author',
|
||||
isIncoming ? 'module-quote__primary__author--incoming' : null
|
||||
this.getClassName('__primary__author'),
|
||||
isIncoming ? this.getClassName('__primary__author--incoming') : null
|
||||
)}
|
||||
>
|
||||
{isFromMe ? i18n('you') : <ContactName title={authorTitle} />}
|
||||
|
@ -392,26 +409,26 @@ export class Quote extends React.Component<Props, State> {
|
|||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__reference-warning',
|
||||
this.getClassName('__reference-warning'),
|
||||
isIncoming
|
||||
? `module-quote--incoming-${conversationColor}`
|
||||
: `module-quote--outgoing-${conversationColor}`
|
||||
? this.getClassName(`--incoming-${conversationColor}`)
|
||||
: this.getClassName(`--outgoing-${conversationColor}`)
|
||||
)}
|
||||
style={{ ...getCustomColorStyle(customColor, true) }}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__reference-warning__icon',
|
||||
this.getClassName('__reference-warning__icon'),
|
||||
isIncoming
|
||||
? 'module-quote__reference-warning__icon--incoming'
|
||||
? this.getClassName('__reference-warning__icon--incoming')
|
||||
: null
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-quote__reference-warning__text',
|
||||
this.getClassName('__reference-warning__text'),
|
||||
isIncoming
|
||||
? 'module-quote__reference-warning__text--incoming'
|
||||
? this.getClassName('__reference-warning__text--incoming')
|
||||
: null
|
||||
)}
|
||||
>
|
||||
|
@ -437,25 +454,28 @@ export class Quote extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="module-quote-container">
|
||||
<div className={this.getClassName('__container')}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
className={classNames(
|
||||
'module-quote',
|
||||
isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',
|
||||
this.getClassName(''),
|
||||
isIncoming
|
||||
? `module-quote--incoming-${conversationColor}`
|
||||
: `module-quote--outgoing-${conversationColor}`,
|
||||
!onClick && 'module-quote--no-click',
|
||||
referencedMessageNotFound && 'module-quote--with-reference-warning',
|
||||
curveTopLeft && 'module-quote--curve-top-left',
|
||||
curveTopRight && 'module-quote--curve-top-right'
|
||||
? this.getClassName('--incoming')
|
||||
: this.getClassName('--outgoing'),
|
||||
isIncoming
|
||||
? this.getClassName(`--incoming-${conversationColor}`)
|
||||
: this.getClassName(`--outgoing-${conversationColor}`),
|
||||
!onClick && this.getClassName('--no-click'),
|
||||
referencedMessageNotFound &&
|
||||
this.getClassName('--with-reference-warning'),
|
||||
curveTopLeft && this.getClassName('--curve-top-left'),
|
||||
curveTopRight && this.getClassName('--curve-top-right')
|
||||
)}
|
||||
style={{ ...getCustomColorStyle(customColor, true) }}
|
||||
>
|
||||
<div className="module-quote__primary">
|
||||
<div className={this.getClassName('__primary')}>
|
||||
{this.renderAuthor()}
|
||||
{this.renderGenericFile()}
|
||||
{this.renderText()}
|
||||
|
@ -470,10 +490,12 @@ export class Quote extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
function ThumbnailImage({
|
||||
className,
|
||||
src,
|
||||
onError,
|
||||
children,
|
||||
}: Readonly<{
|
||||
className: string;
|
||||
src: string;
|
||||
onError: () => void;
|
||||
children: ReactNode;
|
||||
|
@ -507,7 +529,7 @@ function ThumbnailImage({
|
|||
|
||||
return (
|
||||
<div
|
||||
className="module-quote__icon-container"
|
||||
className={className}
|
||||
style={
|
||||
loadedSrc ? { backgroundImage: `url('${encodeURI(loadedSrc)}')` } : {}
|
||||
}
|
||||
|
|
7
ts/model-types.d.ts
vendored
7
ts/model-types.d.ts
vendored
|
@ -85,6 +85,12 @@ export type QuotedMessageType = {
|
|||
messageId: string;
|
||||
};
|
||||
|
||||
type StoryReplyContextType = {
|
||||
attachment?: AttachmentType;
|
||||
authorUuid?: string;
|
||||
messageId: string;
|
||||
};
|
||||
|
||||
export type StickerMessageType = {
|
||||
packId: string;
|
||||
stickerId: number;
|
||||
|
@ -147,6 +153,7 @@ export type MessageAttributesType = {
|
|||
retryOptions?: RetryOptions;
|
||||
sourceDevice?: number;
|
||||
storyId?: string;
|
||||
storyReplyContext?: StoryReplyContextType;
|
||||
supportedVersionAtReceive?: unknown;
|
||||
synced?: boolean;
|
||||
unidentifiedDeliveryReceived?: boolean;
|
||||
|
|
|
@ -1711,6 +1711,8 @@ export class ConversationModel extends window.Backbone
|
|||
log.warn(`cleanModels: Upgraded schema of ${upgraded} messages`);
|
||||
}
|
||||
|
||||
await Promise.all(result.map(model => model.hydrateStoryContext()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -150,6 +150,7 @@ import { findStoryMessage } from '../util/findStoryMessage';
|
|||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
|
||||
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -305,6 +306,33 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
);
|
||||
}
|
||||
|
||||
async hydrateStoryContext(): Promise<void> {
|
||||
const storyId = this.get('storyId');
|
||||
if (!storyId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('storyReplyContext')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await getMessageById(storyId);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = message.get('attachments');
|
||||
|
||||
this.set({
|
||||
storyReplyContext: {
|
||||
attachment: attachments ? attachments[0] : undefined,
|
||||
authorUuid: message.get('sourceUuid'),
|
||||
messageId: message.get('id'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getPropsForMessageDetail(ourConversationId: string): PropsForMessageDetail {
|
||||
const newIdentity = window.i18n('newIdentity');
|
||||
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
||||
|
@ -2212,6 +2240,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
quote,
|
||||
storyId: storyQuote?.id,
|
||||
};
|
||||
|
||||
const dataMessage = await upgradeMessageSchema(withQuoteReference);
|
||||
|
||||
try {
|
||||
|
|
|
@ -2086,7 +2086,7 @@ async function getUnreadByConversationAndMarkRead({
|
|||
) AND
|
||||
expireTimer > 0 AND
|
||||
conversationId = $conversationId AND
|
||||
storyId IS $storyId AND
|
||||
($storyId IS NULL OR storyId IS $storyId) AND
|
||||
received_at <= $newestUnreadAt;
|
||||
`
|
||||
).run({
|
||||
|
@ -2105,7 +2105,7 @@ async function getUnreadByConversationAndMarkRead({
|
|||
WHERE
|
||||
readStatus = ${ReadStatus.Unread} AND
|
||||
conversationId = $conversationId AND
|
||||
storyId IS $storyId AND
|
||||
($storyId IS NULL OR storyId IS $storyId) AND
|
||||
received_at <= $newestUnreadAt
|
||||
ORDER BY received_at DESC, sent_at DESC;
|
||||
`
|
||||
|
@ -2125,7 +2125,7 @@ async function getUnreadByConversationAndMarkRead({
|
|||
WHERE
|
||||
readStatus = ${ReadStatus.Unread} AND
|
||||
conversationId = $conversationId AND
|
||||
storyId IS $storyId AND
|
||||
($storyId IS NULL OR storyId IS $storyId) AND
|
||||
received_at <= $newestUnreadAt;
|
||||
`
|
||||
).run({
|
||||
|
@ -2360,7 +2360,7 @@ function getOlderMessagesByConversationSync(
|
|||
conversationId = $conversationId AND
|
||||
($messageId IS NULL OR id IS NOT $messageId) AND
|
||||
isStory IS 0 AND
|
||||
storyId IS $storyId AND
|
||||
($storyId IS NULL OR storyId IS $storyId) AND
|
||||
(
|
||||
(received_at = $received_at AND sent_at < $sent_at) OR
|
||||
received_at < $received_at
|
||||
|
@ -2453,7 +2453,7 @@ function getNewerMessagesByConversationSync(
|
|||
SELECT json FROM messages WHERE
|
||||
conversationId = $conversationId AND
|
||||
isStory IS 0 AND
|
||||
storyId IS $storyId AND
|
||||
($storyId IS NULL OR storyId IS $storyId) AND
|
||||
(
|
||||
(received_at = $received_at AND sent_at > $sent_at) OR
|
||||
received_at > $received_at
|
||||
|
@ -2483,7 +2483,7 @@ function getOldestMessageForConversation(
|
|||
SELECT * FROM messages WHERE
|
||||
conversationId = $conversationId AND
|
||||
isStory IS 0 AND
|
||||
storyId IS $storyId
|
||||
($storyId IS NULL OR storyId IS $storyId)
|
||||
ORDER BY received_at ASC, sent_at ASC
|
||||
LIMIT 1;
|
||||
`
|
||||
|
@ -2510,7 +2510,7 @@ function getNewestMessageForConversation(
|
|||
SELECT * FROM messages WHERE
|
||||
conversationId = $conversationId AND
|
||||
isStory IS 0 AND
|
||||
storyId IS $storyId
|
||||
($storyId IS NULL OR storyId IS $storyId)
|
||||
ORDER BY received_at DESC, sent_at DESC
|
||||
LIMIT 1;
|
||||
`
|
||||
|
@ -2651,7 +2651,7 @@ function getOldestUnreadMessageForConversation(
|
|||
conversationId = $conversationId AND
|
||||
readStatus = ${ReadStatus.Unread} AND
|
||||
isStory IS 0 AND
|
||||
storyId IS $storyId
|
||||
($storyId IS NULL OR storyId IS $storyId)
|
||||
ORDER BY received_at ASC, sent_at ASC
|
||||
LIMIT 1;
|
||||
`
|
||||
|
@ -2688,7 +2688,7 @@ function getTotalUnreadForConversationSync(
|
|||
conversationId = $conversationId AND
|
||||
readStatus = ${ReadStatus.Unread} AND
|
||||
isStory IS 0 AND
|
||||
storyId IS $storyId;
|
||||
($storyId IS NULL OR storyId IS $storyId);
|
||||
`
|
||||
)
|
||||
.get({
|
||||
|
|
|
@ -432,6 +432,55 @@ export const getReactionsForMessage = createSelectorCreator(
|
|||
(_, reactions): PropsData['reactions'] => reactions
|
||||
);
|
||||
|
||||
export const getPropsForStoryReplyContext = createSelectorCreator(
|
||||
memoizeByRoot,
|
||||
isEqual
|
||||
)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
(
|
||||
message: Pick<
|
||||
MessageWithUIFieldsType,
|
||||
'body' | 'conversationId' | 'storyReplyContext'
|
||||
>,
|
||||
{
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
}: {
|
||||
conversationSelector: GetConversationByIdType;
|
||||
ourConversationId?: string;
|
||||
}
|
||||
): PropsData['storyReplyContext'] => {
|
||||
const { storyReplyContext } = message;
|
||||
if (!storyReplyContext) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contact = conversationSelector(storyReplyContext.authorUuid);
|
||||
|
||||
const authorTitle = contact.title;
|
||||
const isFromMe = contact.id === ourConversationId;
|
||||
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
|
||||
const { conversationColor, customColor } =
|
||||
getConversationColorAttributes(conversation);
|
||||
|
||||
return {
|
||||
authorTitle,
|
||||
conversationColor,
|
||||
customColor,
|
||||
isFromMe,
|
||||
rawAttachment: storyReplyContext.attachment
|
||||
? processQuoteAttachment(storyReplyContext.attachment)
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
(_, storyReplyContext): PropsData['storyReplyContext'] => storyReplyContext
|
||||
);
|
||||
|
||||
export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
@ -651,6 +700,7 @@ export const getPropsForMessage: (
|
|||
getPreviewsForMessage,
|
||||
getReactionsForMessage,
|
||||
getPropsForQuote,
|
||||
getPropsForStoryReplyContext,
|
||||
getShallowPropsForMessage,
|
||||
(
|
||||
_,
|
||||
|
@ -660,6 +710,7 @@ export const getPropsForMessage: (
|
|||
previews: Array<LinkPreviewType>,
|
||||
reactions: PropsData['reactions'],
|
||||
quote: PropsData['quote'],
|
||||
storyReplyContext: PropsData['storyReplyContext'],
|
||||
shallowProps: ShallowPropsType
|
||||
): Omit<PropsForMessage, 'renderingContext'> => {
|
||||
return {
|
||||
|
@ -669,6 +720,7 @@ export const getPropsForMessage: (
|
|||
previews,
|
||||
quote,
|
||||
reactions,
|
||||
storyReplyContext,
|
||||
...shallowProps,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { ConversationType } from '../../state/ducks/conversations';
|
|||
import { UUID } from '../../types/UUID';
|
||||
import type { UUIDStringType } from '../../types/UUID';
|
||||
import { getRandomColor } from './getRandomColor';
|
||||
import { ConversationColors } from '../../types/Colors';
|
||||
|
||||
const FIRST_NAMES = [
|
||||
'James',
|
||||
|
@ -335,6 +336,7 @@ export function getDefaultConversation(
|
|||
avatarPath: getAvatarPath(),
|
||||
badges: [],
|
||||
e164: '+1300555000',
|
||||
conversationColor: ConversationColors[0],
|
||||
color: getRandomColor(),
|
||||
firstName,
|
||||
id: generateUuid(),
|
||||
|
|
|
@ -125,8 +125,8 @@ describe('sql/markRead', () => {
|
|||
assert.lengthOf(await _getAllMessages(), 7);
|
||||
assert.strictEqual(
|
||||
await getTotalUnreadForConversation(conversationId),
|
||||
3,
|
||||
'uread count'
|
||||
4,
|
||||
'unread count'
|
||||
);
|
||||
|
||||
const markedRead = await getUnreadByConversationAndMarkRead({
|
||||
|
@ -138,7 +138,7 @@ describe('sql/markRead', () => {
|
|||
assert.lengthOf(markedRead, 2, 'two messages marked read');
|
||||
assert.strictEqual(
|
||||
await getTotalUnreadForConversation(conversationId),
|
||||
1,
|
||||
2,
|
||||
'unread count'
|
||||
);
|
||||
|
||||
|
@ -160,7 +160,7 @@ describe('sql/markRead', () => {
|
|||
readAt,
|
||||
});
|
||||
|
||||
assert.lengthOf(markedRead2, 1, 'one message marked read');
|
||||
assert.lengthOf(markedRead2, 3, 'three messages marked read');
|
||||
assert.strictEqual(markedRead2[0].id, message7.id, 'should be message7');
|
||||
|
||||
assert.strictEqual(
|
||||
|
|
|
@ -95,7 +95,7 @@ describe('sql/timelineFetches', () => {
|
|||
const messages = await getOlderMessagesByConversation(conversationId, {
|
||||
limit: 5,
|
||||
});
|
||||
assert.lengthOf(messages, 2);
|
||||
assert.lengthOf(messages, 3);
|
||||
|
||||
// Fetched with DESC query, but with reverse() call afterwards
|
||||
assert.strictEqual(messages[0].id, message1.id);
|
||||
|
@ -383,9 +383,9 @@ describe('sql/timelineFetches', () => {
|
|||
limit: 5,
|
||||
});
|
||||
|
||||
assert.lengthOf(messages, 2);
|
||||
assert.strictEqual(messages[0].id, message4.id, 'checking message 4');
|
||||
assert.strictEqual(messages[1].id, message5.id, 'checking message 5');
|
||||
assert.lengthOf(messages, 3);
|
||||
assert.strictEqual(messages[0].id, message3.id, 'checking message 3');
|
||||
assert.strictEqual(messages[1].id, message4.id, 'checking message 4');
|
||||
});
|
||||
|
||||
it('returns N oldest messages for a given story with no parameters', async () => {
|
||||
|
@ -655,14 +655,18 @@ describe('sql/timelineFetches', () => {
|
|||
const metricsInTimeline = await getMessageMetricsForConversation(
|
||||
conversationId
|
||||
);
|
||||
assert.strictEqual(metricsInTimeline?.oldest?.id, oldest.id, 'oldest');
|
||||
assert.strictEqual(
|
||||
metricsInTimeline?.oldest?.id,
|
||||
oldestInStory.id,
|
||||
'oldest'
|
||||
);
|
||||
assert.strictEqual(metricsInTimeline?.newest?.id, newest.id, 'newest');
|
||||
assert.strictEqual(
|
||||
metricsInTimeline?.oldestUnread?.id,
|
||||
oldestUnread.id,
|
||||
'oldestUnread'
|
||||
);
|
||||
assert.strictEqual(metricsInTimeline?.totalUnread, 2, 'totalUnread');
|
||||
assert.strictEqual(metricsInTimeline?.totalUnread, 3, 'totalUnread');
|
||||
|
||||
const metricsInStory = await getMessageMetricsForConversation(
|
||||
conversationId,
|
||||
|
|
Loading…
Reference in a new issue