Adds debugging information to stories
This commit is contained in:
parent
badf9d7dda
commit
06476de6c9
|
@ -7385,6 +7385,30 @@
|
||||||
"message": "Unmute",
|
"message": "Unmute",
|
||||||
"description": "Aria label for unmuting stories"
|
"description": "Aria label for unmuting stories"
|
||||||
},
|
},
|
||||||
|
"StoryDetailsModal__sent-time": {
|
||||||
|
"message": "Sent $time$",
|
||||||
|
"description": "Sent timestamp",
|
||||||
|
"placeholders": {
|
||||||
|
"time": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Today 5:33pm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StoryDetailsModal__file-size": {
|
||||||
|
"message": "File size $size$",
|
||||||
|
"description": "File size description",
|
||||||
|
"placeholders": {
|
||||||
|
"size": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100kb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StoryDetailsModal__copy-timestamp": {
|
||||||
|
"message": "Copy timestamp",
|
||||||
|
"description": "Context menu item to help debugging"
|
||||||
|
},
|
||||||
"StoryViewsNRepliesModal__no-replies": {
|
"StoryViewsNRepliesModal__no-replies": {
|
||||||
"message": "No replies yet",
|
"message": "No replies yet",
|
||||||
"description": "Placeholder text for when there are no replies"
|
"description": "Placeholder text for when there are no replies"
|
||||||
|
@ -7421,6 +7445,14 @@
|
||||||
"message": "Go to chat",
|
"message": "Go to chat",
|
||||||
"description": "Label for menu item to go to conversation"
|
"description": "Label for menu item to go to conversation"
|
||||||
},
|
},
|
||||||
|
"StoryListItem__delete": {
|
||||||
|
"message": "Delete",
|
||||||
|
"description": "Label for menu item to delete a story"
|
||||||
|
},
|
||||||
|
"StoryListItem__info": {
|
||||||
|
"message": "Info",
|
||||||
|
"description": "Label for menu item to get a story's information"
|
||||||
|
},
|
||||||
"StoryListItem__hide-modal--body": {
|
"StoryListItem__hide-modal--body": {
|
||||||
"message": "Hide story? New story updates from $name$ won’t appear at the top of the stories list anymore.",
|
"message": "Hide story? New story updates from $name$ won’t appear at the top of the stories list anymore.",
|
||||||
"description": "Body for the confirmation dialog for hiding a story"
|
"description": "Body for the confirmation dialog for hiding a story"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m15.95 19.5c-.1179.6977-.4787 1.3312-1.0185 1.7887s-1.2239.7094-1.9315.7113h-7c-.79565 0-1.55871-.3161-2.12132-.8787s-.87868-1.3257-.87868-2.1213v-10c0-.79565.31607-1.55871.87868-2.12132s1.32567-.87868 2.12132-.87868h.5v1.5h-.5c-.39782 0-.77936.15804-1.06066.43934s-.43934.66284-.43934 1.06066v10c0 .3978.15804.7794.43934 1.0607s.66284.4393 1.06066.4393h7c.3091-.0013.6103-.098.8623-.277.2521-.179.4427-.4315.5457-.723zm2.05-16h-7c-.3978 0-.7794.15804-1.06066.43934-.2813.2813-.43934.66284-.43934 1.06066v10c0 .3978.15804.7794.43934 1.0607.28126.2813.66286.4393 1.06066.4393h7c.3978 0 .7794-.158 1.0607-.4393s.4393-.6629.4393-1.0607v-10c0-.39782-.158-.77936-.4393-1.06066s-.6629-.43934-1.0607-.43934zm0-1.5c.7956 0 1.5587.31607 2.1213.87868s.8787 1.32567.8787 2.12132v10c0 .7956-.3161 1.5587-.8787 2.1213s-1.3257.8787-2.1213.8787h-7c-.7956 0-1.55871-.3161-2.12132-.8787s-.87868-1.3257-.87868-2.1213v-10c0-.79565.31607-1.55871.87868-2.12132s1.32572-.87868 2.12132-.87868z" fill="#000"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -20,50 +20,6 @@
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
@include button-reset();
|
@include button-reset();
|
||||||
align-items: center;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
|
||||||
height: 32px;
|
|
||||||
justify-content: center;
|
|
||||||
opacity: 0.5;
|
|
||||||
width: 32px;
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg(
|
|
||||||
'../images/icons/v2/collapse-down-20.svg',
|
|
||||||
$color-black
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg(
|
|
||||||
'../images/icons/v2/collapse-down-20.svg',
|
|
||||||
$color-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
@include light-theme() {
|
|
||||||
background-color: $color-gray-05;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme() {
|
|
||||||
background-color: $color-gray-75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__option {
|
&__option {
|
||||||
|
|
|
@ -1,30 +1,34 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
.HueSlider.Slider {
|
.HueSlider {
|
||||||
background-image: linear-gradient(
|
&.Slider {
|
||||||
90deg,
|
background-image: linear-gradient(
|
||||||
hsl(0, 0%, 0%),
|
90deg,
|
||||||
hsl(0, 100%, 50%),
|
hsl(0, 0%, 0%),
|
||||||
hsl(45, 100%, 50%),
|
hsl(0, 100%, 50%),
|
||||||
hsl(90, 100%, 50%),
|
hsl(45, 100%, 50%),
|
||||||
hsl(135, 100%, 50%),
|
hsl(90, 100%, 50%),
|
||||||
hsl(180, 100%, 50%),
|
hsl(135, 100%, 50%),
|
||||||
hsl(225, 100%, 50%),
|
hsl(180, 100%, 50%),
|
||||||
hsl(270, 100%, 50%),
|
hsl(225, 100%, 50%),
|
||||||
hsl(315, 100%, 50%),
|
hsl(270, 100%, 50%),
|
||||||
hsl(0, 0%, 100%)
|
hsl(315, 100%, 50%),
|
||||||
);
|
hsl(0, 0%, 100%)
|
||||||
border-radius: 4px;
|
);
|
||||||
height: 8px;
|
border-radius: 4px;
|
||||||
margin-left: 7px;
|
height: 8px;
|
||||||
width: 280px;
|
margin-left: 7px;
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
&__handle.Slider__handle {
|
&__handle {
|
||||||
border: 7px solid $color-white;
|
&.Slider__handle {
|
||||||
margin-top: -7px;
|
border: 7px solid $color-white;
|
||||||
margin-left: -11px;
|
margin-top: -7px;
|
||||||
height: 22px;
|
margin-left: -11px;
|
||||||
width: 22px;
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,8 @@
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
|
|
||||||
&__tool {
|
&__tool,
|
||||||
|
&__tool__button {
|
||||||
margin-right: 14px;
|
margin-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +171,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
|
display: flex;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
@ -179,31 +181,31 @@
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--draw-pen {
|
&--draw-pen__button {
|
||||||
@include icon('pen-20.svg');
|
@include icon('pen-20.svg');
|
||||||
}
|
}
|
||||||
&--draw-highlighter {
|
&--draw-highlighter__button {
|
||||||
@include icon('pen-highlighter-20.svg');
|
@include icon('pen-highlighter-20.svg');
|
||||||
}
|
}
|
||||||
&--width-thin {
|
&--width-thin__button {
|
||||||
@include icon('pen-light-20.svg');
|
@include icon('pen-light-20.svg');
|
||||||
}
|
}
|
||||||
&--width-regular {
|
&--width-regular__button {
|
||||||
@include icon('pen-regular-20.svg');
|
@include icon('pen-regular-20.svg');
|
||||||
}
|
}
|
||||||
&--width-medium {
|
&--width-medium__button {
|
||||||
@include icon('pen-medium-20.svg');
|
@include icon('pen-medium-20.svg');
|
||||||
}
|
}
|
||||||
&--width-heavy {
|
&--width-heavy__button {
|
||||||
@include icon('pen-heavy-20.svg');
|
@include icon('pen-heavy-20.svg');
|
||||||
}
|
}
|
||||||
&--text-regular {
|
&--text-regular__button {
|
||||||
@include icon('text-regular-20.svg');
|
@include icon('text-regular-20.svg');
|
||||||
}
|
}
|
||||||
&--text-highlight {
|
&--text-highlight__button {
|
||||||
@include icon('text-highlight-20.svg');
|
@include icon('text-highlight-20.svg');
|
||||||
}
|
}
|
||||||
&--text-outline {
|
&--text-outline__button {
|
||||||
@include icon('text-outline-20.svg');
|
@include icon('text-outline-20.svg');
|
||||||
}
|
}
|
||||||
&--rotate {
|
&--rotate {
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message-detail__unix-timestamp-menu__button {
|
||||||
|
}
|
||||||
|
|
||||||
.module-message-detail__unix-timestamp {
|
.module-message-detail__unix-timestamp {
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
color: $color-gray-05;
|
color: $color-gray-05;
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__more {
|
&__more__button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: $color-gray-65;
|
background: $color-gray-65;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
@ -64,7 +64,6 @@
|
||||||
height: 28px;
|
height: 28px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
opacity: 1;
|
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
width: 380px;
|
width: 380px;
|
||||||
padding-top: calc(14px + var(--title-bar-drag-area-height));
|
padding-top: calc(14px + var(--title-bar-drag-area-height));
|
||||||
|
|
||||||
&__settings {
|
&__settings__button {
|
||||||
|
margin-left: 24px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
|
|
|
@ -143,21 +143,17 @@
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
|
|
||||||
&__tool {
|
&__tool,
|
||||||
|
&__tool__button {
|
||||||
margin-right: 14px;
|
margin-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
@mixin icon($icon) {
|
@mixin icon($icon) {
|
||||||
@include svg($icon);
|
@include svg($icon);
|
||||||
opacity: 1;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
|
@ -173,19 +169,19 @@
|
||||||
&--bg-none {
|
&--bg-none {
|
||||||
@include icon('text-effect-off-24.svg');
|
@include icon('text-effect-off-24.svg');
|
||||||
}
|
}
|
||||||
&--font-regular {
|
&--font-regular__button {
|
||||||
@include icon('font-regular.svg');
|
@include icon('font-regular.svg');
|
||||||
}
|
}
|
||||||
&--font-bold {
|
&--font-bold__button {
|
||||||
@include icon('font-bold.svg');
|
@include icon('font-bold.svg');
|
||||||
}
|
}
|
||||||
&--font-serif {
|
&--font-serif__button {
|
||||||
@include icon('font-serif.svg');
|
@include icon('font-serif.svg');
|
||||||
}
|
}
|
||||||
&--font-script {
|
&--font-script__button {
|
||||||
@include icon('font-script.svg');
|
@include icon('font-script.svg');
|
||||||
}
|
}
|
||||||
&--font-condensed {
|
&--font-condensed__button {
|
||||||
@include icon('font-condensed.svg');
|
@include icon('font-condensed.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.StoryDetailsModal {
|
||||||
|
min-width: 320px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__overlay-container {
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__debugger__button {
|
||||||
|
color: $color-gray-25;
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__copy-icon {
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/copy-outline-24.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/copy-outline-24.svg',
|
||||||
|
$color-black
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__contact-container {
|
||||||
|
border-top: 1px solid $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__contact-group__header {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 10px 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__contact {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include font-body-1;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status-timestamp {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,24 +2,26 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
.StoryListItem {
|
.StoryListItem {
|
||||||
@include button-reset;
|
&__button {
|
||||||
align-items: center;
|
@include button-reset;
|
||||||
border-radius: 10px;
|
align-items: center;
|
||||||
display: flex;
|
border-radius: 10px;
|
||||||
height: 96px;
|
display: flex;
|
||||||
padding: 0 10px;
|
height: 96px;
|
||||||
width: 100%;
|
padding: 0 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
@include keyboard-mode {
|
@include keyboard-mode {
|
||||||
&:focus {
|
&:focus {
|
||||||
|
background: $color-gray-65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
background: $color-gray-65;
|
background: $color-gray-65;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $color-gray-65;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -107,8 +109,22 @@
|
||||||
@include color-svg('../images/icons/v2/open-24.svg', $color-white);
|
@include color-svg('../images/icons/v2/open-24.svg', $color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--delete {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/trash-outline-24.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
&--hide {
|
&--hide {
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-white);
|
@include color-svg('../images/icons/v2/x-24.svg', $color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/info-outline-24.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,16 +146,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__more {
|
&__more__button {
|
||||||
@include button-reset;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
|
||||||
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
|
&::after {
|
||||||
|
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
|
||||||
|
content: '';
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
@include keyboard-mode {
|
@include keyboard-mode {
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,33 @@
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__debugger__button {
|
||||||
|
color: $color-gray-25;
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
height: auto;
|
||||||
|
opacity: 1;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
@include dark-theme {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__copy-icon {
|
||||||
|
@include color-svg('../images/icons/v2/copy-outline-24.svg', $color-white);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tabs.StoryViewsNRepliesModal__tabs {
|
.Tabs.StoryViewsNRepliesModal__tabs {
|
||||||
|
|
|
@ -108,12 +108,13 @@
|
||||||
@import './components/StagedLinkPreview.scss';
|
@import './components/StagedLinkPreview.scss';
|
||||||
@import './components/Stories.scss';
|
@import './components/Stories.scss';
|
||||||
@import './components/StoryCreator.scss';
|
@import './components/StoryCreator.scss';
|
||||||
|
@import './components/StoryDetailsModal.scss';
|
||||||
@import './components/StoryImage.scss';
|
@import './components/StoryImage.scss';
|
||||||
@import './components/StoryListItem.scss';
|
@import './components/StoryListItem.scss';
|
||||||
@import './components/StoryReplyQuote.scss';
|
@import './components/StoryReplyQuote.scss';
|
||||||
@import './components/StoriesSettingsModal.scss';
|
@import './components/StoriesSettingsModal.scss';
|
||||||
@import './components/StoryViewsNRepliesModal.scss';
|
|
||||||
@import './components/StoryViewer.scss';
|
@import './components/StoryViewer.scss';
|
||||||
|
@import './components/StoryViewsNRepliesModal.scss';
|
||||||
@import './components/SystemMessage.scss';
|
@import './components/SystemMessage.scss';
|
||||||
@import './components/Tabs.scss';
|
@import './components/Tabs.scss';
|
||||||
@import './components/TextAttachment.scss';
|
@import './components/TextAttachment.scss';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2018-2022 Signal Messenger, LLC
|
// Copyright 2018-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { CSSProperties, KeyboardEvent } from 'react';
|
import type { KeyboardEvent, ReactNode } from 'react';
|
||||||
import type { Options } from '@popperjs/core';
|
import type { Options } from '@popperjs/core';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
@ -11,9 +11,10 @@ import { noop } from 'lodash';
|
||||||
|
|
||||||
import type { Theme } from '../util/theme';
|
import type { Theme } from '../util/theme';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
import { themeClassName } from '../util/theme';
|
import { themeClassName } from '../util/theme';
|
||||||
|
|
||||||
type OptionType<T> = {
|
export type ContextMenuOptionType<T> = {
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
readonly icon?: string;
|
readonly icon?: string;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
|
@ -21,47 +22,53 @@ type OptionType<T> = {
|
||||||
readonly value?: T;
|
readonly value?: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextMenuPropsType<T> = {
|
export type PropsType<T> = {
|
||||||
readonly focusedIndex?: number;
|
readonly children?: ReactNode;
|
||||||
readonly isMenuShowing: boolean;
|
readonly i18n: LocalizerType;
|
||||||
readonly menuOptions: ReadonlyArray<OptionType<T>>;
|
readonly menuOptions: ReadonlyArray<ContextMenuOptionType<T>>;
|
||||||
readonly onClose: () => unknown;
|
readonly moduleClassName?: string;
|
||||||
|
readonly onClick?: () => unknown;
|
||||||
|
readonly onMenuShowingChanged?: (value: boolean) => unknown;
|
||||||
readonly popperOptions?: Pick<Options, 'placement' | 'strategy'>;
|
readonly popperOptions?: Pick<Options, 'placement' | 'strategy'>;
|
||||||
readonly referenceElement: HTMLElement | null;
|
|
||||||
readonly theme?: Theme;
|
readonly theme?: Theme;
|
||||||
readonly title?: string;
|
readonly title?: string;
|
||||||
readonly value?: T;
|
readonly value?: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsType<T> = {
|
export function ContextMenu<T>({
|
||||||
readonly buttonClassName?: string;
|
children,
|
||||||
readonly buttonStyle?: CSSProperties;
|
i18n,
|
||||||
readonly i18n: LocalizerType;
|
|
||||||
} & Pick<
|
|
||||||
ContextMenuPropsType<T>,
|
|
||||||
'menuOptions' | 'popperOptions' | 'theme' | 'title' | 'value'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export function ContextMenuPopper<T>({
|
|
||||||
menuOptions,
|
menuOptions,
|
||||||
focusedIndex,
|
moduleClassName,
|
||||||
isMenuShowing,
|
onClick,
|
||||||
|
onMenuShowingChanged,
|
||||||
popperOptions,
|
popperOptions,
|
||||||
onClose,
|
|
||||||
referenceElement,
|
|
||||||
title,
|
|
||||||
theme,
|
theme,
|
||||||
|
title,
|
||||||
value,
|
value,
|
||||||
}: ContextMenuPropsType<T>): JSX.Element | null {
|
}: PropsType<T>): JSX.Element {
|
||||||
|
const [isMenuShowing, setIsMenuShowing] = useState<boolean>(false);
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [referenceElement, setReferenceElement] =
|
||||||
|
useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: 'top-start',
|
placement: 'top-start',
|
||||||
strategy: 'fixed',
|
strategy: 'fixed',
|
||||||
...popperOptions,
|
...popperOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onMenuShowingChanged) {
|
||||||
|
onMenuShowingChanged(isMenuShowing);
|
||||||
|
}
|
||||||
|
}, [isMenuShowing, onMenuShowingChanged]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMenuShowing) {
|
if (!isMenuShowing) {
|
||||||
return noop;
|
return noop;
|
||||||
|
@ -69,7 +76,7 @@ export function ContextMenuPopper<T>({
|
||||||
|
|
||||||
const handleOutsideClick = (event: MouseEvent) => {
|
const handleOutsideClick = (event: MouseEvent) => {
|
||||||
if (!referenceElement?.contains(event.target as Node)) {
|
if (!referenceElement?.contains(event.target as Node)) {
|
||||||
onClose();
|
setIsMenuShowing(false);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -79,92 +86,10 @@ export function ContextMenuPopper<T>({
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', handleOutsideClick);
|
document.removeEventListener('click', handleOutsideClick);
|
||||||
};
|
};
|
||||||
}, [isMenuShowing, onClose, referenceElement]);
|
}, [isMenuShowing, referenceElement]);
|
||||||
|
|
||||||
if (!isMenuShowing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusTrap
|
|
||||||
focusTrapOptions={{
|
|
||||||
allowOutsideClick: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={theme ? themeClassName(theme) : undefined}>
|
|
||||||
<div
|
|
||||||
className={classNames('ContextMenu__popper', {
|
|
||||||
'ContextMenu__popper--single-item': menuOptions.length === 1,
|
|
||||||
})}
|
|
||||||
ref={setPopperElement}
|
|
||||||
style={styles.popper}
|
|
||||||
{...attributes.popper}
|
|
||||||
>
|
|
||||||
{title && <div className="ContextMenu__title">{title}</div>}
|
|
||||||
{menuOptions.map((option, index) => (
|
|
||||||
<button
|
|
||||||
aria-label={option.label}
|
|
||||||
className={classNames({
|
|
||||||
ContextMenu__option: true,
|
|
||||||
'ContextMenu__option--focused': focusedIndex === index,
|
|
||||||
})}
|
|
||||||
key={option.label}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
option.onClick(option.value);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="ContextMenu__option--container">
|
|
||||||
{option.icon && (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'ContextMenu__option--icon',
|
|
||||||
option.icon
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div className="ContextMenu__option--title">
|
|
||||||
{option.label}
|
|
||||||
</div>
|
|
||||||
{option.description && (
|
|
||||||
<div className="ContextMenu__option--description">
|
|
||||||
{option.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{typeof value !== 'undefined' &&
|
|
||||||
typeof option.value !== 'undefined' &&
|
|
||||||
value === option.value ? (
|
|
||||||
<div className="ContextMenu__option--selected" />
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FocusTrap>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContextMenu<T>({
|
|
||||||
buttonClassName,
|
|
||||||
buttonStyle,
|
|
||||||
i18n,
|
|
||||||
menuOptions,
|
|
||||||
popperOptions,
|
|
||||||
theme,
|
|
||||||
title,
|
|
||||||
value,
|
|
||||||
}: PropsType<T>): JSX.Element {
|
|
||||||
const [menuShowing, setMenuShowing] = useState<boolean>(false);
|
|
||||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||||
if (!menuShowing) {
|
if (!isMenuShowing) {
|
||||||
if (ev.key === 'Enter') {
|
if (ev.key === 'Enter') {
|
||||||
setFocusedIndex(0);
|
setFocusedIndex(0);
|
||||||
}
|
}
|
||||||
|
@ -194,46 +119,101 @@ export function ContextMenu<T>({
|
||||||
const focusedOption = menuOptions[focusedIndex];
|
const focusedOption = menuOptions[focusedIndex];
|
||||||
focusedOption.onClick(focusedOption.value);
|
focusedOption.onClick(focusedOption.value);
|
||||||
}
|
}
|
||||||
setMenuShowing(false);
|
setIsMenuShowing(false);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (ev: KeyboardEvent | React.MouseEvent) => {
|
const handleClick = (ev: KeyboardEvent | React.MouseEvent) => {
|
||||||
setMenuShowing(true);
|
setIsMenuShowing(true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] =
|
const getClassName = getClassNamesFor('ContextMenu', moduleClassName);
|
||||||
useState<HTMLButtonElement | null>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={theme ? themeClassName(theme) : undefined}>
|
<div className={theme ? themeClassName(theme) : undefined}>
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('ContextMenu--button')}
|
aria-label={i18n('ContextMenu--button')}
|
||||||
className={classNames(buttonClassName, {
|
className={classNames(
|
||||||
ContextMenu__button: true,
|
getClassName('__button'),
|
||||||
'ContextMenu__button--active': menuShowing,
|
isMenuShowing ? getClassName('__button--active') : undefined
|
||||||
})}
|
)}
|
||||||
onClick={handleClick}
|
onClick={onClick || handleClick}
|
||||||
|
onContextMenu={handleClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
style={buttonStyle}
|
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
>
|
||||||
{menuShowing && (
|
{children}
|
||||||
<ContextMenuPopper
|
</button>
|
||||||
focusedIndex={focusedIndex}
|
{isMenuShowing && (
|
||||||
isMenuShowing={menuShowing}
|
<FocusTrap
|
||||||
menuOptions={menuOptions}
|
focusTrapOptions={{
|
||||||
onClose={() => setMenuShowing(false)}
|
allowOutsideClick: true,
|
||||||
popperOptions={popperOptions}
|
}}
|
||||||
referenceElement={referenceElement}
|
>
|
||||||
title={title}
|
<div className={theme ? themeClassName(theme) : undefined}>
|
||||||
value={value}
|
<div
|
||||||
/>
|
className={classNames(
|
||||||
|
getClassName('__popper'),
|
||||||
|
menuOptions.length === 1
|
||||||
|
? getClassName('__popper--single-item')
|
||||||
|
: undefined
|
||||||
|
)}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
{title && <div className={getClassName('__title')}>{title}</div>}
|
||||||
|
{menuOptions.map((option, index) => (
|
||||||
|
<button
|
||||||
|
aria-label={option.label}
|
||||||
|
className={classNames(
|
||||||
|
getClassName('__option'),
|
||||||
|
focusedIndex === index
|
||||||
|
? getClassName('__option--focused')
|
||||||
|
: undefined
|
||||||
|
)}
|
||||||
|
key={option.label}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
option.onClick(option.value);
|
||||||
|
setIsMenuShowing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={getClassName('__option--container')}>
|
||||||
|
{option.icon && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
getClassName('__option--icon'),
|
||||||
|
option.icon
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className={getClassName('__option--title')}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
{option.description && (
|
||||||
|
<div className={getClassName('__option--description')}>
|
||||||
|
{option.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{typeof value !== 'undefined' &&
|
||||||
|
typeof option.value !== 'undefined' &&
|
||||||
|
value === option.value ? (
|
||||||
|
<div className={getClassName('__option--selected')} />
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusTrap>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -569,14 +569,6 @@ export const MediaEditor = ({
|
||||||
value={sliderValue}
|
value={sliderValue}
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
buttonClassName={classNames('MediaEditor__tools__tool', {
|
|
||||||
'MediaEditor__tools__button--text-regular':
|
|
||||||
textStyle === TextStyle.Regular,
|
|
||||||
'MediaEditor__tools__button--text-highlight':
|
|
||||||
textStyle === TextStyle.Highlight,
|
|
||||||
'MediaEditor__tools__button--text-outline':
|
|
||||||
textStyle === TextStyle.Outline,
|
|
||||||
})}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
menuOptions={[
|
menuOptions={[
|
||||||
{
|
{
|
||||||
|
@ -598,6 +590,14 @@ export const MediaEditor = ({
|
||||||
value: TextStyle.Outline,
|
value: TextStyle.Outline,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
moduleClassName={classNames('MediaEditor__tools__tool', {
|
||||||
|
'MediaEditor__tools__button--text-regular':
|
||||||
|
textStyle === TextStyle.Regular,
|
||||||
|
'MediaEditor__tools__button--text-highlight':
|
||||||
|
textStyle === TextStyle.Highlight,
|
||||||
|
'MediaEditor__tools__button--text-outline':
|
||||||
|
textStyle === TextStyle.Outline,
|
||||||
|
})}
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
value={textStyle}
|
value={textStyle}
|
||||||
/>
|
/>
|
||||||
|
@ -628,11 +628,6 @@ export const MediaEditor = ({
|
||||||
value={sliderValue}
|
value={sliderValue}
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
buttonClassName={classNames('MediaEditor__tools__tool', {
|
|
||||||
'MediaEditor__tools__button--draw-pen': drawTool === DrawTool.Pen,
|
|
||||||
'MediaEditor__tools__button--draw-highlighter':
|
|
||||||
drawTool === DrawTool.Highlighter,
|
|
||||||
})}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
menuOptions={[
|
menuOptions={[
|
||||||
{
|
{
|
||||||
|
@ -648,20 +643,15 @@ export const MediaEditor = ({
|
||||||
value: DrawTool.Highlighter,
|
value: DrawTool.Highlighter,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
moduleClassName={classNames('MediaEditor__tools__tool', {
|
||||||
|
'MediaEditor__tools__button--draw-pen': drawTool === DrawTool.Pen,
|
||||||
|
'MediaEditor__tools__button--draw-highlighter':
|
||||||
|
drawTool === DrawTool.Highlighter,
|
||||||
|
})}
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
value={drawTool}
|
value={drawTool}
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
buttonClassName={classNames('MediaEditor__tools__tool', {
|
|
||||||
'MediaEditor__tools__button--width-thin':
|
|
||||||
drawWidth === DrawWidth.Thin,
|
|
||||||
'MediaEditor__tools__button--width-regular':
|
|
||||||
drawWidth === DrawWidth.Regular,
|
|
||||||
'MediaEditor__tools__button--width-medium':
|
|
||||||
drawWidth === DrawWidth.Medium,
|
|
||||||
'MediaEditor__tools__button--width-heavy':
|
|
||||||
drawWidth === DrawWidth.Heavy,
|
|
||||||
})}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
menuOptions={[
|
menuOptions={[
|
||||||
{
|
{
|
||||||
|
@ -689,6 +679,16 @@ export const MediaEditor = ({
|
||||||
value: DrawWidth.Heavy,
|
value: DrawWidth.Heavy,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
moduleClassName={classNames('MediaEditor__tools__tool', {
|
||||||
|
'MediaEditor__tools__button--width-thin':
|
||||||
|
drawWidth === DrawWidth.Thin,
|
||||||
|
'MediaEditor__tools__button--width-regular':
|
||||||
|
drawWidth === DrawWidth.Regular,
|
||||||
|
'MediaEditor__tools__button--width-medium':
|
||||||
|
drawWidth === DrawWidth.Medium,
|
||||||
|
'MediaEditor__tools__button--width-heavy':
|
||||||
|
drawWidth === DrawWidth.Heavy,
|
||||||
|
})}
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
value={drawWidth}
|
value={drawWidth}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -83,7 +83,10 @@ export const MyStories = ({
|
||||||
aria-label={i18n('MyStories__story')}
|
aria-label={i18n('MyStories__story')}
|
||||||
className="MyStories__story__preview"
|
className="MyStories__story__preview"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
viewStory(story.messageId, StoryViewModeType.Single)
|
viewStory({
|
||||||
|
storyId: story.messageId,
|
||||||
|
storyViewMode: StoryViewModeType.Single,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -120,9 +123,15 @@ export const MyStories = ({
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
buttonClassName="MyStories__story__more"
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
menuOptions={[
|
menuOptions={[
|
||||||
|
{
|
||||||
|
icon: 'MyStories__icon--forward',
|
||||||
|
label: i18n('forward'),
|
||||||
|
onClick: () => {
|
||||||
|
onForward(story.messageId);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'MyStories__icon--save',
|
icon: 'MyStories__icon--save',
|
||||||
label: i18n('save'),
|
label: i18n('save'),
|
||||||
|
@ -131,10 +140,14 @@ export const MyStories = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'MyStories__icon--forward',
|
icon: 'StoryListItem__icon--info',
|
||||||
label: i18n('forward'),
|
label: i18n('StoryListItem__info'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onForward(story.messageId);
|
viewStory({
|
||||||
|
storyId: story.messageId,
|
||||||
|
storyViewMode: StoryViewModeType.Single,
|
||||||
|
shouldShowDetailsModal: true,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -145,6 +158,7 @@ export const MyStories = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
moduleClassName="MyStories__story__more"
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const MyStoriesButton = ({
|
||||||
<div className="Stories__my-stories">
|
<div className="Stories__my-stories">
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('StoryListItem__label')}
|
aria-label={i18n('StoryListItem__label')}
|
||||||
className="StoryListItem"
|
className="StoryListItem__button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -101,12 +101,12 @@ export const Stories = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onStoriesSettings={showStoriesSettings}
|
onStoriesSettings={showStoriesSettings}
|
||||||
onStoryClicked={viewUserStories}
|
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
showConversation={showConversation}
|
showConversation={showConversation}
|
||||||
stories={stories}
|
stories={stories}
|
||||||
toggleHideStories={toggleHideStories}
|
toggleHideStories={toggleHideStories}
|
||||||
toggleStoriesView={toggleStoriesView}
|
toggleStoriesView={toggleStoriesView}
|
||||||
|
viewUserStories={viewUserStories}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -66,12 +66,12 @@ export type PropsType = {
|
||||||
onAddStory: () => unknown;
|
onAddStory: () => unknown;
|
||||||
onMyStoriesClicked: () => unknown;
|
onMyStoriesClicked: () => unknown;
|
||||||
onStoriesSettings: () => unknown;
|
onStoriesSettings: () => unknown;
|
||||||
onStoryClicked: (conversationId: string) => unknown;
|
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
stories: Array<ConversationStoryType>;
|
stories: Array<ConversationStoryType>;
|
||||||
toggleHideStories: (conversationId: string) => unknown;
|
toggleHideStories: (conversationId: string) => unknown;
|
||||||
toggleStoriesView: () => unknown;
|
toggleStoriesView: () => unknown;
|
||||||
|
viewUserStories: (conversationId: string) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoriesPane = ({
|
export const StoriesPane = ({
|
||||||
|
@ -82,12 +82,12 @@ export const StoriesPane = ({
|
||||||
onAddStory,
|
onAddStory,
|
||||||
onMyStoriesClicked,
|
onMyStoriesClicked,
|
||||||
onStoriesSettings,
|
onStoriesSettings,
|
||||||
onStoryClicked,
|
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
showConversation,
|
showConversation,
|
||||||
stories,
|
stories,
|
||||||
toggleHideStories,
|
toggleHideStories,
|
||||||
toggleStoriesView,
|
toggleStoriesView,
|
||||||
|
viewUserStories,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isShowingHiddenStories, setIsShowingHiddenStories] = useState(false);
|
const [isShowingHiddenStories, setIsShowingHiddenStories] = useState(false);
|
||||||
|
@ -122,7 +122,6 @@ export const StoriesPane = ({
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
buttonClassName="Stories__pane__settings"
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
menuOptions={[
|
menuOptions={[
|
||||||
{
|
{
|
||||||
|
@ -130,6 +129,7 @@ export const StoriesPane = ({
|
||||||
label: i18n('StoriesSettings__context-menu'),
|
label: i18n('StoriesSettings__context-menu'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
moduleClassName="Stories__pane__settings"
|
||||||
popperOptions={{
|
popperOptions={{
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
strategy: 'absolute',
|
strategy: 'absolute',
|
||||||
|
@ -166,9 +166,6 @@ export const StoriesPane = ({
|
||||||
group={story.group}
|
group={story.group}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
key={story.storyView.timestamp}
|
key={story.storyView.timestamp}
|
||||||
onClick={() => {
|
|
||||||
onStoryClicked(story.conversationId);
|
|
||||||
}}
|
|
||||||
onHideStory={toggleHideStories}
|
onHideStory={toggleHideStories}
|
||||||
onGoToConversation={conversationId => {
|
onGoToConversation={conversationId => {
|
||||||
showConversation({ conversationId });
|
showConversation({ conversationId });
|
||||||
|
@ -176,6 +173,7 @@ export const StoriesPane = ({
|
||||||
}}
|
}}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
story={story.storyView}
|
story={story.storyView}
|
||||||
|
viewUserStories={viewUserStories}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{Boolean(hiddenStories.length) && (
|
{Boolean(hiddenStories.length) && (
|
||||||
|
@ -195,9 +193,6 @@ export const StoriesPane = ({
|
||||||
key={story.storyView.timestamp}
|
key={story.storyView.timestamp}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isHidden
|
isHidden
|
||||||
onClick={() => {
|
|
||||||
onStoryClicked(story.conversationId);
|
|
||||||
}}
|
|
||||||
onHideStory={toggleHideStories}
|
onHideStory={toggleHideStories}
|
||||||
onGoToConversation={conversationId => {
|
onGoToConversation={conversationId => {
|
||||||
showConversation({ conversationId });
|
showConversation({ conversationId });
|
||||||
|
@ -205,6 +200,7 @@ export const StoriesPane = ({
|
||||||
}}
|
}}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
story={story.storyView}
|
story={story.storyView}
|
||||||
|
viewUserStories={viewUserStories}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -267,18 +267,6 @@ export const StoryCreator = ({
|
||||||
value={sliderValue}
|
value={sliderValue}
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
buttonClassName={classNames('StoryCreator__tools__tool', {
|
|
||||||
'StoryCreator__tools__button--font-regular':
|
|
||||||
textStyle === TextStyle.Regular,
|
|
||||||
'StoryCreator__tools__button--font-bold':
|
|
||||||
textStyle === TextStyle.Bold,
|
|
||||||
'StoryCreator__tools__button--font-serif':
|
|
||||||
textStyle === TextStyle.Serif,
|
|
||||||
'StoryCreator__tools__button--font-script':
|
|
||||||
textStyle === TextStyle.Script,
|
|
||||||
'StoryCreator__tools__button--font-condensed':
|
|
||||||
textStyle === TextStyle.Condensed,
|
|
||||||
})}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
menuOptions={[
|
menuOptions={[
|
||||||
{
|
{
|
||||||
|
@ -312,6 +300,18 @@ export const StoryCreator = ({
|
||||||
value: TextStyle.Condensed,
|
value: TextStyle.Condensed,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
moduleClassName={classNames('StoryCreator__tools__tool', {
|
||||||
|
'StoryCreator__tools__button--font-regular':
|
||||||
|
textStyle === TextStyle.Regular,
|
||||||
|
'StoryCreator__tools__button--font-bold':
|
||||||
|
textStyle === TextStyle.Bold,
|
||||||
|
'StoryCreator__tools__button--font-serif':
|
||||||
|
textStyle === TextStyle.Serif,
|
||||||
|
'StoryCreator__tools__button--font-script':
|
||||||
|
textStyle === TextStyle.Script,
|
||||||
|
'StoryCreator__tools__button--font-condensed':
|
||||||
|
textStyle === TextStyle.Condensed,
|
||||||
|
})}
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
value={textStyle}
|
value={textStyle}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Meta, Story } from '@storybook/react';
|
||||||
|
import React from 'react';
|
||||||
|
import casual from 'casual';
|
||||||
|
|
||||||
|
import type { PropsType } from './StoryDetailsModal';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
|
import { StoryDetailsModal } from './StoryDetailsModal';
|
||||||
|
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
|
||||||
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/StoryDetailsModal',
|
||||||
|
component: StoryDetailsModal,
|
||||||
|
argTypes: {
|
||||||
|
getPreferredBadge: { action: true },
|
||||||
|
i18n: {
|
||||||
|
defaultValue: i18n,
|
||||||
|
},
|
||||||
|
onClose: { action: true },
|
||||||
|
sender: {
|
||||||
|
defaultValue: getDefaultConversation(),
|
||||||
|
},
|
||||||
|
sendState: {
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
defaultValue: fakeAttachment().size,
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
defaultValue: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
const Template: Story<PropsType> = args => <StoryDetailsModal {...args} />;
|
||||||
|
|
||||||
|
export const MyStory = Template.bind({});
|
||||||
|
MyStory.args = {
|
||||||
|
sendState: [
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Delivered,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Delivered,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Delivered,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Delivered,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Viewed,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Viewed,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Viewed,
|
||||||
|
updatedAt: casual.unix_time,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OtherStory = Template.bind({});
|
||||||
|
OtherStory.args = {};
|
|
@ -0,0 +1,244 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import formatFileSize from 'filesize';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
|
import type { StorySendStateType, StoryViewType } from '../types/Stories';
|
||||||
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
|
import { ContactName } from './conversation/ContactName';
|
||||||
|
import { ContextMenu } from './ContextMenu';
|
||||||
|
import { Intl } from './Intl';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
|
import { Theme } from '../util/theme';
|
||||||
|
import { ThemeType } from '../types/Util';
|
||||||
|
import { Time } from './Time';
|
||||||
|
import { formatDateTimeLong } from '../util/timestamp';
|
||||||
|
import { groupBy } from '../util/mapUtil';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
onClose: () => unknown;
|
||||||
|
sender: StoryViewType['sender'];
|
||||||
|
sendState?: Array<StorySendStateType>;
|
||||||
|
size?: number;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactSortCollator = new window.Intl.Collator();
|
||||||
|
|
||||||
|
function getI18nKey(sendStatus: SendStatus | undefined): string {
|
||||||
|
if (sendStatus === SendStatus.Failed) {
|
||||||
|
return 'MessageDetailsHeader--Failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendStatus === SendStatus.Viewed) {
|
||||||
|
return 'MessageDetailsHeader--Viewed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendStatus === SendStatus.Read) {
|
||||||
|
return 'MessageDetailsHeader--Read';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendStatus === SendStatus.Delivered) {
|
||||||
|
return 'MessageDetailsHeader--Delivered';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendStatus === SendStatus.Sent) {
|
||||||
|
return 'MessageDetailsHeader--Sent';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendStatus === SendStatus.Pending) {
|
||||||
|
return 'MessageDetailsHeader--Pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'from';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoryDetailsModal = ({
|
||||||
|
getPreferredBadge,
|
||||||
|
i18n,
|
||||||
|
onClose,
|
||||||
|
sender,
|
||||||
|
sendState,
|
||||||
|
size,
|
||||||
|
timestamp,
|
||||||
|
}: PropsType): JSX.Element => {
|
||||||
|
const contactsBySendStatus = sendState
|
||||||
|
? groupBy(sendState, contact => contact.status)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let content: JSX.Element;
|
||||||
|
if (contactsBySendStatus) {
|
||||||
|
content = (
|
||||||
|
<div className="StoryDetailsModal__contact-container">
|
||||||
|
{[
|
||||||
|
SendStatus.Failed,
|
||||||
|
SendStatus.Viewed,
|
||||||
|
SendStatus.Read,
|
||||||
|
SendStatus.Delivered,
|
||||||
|
SendStatus.Sent,
|
||||||
|
SendStatus.Pending,
|
||||||
|
].map(sendStatus => {
|
||||||
|
const contacts = contactsBySendStatus.get(sendStatus);
|
||||||
|
|
||||||
|
if (!contacts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18nKey = getI18nKey(sendStatus);
|
||||||
|
|
||||||
|
const sortedContacts = [...contacts].sort((a, b) =>
|
||||||
|
contactSortCollator.compare(a.recipient.title, b.recipient.title)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={i18nKey} className="StoryDetailsModal__contact-group">
|
||||||
|
<div className="StoryDetailsModal__contact-group__header">
|
||||||
|
{i18n(i18nKey)}
|
||||||
|
</div>
|
||||||
|
{sortedContacts.map(status => {
|
||||||
|
const contact = status.recipient;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={contact.id} className="StoryDetailsModal__contact">
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||||
|
avatarPath={contact.avatarPath}
|
||||||
|
badge={getPreferredBadge(contact.badges)}
|
||||||
|
color={contact.color}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={contact.isMe}
|
||||||
|
name={contact.profileName}
|
||||||
|
phoneNumber={contact.phoneNumber}
|
||||||
|
profileName={contact.profileName}
|
||||||
|
sharedGroupNames={contact.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_SIX}
|
||||||
|
theme={ThemeType.dark}
|
||||||
|
title={contact.title}
|
||||||
|
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||||
|
/>
|
||||||
|
<div className="StoryDetailsModal__contact__text">
|
||||||
|
<ContactName title={contact.title} />
|
||||||
|
</div>
|
||||||
|
{status.updatedAt && (
|
||||||
|
<Time
|
||||||
|
className="StoryDetailsModal__status-timestamp"
|
||||||
|
timestamp={status.updatedAt}
|
||||||
|
>
|
||||||
|
{formatDateTimeLong(i18n, status.updatedAt)}
|
||||||
|
</Time>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<div className="StoryDetailsModal__contact-container">
|
||||||
|
<div className="StoryDetailsModal__contact-group">
|
||||||
|
<div className="StoryDetailsModal__contact-group__header">
|
||||||
|
{i18n('sent')}
|
||||||
|
</div>
|
||||||
|
<div className="StoryDetailsModal__contact">
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={sender.acceptedMessageRequest}
|
||||||
|
avatarPath={sender.avatarPath}
|
||||||
|
badge={getPreferredBadge(sender.badges)}
|
||||||
|
color={sender.color}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={sender.isMe}
|
||||||
|
name={sender.profileName}
|
||||||
|
profileName={sender.profileName}
|
||||||
|
sharedGroupNames={sender.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_SIX}
|
||||||
|
theme={ThemeType.dark}
|
||||||
|
title={sender.title}
|
||||||
|
/>
|
||||||
|
<div className="StoryDetailsModal__contact__text">
|
||||||
|
<div className="StoryDetailsModal__contact__name">
|
||||||
|
<ContactName title={sender.title} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Time
|
||||||
|
className="StoryDetailsModal__status-timestamp"
|
||||||
|
timestamp={timestamp}
|
||||||
|
>
|
||||||
|
{formatDateTimeLong(i18n, timestamp)}
|
||||||
|
</Time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
hasXButton
|
||||||
|
i18n={i18n}
|
||||||
|
moduleClassName="StoryDetailsModal"
|
||||||
|
onClose={onClose}
|
||||||
|
useFocusTrap={false}
|
||||||
|
theme={Theme.Dark}
|
||||||
|
title={
|
||||||
|
<ContextMenu
|
||||||
|
i18n={i18n}
|
||||||
|
menuOptions={[
|
||||||
|
{
|
||||||
|
icon: 'StoryDetailsModal__copy-icon',
|
||||||
|
label: i18n('StoryDetailsModal__copy-timestamp'),
|
||||||
|
onClick: () => {
|
||||||
|
window.navigator.clipboard.writeText(String(timestamp));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
moduleClassName="StoryDetailsModal__debugger"
|
||||||
|
popperOptions={{
|
||||||
|
placement: 'bottom',
|
||||||
|
strategy: 'absolute',
|
||||||
|
}}
|
||||||
|
theme={Theme.Dark}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="StoryDetailsModal__sent-time"
|
||||||
|
components={[
|
||||||
|
<Time
|
||||||
|
className="StoryDetailsModal__debugger__button__text"
|
||||||
|
timestamp={timestamp}
|
||||||
|
>
|
||||||
|
{formatDateTimeLong(i18n, timestamp)}
|
||||||
|
</Time>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{size && (
|
||||||
|
<div>
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="StoryDetailsModal__file-size"
|
||||||
|
components={[
|
||||||
|
<span className="StoryDetailsModal__debugger__button__text">
|
||||||
|
{formatFileSize(size)}
|
||||||
|
</span>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ContextMenu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -23,7 +23,6 @@ export default {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultValue: i18n,
|
defaultValue: i18n,
|
||||||
},
|
},
|
||||||
onClick: { action: true },
|
|
||||||
onGoToConversation: { action: true },
|
onGoToConversation: { action: true },
|
||||||
onHideStory: { action: true },
|
onHideStory: { action: true },
|
||||||
queueStoryDownload: { action: true },
|
queueStoryDownload: { action: true },
|
||||||
|
@ -34,6 +33,7 @@ export default {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
viewUserStories: { action: true },
|
||||||
},
|
},
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type { LocalizerType } from '../types/Util';
|
||||||
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
|
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { ContextMenuPopper } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
import { HasStories } from '../types/Stories';
|
import { HasStories } from '../types/Stories';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
|
@ -15,27 +15,27 @@ import { getAvatarColor } from '../types/Colors';
|
||||||
|
|
||||||
export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
|
export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onClick: () => unknown;
|
|
||||||
onGoToConversation: (conversationId: string) => unknown;
|
onGoToConversation: (conversationId: string) => unknown;
|
||||||
onHideStory: (conversationId: string) => unknown;
|
onHideStory: (conversationId: string) => unknown;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
story: StoryViewType;
|
story: StoryViewType;
|
||||||
|
viewUserStories: (
|
||||||
|
conversationId: string,
|
||||||
|
shouldShowDetailsModal?: boolean
|
||||||
|
) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoryListItem = ({
|
export const StoryListItem = ({
|
||||||
group,
|
group,
|
||||||
i18n,
|
i18n,
|
||||||
isHidden,
|
isHidden,
|
||||||
onClick,
|
|
||||||
onGoToConversation,
|
onGoToConversation,
|
||||||
onHideStory,
|
onHideStory,
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
story,
|
story,
|
||||||
|
viewUserStories,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
||||||
const [isShowingContextMenu, setIsShowingContextMenu] = useState(false);
|
|
||||||
const [referenceElement, setReferenceElement] =
|
|
||||||
useState<HTMLButtonElement | null>(null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attachment,
|
attachment,
|
||||||
|
@ -72,21 +72,42 @@ export const StoryListItem = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<ContextMenu
|
||||||
aria-label={i18n('StoryListItem__label')}
|
aria-label={i18n('StoryListItem__label')}
|
||||||
className={classNames('StoryListItem', {
|
i18n={i18n}
|
||||||
|
menuOptions={[
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--hide',
|
||||||
|
label: isHidden
|
||||||
|
? i18n('StoryListItem__unhide')
|
||||||
|
: i18n('StoryListItem__hide'),
|
||||||
|
onClick: () => {
|
||||||
|
if (isHidden) {
|
||||||
|
onHideStory(sender.id);
|
||||||
|
} else {
|
||||||
|
setHasConfirmHideStory(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--info',
|
||||||
|
label: i18n('StoryListItem__info'),
|
||||||
|
onClick: () => viewUserStories(sender.id, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--chat',
|
||||||
|
label: i18n('StoryListItem__go-to-chat'),
|
||||||
|
onClick: () => onGoToConversation(sender.id),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
moduleClassName={classNames('StoryListItem', {
|
||||||
'StoryListItem--hidden': isHidden,
|
'StoryListItem--hidden': isHidden,
|
||||||
})}
|
})}
|
||||||
onClick={onClick}
|
onClick={() => viewUserStories(sender.id)}
|
||||||
onContextMenu={ev => {
|
popperOptions={{
|
||||||
ev.preventDefault();
|
placement: 'bottom',
|
||||||
ev.stopPropagation();
|
strategy: 'absolute',
|
||||||
|
|
||||||
setIsShowingContextMenu(true);
|
|
||||||
}}
|
}}
|
||||||
ref={setReferenceElement}
|
|
||||||
tabIndex={0}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
acceptedMessageRequest={acceptedMessageRequest}
|
acceptedMessageRequest={acceptedMessageRequest}
|
||||||
|
@ -133,38 +154,7 @@ export const StoryListItem = ({
|
||||||
storyId={story.messageId}
|
storyId={story.messageId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</ContextMenu>
|
||||||
<ContextMenuPopper
|
|
||||||
isMenuShowing={isShowingContextMenu}
|
|
||||||
menuOptions={[
|
|
||||||
{
|
|
||||||
icon: 'StoryListItem__icon--hide',
|
|
||||||
label: isHidden
|
|
||||||
? i18n('StoryListItem__unhide')
|
|
||||||
: i18n('StoryListItem__hide'),
|
|
||||||
onClick: () => {
|
|
||||||
if (isHidden) {
|
|
||||||
onHideStory(sender.id);
|
|
||||||
} else {
|
|
||||||
setHasConfirmHideStory(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'StoryListItem__icon--chat',
|
|
||||||
label: i18n('StoryListItem__go-to-chat'),
|
|
||||||
onClick: () => {
|
|
||||||
onGoToConversation(sender.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClose={() => setIsShowingContextMenu(false)}
|
|
||||||
popperOptions={{
|
|
||||||
placement: 'bottom',
|
|
||||||
strategy: 'absolute',
|
|
||||||
}}
|
|
||||||
referenceElement={referenceElement}
|
|
||||||
/>
|
|
||||||
{hasConfirmHideStory && (
|
{hasConfirmHideStory && (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
actions={[
|
actions={[
|
||||||
|
|
|
@ -121,3 +121,22 @@ LongCaption.args = {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const YourStory = Template.bind({});
|
||||||
|
{
|
||||||
|
const storyView = getFakeStoryView(
|
||||||
|
'/fixtures/nathan-anderson-316188-unsplash.jpg'
|
||||||
|
);
|
||||||
|
|
||||||
|
YourStory.args = {
|
||||||
|
story: {
|
||||||
|
...storyView,
|
||||||
|
sender: {
|
||||||
|
...storyView.sender,
|
||||||
|
isMe: true,
|
||||||
|
},
|
||||||
|
sendState: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
YourStory.storyName = 'Your story';
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import React, {
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useSpring, animated, to } from '@react-spring/web';
|
import { useSpring, animated, to } from '@react-spring/web';
|
||||||
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
||||||
|
import type { ContextMenuOptionType } from './ContextMenu';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
|
@ -23,13 +24,14 @@ import * as log from '../logging/log';
|
||||||
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
|
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { ContextMenuPopper } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
import { Intl } from './Intl';
|
import { Intl } from './Intl';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
|
import { StoryDetailsModal } from './StoryDetailsModal';
|
||||||
|
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
|
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
|
||||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import { ToastType } from '../state/ducks/toast';
|
import { ToastType } from '../state/ducks/toast';
|
||||||
import { getAvatarColor } from '../types/Colors';
|
import { getAvatarColor } from '../types/Colors';
|
||||||
|
@ -40,6 +42,7 @@ import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
|
deleteStoryForEveryone: (story: StoryViewType) => unknown;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
group?: Pick<
|
group?: Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -74,6 +77,7 @@ export type PropsType = {
|
||||||
recentEmojis?: Array<string>;
|
recentEmojis?: Array<string>;
|
||||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||||
replyState?: ReplyStateType;
|
replyState?: ReplyStateType;
|
||||||
|
shouldShowDetailsModal?: boolean;
|
||||||
showToast: ShowToastActionCreatorType;
|
showToast: ShowToastActionCreatorType;
|
||||||
skinTone?: number;
|
skinTone?: number;
|
||||||
story: StoryViewType;
|
story: StoryViewType;
|
||||||
|
@ -95,6 +99,7 @@ enum Arrow {
|
||||||
|
|
||||||
export const StoryViewer = ({
|
export const StoryViewer = ({
|
||||||
currentIndex,
|
currentIndex,
|
||||||
|
deleteStoryForEveryone,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
group,
|
group,
|
||||||
hasAllStoriesMuted,
|
hasAllStoriesMuted,
|
||||||
|
@ -114,6 +119,7 @@ export const StoryViewer = ({
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
replyState,
|
replyState,
|
||||||
|
shouldShowDetailsModal,
|
||||||
showToast,
|
showToast,
|
||||||
skinTone,
|
skinTone,
|
||||||
story,
|
story,
|
||||||
|
@ -121,12 +127,14 @@ export const StoryViewer = ({
|
||||||
toggleHasAllStoriesMuted,
|
toggleHasAllStoriesMuted,
|
||||||
viewStory,
|
viewStory,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
|
const [isShowingContextMenu, setIsShowingContextMenu] =
|
||||||
|
useState<boolean>(false);
|
||||||
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
||||||
const [isShowingContextMenu, setIsShowingContextMenu] = useState(false);
|
|
||||||
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
||||||
const [referenceElement, setReferenceElement] =
|
|
||||||
useState<HTMLButtonElement | null>(null);
|
|
||||||
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
||||||
|
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
|
||||||
|
StoryViewType | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
const { attachment, canReply, isHidden, messageId, sendState, timestamp } =
|
const { attachment, canReply, isHidden, messageId, sendState, timestamp } =
|
||||||
story;
|
story;
|
||||||
|
@ -143,19 +151,25 @@ export const StoryViewer = ({
|
||||||
title,
|
title,
|
||||||
} = story.sender;
|
} = story.sender;
|
||||||
|
|
||||||
const [hasReplyModal, setHasReplyModal] = useState(false);
|
const [hasStoryViewsNRepliesModal, setHasStoryViewsNRepliesModal] =
|
||||||
|
useState(false);
|
||||||
|
const [hasStoryDetailsModal, setHasStoryDetailsModal] = useState(
|
||||||
|
Boolean(shouldShowDetailsModal)
|
||||||
|
);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
viewStory();
|
viewStory({
|
||||||
|
closeViewer: true,
|
||||||
|
});
|
||||||
}, [viewStory]);
|
}, [viewStory]);
|
||||||
|
|
||||||
const onEscape = useCallback(() => {
|
const onEscape = useCallback(() => {
|
||||||
if (hasReplyModal) {
|
if (hasStoryViewsNRepliesModal) {
|
||||||
setHasReplyModal(false);
|
setHasStoryViewsNRepliesModal(false);
|
||||||
} else {
|
} else {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [hasReplyModal, onClose]);
|
}, [hasStoryViewsNRepliesModal, onClose]);
|
||||||
|
|
||||||
useEscapeHandling(onEscape);
|
useEscapeHandling(onEscape);
|
||||||
|
|
||||||
|
@ -225,11 +239,11 @@ export const StoryViewer = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 100) {
|
if (value === 100) {
|
||||||
viewStory(
|
viewStory({
|
||||||
story.messageId,
|
storyId: story.messageId,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
StoryViewDirectionType.Next
|
viewDirection: StoryViewDirectionType.Next,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -263,7 +277,8 @@ export const StoryViewer = ({
|
||||||
const shouldPauseViewing =
|
const shouldPauseViewing =
|
||||||
hasConfirmHideStory ||
|
hasConfirmHideStory ||
|
||||||
hasExpandedCaption ||
|
hasExpandedCaption ||
|
||||||
hasReplyModal ||
|
hasStoryDetailsModal ||
|
||||||
|
hasStoryViewsNRepliesModal ||
|
||||||
isShowingContextMenu ||
|
isShowingContextMenu ||
|
||||||
pauseStory ||
|
pauseStory ||
|
||||||
Boolean(reactionEmoji);
|
Boolean(reactionEmoji);
|
||||||
|
@ -284,15 +299,19 @@ export const StoryViewer = ({
|
||||||
const navigateStories = useCallback(
|
const navigateStories = useCallback(
|
||||||
(ev: KeyboardEvent) => {
|
(ev: KeyboardEvent) => {
|
||||||
if (ev.key === 'ArrowRight') {
|
if (ev.key === 'ArrowRight') {
|
||||||
viewStory(story.messageId, storyViewMode, StoryViewDirectionType.Next);
|
viewStory({
|
||||||
|
storyId: story.messageId,
|
||||||
|
storyViewMode,
|
||||||
|
viewDirection: StoryViewDirectionType.Next,
|
||||||
|
});
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
} else if (ev.key === 'ArrowLeft') {
|
} else if (ev.key === 'ArrowLeft') {
|
||||||
viewStory(
|
viewStory({
|
||||||
story.messageId,
|
storyId: story.messageId,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
StoryViewDirectionType.Previous
|
viewDirection: StoryViewDirectionType.Previous,
|
||||||
);
|
});
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
|
@ -357,10 +376,50 @@ export const StoryViewer = ({
|
||||||
const replyCount = replies.length;
|
const replyCount = replies.length;
|
||||||
const viewCount = views.length;
|
const viewCount = views.length;
|
||||||
|
|
||||||
const shouldShowContextMenu = !sendState;
|
|
||||||
|
|
||||||
const hasPrevNextArrows = storyViewMode !== StoryViewModeType.Single;
|
const hasPrevNextArrows = storyViewMode !== StoryViewModeType.Single;
|
||||||
|
|
||||||
|
const contextMenuOptions: ReadonlyArray<ContextMenuOptionType<unknown>> =
|
||||||
|
sendState
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--info',
|
||||||
|
label: i18n('StoryListItem__info'),
|
||||||
|
onClick: () => setHasStoryDetailsModal(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--delete',
|
||||||
|
label: i18n('StoryListItem__delete'),
|
||||||
|
onClick: () => setConfirmDeleteStory(story),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--info',
|
||||||
|
label: i18n('StoryListItem__info'),
|
||||||
|
onClick: () => setHasStoryDetailsModal(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--hide',
|
||||||
|
label: isHidden
|
||||||
|
? i18n('StoryListItem__unhide')
|
||||||
|
: i18n('StoryListItem__hide'),
|
||||||
|
onClick: () => {
|
||||||
|
if (isHidden) {
|
||||||
|
onHideStory(id);
|
||||||
|
} else {
|
||||||
|
setHasConfirmHideStory(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--chat',
|
||||||
|
label: i18n('StoryListItem__go-to-chat'),
|
||||||
|
onClick: () => {
|
||||||
|
onGoToConversation(id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
||||||
<div className="StoryViewer">
|
<div className="StoryViewer">
|
||||||
|
@ -379,11 +438,11 @@ export const StoryViewer = ({
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
viewStory(
|
viewStory({
|
||||||
story.messageId,
|
storyId: story.messageId,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
StoryViewDirectionType.Previous
|
viewDirection: StoryViewDirectionType.Previous,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
onMouseMove={() => setArrowToShow(Arrow.Left)}
|
onMouseMove={() => setArrowToShow(Arrow.Left)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -519,15 +578,14 @@ export const StoryViewer = ({
|
||||||
onClick={toggleHasAllStoriesMuted}
|
onClick={toggleHasAllStoriesMuted}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
{shouldShowContextMenu && (
|
<ContextMenu
|
||||||
<button
|
aria-label={i18n('MyStories__more')}
|
||||||
aria-label={i18n('MyStories__more')}
|
i18n={i18n}
|
||||||
className="StoryViewer__more"
|
menuOptions={contextMenuOptions}
|
||||||
onClick={() => setIsShowingContextMenu(true)}
|
moduleClassName="StoryViewer__more"
|
||||||
ref={setReferenceElement}
|
onMenuShowingChanged={setIsShowingContextMenu}
|
||||||
type="button"
|
theme={Theme.Dark}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__progress">
|
<div className="StoryViewer__progress">
|
||||||
|
@ -555,14 +613,14 @@ export const StoryViewer = ({
|
||||||
{canReply && (
|
{canReply && (
|
||||||
<button
|
<button
|
||||||
className="StoryViewer__reply"
|
className="StoryViewer__reply"
|
||||||
onClick={() => setHasReplyModal(true)}
|
onClick={() => setHasStoryViewsNRepliesModal(true)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{viewCount > 0 || replyCount > 0 ? (
|
{sendState || replyCount > 0 ? (
|
||||||
<span className="StoryViewer__reply__chevron">
|
<span className="StoryViewer__reply__chevron">
|
||||||
{viewCount > 0 &&
|
{sendState &&
|
||||||
(viewCount === 1 ? (
|
(viewCount === 1 ? (
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -593,7 +651,7 @@ export const StoryViewer = ({
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{!viewCount && !replyCount && (
|
{!sendState && !replyCount && (
|
||||||
<span className="StoryViewer__reply__arrow">
|
<span className="StoryViewer__reply__arrow">
|
||||||
{isGroupStory
|
{isGroupStory
|
||||||
? i18n('StoryViewer__reply-group')
|
? i18n('StoryViewer__reply-group')
|
||||||
|
@ -615,11 +673,11 @@ export const StoryViewer = ({
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
viewStory(
|
viewStory({
|
||||||
story.messageId,
|
storyId: story.messageId,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
StoryViewDirectionType.Next
|
viewDirection: StoryViewDirectionType.Next,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
onMouseMove={() => setArrowToShow(Arrow.Right)}
|
onMouseMove={() => setArrowToShow(Arrow.Right)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -634,51 +692,35 @@ export const StoryViewer = ({
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ContextMenuPopper
|
{hasStoryDetailsModal && (
|
||||||
isMenuShowing={isShowingContextMenu}
|
<StoryDetailsModal
|
||||||
menuOptions={[
|
getPreferredBadge={getPreferredBadge}
|
||||||
{
|
i18n={i18n}
|
||||||
icon: 'StoryListItem__icon--hide',
|
onClose={() => setHasStoryDetailsModal(false)}
|
||||||
label: isHidden
|
sender={story.sender}
|
||||||
? i18n('StoryListItem__unhide')
|
sendState={sendState}
|
||||||
: i18n('StoryListItem__hide'),
|
size={attachment?.size}
|
||||||
onClick: () => {
|
timestamp={timestamp}
|
||||||
if (isHidden) {
|
/>
|
||||||
onHideStory(id);
|
)}
|
||||||
} else {
|
{hasStoryViewsNRepliesModal && (
|
||||||
setHasConfirmHideStory(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'StoryListItem__icon--chat',
|
|
||||||
label: i18n('StoryListItem__go-to-chat'),
|
|
||||||
onClick: () => {
|
|
||||||
onGoToConversation(id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClose={() => setIsShowingContextMenu(false)}
|
|
||||||
referenceElement={referenceElement}
|
|
||||||
theme={Theme.Dark}
|
|
||||||
/>
|
|
||||||
{hasReplyModal && canReply && (
|
|
||||||
<StoryViewsNRepliesModal
|
<StoryViewsNRepliesModal
|
||||||
authorTitle={firstName || title}
|
authorTitle={firstName || title}
|
||||||
|
canReply={Boolean(canReply)}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupStory={isGroupStory}
|
isGroupStory={isGroupStory}
|
||||||
isMyStory={isMe}
|
isMyStory={isMe}
|
||||||
onClose={() => setHasReplyModal(false)}
|
onClose={() => setHasStoryViewsNRepliesModal(false)}
|
||||||
onReact={emoji => {
|
onReact={emoji => {
|
||||||
onReactToStory(emoji, story);
|
onReactToStory(emoji, story);
|
||||||
setHasReplyModal(false);
|
setHasStoryViewsNRepliesModal(false);
|
||||||
setReactionEmoji(emoji);
|
setReactionEmoji(emoji);
|
||||||
showToast(ToastType.StoryReact);
|
showToast(ToastType.StoryReact);
|
||||||
}}
|
}}
|
||||||
onReply={(message, mentions, replyTimestamp) => {
|
onReply={(message, mentions, replyTimestamp) => {
|
||||||
if (!isGroupStory) {
|
if (!isGroupStory) {
|
||||||
setHasReplyModal(false);
|
setHasStoryViewsNRepliesModal(false);
|
||||||
}
|
}
|
||||||
onReplyToStory(message, mentions, replyTimestamp, story);
|
onReplyToStory(message, mentions, replyTimestamp, story);
|
||||||
showToast(ToastType.StoryReply);
|
showToast(ToastType.StoryReply);
|
||||||
|
@ -712,6 +754,21 @@ export const StoryViewer = ({
|
||||||
{i18n('StoryListItem__hide-modal--body', [String(firstName)])}
|
{i18n('StoryListItem__hide-modal--body', [String(firstName)])}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
)}
|
)}
|
||||||
|
{confirmDeleteStory && (
|
||||||
|
<ConfirmationDialog
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: i18n('delete'),
|
||||||
|
action: () => deleteStoryForEveryone(confirmDeleteStory),
|
||||||
|
style: 'negative',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => setConfirmDeleteStory(undefined)}
|
||||||
|
>
|
||||||
|
{i18n('MyStories__delete')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Meta, Story } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
|
|
||||||
import type { PropsType } from './StoryViewsNRepliesModal';
|
import type { PropsType } from './StoryViewsNRepliesModal';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
|
@ -18,35 +18,50 @@ const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/StoryViewsNRepliesModal',
|
title: 'Components/StoryViewsNRepliesModal',
|
||||||
};
|
component: StoryViewsNRepliesModal,
|
||||||
|
argTypes: {
|
||||||
function getDefaultProps(): PropsType {
|
authorTitle: {
|
||||||
return {
|
defaultValue: getDefaultConversation().title,
|
||||||
authorTitle: getDefaultConversation().title,
|
},
|
||||||
getPreferredBadge: () => undefined,
|
canReply: {
|
||||||
i18n,
|
defaultValue: true,
|
||||||
isMyStory: false,
|
},
|
||||||
onClose: action('onClose'),
|
getPreferredBadge: { action: true },
|
||||||
onSetSkinTone: action('onSetSkinTone'),
|
i18n: {
|
||||||
onReact: action('onReact'),
|
defaultValue: i18n,
|
||||||
onReply: action('onReply'),
|
},
|
||||||
onTextTooLong: action('onTextTooLong'),
|
isMyStory: {
|
||||||
onUseEmoji: action('onUseEmoji'),
|
defaultValue: false,
|
||||||
preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
},
|
||||||
renderEmojiPicker: () => <div />,
|
onClose: { action: true },
|
||||||
replies: [],
|
onSetSkinTone: { action: true },
|
||||||
storyPreviewAttachment: fakeAttachment({
|
onReact: { action: true },
|
||||||
thumbnail: {
|
onReply: { action: true },
|
||||||
contentType: IMAGE_JPEG,
|
onTextTooLong: { action: true },
|
||||||
height: 64,
|
onUseEmoji: { action: true },
|
||||||
objectUrl: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
preferredReactionEmoji: {
|
||||||
path: '',
|
defaultValue: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
||||||
width: 40,
|
},
|
||||||
},
|
renderEmojiPicker: { action: true },
|
||||||
}),
|
replies: {
|
||||||
views: [],
|
defaultValue: [],
|
||||||
};
|
},
|
||||||
}
|
storyPreviewAttachment: {
|
||||||
|
defaultValue: fakeAttachment({
|
||||||
|
thumbnail: {
|
||||||
|
contentType: IMAGE_JPEG,
|
||||||
|
height: 64,
|
||||||
|
objectUrl: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
||||||
|
path: '',
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
function getViewsAndReplies() {
|
function getViewsAndReplies() {
|
||||||
const p1 = getDefaultConversation();
|
const p1 = getDefaultConversation();
|
||||||
|
@ -107,47 +122,51 @@ function getViewsAndReplies() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CanReply = (): JSX.Element => (
|
const Template: Story<PropsType> = args => (
|
||||||
<StoryViewsNRepliesModal {...getDefaultProps()} />
|
<StoryViewsNRepliesModal {...args} />
|
||||||
);
|
);
|
||||||
|
|
||||||
CanReply.story = {
|
export const CanReply = Template.bind({});
|
||||||
name: 'Can reply',
|
CanReply.args = {};
|
||||||
|
CanReply.storyName = 'Can reply';
|
||||||
|
|
||||||
|
export const ViewsOnly = Template.bind({});
|
||||||
|
ViewsOnly.args = {
|
||||||
|
isMyStory: true,
|
||||||
|
views: getViewsAndReplies().views,
|
||||||
};
|
};
|
||||||
|
ViewsOnly.storyName = 'Views only';
|
||||||
|
|
||||||
export const ViewsOnly = (): JSX.Element => (
|
export const InAGroupNoReplies = Template.bind({});
|
||||||
<StoryViewsNRepliesModal
|
InAGroupNoReplies.args = {
|
||||||
{...getDefaultProps()}
|
isGroupStory: true,
|
||||||
isMyStory
|
|
||||||
views={getViewsAndReplies().views}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
ViewsOnly.story = {
|
|
||||||
name: 'Views only',
|
|
||||||
};
|
};
|
||||||
|
InAGroupNoReplies.storyName = 'In a group (no replies)';
|
||||||
|
|
||||||
export const InAGroupNoReplies = (): JSX.Element => (
|
export const InAGroup = Template.bind({});
|
||||||
<StoryViewsNRepliesModal {...getDefaultProps()} isGroupStory />
|
{
|
||||||
);
|
|
||||||
|
|
||||||
InAGroupNoReplies.story = {
|
|
||||||
name: 'In a group (no replies)',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InAGroup = (): JSX.Element => {
|
|
||||||
const { views, replies } = getViewsAndReplies();
|
const { views, replies } = getViewsAndReplies();
|
||||||
|
InAGroup.args = {
|
||||||
|
isGroupStory: true,
|
||||||
|
replies,
|
||||||
|
views,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
InAGroup.storyName = 'In a group';
|
||||||
|
|
||||||
return (
|
export const CantReply = Template.bind({});
|
||||||
<StoryViewsNRepliesModal
|
CantReply.args = {
|
||||||
{...getDefaultProps()}
|
canReply: false,
|
||||||
isGroupStory
|
|
||||||
replies={replies}
|
|
||||||
views={views}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
InAGroup.story = {
|
export const InAGroupCantReply = Template.bind({});
|
||||||
name: 'In a group',
|
{
|
||||||
};
|
const { views, replies } = getViewsAndReplies();
|
||||||
|
InAGroupCantReply.args = {
|
||||||
|
canReply: false,
|
||||||
|
isGroupStory: true,
|
||||||
|
replies,
|
||||||
|
views,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
InAGroupCantReply.storyName = "In a group (can't reply)";
|
||||||
|
|
|
@ -34,6 +34,7 @@ enum Tab {
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
authorTitle: string;
|
authorTitle: string;
|
||||||
|
canReply: boolean;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isGroupStory?: boolean;
|
isGroupStory?: boolean;
|
||||||
|
@ -59,6 +60,7 @@ export type PropsType = {
|
||||||
|
|
||||||
export const StoryViewsNRepliesModal = ({
|
export const StoryViewsNRepliesModal = ({
|
||||||
authorTitle,
|
authorTitle,
|
||||||
|
canReply,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
isGroupStory,
|
isGroupStory,
|
||||||
|
@ -76,7 +78,7 @@ export const StoryViewsNRepliesModal = ({
|
||||||
skinTone,
|
skinTone,
|
||||||
storyPreviewAttachment,
|
storyPreviewAttachment,
|
||||||
views,
|
views,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element | null => {
|
||||||
const inputApiRef = useRef<InputApi | undefined>();
|
const inputApiRef = useRef<InputApi | undefined>();
|
||||||
const [bottom, setBottom] = useState<HTMLDivElement | null>(null);
|
const [bottom, setBottom] = useState<HTMLDivElement | null>(null);
|
||||||
const [messageBodyText, setMessageBodyText] = useState('');
|
const [messageBodyText, setMessageBodyText] = useState('');
|
||||||
|
@ -117,7 +119,7 @@ export const StoryViewsNRepliesModal = ({
|
||||||
|
|
||||||
let composerElement: JSX.Element | undefined;
|
let composerElement: JSX.Element | undefined;
|
||||||
|
|
||||||
if (!isMyStory) {
|
if (!isMyStory && canReply) {
|
||||||
composerElement = (
|
composerElement = (
|
||||||
<>
|
<>
|
||||||
{!isGroupStory && (
|
{!isGroupStory && (
|
||||||
|
@ -373,6 +375,10 @@ export const StoryViewsNRepliesModal = ({
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
|
if (!tabsElement && !viewsElement && !repliesElement && !composerElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -1551,7 +1551,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isViewOnce={false}
|
isViewOnce={false}
|
||||||
moduleClassName="StoryReplyQuote"
|
moduleClassName="StoryReplyQuote"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
viewStory(storyReplyContext.storyId, StoryViewModeType.Single);
|
viewStory({
|
||||||
|
storyId: storyReplyContext.storyId,
|
||||||
|
storyViewMode: StoryViewModeType.Single,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
rawAttachment={storyReplyContext.rawAttachment}
|
rawAttachment={storyReplyContext.rawAttachment}
|
||||||
reactionEmoji={storyReplyContext.emoji}
|
reactionEmoji={storyReplyContext.emoji}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { noop } from 'lodash';
|
||||||
|
|
||||||
import { Avatar, AvatarSize } from '../Avatar';
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
|
import { ContextMenu } from '../ContextMenu';
|
||||||
import { Time } from '../Time';
|
import { Time } from '../Time';
|
||||||
import type {
|
import type {
|
||||||
Props as MessagePropsType,
|
Props as MessagePropsType,
|
||||||
|
@ -392,12 +393,27 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
<tr>
|
<tr>
|
||||||
<td className="module-message-detail__label">{i18n('sent')}</td>
|
<td className="module-message-detail__label">{i18n('sent')}</td>
|
||||||
<td>
|
<td>
|
||||||
<Time timestamp={sentAt}>
|
<ContextMenu
|
||||||
{formatDateTimeLong(i18n, sentAt)}
|
i18n={i18n}
|
||||||
</Time>{' '}
|
menuOptions={[
|
||||||
<span className="module-message-detail__unix-timestamp">
|
{
|
||||||
({sentAt})
|
icon: 'StoryDetailsModal__copy-icon',
|
||||||
</span>
|
label: i18n('StoryDetailsModal__copy-timestamp'),
|
||||||
|
onClick: () => {
|
||||||
|
window.navigator.clipboard.writeText(String(sentAt));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Time timestamp={sentAt}>
|
||||||
|
{formatDateTimeLong(i18n, sentAt)}
|
||||||
|
</Time>{' '}
|
||||||
|
<span className="module-message-detail__unix-timestamp">
|
||||||
|
({sentAt})
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
</ContextMenu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{receivedAt && message.direction === 'incoming' ? (
|
{receivedAt && message.direction === 'incoming' ? (
|
||||||
|
|
|
@ -63,6 +63,7 @@ export type StoryDataType = {
|
||||||
export type SelectedStoryDataType = {
|
export type SelectedStoryDataType = {
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
numStories: number;
|
numStories: number;
|
||||||
|
shouldShowDetailsModal: boolean;
|
||||||
story: StoryDataType;
|
story: StoryDataType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -616,7 +617,8 @@ const getSelectedStoryDataForConversationId = (
|
||||||
};
|
};
|
||||||
|
|
||||||
function viewUserStories(
|
function viewUserStories(
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
shouldShowDetailsModal = false
|
||||||
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> {
|
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { currentIndex, hasUnread, numStories, storiesByConversationId } =
|
const { currentIndex, hasUnread, numStories, storiesByConversationId } =
|
||||||
|
@ -630,6 +632,7 @@ function viewUserStories(
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex,
|
currentIndex,
|
||||||
numStories,
|
numStories,
|
||||||
|
shouldShowDetailsModal,
|
||||||
story,
|
story,
|
||||||
},
|
},
|
||||||
storyViewMode: hasUnread
|
storyViewMode: hasUnread
|
||||||
|
@ -640,19 +643,23 @@ function viewUserStories(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewStoryActionCreatorType = (
|
export type ViewStoryActionCreatorType = (opts: {
|
||||||
storyId?: string,
|
closeViewer?: boolean;
|
||||||
storyViewMode?: StoryViewModeType,
|
storyId?: string;
|
||||||
viewDirection?: StoryViewDirectionType
|
storyViewMode?: StoryViewModeType;
|
||||||
) => unknown;
|
viewDirection?: StoryViewDirectionType;
|
||||||
|
shouldShowDetailsModal?: boolean;
|
||||||
|
}) => unknown;
|
||||||
|
|
||||||
const viewStory: ViewStoryActionCreatorType = (
|
const viewStory: ViewStoryActionCreatorType = ({
|
||||||
|
closeViewer,
|
||||||
|
shouldShowDetailsModal = false,
|
||||||
storyId,
|
storyId,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
viewDirection
|
viewDirection,
|
||||||
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
}): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!storyId || !storyViewMode) {
|
if (closeViewer || !storyId || !storyViewMode) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: VIEW_STORY,
|
type: VIEW_STORY,
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
|
@ -691,6 +698,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex,
|
currentIndex,
|
||||||
numStories,
|
numStories,
|
||||||
|
shouldShowDetailsModal,
|
||||||
story,
|
story,
|
||||||
},
|
},
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
|
@ -713,6 +721,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex: nextIndex,
|
currentIndex: nextIndex,
|
||||||
numStories,
|
numStories,
|
||||||
|
shouldShowDetailsModal: false,
|
||||||
story: nextStory,
|
story: nextStory,
|
||||||
},
|
},
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
|
@ -732,6 +741,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex: nextIndex,
|
currentIndex: nextIndex,
|
||||||
numStories,
|
numStories,
|
||||||
|
shouldShowDetailsModal: false,
|
||||||
story: nextStory,
|
story: nextStory,
|
||||||
},
|
},
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
|
@ -759,6 +769,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex: nextSelectedStoryData.currentIndex,
|
currentIndex: nextSelectedStoryData.currentIndex,
|
||||||
numStories: nextSelectedStoryData.numStories,
|
numStories: nextSelectedStoryData.numStories,
|
||||||
|
shouldShowDetailsModal: false,
|
||||||
story: unreadStory,
|
story: unreadStory,
|
||||||
},
|
},
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
|
@ -819,6 +830,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
numStories: nextSelectedStoryData.numStories,
|
numStories: nextSelectedStoryData.numStories,
|
||||||
|
shouldShowDetailsModal: false,
|
||||||
story: nextSelectedStoryData.storiesByConversationId[0],
|
story: nextSelectedStoryData.storiesByConversationId[0],
|
||||||
},
|
},
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
|
@ -855,6 +867,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
selectedStoryData: {
|
selectedStoryData: {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
numStories: nextSelectedStoryData.numStories,
|
numStories: nextSelectedStoryData.numStories,
|
||||||
|
shouldShowDetailsModal: false,
|
||||||
story: nextSelectedStoryData.storiesByConversationId[0],
|
story: nextSelectedStoryData.storiesByConversationId[0],
|
||||||
},
|
},
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
|
|
|
@ -101,6 +101,7 @@ export function getStoryView(
|
||||||
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
|
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
|
||||||
'acceptedMessageRequest',
|
'acceptedMessageRequest',
|
||||||
'avatarPath',
|
'avatarPath',
|
||||||
|
'badges',
|
||||||
'color',
|
'color',
|
||||||
'firstName',
|
'firstName',
|
||||||
'hideStory',
|
'hideStory',
|
||||||
|
@ -132,17 +133,7 @@ export function getStoryView(
|
||||||
|
|
||||||
innerSendState.push({
|
innerSendState.push({
|
||||||
...recipientSendState,
|
...recipientSendState,
|
||||||
recipient: pick(recipient, [
|
recipient,
|
||||||
'acceptedMessageRequest',
|
|
||||||
'avatarPath',
|
|
||||||
'color',
|
|
||||||
'id',
|
|
||||||
'isMe',
|
|
||||||
'name',
|
|
||||||
'profileName',
|
|
||||||
'sharedGroupNames',
|
|
||||||
'title',
|
|
||||||
]),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,7 @@ export function SmartStoryViewer(): JSX.Element | null {
|
||||||
recentEmojis={recentEmojis}
|
recentEmojis={recentEmojis}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
replyState={replyState}
|
replyState={replyState}
|
||||||
|
shouldShowDetailsModal={selectedStoryData.shouldShowDetailsModal}
|
||||||
showToast={showToast}
|
showToast={showToast}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
story={storyView}
|
story={storyView}
|
||||||
|
|
|
@ -52,18 +52,7 @@ export type ConversationStoryType = {
|
||||||
|
|
||||||
export type StorySendStateType = {
|
export type StorySendStateType = {
|
||||||
isAllowedToReplyToStory?: boolean;
|
isAllowedToReplyToStory?: boolean;
|
||||||
recipient: Pick<
|
recipient: ConversationType;
|
||||||
ConversationType,
|
|
||||||
| 'acceptedMessageRequest'
|
|
||||||
| 'avatarPath'
|
|
||||||
| 'color'
|
|
||||||
| 'id'
|
|
||||||
| 'isMe'
|
|
||||||
| 'name'
|
|
||||||
| 'profileName'
|
|
||||||
| 'sharedGroupNames'
|
|
||||||
| 'title'
|
|
||||||
>;
|
|
||||||
status: SendStatus;
|
status: SendStatus;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
};
|
};
|
||||||
|
@ -80,6 +69,7 @@ export type StoryViewType = {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
| 'acceptedMessageRequest'
|
| 'acceptedMessageRequest'
|
||||||
| 'avatarPath'
|
| 'avatarPath'
|
||||||
|
| 'badges'
|
||||||
| 'color'
|
| 'color'
|
||||||
| 'firstName'
|
| 'firstName'
|
||||||
| 'id'
|
| 'id'
|
||||||
|
|
|
@ -7,11 +7,16 @@ export function getClassNamesFor(
|
||||||
...modules: Array<string | undefined>
|
...modules: Array<string | undefined>
|
||||||
): (modifier?: string) => string {
|
): (modifier?: string) => string {
|
||||||
return modifier => {
|
return modifier => {
|
||||||
const cx = modules.map(parentModule =>
|
if (modifier === undefined) {
|
||||||
parentModule && modifier !== undefined
|
return '';
|
||||||
? `${parentModule}${modifier}`
|
}
|
||||||
: undefined
|
|
||||||
);
|
const cx = modules
|
||||||
|
.flatMap(m => (m ? m.split(' ') : undefined))
|
||||||
|
.map(parentModule =>
|
||||||
|
parentModule ? `${parentModule}${modifier}` : undefined
|
||||||
|
);
|
||||||
|
|
||||||
return classNames(cx);
|
return classNames(cx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue