Spam Reporting UI changes
Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
|
@ -116,7 +116,6 @@ window.SignalContext = {
|
|||
|
||||
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
|
||||
getPreferredSystemLocales: () => ['en'],
|
||||
getResolvedMessagesLocaleDirection: () => 'ltr',
|
||||
getLocaleOverride: () => null,
|
||||
getLocaleDisplayNames: () => ({ en: { en: 'English' } }),
|
||||
};
|
||||
|
@ -133,6 +132,9 @@ const withGlobalTypesProvider = (Story, context) => {
|
|||
const mode = context.globals.mode;
|
||||
const direction = context.globals.direction ?? 'auto';
|
||||
|
||||
window.SignalContext.getResolvedMessagesLocaleDirection = () =>
|
||||
direction === 'auto' ? 'ltr' : direction;
|
||||
|
||||
// Adding it to the body as well so that we can cover modals and other
|
||||
// components that are rendered outside of this decorator container
|
||||
if (theme === 'light') {
|
||||
|
|
|
@ -427,6 +427,26 @@
|
|||
"messageformat": "Select messages",
|
||||
"description": "Shown in menu for conversation, allows the user to start selecting multiple messages in the conversation"
|
||||
},
|
||||
"icu:ConversationHeader__MenuItem--Accept": {
|
||||
"messageformat": "Accept",
|
||||
"description": "Shown in menu for conversation, allows the user to accept a message request"
|
||||
},
|
||||
"icu:ConversationHeader__MenuItem--Block": {
|
||||
"messageformat": "Block",
|
||||
"description": "Shown in menu for conversation, allows the user to block the contact"
|
||||
},
|
||||
"icu:ConversationHeader__MenuItem--Unblock": {
|
||||
"messageformat": "Unblock",
|
||||
"description": "Shown in menu for conversation, allows the user to unblock the contact"
|
||||
},
|
||||
"icu:ConversationHeader__MenuItem--ReportSpam": {
|
||||
"messageformat": "Report Spam",
|
||||
"description": "Shown in menu for conversation, allows the user to report the conversation as spam"
|
||||
},
|
||||
"icu:ConversationHeader__MenuItem--DeleteChat": {
|
||||
"messageformat": "Delete Chat",
|
||||
"description": "Shown in menu for conversation, allows the user to delete the conversation"
|
||||
},
|
||||
"icu:ContactListItem__menu": {
|
||||
"messageformat": "Manage Contact",
|
||||
"description": "Shown as aria label for context menu for a contact"
|
||||
|
@ -3387,6 +3407,62 @@
|
|||
"messageformat": "All",
|
||||
"description": "Shown in reaction viewer as the title for the 'all' category"
|
||||
},
|
||||
"icu:SafetyTipsModal__Title": {
|
||||
"messageformat": "Safety Tips",
|
||||
"description": "Title of the safety tips modal"
|
||||
},
|
||||
"icu:SafetyTipsModal__Description": {
|
||||
"messageformat": "Be careful when accepting message requests from people you don’t know. Watch out for:",
|
||||
"description": "Description of the safety tips modal"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipTitle--Crypto": {
|
||||
"messageformat": "Crypto or money scams",
|
||||
"description": "Title of the crypto safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipDescription--Crypto": {
|
||||
"messageformat": "If someone you don’t know messages about cryptocurrency (like Bitcoin) or a financial opportunity, be careful—it’s likely spam.",
|
||||
"description": "Description of the crypto safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipTitle--Vague": {
|
||||
"messageformat": "Vague or irrelevant messages",
|
||||
"description": "Title of the vague safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipDescription--Vague": {
|
||||
"messageformat": "Spammers often start with a simple message like “Hi” to draw you in. If you respond they may engage you further.",
|
||||
"description": "Description of the vague safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipTitle--Links": {
|
||||
"messageformat": "Messages with links",
|
||||
"description": "Title of the links safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipDescription--Links": {
|
||||
"messageformat": "Be careful of messages from people you don’t know that have links to websites. Never visit links from people you don’t trust.",
|
||||
"description": "Description of the links safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipTitle--Business": {
|
||||
"messageformat": "Fake businesses and institutions",
|
||||
"description": "Title of the business safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__TipDescription--Business": {
|
||||
"messageformat": "Be careful of businesses or government agencies contacting you. Messages involving tax agencies, couriers, and more can be spam.",
|
||||
"description": "Description of the business safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__DotLabel": {
|
||||
"messageformat": "Go to page {page, number}",
|
||||
"description": "Label for the dots in the safety tips modal that when clicked will take you to a specific tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__Button--Previous": {
|
||||
"messageformat": "Previous tip",
|
||||
"description": "Button to go to the previous safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__Button--Next": {
|
||||
"messageformat": "Next tip",
|
||||
"description": "Button to go to the next safety tip"
|
||||
},
|
||||
"icu:SafetyTipsModal__Button--Done": {
|
||||
"messageformat": "Done",
|
||||
"description": "Button to close the safety tips modal when you've reached the last tip"
|
||||
},
|
||||
"icu:MessageRequests--message-direct": {
|
||||
"messageformat": "Let {name} message you and share your name and photo with them? They won’t know you’ve seen their messages until you accept.",
|
||||
"description": "Shown as the message for a message request in a direct message"
|
||||
|
@ -3451,6 +3527,42 @@
|
|||
"messageformat": "You will no longer receive messages or updates from this group and members won't be able to add you to this group again.",
|
||||
"description": "Shown as the body in the confirmation modal for blocking a group message request"
|
||||
},
|
||||
"icu:MessageRequests--reportAndMaybeBlock": {
|
||||
"messageformat": "Report...",
|
||||
"description": "Shown as a button to let the user report a message request and maybe block the user"
|
||||
},
|
||||
"icu:MessageRequests--ReportAndMaybeBlockModal-title": {
|
||||
"messageformat": "Report as spam?",
|
||||
"description": "Shown as the title in the modal for reporting and maybe blocking a message request"
|
||||
},
|
||||
"icu:MessageRequests--ReportAndMaybeBlockModal-body--direct": {
|
||||
"messageformat": "Signal will be notified that this person may be sending spam. Signal can’t see the content of any chats.",
|
||||
"description": "Shown as the body in the modal for reporting and maybe blocking a message request in a direct conversation"
|
||||
},
|
||||
"icu:MessageRequests--ReportAndMaybeBlockModal-body--group--unknown-contact": {
|
||||
"messageformat": "Signal will be notified that the person who invited you to this group may be sending spam. Signal can’t see the content of any chats.",
|
||||
"description": "Shown as the body in the modal for reporting and maybe blocking a message request in a group conversation when the contact is unknown"
|
||||
},
|
||||
"icu:MessageRequests--ReportAndMaybeBlockModal-body--group": {
|
||||
"messageformat": "Signal will be notified that {name}, who invited you to this group, may be sending spam. Signal can’t see the content of any chats.",
|
||||
"description": "Shown as the body in the modal for reporting and maybe blocking a message request in a group conversation"
|
||||
},
|
||||
"icu:MessageRequests--ReportAndMaybeBlockModal-report": {
|
||||
"messageformat": "Report Spam",
|
||||
"description": "Shown as a button to let the user report a message request"
|
||||
},
|
||||
"icu:MessageRequests--ReportAndMaybeBlockModal-reportAndBlock": {
|
||||
"messageformat": "Report and Block",
|
||||
"description": "Shown as a button to let the user report a message request and block the user"
|
||||
},
|
||||
"icu:MessageRequests--AcceptedOptionsModal--body": {
|
||||
"messageformat": "You accepted a message request from {name}. If this was a mistake, you can choose an action below.",
|
||||
"description": "Shown as the body in the modal for accepting a message request in a direct conversation"
|
||||
},
|
||||
"icu:MessageRequests--report-spam-success-toast": {
|
||||
"messageformat": "Reported as spam.",
|
||||
"description": "Shown in a toast when you successfully report a user as spam"
|
||||
},
|
||||
"icu:MessageRequests--delete": {
|
||||
"messageformat": "Delete",
|
||||
"description": "Shown as a button to let the user delete any message request"
|
||||
|
@ -5235,6 +5347,10 @@
|
|||
"messageformat": "Learn more",
|
||||
"description": "Shown on the message request warning. Clicking this button will open a dialog with more information"
|
||||
},
|
||||
"icu:MessageRequestWarning__safety-tips": {
|
||||
"messageformat": "Safety Tips",
|
||||
"description": "Shown on the message request warning. Clicking this button will open a dialog with safety tips"
|
||||
},
|
||||
"icu:MessageRequestWarning__dialog__details": {
|
||||
"messageformat": "You have no groups in common with this person. Review requests carefully before accepting to avoid unwanted messages.",
|
||||
"description": "Shown in the message request warning dialog. Gives more information about message requests"
|
||||
|
@ -6319,6 +6435,26 @@
|
|||
"messageformat": "Check your primary device for this payment’s status",
|
||||
"description": "Payment event notification check device label"
|
||||
},
|
||||
"icu:MessageRequestResponseNotification__Message--Accepted": {
|
||||
"messageformat": "You accepted the message request",
|
||||
"description": "Message request response notification message when the user accepted the message request or unblocked another user"
|
||||
},
|
||||
"icu:MessageRequestResponseNotification__Message--Reported": {
|
||||
"messageformat": "Reported as spam",
|
||||
"description": "Message request response notification message when the user reported the message request as spam"
|
||||
},
|
||||
"icu:MessageRequestResponseNotification__Message--Blocked": {
|
||||
"messageformat": "You blocked this person",
|
||||
"description": "Message request response notification message when the user blocked another user"
|
||||
},
|
||||
"icu:MessageRequestResponseNotification__Button--Options": {
|
||||
"messageformat": "Options",
|
||||
"description": "Message request response notification button to show options"
|
||||
},
|
||||
"icu:MessageRequestResponseNotification__Button--LearnMore": {
|
||||
"messageformat": "Learn More",
|
||||
"description": "Message request response notification button to learn more"
|
||||
},
|
||||
"icu:SignalConnectionsModal__title": {
|
||||
"messageformat": "Signal Connections",
|
||||
"description": "The phrase/term: 'Signal Connections'"
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#block__a)"><path d="M10 .938a9.063 9.063 0 1 0 0 18.125A9.063 9.063 0 0 0 10 .938ZM2.396 10A7.604 7.604 0 0 1 14.83 4.127L4.127 14.831A7.573 7.573 0 0 1 2.396 10Zm2.762 5.863L15.863 5.158A7.604 7.604 0 0 1 5.157 15.864Z" fill="#000"/></g><defs><clipPath id="block__a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>
|
||||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.354a8.646 8.646 0 1 0 0 17.292 8.646 8.646 0 0 0 0-17.292ZM2.813 10A7.187 7.187 0 0 1 14.54 4.428L4.428 14.541A7.158 7.158 0 0 1 2.813 10Zm2.646 5.572A7.187 7.187 0 0 0 15.571 5.459L5.46 15.572Z" fill="#000"/></svg>
|
Before Width: | Height: | Size: 423 B After Width: | Height: | Size: 345 B |
1
images/icons/v3/spam/spam.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none"><path fill="#000" d="M10 5.417a.995.995 0 0 0-.993 1.071l.353 4.586a.642.642 0 0 0 1.28 0l.352-4.586A.996.996 0 0 0 10 5.417Zm0 7.291a1.042 1.042 0 1 0 0 2.084 1.042 1.042 0 0 0 0-2.084Z"/><path fill="#000" fill-rule="evenodd" d="M12.589 1.354H7.41c-.635 0-1.245.253-1.694.702l-3.66 3.661a2.396 2.396 0 0 0-.702 1.694v5.178c0 .635.253 1.245.702 1.694l3.661 3.661c.45.45 1.059.702 1.694.702h5.178c.635 0 1.245-.253 1.694-.702l3.661-3.661c.45-.45.702-1.059.702-1.694V7.41c0-.635-.253-1.245-.702-1.694l-3.661-3.661a2.396 2.396 0 0 0-1.694-.702ZM7.41 2.812h5.178c.248 0 .487.1.663.275l3.66 3.661a.938.938 0 0 1 .276.663v5.178a.938.938 0 0 1-.275.663l-3.661 3.66a.938.938 0 0 1-.663.276H7.41a.938.938 0 0 1-.663-.275l-3.661-3.661a.937.937 0 0 1-.275-.663V7.41c0-.249.1-.487.275-.663l3.661-3.661a.937.937 0 0 1 .663-.275Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 919 B |
1
images/icons/v3/thread/thread.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none"><path fill="#000" d="M6.667 2.812a4.27 4.27 0 0 0-3.268 7.021c.19.226.3.527.273.848l-.142 1.685 1.333-.926c.276-.192.608-.244.909-.18.19.04.384.069.583.083.006.504.066.995.174 1.468a5.749 5.749 0 0 1-.94-.1l-1.772 1.232c-.796.553-1.876-.071-1.795-1.037l.186-2.224a5.73 5.73 0 1 1 9.548-6.232 6.93 6.93 0 0 0-1.398.485 4.269 4.269 0 0 0-3.691-2.123Z"/><path fill="#000" fill-rule="evenodd" d="M13.333 5.52a5.73 5.73 0 0 1 4.468 9.317l.192 2.29c.081.965-1 1.59-1.796 1.036l-1.836-1.276a5.73 5.73 0 1 1-1.028-11.366Zm4.271 5.73a4.27 4.27 0 1 0-3.413 4.185c.297-.06.625-.008.898.182l1.395.97-.147-1.75c-.026-.32.083-.62.271-.846a4.25 4.25 0 0 0 .996-2.741Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 756 B |
BIN
images/safety-tips/safety-tip-business.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
images/safety-tips/safety-tip-crypto.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
images/safety-tips/safety-tip-links.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
images/safety-tips/safety-tip-vague.png
Normal file
After Width: | Height: | Size: 11 KiB |
|
@ -548,6 +548,8 @@ message SyncMessage {
|
|||
DELETE = 2;
|
||||
BLOCK = 3;
|
||||
BLOCK_AND_DELETE = 4;
|
||||
SPAM = 5;
|
||||
BLOCK_AND_SPAM = 6;
|
||||
}
|
||||
|
||||
optional string threadE164 = 1;
|
||||
|
|
|
@ -92,6 +92,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__safety-tips-button {
|
||||
border-radius: 9999px;
|
||||
padding-block: 6px;
|
||||
padding-inline: 14px;
|
||||
margin-top: 12px;
|
||||
@include font-subtitle;
|
||||
}
|
||||
|
||||
&__membership {
|
||||
@include font-body-2;
|
||||
user-select: none;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.MessageRequestActionsConfirmation__ModalHost__width-container {
|
||||
min-width: 480px;
|
||||
}
|
220
stylesheets/components/SafetyTipsModal.scss
Normal file
|
@ -0,0 +1,220 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
$SafetyTipsModal__paddingInline: 32px;
|
||||
$SafetyTipsModal__paddingBlock: 24px;
|
||||
|
||||
.SafetyTipsModal {
|
||||
.module-Modal__headerTitle {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.module-Modal__title {
|
||||
padding-top: 20px;
|
||||
@include font-title-1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.module-Modal__body {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.module-Modal__button-footer {
|
||||
padding-block: $SafetyTipsModal__paddingBlock;
|
||||
padding-inline: $SafetyTipsModal__paddingInline;
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__width-container {
|
||||
max-width: 420px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.SafetyTipsModal__Description {
|
||||
margin: 0;
|
||||
padding-inline: $SafetyTipsModal__paddingInline;
|
||||
padding-bottom: 24px;
|
||||
text-align: center;
|
||||
@include font-body-1;
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__Footer {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.SafetyTipsModal__Button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.SafetyTipsModal__Button--Previous {
|
||||
&,
|
||||
&:is(:disabled, [aria-disabled='true']) {
|
||||
@include any-theme {
|
||||
background: transparent;
|
||||
}
|
||||
@include light-theme {
|
||||
color: $color-accent-blue;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:disabled, [aria-disabled='true']) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:not(:disabled):not([aria-disabled='true']) {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include light-theme {
|
||||
background: $color-gray-15;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-65;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
@include light-theme {
|
||||
background: $color-gray-20;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__CardWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $SafetyTipsModal__paddingInline;
|
||||
overflow: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
padding-inline: $SafetyTipsModal__paddingInline;
|
||||
}
|
||||
|
||||
.SafetyTipsModal__Card {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
scroll-snap-align: center;
|
||||
padding-block: 14px 32px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 18px;
|
||||
text-align: center;
|
||||
@include light-theme {
|
||||
background: $color-gray-02;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-75;
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__CardImage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
vertical-align: top;
|
||||
border-radius: 12px;
|
||||
@include light-theme {
|
||||
background: white;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-65;
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__CardTitle {
|
||||
margin-block: 14px 0;
|
||||
@include font-title-2;
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__CardDescription {
|
||||
margin-block: 8px 0;
|
||||
@include font-body-1;
|
||||
@include light-theme {
|
||||
color: $color-gray-62;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-20;
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__Dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-block: 24px 20px;
|
||||
}
|
||||
|
||||
.SafetyTipsModal__DotsButton {
|
||||
@include button-reset;
|
||||
padding: 4px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
transition: background 100ms ease;
|
||||
@include light-theme {
|
||||
background: rgba($color-black, 30%);
|
||||
}
|
||||
@include dark-theme {
|
||||
background: rgba($color-white, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
&:not([aria-current]) {
|
||||
&:hover,
|
||||
&:focus {
|
||||
&::before {
|
||||
@include light-theme {
|
||||
background: rgba($color-black, 45%);
|
||||
}
|
||||
@include dark-theme {
|
||||
background: rgba($color-white, 45%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-current]::before {
|
||||
@include light-theme {
|
||||
background: $color-black;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
&::before {
|
||||
box-shadow: 0 0 0 2px $color-white, 0 0 0 4px $color-accent-blue;
|
||||
}
|
||||
}
|
||||
@include dark-keyboard-mode {
|
||||
&::before {
|
||||
box-shadow: 0 0 0 2px $color-gray-80, 0 0 0 4px $color-accent-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SafetyTipsModal__DotsButtonLabel {
|
||||
@include sr-only;
|
||||
}
|
|
@ -247,6 +247,18 @@
|
|||
'../images/icons/v3/merge/merge-compact.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--icon-thread::before {
|
||||
@include system-message-icon('../images/icons/v3/thread/thread.svg');
|
||||
}
|
||||
|
||||
&--icon-spam::before {
|
||||
@include system-message-icon('../images/icons/v3/spam/spam.svg');
|
||||
}
|
||||
|
||||
&--icon-block::before {
|
||||
@include system-message-icon('../images/icons/v3/block/block.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
@import './components/MediaQualitySelector.scss';
|
||||
@import './components/MessageAudio.scss';
|
||||
@import './components/MessageBody.scss';
|
||||
@import './components/MessageRequestActionsConfirmation.scss';
|
||||
@import './components/MessageTextRenderer.scss';
|
||||
@import './components/MessageDetail.scss';
|
||||
@import './components/MiniPlayer.scss';
|
||||
|
@ -133,6 +134,7 @@
|
|||
@import './components/SafetyNumberChangeDialog.scss';
|
||||
@import './components/SafetyNumberOnboarding.scss';
|
||||
@import './components/SafetyNumberViewer.scss';
|
||||
@import './components/SafetyTipsModal.scss';
|
||||
@import './components/ScrollDownButton.scss';
|
||||
@import './components/SearchInput.scss';
|
||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||
|
|
|
@ -108,7 +108,7 @@ export default {
|
|||
blockConversation: action('blockConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
title: '',
|
||||
conversationName: getDefaultConversation(),
|
||||
// GroupV1 Disabled Actions
|
||||
showGV2MigrationDialog: action('showGV2MigrationDialog'),
|
||||
// GroupV2
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
|
@ -43,6 +43,7 @@ import type { AciString } from '../types/ServiceId';
|
|||
import { AudioCapture } from './conversation/AudioCapture';
|
||||
import { CompositionUpload } from './CompositionUpload';
|
||||
import type {
|
||||
ConversationRemovalStage,
|
||||
ConversationType,
|
||||
PushPanelForConversationActionType,
|
||||
ShowConversationType,
|
||||
|
@ -73,16 +74,16 @@ import type { ShowToastAction } from '../state/ducks/toast';
|
|||
import type { DraftEditMessageType } from '../model-types.d';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
acceptedMessageRequest?: boolean;
|
||||
removalStage?: 'justNotification' | 'messageRequest';
|
||||
acceptedMessageRequest: boolean | null;
|
||||
removalStage: ConversationRemovalStage | null;
|
||||
addAttachment: (
|
||||
conversationId: string,
|
||||
attachment: InMemoryAttachmentDraftType
|
||||
) => unknown;
|
||||
announcementsOnly?: boolean;
|
||||
areWeAdmin?: boolean;
|
||||
areWePending?: boolean;
|
||||
areWePendingApproval?: boolean;
|
||||
announcementsOnly: boolean | null;
|
||||
areWeAdmin: boolean | null;
|
||||
areWePending: boolean | null;
|
||||
areWePendingApproval: boolean | null;
|
||||
cancelRecording: () => unknown;
|
||||
completeRecording: (
|
||||
conversationId: string,
|
||||
|
@ -93,29 +94,29 @@ export type OwnProps = Readonly<{
|
|||
) => HydratedBodyRangesType | undefined;
|
||||
conversationId: string;
|
||||
discardEditMessage: (id: string) => unknown;
|
||||
draftEditMessage?: DraftEditMessageType;
|
||||
draftEditMessage: DraftEditMessageType | null;
|
||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
||||
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType | null;
|
||||
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||||
focusCounter: number;
|
||||
groupAdmins: Array<ConversationType>;
|
||||
groupVersion?: 1 | 2;
|
||||
groupVersion: 1 | 2 | null;
|
||||
i18n: LocalizerType;
|
||||
imageToBlurHash: typeof imageToBlurHash;
|
||||
isDisabled: boolean;
|
||||
isFetchingUUID?: boolean;
|
||||
isFetchingUUID: boolean | null;
|
||||
isFormattingEnabled: boolean;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
isMissingMandatoryProfileSharing?: boolean;
|
||||
isSignalConversation?: boolean;
|
||||
lastEditableMessageId?: string;
|
||||
isGroupV1AndDisabled: boolean | null;
|
||||
isMissingMandatoryProfileSharing: boolean | null;
|
||||
isSignalConversation: boolean | null;
|
||||
lastEditableMessageId: string | null;
|
||||
recordingState: RecordingState;
|
||||
messageCompositionId: string;
|
||||
shouldHidePopovers?: boolean;
|
||||
isSMSOnly?: boolean;
|
||||
left?: boolean;
|
||||
shouldHidePopovers: boolean | null;
|
||||
isSMSOnly: boolean | null;
|
||||
left: boolean | null;
|
||||
linkPreviewLoading: boolean;
|
||||
linkPreviewResult?: LinkPreviewType;
|
||||
linkPreviewResult: LinkPreviewType | null;
|
||||
onClearAttachments(conversationId: string): unknown;
|
||||
onCloseLinkPreview(conversationId: string): unknown;
|
||||
platform: string;
|
||||
|
@ -149,15 +150,15 @@ export type OwnProps = Readonly<{
|
|||
voiceNoteAttachment?: InMemoryAttachmentDraftType;
|
||||
}
|
||||
): unknown;
|
||||
quotedMessageId?: string;
|
||||
quotedMessageProps?: ReadonlyDeep<
|
||||
quotedMessageId: string | null;
|
||||
quotedMessageProps: null | ReadonlyDeep<
|
||||
Omit<
|
||||
QuoteProps,
|
||||
'i18n' | 'onClick' | 'onClose' | 'withContentAbove' | 'isCompose'
|
||||
>
|
||||
>;
|
||||
quotedMessageAuthorAci?: AciString;
|
||||
quotedMessageSentAt?: number;
|
||||
quotedMessageAuthorAci: AciString | null;
|
||||
quotedMessageSentAt: number | null;
|
||||
|
||||
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
||||
|
@ -210,6 +211,7 @@ export type Props = Pick<
|
|||
| 'blessedPacks'
|
||||
| 'recentStickers'
|
||||
| 'clearInstalledStickerPack'
|
||||
| 'showIntroduction'
|
||||
| 'clearShowIntroduction'
|
||||
| 'showPickerHint'
|
||||
| 'clearShowPickerHint'
|
||||
|
@ -220,7 +222,7 @@ export type Props = Pick<
|
|||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
} & OwnProps;
|
||||
|
||||
export function CompositionArea({
|
||||
export const CompositionArea = memo(function CompositionArea({
|
||||
// Base props
|
||||
addAttachment,
|
||||
conversationId,
|
||||
|
@ -291,6 +293,7 @@ export function CompositionArea({
|
|||
recentStickers,
|
||||
clearInstalledStickerPack,
|
||||
sendStickerMessage,
|
||||
showIntroduction,
|
||||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
|
@ -301,14 +304,18 @@ export function CompositionArea({
|
|||
conversationType,
|
||||
groupVersion,
|
||||
isBlocked,
|
||||
isHidden,
|
||||
isReported,
|
||||
isMissingMandatoryProfileSharing,
|
||||
left,
|
||||
removalStage,
|
||||
acceptConversation,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
deleteConversation,
|
||||
title,
|
||||
conversationName,
|
||||
addedByName,
|
||||
// GroupV1 Disabled Actions
|
||||
isGroupV1AndDisabled,
|
||||
showGV2MigrationDialog,
|
||||
|
@ -356,8 +363,8 @@ export function CompositionArea({
|
|||
bodyRanges,
|
||||
message,
|
||||
// sent timestamp for the quote
|
||||
quoteSentAt: quotedMessageSentAt,
|
||||
quoteAuthorAci: quotedMessageAuthorAci,
|
||||
quoteSentAt: quotedMessageSentAt ?? undefined,
|
||||
quoteAuthorAci: quotedMessageAuthorAci ?? undefined,
|
||||
targetMessageId: editedMessageId,
|
||||
});
|
||||
} else {
|
||||
|
@ -469,12 +476,7 @@ export function CompositionArea({
|
|||
) {
|
||||
inputApiRef.current.reset();
|
||||
}
|
||||
}, [
|
||||
messageCompositionId,
|
||||
sendCounter,
|
||||
previousMessageCompositionId,
|
||||
previousSendCounter,
|
||||
]);
|
||||
}, [messageCompositionId, sendCounter, previousMessageCompositionId, previousSendCounter]);
|
||||
|
||||
const insertEmoji = useCallback(
|
||||
(e: EmojiPickDataType) => {
|
||||
|
@ -504,7 +506,7 @@ export function CompositionArea({
|
|||
|
||||
inputApiRef.current?.setContents(
|
||||
draftEditMessageBody ?? '',
|
||||
draftBodyRanges,
|
||||
draftBodyRanges ?? undefined,
|
||||
true
|
||||
);
|
||||
}, [draftBodyRanges, draftEditMessageBody, hasEditDraftChanged]);
|
||||
|
@ -520,7 +522,11 @@ export function CompositionArea({
|
|||
return;
|
||||
}
|
||||
|
||||
inputApiRef.current?.setContents(draftText, draftBodyRanges, true);
|
||||
inputApiRef.current?.setContents(
|
||||
draftText,
|
||||
draftBodyRanges ?? undefined,
|
||||
true
|
||||
);
|
||||
}, [conversationId, draftBodyRanges, draftText, previousConversationId]);
|
||||
|
||||
const handleToggleLarge = useCallback(() => {
|
||||
|
@ -637,6 +643,7 @@ export function CompositionArea({
|
|||
onPickSticker={(packId, stickerId) =>
|
||||
sendStickerMessage(conversationId, { packId, stickerId })
|
||||
}
|
||||
showIntroduction={showIntroduction}
|
||||
clearShowIntroduction={clearShowIntroduction}
|
||||
showPickerHint={showPickerHint}
|
||||
clearShowPickerHint={clearShowPickerHint}
|
||||
|
@ -735,16 +742,19 @@ export function CompositionArea({
|
|||
) {
|
||||
return (
|
||||
<MessageRequestActions
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
conversationId={conversationId}
|
||||
addedByName={addedByName}
|
||||
conversationType={conversationType}
|
||||
deleteConversation={deleteConversation}
|
||||
conversationId={conversationId}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
isHidden={removalStage !== undefined}
|
||||
title={title}
|
||||
isHidden={isHidden}
|
||||
isReported={isReported}
|
||||
acceptConversation={acceptConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
deleteConversation={deleteConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -788,14 +798,18 @@ export function CompositionArea({
|
|||
) {
|
||||
return (
|
||||
<MandatoryProfileSharingActions
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
addedByName={addedByName}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
deleteConversation={deleteConversation}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
title={title}
|
||||
isBlocked={isBlocked}
|
||||
isReported={isReported}
|
||||
acceptConversation={acceptConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
deleteConversation={deleteConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -993,7 +1007,7 @@ export function CompositionArea({
|
|||
platform={platform}
|
||||
sendCounter={sendCounter}
|
||||
shouldHidePopovers={shouldHidePopovers}
|
||||
skinTone={skinTone}
|
||||
skinTone={skinTone ?? null}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={theme}
|
||||
/>
|
||||
|
@ -1031,4 +1045,4 @@ export function CompositionArea({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,30 +21,38 @@ export default {
|
|||
args: {},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
i18n,
|
||||
disabled: overrideProps.disabled ?? false,
|
||||
draftText: overrideProps.draftText || undefined,
|
||||
draftBodyRanges: overrideProps.draftBodyRanges || [],
|
||||
clearQuotedMessage: action('clearQuotedMessage'),
|
||||
getPreferredBadge: () => undefined,
|
||||
getQuotedMessage: action('getQuotedMessage'),
|
||||
isFormattingEnabled:
|
||||
overrideProps.isFormattingEnabled === false
|
||||
? overrideProps.isFormattingEnabled
|
||||
: true,
|
||||
large: overrideProps.large ?? false,
|
||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onSubmit: action('onSubmit'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
platform: 'darwin',
|
||||
sendCounter: 0,
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers ?? [],
|
||||
skinTone: overrideProps.skinTone ?? undefined,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
});
|
||||
const useProps = (overrideProps: Partial<Props> = {}): Props => {
|
||||
const conversation = getDefaultConversation();
|
||||
return {
|
||||
i18n,
|
||||
conversationId: conversation.id,
|
||||
disabled: overrideProps.disabled ?? false,
|
||||
draftText: overrideProps.draftText ?? null,
|
||||
draftEditMessage: overrideProps.draftEditMessage ?? null,
|
||||
draftBodyRanges: overrideProps.draftBodyRanges || [],
|
||||
clearQuotedMessage: action('clearQuotedMessage'),
|
||||
getPreferredBadge: () => undefined,
|
||||
getQuotedMessage: action('getQuotedMessage'),
|
||||
isFormattingEnabled:
|
||||
overrideProps.isFormattingEnabled === false
|
||||
? overrideProps.isFormattingEnabled
|
||||
: true,
|
||||
large: overrideProps.large ?? false,
|
||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onSubmit: action('onSubmit'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
platform: 'darwin',
|
||||
sendCounter: 0,
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers ?? [],
|
||||
skinTone: overrideProps.skinTone ?? null,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
inputApi: null,
|
||||
shouldHidePopovers: null,
|
||||
linkPreviewResult: null,
|
||||
};
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
const props = useProps();
|
||||
|
|
|
@ -96,22 +96,22 @@ export type InputApi = {
|
|||
|
||||
export type Props = Readonly<{
|
||||
children?: React.ReactNode;
|
||||
conversationId?: string;
|
||||
conversationId: string | null;
|
||||
i18n: LocalizerType;
|
||||
disabled?: boolean;
|
||||
draftEditMessage?: DraftEditMessageType;
|
||||
draftEditMessage: DraftEditMessageType | null;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
large?: boolean;
|
||||
inputApi?: React.MutableRefObject<InputApi | undefined>;
|
||||
large: boolean | null;
|
||||
inputApi: React.MutableRefObject<InputApi | undefined> | null;
|
||||
isFormattingEnabled: boolean;
|
||||
sendCounter: number;
|
||||
skinTone?: EmojiPickDataType['skinTone'];
|
||||
draftText?: string;
|
||||
draftBodyRanges?: HydratedBodyRangesType;
|
||||
skinTone: NonNullable<EmojiPickDataType['skinTone']> | null;
|
||||
draftText: string | null;
|
||||
draftBodyRanges: HydratedBodyRangesType | null;
|
||||
moduleClassName?: string;
|
||||
theme: ThemeType;
|
||||
placeholder?: string;
|
||||
sortedGroupMembers?: ReadonlyArray<ConversationType>;
|
||||
sortedGroupMembers: ReadonlyArray<ConversationType> | null;
|
||||
scrollerRef?: React.RefObject<HTMLDivElement>;
|
||||
onDirtyChange?(dirty: boolean): unknown;
|
||||
onEditorStateChange?(options: {
|
||||
|
@ -132,11 +132,11 @@ export type Props = Readonly<{
|
|||
): unknown;
|
||||
onScroll?: (ev: React.UIEvent<HTMLElement>) => void;
|
||||
platform: string;
|
||||
shouldHidePopovers?: boolean;
|
||||
shouldHidePopovers: boolean | null;
|
||||
getQuotedMessage?(): unknown;
|
||||
clearQuotedMessage?(): unknown;
|
||||
linkPreviewLoading?: boolean;
|
||||
linkPreviewResult?: LinkPreviewType;
|
||||
linkPreviewResult: LinkPreviewType | null;
|
||||
onCloseLinkPreview?(conversationId: string): unknown;
|
||||
}>;
|
||||
|
||||
|
@ -562,7 +562,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
onEditorStateChange({
|
||||
bodyRanges,
|
||||
caretLocation: selection ? selection.index : undefined,
|
||||
conversationId,
|
||||
conversationId: conversationId ?? undefined,
|
||||
messageText: text,
|
||||
sendCounter,
|
||||
});
|
||||
|
@ -612,7 +612,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
React.useEffect(() => {
|
||||
const emojiCompletion = emojiCompletionRef.current;
|
||||
|
||||
if (emojiCompletion === undefined || skinTone === undefined) {
|
||||
if (emojiCompletion == null || skinTone == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|||
import * as grapheme from '../util/grapheme';
|
||||
|
||||
export type CompositionTextAreaProps = {
|
||||
bodyRanges?: HydratedBodyRangesType;
|
||||
bodyRanges: HydratedBodyRangesType | null;
|
||||
i18n: LocalizerType;
|
||||
isFormattingEnabled: boolean;
|
||||
maxLength?: number;
|
||||
|
@ -153,6 +153,17 @@ export function CompositionTextArea({
|
|||
scrollerRef={scrollerRef}
|
||||
sendCounter={0}
|
||||
theme={theme}
|
||||
skinTone={skinTone ?? null}
|
||||
// These do not apply in the forward modal because there isn't
|
||||
// strictly one conversation
|
||||
conversationId={null}
|
||||
sortedGroupMembers={null}
|
||||
// we don't edit in this context
|
||||
draftEditMessage={null}
|
||||
// rendered in the forward modal
|
||||
linkPreviewResult={null}
|
||||
// Panels appear behind this modal
|
||||
shouldHidePopovers={null}
|
||||
/>
|
||||
<div className="CompositionTextArea__emoji">
|
||||
<EmojiButton
|
||||
|
|
|
@ -470,7 +470,7 @@ function ForwardMessageEditor({
|
|||
) : null}
|
||||
|
||||
<RenderCompositionTextArea
|
||||
bodyRanges={draft.bodyRanges}
|
||||
bodyRanges={draft.bodyRanges ?? null}
|
||||
draftText={draft.messageBody ?? ''}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
EditHistoryMessagesType,
|
||||
FormattingWarningDataType,
|
||||
ForwardMessagesPropsType,
|
||||
MessageRequestActionsConfirmationPropsType,
|
||||
SafetyNumberChangedBlockingDataType,
|
||||
SendEditWarningDataType,
|
||||
UserNotFoundModalStateType,
|
||||
|
@ -59,6 +60,9 @@ export type PropsType = {
|
|||
// ForwardMessageModal
|
||||
forwardMessagesProps: ForwardMessagesPropsType | undefined;
|
||||
renderForwardMessagesModal: () => JSX.Element;
|
||||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
renderMessageRequestActionsConfirmation: () => JSX.Element;
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible: boolean;
|
||||
renderProfileEditor: () => JSX.Element;
|
||||
|
@ -130,6 +134,9 @@ export function GlobalModalContainer({
|
|||
// ForwardMessageModal
|
||||
forwardMessagesProps,
|
||||
renderForwardMessagesModal,
|
||||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps,
|
||||
renderMessageRequestActionsConfirmation,
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible,
|
||||
renderProfileEditor,
|
||||
|
@ -223,6 +230,10 @@ export function GlobalModalContainer({
|
|||
return renderForwardMessagesModal();
|
||||
}
|
||||
|
||||
if (messageRequestActionsConfirmationProps) {
|
||||
return renderMessageRequestActionsConfirmation();
|
||||
}
|
||||
|
||||
if (isProfileEditorVisible) {
|
||||
return renderProfileEditor();
|
||||
}
|
||||
|
|
|
@ -176,13 +176,12 @@ export function MediaEditor({
|
|||
const [isEmojiPopperOpen, setEmojiPopperOpen] = useState<boolean>(false);
|
||||
|
||||
const [caption, setCaption] = useState(draftText ?? '');
|
||||
const [captionBodyRanges, setCaptionBodyRanges] = useState<
|
||||
DraftBodyRanges | undefined
|
||||
>(draftBodyRanges);
|
||||
const [captionBodyRanges, setCaptionBodyRanges] =
|
||||
useState<DraftBodyRanges | null>(draftBodyRanges);
|
||||
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const hydratedBodyRanges = useMemo(
|
||||
() => hydrateRanges(captionBodyRanges, conversationSelector),
|
||||
() => hydrateRanges(captionBodyRanges ?? undefined, conversationSelector),
|
||||
[captionBodyRanges, conversationSelector]
|
||||
);
|
||||
|
||||
|
@ -1297,7 +1296,7 @@ export function MediaEditor({
|
|||
<div className="MediaEditor__tools--input dark-theme">
|
||||
<CompositionInput
|
||||
draftText={caption}
|
||||
draftBodyRanges={hydratedBodyRanges}
|
||||
draftBodyRanges={hydratedBodyRanges ?? null}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
|
@ -1308,6 +1307,7 @@ export function MediaEditor({
|
|||
setCaptionBodyRanges(bodyRanges);
|
||||
setCaption(messageText);
|
||||
}}
|
||||
skinTone={skinTone ?? null}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={noop}
|
||||
onTextTooLong={onTextTooLong}
|
||||
|
@ -1316,6 +1316,16 @@ export function MediaEditor({
|
|||
sendCounter={0}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={ThemeType.dark}
|
||||
// Only needed for state updates and we need to override those
|
||||
conversationId={null}
|
||||
// Cannot enter media editor while editing
|
||||
draftEditMessage={null}
|
||||
// We don't use the large editor mode
|
||||
large={null}
|
||||
// panels do not appear over the media editor
|
||||
shouldHidePopovers={null}
|
||||
// link previews not displayed with media
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
<EmojiButton
|
||||
className="StoryViewsNRepliesModal__emoji-button"
|
||||
|
@ -1394,7 +1404,7 @@ export function MediaEditor({
|
|||
contentType: IMAGE_PNG,
|
||||
data,
|
||||
caption: caption !== '' ? caption : undefined,
|
||||
captionBodyRanges,
|
||||
captionBodyRanges: captionBodyRanges ?? undefined,
|
||||
blurHash,
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
|||
import { noop } from 'lodash';
|
||||
import { animated } from '@react-spring/web';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { ModalHost } from './ModalHost';
|
||||
import type { Theme } from '../util/theme';
|
||||
|
@ -37,6 +38,7 @@ type PropsType = {
|
|||
title?: ReactNode;
|
||||
useFocusTrap?: boolean;
|
||||
padded?: boolean;
|
||||
['aria-describedby']?: string;
|
||||
};
|
||||
|
||||
export type ModalPropsType = PropsType & {
|
||||
|
@ -65,6 +67,7 @@ export function Modal({
|
|||
hasFooterDivider = false,
|
||||
noTransform = false,
|
||||
padded = true,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
}: Readonly<ModalPropsType>): JSX.Element | null {
|
||||
const { close, isClosed, modalStyles, overlayStyles } = useAnimated(
|
||||
onClose,
|
||||
|
@ -132,6 +135,7 @@ export function Modal({
|
|||
padded={padded}
|
||||
hasHeaderDivider={hasHeaderDivider}
|
||||
hasFooterDivider={hasFooterDivider}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
>
|
||||
{children}
|
||||
</ModalPage>
|
||||
|
@ -173,6 +177,7 @@ export function ModalPage({
|
|||
padded = true,
|
||||
hasHeaderDivider = false,
|
||||
hasFooterDivider = false,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
}: ModalPageProps): JSX.Element {
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
@ -188,6 +193,8 @@ export function ModalPage({
|
|||
);
|
||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||
|
||||
const [id] = useState(() => uuid());
|
||||
|
||||
useScrollObserver(bodyRef, bodyInnerRef, scroll => {
|
||||
setScrolled(isScrolled(scroll));
|
||||
setScrolledToBottom(isScrolledToBottom(scroll));
|
||||
|
@ -198,7 +205,7 @@ export function ModalPage({
|
|||
<>
|
||||
{/* We don't want the click event to propagate to its container node. */}
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<div
|
||||
className={classNames(
|
||||
getClassName(''),
|
||||
|
@ -209,6 +216,10 @@ export function ModalPage({
|
|||
hasFooterDivider && getClassName('--footer-divider')
|
||||
)}
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
tabIndex={-1}
|
||||
aria-labelledby={title ? `${id}-title` : undefined}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
@ -234,6 +245,7 @@ export function ModalPage({
|
|||
)}
|
||||
{title && (
|
||||
<h1
|
||||
id={`${id}-title`}
|
||||
className={classNames(
|
||||
getClassName('__title'),
|
||||
hasXButton ? getClassName('__title--with-x-button') : null
|
||||
|
|
24
ts/components/SafetyTipsModal.stories.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import type { SafetyTipsModalProps } from './SafetyTipsModal';
|
||||
import { SafetyTipsModal } from './SafetyTipsModal';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SafetyTipsModal',
|
||||
component: SafetyTipsModal,
|
||||
args: {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
},
|
||||
} satisfies ComponentMeta<SafetyTipsModalProps>;
|
||||
|
||||
export function Default(args: SafetyTipsModalProps): JSX.Element {
|
||||
return <SafetyTipsModal {...args} />;
|
||||
}
|
216
ts/components/SafetyTipsModal.tsx
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { UIEvent } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { Modal } from './Modal';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
|
||||
export type SafetyTipsModalProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose(): void;
|
||||
}>;
|
||||
|
||||
export function SafetyTipsModal({
|
||||
i18n,
|
||||
onClose,
|
||||
}: SafetyTipsModalProps): JSX.Element {
|
||||
const pages = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'crypto',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Crypto'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Crypto'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-crypto.png',
|
||||
},
|
||||
{
|
||||
key: 'vague',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Vague'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Vague'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-vague.png',
|
||||
},
|
||||
{
|
||||
key: 'links',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Links'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Links'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-links.png',
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Business'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Business'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-business.png',
|
||||
},
|
||||
];
|
||||
}, [i18n]);
|
||||
|
||||
const [modalId] = useState(() => uuid());
|
||||
const [cardWrapperId] = useState(() => uuid());
|
||||
|
||||
function getCardIdForPage(pageIndex: number) {
|
||||
return `${cardWrapperId}_${pages[pageIndex].key}`;
|
||||
}
|
||||
|
||||
const maxPageIndex = pages.length - 1;
|
||||
const [pageIndex, setPageIndexInner] = useState(0);
|
||||
const reducedMotion = useReducedMotion();
|
||||
const scrollEndTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [hasPageIndexChanged, setHasPageIndexChanged] = useState(false);
|
||||
function setPageIndex(nextPageIndex: number) {
|
||||
setPageIndexInner(nextPageIndex);
|
||||
setHasPageIndexChanged(true);
|
||||
}
|
||||
|
||||
function clearScrollEndTimer() {
|
||||
if (scrollEndTimer.current != null) {
|
||||
clearTimeout(scrollEndTimer.current);
|
||||
scrollEndTimer.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearScrollEndTimer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function scrollToPageIndex(nextPageIndex: number) {
|
||||
clearScrollEndTimer();
|
||||
setPageIndex(nextPageIndex);
|
||||
document.getElementById(getCardIdForPage(nextPageIndex))?.scrollIntoView({
|
||||
inline: 'center',
|
||||
behavior: reducedMotion ? 'instant' : 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
function handleScroll(event: UIEvent) {
|
||||
clearScrollEndTimer();
|
||||
const { scrollWidth, scrollLeft, clientWidth } = event.currentTarget;
|
||||
const maxScrollLeft = scrollWidth - clientWidth;
|
||||
const absScrollLeft = Math.abs(scrollLeft);
|
||||
const percentScrolled = absScrollLeft / maxScrollLeft;
|
||||
const scrolledPageIndex = Math.round(percentScrolled * maxPageIndex);
|
||||
scrollEndTimer.current = setTimeout(() => {
|
||||
setPageIndex(scrolledPageIndex);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
modalName="SafetyTipsModal"
|
||||
moduleClassName="SafetyTipsModal"
|
||||
noMouseClose
|
||||
hasXButton
|
||||
padded={false}
|
||||
title={i18n('icu:SafetyTipsModal__Title')}
|
||||
onClose={onClose}
|
||||
aria-describedby={`${modalId}-description`}
|
||||
modalFooter={
|
||||
<>
|
||||
<Button
|
||||
className="SafetyTipsModal__Button SafetyTipsModal__Button--Previous"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
aria-disabled={pageIndex === 0}
|
||||
aria-controls={cardWrapperId}
|
||||
onClick={() => {
|
||||
if (pageIndex === 0) {
|
||||
return;
|
||||
}
|
||||
scrollToPageIndex(pageIndex - 1);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:SafetyTipsModal__Button--Previous')}
|
||||
</Button>
|
||||
{pageIndex < maxPageIndex ? (
|
||||
<Button
|
||||
className="SafetyTipsModal__Button SafetyTipsModal__Button--Next"
|
||||
variant={ButtonVariant.Primary}
|
||||
aria-controls={cardWrapperId}
|
||||
onClick={() => {
|
||||
if (pageIndex === maxPageIndex) {
|
||||
return;
|
||||
}
|
||||
scrollToPageIndex(pageIndex + 1);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:SafetyTipsModal__Button--Next')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="SafetyTipsModal__Button SafetyTipsModal__Button--Next"
|
||||
variant={ButtonVariant.Primary}
|
||||
onClick={onClose}
|
||||
>
|
||||
{i18n('icu:SafetyTipsModal__Button--Done')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="SafetyTipsModal__Description" id={`${modalId}-description`}>
|
||||
{i18n('icu:SafetyTipsModal__Description')}
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
className="SafetyTipsModal__CardWrapper"
|
||||
id={cardWrapperId}
|
||||
aria-live={hasPageIndexChanged ? 'assertive' : undefined}
|
||||
aria-atomic
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{pages.map((page, index) => {
|
||||
const isCurrentPage = pageIndex === index;
|
||||
return (
|
||||
<div
|
||||
id={getCardIdForPage(index)}
|
||||
key={page.key}
|
||||
className="SafetyTipsModal__Card"
|
||||
aria-hidden={!isCurrentPage}
|
||||
>
|
||||
<img
|
||||
role="presentation"
|
||||
alt=""
|
||||
className="SafetyTipsModal__CardImage"
|
||||
src={page.imageUrl}
|
||||
width={664}
|
||||
height={314}
|
||||
/>
|
||||
<h2 className="SafetyTipsModal__CardTitle">{page.title}</h2>
|
||||
<p className="SafetyTipsModal__CardDescription">
|
||||
{page.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="SafetyTipsModal__Dots">
|
||||
{pages.map((page, index) => {
|
||||
const isCurrentPage = pageIndex === index;
|
||||
return (
|
||||
<button
|
||||
key={page.key}
|
||||
className="SafetyTipsModal__DotsButton"
|
||||
type="button"
|
||||
aria-controls={cardWrapperId}
|
||||
aria-current={isCurrentPage ? 'step' : undefined}
|
||||
onClick={() => {
|
||||
scrollToPageIndex(index);
|
||||
}}
|
||||
>
|
||||
<div className="SafetyTipsModal__DotsButtonLabel">
|
||||
{i18n('icu:SafetyTipsModal__DotLabel', {
|
||||
page: index + 1,
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -96,7 +96,11 @@ export type PropsType = {
|
|||
> &
|
||||
Pick<
|
||||
MediaEditorPropsType,
|
||||
'isFormattingEnabled' | 'onPickEmoji' | 'onTextTooLong' | 'platform'
|
||||
| 'isFormattingEnabled'
|
||||
| 'onPickEmoji'
|
||||
| 'onTextTooLong'
|
||||
| 'platform'
|
||||
| 'sortedGroupMembers'
|
||||
>;
|
||||
|
||||
export function StoryCreator({
|
||||
|
@ -139,6 +143,7 @@ export function StoryCreator({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
skinTone,
|
||||
sortedGroupMembers,
|
||||
theme,
|
||||
toggleGroupsForStorySend,
|
||||
toggleSignalConnectionsModal,
|
||||
|
@ -272,6 +277,9 @@ export function StoryCreator({
|
|||
platform={platform}
|
||||
recentStickers={recentStickers}
|
||||
skinTone={skinTone}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
draftText={null}
|
||||
draftBodyRanges={null}
|
||||
/>
|
||||
)}
|
||||
{!file && (
|
||||
|
|
|
@ -258,8 +258,15 @@ export function StoryViewsNRepliesModal({
|
|||
}
|
||||
platform={platform}
|
||||
sendCounter={0}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
skinTone={skinTone ?? null}
|
||||
sortedGroupMembers={sortedGroupMembers ?? null}
|
||||
theme={ThemeType.dark}
|
||||
conversationId={null}
|
||||
draftBodyRanges={null}
|
||||
draftEditMessage={null}
|
||||
large={null}
|
||||
shouldHidePopovers={null}
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
<EmojiButton
|
||||
className="StoryViewsNRepliesModal__emoji-button"
|
||||
|
|
|
@ -121,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.PinnedConversationsFull };
|
||||
case ToastType.ReactionFailed:
|
||||
return { toastType: ToastType.ReactionFailed };
|
||||
case ToastType.ReportedSpam:
|
||||
return { toastType: ToastType.ReportedSpam };
|
||||
case ToastType.ReportedSpamAndBlocked:
|
||||
return { toastType: ToastType.ReportedSpamAndBlocked };
|
||||
case ToastType.StickerPackInstallFailed:
|
||||
|
|
|
@ -371,6 +371,14 @@ export function renderToast({
|
|||
return <Toast onClose={hideToast}>{i18n('icu:Reactions--error')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReportedSpam) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:MessageRequests--report-spam-success-toast')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReportedSpamAndBlocked) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
|
|
@ -1,21 +1,47 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Emojify } from './Emojify';
|
||||
import type { ContactNameColorType } from '../../types/Colors';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import { isSignalConversation as getIsSignalConversation } from '../../util/isSignalConversation';
|
||||
|
||||
export type PropsType = {
|
||||
export type ContactNameData = {
|
||||
contactNameColor?: ContactNameColorType;
|
||||
firstName?: string;
|
||||
isSignalConversation?: boolean;
|
||||
isMe?: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function useContactNameData(
|
||||
conversation: ConversationType | null,
|
||||
contactNameColor?: ContactNameColorType
|
||||
): ContactNameData | null {
|
||||
const { firstName, title, isMe } = conversation ?? {};
|
||||
const isSignalConversation =
|
||||
conversation != null ? getIsSignalConversation(conversation) : null;
|
||||
return useMemo(() => {
|
||||
if (title == null || isSignalConversation == null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
contactNameColor,
|
||||
firstName,
|
||||
isSignalConversation,
|
||||
isMe,
|
||||
title,
|
||||
};
|
||||
}, [contactNameColor, firstName, isSignalConversation, isMe, title]);
|
||||
}
|
||||
|
||||
export type PropsType = ContactNameData & {
|
||||
module?: string;
|
||||
preferFirstName?: boolean;
|
||||
title: string;
|
||||
onClick?: VoidFunction;
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
|||
|
||||
const getCommonProps = () => ({
|
||||
acceptConversation: action('acceptConversation'),
|
||||
reportSpam: action('reportSpam'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
conversationId: 'some-conversation-id',
|
||||
|
|
|
@ -50,6 +50,7 @@ export type ReviewPropsType = Readonly<
|
|||
export type PropsType = {
|
||||
conversationId: string;
|
||||
acceptConversation: (conversationId: string) => unknown;
|
||||
reportSpam: (conversationId: string) => unknown;
|
||||
blockAndReportSpam: (conversationId: string) => unknown;
|
||||
blockConversation: (conversationId: string) => unknown;
|
||||
deleteConversation: (conversationId: string) => unknown;
|
||||
|
@ -75,6 +76,7 @@ enum ConfirmationStateType {
|
|||
export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
||||
const {
|
||||
acceptConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
conversationId,
|
||||
|
@ -111,19 +113,23 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
case ConfirmationStateType.ConfirmingBlock:
|
||||
return (
|
||||
<MessageRequestActionsConfirmation
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
addedByName={affectedConversation}
|
||||
conversationId={affectedConversation.id}
|
||||
conversationType="direct"
|
||||
deleteConversation={deleteConversation}
|
||||
conversationType={affectedConversation.type}
|
||||
conversationName={affectedConversation}
|
||||
i18n={i18n}
|
||||
title={affectedConversation.title}
|
||||
isBlocked={affectedConversation.isBlocked ?? false}
|
||||
isReported={affectedConversation.isReported ?? false}
|
||||
state={
|
||||
type === ConfirmationStateType.ConfirmingDelete
|
||||
? MessageRequestState.deleting
|
||||
: MessageRequestState.blocking
|
||||
}
|
||||
acceptConversation={acceptConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
deleteConversation={deleteConversation}
|
||||
onChangeState={messageRequestState => {
|
||||
switch (messageRequestState) {
|
||||
case MessageRequestState.blocking:
|
||||
|
@ -138,10 +144,12 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
affectedConversation,
|
||||
});
|
||||
break;
|
||||
case MessageRequestState.reportingAndMaybeBlocking:
|
||||
case MessageRequestState.acceptedOptions:
|
||||
case MessageRequestState.unblocking:
|
||||
assertDev(
|
||||
false,
|
||||
'Got unexpected MessageRequestState.unblocking state. Clearing confiration state'
|
||||
`Got unexpected MessageRequestState.${MessageRequestState[messageRequestState]} state. Clearing confiration state`
|
||||
);
|
||||
setConfirmationState(undefined);
|
||||
break;
|
||||
|
|
|
@ -29,8 +29,15 @@ type ItemsType = Array<{
|
|||
props: Omit<ComponentProps<typeof ConversationHeader>, 'theme'>;
|
||||
}>;
|
||||
|
||||
const commonConversation = getDefaultConversation();
|
||||
const commonProps = {
|
||||
...getDefaultConversation(),
|
||||
...commonConversation,
|
||||
conversationId: commonConversation.id,
|
||||
conversationType: commonConversation.type,
|
||||
conversationName: commonConversation,
|
||||
addedByName: null,
|
||||
isBlocked: commonConversation.isBlocked ?? false,
|
||||
isReported: commonConversation.isReported ?? false,
|
||||
|
||||
cannotLeaveBecauseYouAreLastAdmin: false,
|
||||
showBackButton: false,
|
||||
|
@ -59,6 +66,12 @@ const commonProps = {
|
|||
setMuteExpiration: action('onSetMuteNotifications'),
|
||||
setPinned: action('setPinned'),
|
||||
viewUserStories: action('viewUserStories'),
|
||||
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
reportSpam: action('reportSpam'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
};
|
||||
|
||||
export function PrivateConvo(): JSX.Element {
|
||||
|
|
|
@ -41,6 +41,12 @@ import { PanelType } from '../../types/Panels';
|
|||
import { UserText } from '../UserText';
|
||||
import { Alert } from '../Alert';
|
||||
import { SizeObserver } from '../../hooks/useSizeObserver';
|
||||
import type { MessageRequestActionsConfirmationBaseProps } from './MessageRequestActionsConfirmation';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
import type { ContactNameData } from './ContactName';
|
||||
|
||||
export enum OutgoingCallButtonStyle {
|
||||
None,
|
||||
|
@ -60,6 +66,8 @@ export type PropsDataType = {
|
|||
isSelectMode: boolean;
|
||||
isSignalConversation?: boolean;
|
||||
theme: ThemeType;
|
||||
addedByName: ContactNameData | null;
|
||||
conversationName: ContactNameData;
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
|
@ -72,6 +80,8 @@ export type PropsDataType = {
|
|||
| 'groupVersion'
|
||||
| 'id'
|
||||
| 'isArchived'
|
||||
| 'isBlocked'
|
||||
| 'isReported'
|
||||
| 'isMe'
|
||||
| 'isPinned'
|
||||
| 'isVerified'
|
||||
|
@ -81,6 +91,7 @@ export type PropsDataType = {
|
|||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'removalStage'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
|
@ -106,7 +117,7 @@ export type PropsActionsType = {
|
|||
setMuteExpiration: (conversationId: string, seconds: number) => void;
|
||||
setPinned: (conversationId: string, value: boolean) => void;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
};
|
||||
} & MessageRequestActionsConfirmationBaseProps;
|
||||
|
||||
export type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
|
@ -127,6 +138,7 @@ type StateType = {
|
|||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: boolean;
|
||||
isNarrow: boolean;
|
||||
modalState: ModalState;
|
||||
messageRequestState: MessageRequestState;
|
||||
};
|
||||
|
||||
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
||||
|
@ -149,6 +161,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false,
|
||||
isNarrow: false,
|
||||
modalState: ModalState.NothingOpen,
|
||||
messageRequestState: MessageRequestState.default,
|
||||
};
|
||||
|
||||
this.menuTriggerRef = React.createRef();
|
||||
|
@ -156,6 +169,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
this.showMenuBound = this.showMenu.bind(this);
|
||||
}
|
||||
|
||||
private handleMessageRequestStateChange = (
|
||||
state: MessageRequestState
|
||||
): void => {
|
||||
this.setState({ messageRequestState: state });
|
||||
};
|
||||
|
||||
private showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
|
||||
if (this.menuTriggerRef.current) {
|
||||
this.menuTriggerRef.current.handleContextClick(event);
|
||||
|
@ -328,6 +347,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
private renderMenu(triggerId: string): ReactNode {
|
||||
const {
|
||||
acceptConversation,
|
||||
acceptedMessageRequest,
|
||||
canChangeTimer,
|
||||
cannotLeaveBecauseYouAreLastAdmin,
|
||||
|
@ -336,6 +356,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
i18n,
|
||||
id,
|
||||
isArchived,
|
||||
isBlocked,
|
||||
isMissingMandatoryProfileSharing,
|
||||
isPinned,
|
||||
isSignalConversation,
|
||||
|
@ -431,6 +452,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
this.setState({ hasDeleteMessagesConfirmation: true })
|
||||
|
@ -491,98 +513,164 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return createPortal(
|
||||
<ContextMenu id={triggerId} rtl={isRTL}>
|
||||
{disableTimerChanges ? null : (
|
||||
<SubMenu hoverDelay={1} title={disappearingTitle} rtl={!isRTL}>
|
||||
{expireDurations}
|
||||
</SubMenu>
|
||||
)}
|
||||
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
||||
{muteOptions.map(item => (
|
||||
{!acceptedMessageRequest && (
|
||||
<>
|
||||
{!isBlocked && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
messageRequestState: MessageRequestState.blocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Block')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isBlocked && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
messageRequestState: MessageRequestState.unblocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Unblock')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isBlocked && (
|
||||
<MenuItem onClick={acceptConversation}>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Accept')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
setMuteExpiration(id, item.value);
|
||||
this.setState({
|
||||
messageRequestState:
|
||||
MessageRequestState.reportingAndMaybeBlocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
{i18n('icu:ConversationHeader__MenuItem--ReportSpam')}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
{!isGroup || hasGV2AdminEnabled ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ConversationDetails,
|
||||
})
|
||||
}
|
||||
>
|
||||
{isGroup
|
||||
? i18n('icu:showConversationDetails')
|
||||
: i18n('icu:showConversationDetails--direct')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
onClick={() => pushPanelForConversation({ type: PanelType.AllMedia })}
|
||||
>
|
||||
{i18n('icu:viewRecentMedia')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggleSelectMode(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__menu__selectMessages')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
{!markedUnread ? (
|
||||
<MenuItem onClick={() => onMarkUnread(id)}>
|
||||
{i18n('icu:markUnread')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{isPinned ? (
|
||||
<MenuItem onClick={() => setPinned(id, false)}>
|
||||
{i18n('icu:unpinConversation')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => setPinned(id, true)}>
|
||||
{i18n('icu:pinConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isArchived ? (
|
||||
<MenuItem onClick={() => onMoveToInbox(id)}>
|
||||
{i18n('icu:moveConversationToInbox')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => onArchive(id)}>
|
||||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })}
|
||||
>
|
||||
{i18n('icu:deleteMessagesInConversation')}
|
||||
</MenuItem>
|
||||
{isGroup && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
|
||||
messageRequestState: MessageRequestState.deleting,
|
||||
});
|
||||
} else {
|
||||
this.setState({ hasLeaveGroupConfirmation: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--DeleteChat')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{acceptedMessageRequest && (
|
||||
<>
|
||||
{disableTimerChanges ? null : (
|
||||
<SubMenu hoverDelay={1} title={disappearingTitle} rtl={!isRTL}>
|
||||
{expireDurations}
|
||||
</SubMenu>
|
||||
)}
|
||||
</MenuItem>
|
||||
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
||||
{muteOptions.map(item => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
setMuteExpiration(id, item.value);
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
{!isGroup || hasGV2AdminEnabled ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ConversationDetails,
|
||||
})
|
||||
}
|
||||
>
|
||||
{isGroup
|
||||
? i18n('icu:showConversationDetails')
|
||||
: i18n('icu:showConversationDetails--direct')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation({ type: PanelType.AllMedia })
|
||||
}
|
||||
>
|
||||
{i18n('icu:viewRecentMedia')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggleSelectMode(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__menu__selectMessages')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
{!markedUnread ? (
|
||||
<MenuItem onClick={() => onMarkUnread(id)}>
|
||||
{i18n('icu:markUnread')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{isPinned ? (
|
||||
<MenuItem onClick={() => setPinned(id, false)}>
|
||||
{i18n('icu:unpinConversation')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => setPinned(id, true)}>
|
||||
{i18n('icu:pinConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isArchived ? (
|
||||
<MenuItem onClick={() => onMoveToInbox(id)}>
|
||||
{i18n('icu:moveConversationToInbox')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => onArchive(id)}>
|
||||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
messageRequestState: MessageRequestState.blocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Block')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
this.setState({ hasDeleteMessagesConfirmation: true })
|
||||
}
|
||||
>
|
||||
{i18n('icu:deleteMessagesInConversation')}
|
||||
</MenuItem>
|
||||
{isGroup && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
this.setState({
|
||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
|
||||
});
|
||||
} else {
|
||||
this.setState({ hasLeaveGroupConfirmation: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ContextMenu>,
|
||||
document.body
|
||||
|
@ -751,25 +839,35 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
public override render(): ReactNode {
|
||||
const {
|
||||
addedByName,
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
conversationName,
|
||||
expireTimer,
|
||||
hasPanelShowing,
|
||||
i18n,
|
||||
id,
|
||||
isBlocked,
|
||||
isReported,
|
||||
isSMSOnly,
|
||||
isSignalConversation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
outgoingCallButtonStyle,
|
||||
setDisappearingMessages,
|
||||
type,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
} = this.props;
|
||||
|
||||
if (hasPanelShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isNarrow, modalState } = this.state;
|
||||
const { isNarrow, modalState, messageRequestState } = this.state;
|
||||
const triggerId = `conversation-${id}`;
|
||||
|
||||
let modalNode: ReactNode;
|
||||
|
@ -829,6 +927,22 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
{this.renderSearchButton()}
|
||||
{this.renderMoreButton(triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
conversationId={id}
|
||||
conversationType={type}
|
||||
addedByName={addedByName}
|
||||
conversationName={conversationName}
|
||||
isBlocked={isBlocked ?? false}
|
||||
isReported={isReported ?? false}
|
||||
state={messageRequestState}
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
reportSpam={reportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
onChangeState={this.handleMessageRequestStateChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SizeObserver>
|
||||
|
|
|
@ -15,6 +15,8 @@ import { StoryViewModeType } from '../../types/Stories';
|
|||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal';
|
||||
|
||||
export type Props = {
|
||||
about?: string;
|
||||
|
@ -42,6 +44,7 @@ const renderMembershipRow = ({
|
|||
i18n,
|
||||
isMe,
|
||||
onClickMessageRequestWarning,
|
||||
onToggleSafetyTips,
|
||||
phoneNumber,
|
||||
sharedGroupNames,
|
||||
}: Pick<
|
||||
|
@ -54,6 +57,7 @@ const renderMembershipRow = ({
|
|||
> &
|
||||
Required<Pick<Props, 'sharedGroupNames'>> & {
|
||||
onClickMessageRequestWarning: () => void;
|
||||
onToggleSafetyTips: (showSafetyTips: boolean) => void;
|
||||
}) => {
|
||||
if (conversationType !== 'direct') {
|
||||
return null;
|
||||
|
@ -67,6 +71,20 @@ const renderMembershipRow = ({
|
|||
);
|
||||
}
|
||||
|
||||
const safetyTipsButton = (
|
||||
<div>
|
||||
<Button
|
||||
className="module-conversation-hero__safety-tips-button"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
onToggleSafetyTips(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequestWarning__safety-tips')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (sharedGroupNames.length > 0) {
|
||||
return (
|
||||
<div className="module-conversation-hero__membership">
|
||||
|
@ -76,6 +94,7 @@ const renderMembershipRow = ({
|
|||
nameClassName="module-conversation-hero__membership__name"
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
{safetyTipsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -86,6 +105,7 @@ const renderMembershipRow = ({
|
|||
return (
|
||||
<div className="module-conversation-hero__membership">
|
||||
{i18n('icu:no-groups-in-common')}
|
||||
{safetyTipsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -107,6 +127,7 @@ const renderMembershipRow = ({
|
|||
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||
</button>
|
||||
</div>
|
||||
{safetyTipsButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -136,6 +157,7 @@ export function ConversationHero({
|
|||
viewUserStories,
|
||||
toggleAboutContactModal,
|
||||
}: Props): JSX.Element {
|
||||
const [isShowingSafetyTips, setIsShowingSafetyTips] = useState(false);
|
||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||
useState(false);
|
||||
const closeMessageRequestWarning = () => {
|
||||
|
@ -248,6 +270,9 @@ export function ConversationHero({
|
|||
onClickMessageRequestWarning() {
|
||||
setIsShowingMessageRequestWarning(true);
|
||||
},
|
||||
onToggleSafetyTips(showSafetyTips: boolean) {
|
||||
setIsShowingSafetyTips(showSafetyTips);
|
||||
},
|
||||
phoneNumber,
|
||||
sharedGroupNames,
|
||||
})}
|
||||
|
@ -277,6 +302,15 @@ export function ConversationHero({
|
|||
{i18n('icu:MessageRequestWarning__dialog__details')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isShowingSafetyTips && (
|
||||
<SafetyTipsModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setIsShowingSafetyTips(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
/* eslint-enable no-nested-ternary */
|
||||
|
|
|
@ -8,9 +8,17 @@ import type { Props } from './MandatoryProfileSharingActions';
|
|||
import { MandatoryProfileSharingActions } from './MandatoryProfileSharingActions';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type Args = {
|
||||
conversationType: 'direct' | 'group';
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/MandatoryProfileSharingActions',
|
||||
argTypes: {
|
||||
|
@ -20,34 +28,43 @@ export default {
|
|||
options: ['direct', 'group'],
|
||||
},
|
||||
},
|
||||
firstName: { control: { type: 'text' } },
|
||||
title: { control: { type: 'text' } },
|
||||
},
|
||||
args: {
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
conversationType: 'direct',
|
||||
firstName: 'Cayce',
|
||||
title: 'Cayce Bollard',
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
} satisfies Meta<Args>;
|
||||
|
||||
export function Direct(args: Props): JSX.Element {
|
||||
function Example(args: Args) {
|
||||
const conversation =
|
||||
args.conversationType === 'group'
|
||||
? getDefaultGroup()
|
||||
: getDefaultConversation();
|
||||
const addedBy =
|
||||
args.conversationType === 'group' ? getDefaultConversation() : conversation;
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MandatoryProfileSharingActions {...args} />
|
||||
<MandatoryProfileSharingActions
|
||||
addedByName={addedBy}
|
||||
conversationType={conversation.type}
|
||||
conversationId={conversation.id}
|
||||
conversationName={conversation}
|
||||
i18n={i18n}
|
||||
isBlocked={conversation.isBlocked ?? false}
|
||||
isReported={conversation.isReported ?? false}
|
||||
acceptConversation={action('acceptConversation')}
|
||||
blockAndReportSpam={action('blockAndReportSpam')}
|
||||
blockConversation={action('blockConversation')}
|
||||
deleteConversation={action('deleteConversation')}
|
||||
reportSpam={action('reportSpam')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Direct(args: Props): JSX.Element {
|
||||
return <Example {...args} conversationType="direct" />;
|
||||
}
|
||||
|
||||
export function Group(args: Props): JSX.Element {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MandatoryProfileSharingActions {...args} conversationType="group" />
|
||||
</div>
|
||||
);
|
||||
return <Example {...args} conversationType="group" />;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { PropsType as ContactNameProps } from './ContactName';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import type { Props as MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import type { MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
|
@ -15,17 +14,20 @@ import type { LocalizerType } from '../../types/Util';
|
|||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
firstName?: string;
|
||||
} & Omit<ContactNameProps, 'module'> &
|
||||
Pick<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
| 'acceptConversation'
|
||||
| 'blockAndReportSpam'
|
||||
| 'blockConversation'
|
||||
| 'conversationId'
|
||||
| 'conversationType'
|
||||
| 'deleteConversation'
|
||||
>;
|
||||
} & Pick<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
| 'addedByName'
|
||||
| 'conversationId'
|
||||
| 'conversationType'
|
||||
| 'conversationName'
|
||||
| 'isBlocked'
|
||||
| 'isReported'
|
||||
| 'acceptConversation'
|
||||
| 'reportSpam'
|
||||
| 'blockAndReportSpam'
|
||||
| 'blockConversation'
|
||||
| 'deleteConversation'
|
||||
>;
|
||||
|
||||
const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
||||
<a
|
||||
|
@ -39,15 +41,18 @@ const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
|||
);
|
||||
|
||||
export function MandatoryProfileSharingActions({
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
addedByName,
|
||||
conversationId,
|
||||
conversationType,
|
||||
deleteConversation,
|
||||
firstName,
|
||||
conversationName,
|
||||
i18n,
|
||||
title,
|
||||
isBlocked,
|
||||
isReported,
|
||||
acceptConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
deleteConversation,
|
||||
}: Props): JSX.Element {
|
||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||
|
||||
|
@ -56,7 +61,7 @@ export function MandatoryProfileSharingActions({
|
|||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName firstName={firstName} title={title} preferFirstName />
|
||||
<ContactName {...conversationName} preferFirstName />
|
||||
</strong>
|
||||
);
|
||||
|
||||
|
@ -64,19 +69,23 @@ export function MandatoryProfileSharingActions({
|
|||
<>
|
||||
{mrState !== MessageRequestState.default ? (
|
||||
<MessageRequestActionsConfirmation
|
||||
addedByName={addedByName}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
isReported={isReported}
|
||||
state={mrState}
|
||||
acceptConversation={() => {
|
||||
throw new Error(
|
||||
'Should not be able to unblock from MandatoryProfileSharingActions'
|
||||
);
|
||||
}}
|
||||
blockConversation={blockConversation}
|
||||
conversationId={conversationId}
|
||||
deleteConversation={deleteConversation}
|
||||
i18n={i18n}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
title={title}
|
||||
conversationType={conversationType}
|
||||
state={mrState}
|
||||
onChangeState={setMrState}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -4,13 +4,23 @@
|
|||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props } from './MessageRequestActions';
|
||||
import { MessageRequestActions } from './MessageRequestActions';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type Args = {
|
||||
conversationType: 'direct' | 'group';
|
||||
isBlocked: boolean;
|
||||
isHidden: boolean;
|
||||
isReported: boolean;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/MessageRequestActions',
|
||||
argTypes: {
|
||||
|
@ -20,19 +30,9 @@ export default {
|
|||
options: ['direct', 'group'],
|
||||
},
|
||||
},
|
||||
firstName: { control: { type: 'text' } },
|
||||
title: { control: { type: 'text' } },
|
||||
},
|
||||
args: {
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
conversationType: 'direct',
|
||||
firstName: 'Cayce',
|
||||
title: 'Cayce Bollard',
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
},
|
||||
decorators: [
|
||||
(Story: React.ComponentType): JSX.Element => {
|
||||
|
@ -43,20 +43,62 @@ export default {
|
|||
);
|
||||
},
|
||||
],
|
||||
} satisfies Meta<Props>;
|
||||
} satisfies Meta<Args>;
|
||||
|
||||
export function Direct(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} />;
|
||||
function Example(args: Args): JSX.Element {
|
||||
const conversation =
|
||||
args.conversationType === 'group'
|
||||
? getDefaultGroup()
|
||||
: getDefaultConversation();
|
||||
const addedBy =
|
||||
args.conversationType === 'group' ? getDefaultConversation() : conversation;
|
||||
return (
|
||||
<MessageRequestActions
|
||||
addedByName={addedBy}
|
||||
conversationType={conversation.type}
|
||||
conversationId={conversation.id}
|
||||
conversationName={conversation}
|
||||
i18n={i18n}
|
||||
isBlocked={args.isBlocked}
|
||||
isHidden={args.isHidden}
|
||||
isReported={args.isReported}
|
||||
acceptConversation={action('acceptConversation')}
|
||||
blockAndReportSpam={action('blockAndReportSpam')}
|
||||
blockConversation={action('blockConversation')}
|
||||
deleteConversation={action('deleteConversation')}
|
||||
reportSpam={action('reportSpam')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DirectBlocked(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} isBlocked />;
|
||||
export function Direct(args: Args): JSX.Element {
|
||||
return <Example {...args} />;
|
||||
}
|
||||
|
||||
export function Group(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} conversationType="group" />;
|
||||
export function DirectBlocked(args: Args): JSX.Element {
|
||||
return <Example {...args} isBlocked />;
|
||||
}
|
||||
|
||||
export function GroupBlocked(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} conversationType="group" isBlocked />;
|
||||
export function DirectReported(args: Args): JSX.Element {
|
||||
return <Example {...args} isReported />;
|
||||
}
|
||||
|
||||
export function DirectBlockedAndReported(args: Args): JSX.Element {
|
||||
return <Example {...args} isBlocked isReported />;
|
||||
}
|
||||
|
||||
export function Group(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" />;
|
||||
}
|
||||
|
||||
export function GroupBlocked(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" isBlocked />;
|
||||
}
|
||||
|
||||
export function GroupReported(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" isReported />;
|
||||
}
|
||||
|
||||
export function GroupBlockedAndReported(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" isBlocked isReported />;
|
||||
}
|
||||
|
|
|
@ -2,52 +2,57 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { PropsType as ContactNameProps } from './ContactName';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import type { Props as MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import type { MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
import { Intl } from '../Intl';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
isHidden?: boolean;
|
||||
} & Omit<ContactNameProps, 'module'> &
|
||||
Omit<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
'i18n' | 'state' | 'onChangeState'
|
||||
>;
|
||||
isHidden: boolean | null;
|
||||
} & Omit<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
'i18n' | 'state' | 'onChangeState'
|
||||
>;
|
||||
|
||||
export function MessageRequestActions({
|
||||
addedByName,
|
||||
conversationId,
|
||||
conversationType,
|
||||
conversationName,
|
||||
i18n,
|
||||
isBlocked,
|
||||
isHidden,
|
||||
isReported,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
conversationId,
|
||||
conversationType,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
firstName,
|
||||
i18n,
|
||||
isHidden,
|
||||
isBlocked,
|
||||
title,
|
||||
}: Props): JSX.Element {
|
||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||
|
||||
const name = (
|
||||
<strong
|
||||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName firstName={firstName} title={title} preferFirstName />
|
||||
</strong>
|
||||
);
|
||||
const nameValue =
|
||||
conversationType === 'direct' ? conversationName : addedByName;
|
||||
|
||||
let message: JSX.Element | undefined;
|
||||
if (conversationType === 'direct') {
|
||||
strictAssert(nameValue != null, 'nameValue is null');
|
||||
const name = (
|
||||
<strong
|
||||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName {...nameValue} preferFirstName />
|
||||
</strong>
|
||||
);
|
||||
|
||||
if (isBlocked) {
|
||||
message = (
|
||||
<Intl
|
||||
|
@ -87,39 +92,26 @@ export function MessageRequestActions({
|
|||
<>
|
||||
{mrState !== MessageRequestState.default ? (
|
||||
<MessageRequestActionsConfirmation
|
||||
addedByName={addedByName}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
isReported={isReported}
|
||||
state={mrState}
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
reportSpam={reportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
i18n={i18n}
|
||||
onChangeState={setMrState}
|
||||
state={mrState}
|
||||
title={title}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-message-request-actions">
|
||||
<p className="module-message-request-actions__message">{message}</p>
|
||||
<div className="module-message-request-actions__buttons">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.deleting);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
>
|
||||
{i18n('icu:MessageRequests--delete')}
|
||||
</Button>
|
||||
{isBlocked ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.unblocking);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
) : (
|
||||
{!isBlocked && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.blocking);
|
||||
|
@ -129,6 +121,36 @@ export function MessageRequestActions({
|
|||
{i18n('icu:MessageRequests--block')}
|
||||
</Button>
|
||||
)}
|
||||
{(isReported || isBlocked) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.deleting);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
>
|
||||
{i18n('icu:MessageRequests--delete')}
|
||||
</Button>
|
||||
)}
|
||||
{!isReported && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.reportingAndMaybeBlocking);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
>
|
||||
{i18n('icu:MessageRequests--reportAndMaybeBlock')}
|
||||
</Button>
|
||||
)}
|
||||
{isBlocked && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.unblocking);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
)}
|
||||
{!isBlocked ? (
|
||||
<Button
|
||||
onClick={() => acceptConversation(conversationId)}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { PropsType as ContactNameProps } from './ContactName';
|
||||
import type { ContactNameData } from './ContactName';
|
||||
import { ContactName } from './ContactName';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Intl } from '../Intl';
|
||||
|
@ -12,38 +12,53 @@ export enum MessageRequestState {
|
|||
blocking,
|
||||
deleting,
|
||||
unblocking,
|
||||
reportingAndMaybeBlocking,
|
||||
acceptedOptions,
|
||||
default,
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
acceptConversation(conversationId: string): unknown;
|
||||
blockAndReportSpam(conversationId: string): unknown;
|
||||
blockConversation(conversationId: string): unknown;
|
||||
export type MessageRequestActionsConfirmationBaseProps = {
|
||||
addedByName: ContactNameData | null;
|
||||
conversationId: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
deleteConversation(conversationId: string): unknown;
|
||||
i18n: LocalizerType;
|
||||
isBlocked?: boolean;
|
||||
onChangeState(state: MessageRequestState): unknown;
|
||||
state: MessageRequestState;
|
||||
} & Omit<ContactNameProps, 'module'>;
|
||||
conversationName: ContactNameData;
|
||||
isBlocked: boolean;
|
||||
isReported: boolean;
|
||||
acceptConversation(conversationId: string): void;
|
||||
blockAndReportSpam(conversationId: string): void;
|
||||
blockConversation(conversationId: string): void;
|
||||
reportSpam(conversationId: string): void;
|
||||
deleteConversation(conversationId: string): void;
|
||||
};
|
||||
|
||||
export type MessageRequestActionsConfirmationProps =
|
||||
MessageRequestActionsConfirmationBaseProps & {
|
||||
i18n: LocalizerType;
|
||||
state: MessageRequestState;
|
||||
onChangeState(state: MessageRequestState): void;
|
||||
};
|
||||
|
||||
export function MessageRequestActionsConfirmation({
|
||||
addedByName,
|
||||
conversationId,
|
||||
conversationType,
|
||||
conversationName,
|
||||
i18n,
|
||||
isBlocked,
|
||||
state,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
conversationId,
|
||||
conversationType,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
i18n,
|
||||
onChangeState,
|
||||
state,
|
||||
title,
|
||||
}: Props): JSX.Element | null {
|
||||
}: MessageRequestActionsConfirmationProps): JSX.Element | null {
|
||||
if (state === MessageRequestState.blocking) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.blocking"
|
||||
dialogName="messageRequestActionsConfirmation.blocking"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
|
@ -54,7 +69,13 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--block-direct-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
title: (
|
||||
<ContactName
|
||||
key="name"
|
||||
{...conversationName}
|
||||
preferFirstName
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -62,21 +83,18 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--block-group-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
title: (
|
||||
<ContactName
|
||||
key="name"
|
||||
{...conversationName}
|
||||
preferFirstName
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
actions={[
|
||||
...(conversationType === 'direct'
|
||||
? [
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block-and-report-spam'),
|
||||
action: () => blockAndReportSpam(conversationId),
|
||||
style: 'negative' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block'),
|
||||
action: () => blockConversation(conversationId),
|
||||
|
@ -91,10 +109,62 @@ export function MessageRequestActionsConfirmation({
|
|||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.reportingAndMaybeBlocking) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.reportingAndMaybeBlocking"
|
||||
dialogName="messageRequestActionsConfirmation.reportingAndMaybeBlocking"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
title={i18n('icu:MessageRequests--ReportAndMaybeBlockModal-title')}
|
||||
actions={[
|
||||
...(!isBlocked
|
||||
? ([
|
||||
{
|
||||
text: i18n(
|
||||
'icu:MessageRequests--ReportAndMaybeBlockModal-reportAndBlock'
|
||||
),
|
||||
action: () => blockAndReportSpam(conversationId),
|
||||
style: 'negative',
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
{
|
||||
text: i18n('icu:MessageRequests--ReportAndMaybeBlockModal-report'),
|
||||
action: () => reportSpam(conversationId),
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{conversationType === 'direct' ? (
|
||||
i18n('icu:MessageRequests--ReportAndMaybeBlockModal-body--direct')
|
||||
) : addedByName == null ? (
|
||||
i18n(
|
||||
'icu:MessageRequests--ReportAndMaybeBlockModal-body--group--unknown-contact'
|
||||
)
|
||||
) : (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:MessageRequests--ReportAndMaybeBlockModal-body--group"
|
||||
components={{
|
||||
name: <ContactName key="name" {...addedByName} preferFirstName />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.unblocking) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.unblocking"
|
||||
dialogName="messageRequestActionsConfirmation.unblocking"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
|
@ -104,7 +174,9 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--unblock-direct-confirm-title"
|
||||
components={{
|
||||
name: <ContactName key="name" title={title} />,
|
||||
name: (
|
||||
<ContactName key="name" {...conversationName} preferFirstName />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -126,7 +198,9 @@ export function MessageRequestActionsConfirmation({
|
|||
if (state === MessageRequestState.deleting) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.deleting"
|
||||
dialogName="messageRequestActionsConfirmation.deleting"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
|
@ -142,7 +216,13 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--delete-group-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
title: (
|
||||
<ContactName
|
||||
key="name"
|
||||
{...conversationName}
|
||||
preferFirstName
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -165,5 +245,42 @@ export function MessageRequestActionsConfirmation({
|
|||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.acceptedOptions) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.acceptedOptions"
|
||||
dialogName="messageRequestActionsConfirmation.acceptedOptions"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('icu:MessageRequests--reportAndMaybeBlock'),
|
||||
action: () =>
|
||||
onChangeState(MessageRequestState.reportingAndMaybeBlocking),
|
||||
style: 'negative',
|
||||
},
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block'),
|
||||
action: () => onChangeState(MessageRequestState.blocking),
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:MessageRequests--AcceptedOptionsModal--body"
|
||||
components={{
|
||||
name: (
|
||||
<ContactName key="name" {...conversationName} preferFirstName />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { MessageRequestState } from './MessageRequestActionsConfirmation';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal';
|
||||
import { MessageRequestResponseEvent } from '../../types/MessageRequestResponseEvent';
|
||||
|
||||
export type MessageRequestResponseNotificationData = {
|
||||
messageRequestResponseEvent: MessageRequestResponseEvent;
|
||||
};
|
||||
|
||||
export type MessageRequestResponseNotificationProps =
|
||||
MessageRequestResponseNotificationData & {
|
||||
i18n: LocalizerType;
|
||||
isBlocked: boolean;
|
||||
onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void;
|
||||
};
|
||||
|
||||
export function MessageRequestResponseNotification({
|
||||
i18n,
|
||||
isBlocked,
|
||||
messageRequestResponseEvent: event,
|
||||
onOpenMessageRequestActionsConfirmation,
|
||||
}: MessageRequestResponseNotificationProps): JSX.Element | null {
|
||||
const [isSafetyTipsModalOpen, setIsSafetyTipsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{event === MessageRequestResponseEvent.ACCEPT && (
|
||||
<SystemMessage
|
||||
icon="thread"
|
||||
contents={i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Accepted'
|
||||
)}
|
||||
button={
|
||||
isBlocked ? null : (
|
||||
<Button
|
||||
className="MessageRequestResponseNotification__Button"
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SystemMessage}
|
||||
onClick={() => {
|
||||
onOpenMessageRequestActionsConfirmation(
|
||||
MessageRequestState.acceptedOptions
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:MessageRequestResponseNotification__Button--Options'
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{event === MessageRequestResponseEvent.BLOCK && (
|
||||
<SystemMessage
|
||||
icon="block"
|
||||
contents={i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Blocked'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{event === MessageRequestResponseEvent.SPAM && (
|
||||
<SystemMessage
|
||||
icon="spam"
|
||||
contents={i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Reported'
|
||||
)}
|
||||
button={
|
||||
<Button
|
||||
className="MessageRequestResponseNotification__Button"
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SystemMessage}
|
||||
onClick={() => {
|
||||
setIsSafetyTipsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:MessageRequestResponseNotification__Button--LearnMore'
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isSafetyTipsModalOpen && (
|
||||
<SafetyTipsModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setIsSafetyTipsModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -16,6 +16,7 @@ export type PropsType = {
|
|||
| 'audio-incoming'
|
||||
| 'audio-missed'
|
||||
| 'audio-outgoing'
|
||||
| 'block'
|
||||
| 'group'
|
||||
| 'group-access'
|
||||
| 'group-add'
|
||||
|
@ -30,6 +31,7 @@ export type PropsType = {
|
|||
| 'phone'
|
||||
| 'profile'
|
||||
| 'safety-number'
|
||||
| 'spam'
|
||||
| 'session-refresh'
|
||||
| 'thread'
|
||||
| 'timer'
|
||||
|
|
|
@ -335,6 +335,10 @@ const actions = () => ({
|
|||
viewStory: action('viewStory'),
|
||||
|
||||
onReplyToMessage: action('onReplyToMessage'),
|
||||
|
||||
onOpenMessageRequestActionsConfirmation: action(
|
||||
'onOpenMessageRequestActionsConfirmation'
|
||||
),
|
||||
});
|
||||
|
||||
const renderItem = ({
|
||||
|
@ -350,6 +354,7 @@ const renderItem = ({
|
|||
getPreferredBadge={() => undefined}
|
||||
id=""
|
||||
isTargeted={false}
|
||||
isBlocked={false}
|
||||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
isNextItemCallingNotification={false}
|
||||
|
@ -442,6 +447,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
getTimestampForMessage: Date.now,
|
||||
haveNewest: overrideProps.haveNewest ?? false,
|
||||
haveOldest: overrideProps.haveOldest ?? false,
|
||||
isBlocked: false,
|
||||
isConversationSelected: true,
|
||||
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
|
||||
items: overrideProps.items ?? Object.keys(items),
|
||||
|
|
|
@ -81,6 +81,7 @@ export type PropsDataType = {
|
|||
|
||||
type PropsHousekeepingType = {
|
||||
id: string;
|
||||
isBlocked: boolean;
|
||||
isConversationSelected: boolean;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
isIncomingMessageRequest: boolean;
|
||||
|
@ -121,6 +122,7 @@ type PropsHousekeepingType = {
|
|||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
conversationId: string;
|
||||
isBlocked: boolean;
|
||||
isOldestTimelineItem: boolean;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
|
@ -786,6 +788,7 @@ export class Timeline extends React.Component<
|
|||
i18n,
|
||||
id,
|
||||
invitedContactsForNewlyCreatedGroup,
|
||||
isBlocked,
|
||||
isConversationSelected,
|
||||
isGroupV1AndDisabled,
|
||||
items,
|
||||
|
@ -928,6 +931,7 @@ export class Timeline extends React.Component<
|
|||
containerElementRef: this.containerRef,
|
||||
containerWidthBreakpoint: widthBreakpoint,
|
||||
conversationId: id,
|
||||
isBlocked,
|
||||
isOldestTimelineItem: haveOldest && itemIndex === 0,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
|
|
|
@ -59,6 +59,7 @@ const getDefaultProps = () => ({
|
|||
id: 'asdf',
|
||||
isNextItemCallingNotification: false,
|
||||
isTargeted: false,
|
||||
isBlocked: false,
|
||||
interactionMode: 'keyboard' as const,
|
||||
theme: ThemeType.light,
|
||||
platform: 'darwin',
|
||||
|
@ -118,6 +119,9 @@ const getDefaultProps = () => ({
|
|||
viewStory: action('viewStory'),
|
||||
|
||||
onReplyToMessage: action('onReplyToMessage'),
|
||||
onOpenMessageRequestActionsConfirmation: action(
|
||||
'onOpenMessageRequestActionsConfirmation'
|
||||
),
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -56,6 +56,11 @@ import type { PropsDataType as PhoneNumberDiscoveryNotificationPropsType } from
|
|||
import { PhoneNumberDiscoveryNotification } from './PhoneNumberDiscoveryNotification';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import { TimelineMessage } from './TimelineMessage';
|
||||
import {
|
||||
MessageRequestResponseNotification,
|
||||
type MessageRequestResponseNotificationData,
|
||||
} from './MessageRequestResponseNotification';
|
||||
import type { MessageRequestState } from './MessageRequestActionsConfirmation';
|
||||
|
||||
type CallHistoryType = {
|
||||
type: 'callHistory';
|
||||
|
@ -137,6 +142,10 @@ type PaymentEventType = {
|
|||
type: 'paymentEvent';
|
||||
data: Omit<PaymentEventNotificationPropsType, 'i18n'>;
|
||||
};
|
||||
type MessageRequestResponseNotificationType = {
|
||||
type: 'messageRequestResponse';
|
||||
data: MessageRequestResponseNotificationData;
|
||||
};
|
||||
|
||||
export type TimelineItemType = (
|
||||
| CallHistoryType
|
||||
|
@ -159,6 +168,7 @@ export type TimelineItemType = (
|
|||
| UnsupportedMessageType
|
||||
| VerificationNotificationType
|
||||
| PaymentEventType
|
||||
| MessageRequestResponseNotificationType
|
||||
) & { timestamp: number };
|
||||
|
||||
type PropsLocalType = {
|
||||
|
@ -166,10 +176,12 @@ type PropsLocalType = {
|
|||
conversationId: string;
|
||||
item?: TimelineItemType;
|
||||
id: string;
|
||||
isBlocked: boolean;
|
||||
isNextItemCallingNotification: boolean;
|
||||
isTargeted: boolean;
|
||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||
shouldRenderDateHeader: boolean;
|
||||
onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void;
|
||||
platform: string;
|
||||
renderContact: SmartContactRendererType<JSX.Element>;
|
||||
renderUniversalTimerNotification: () => JSX.Element;
|
||||
|
@ -203,9 +215,11 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
getPreferredBadge,
|
||||
i18n,
|
||||
id,
|
||||
isBlocked,
|
||||
isNextItemCallingNotification,
|
||||
isTargeted,
|
||||
item,
|
||||
onOpenMessageRequestActionsConfirmation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
platform,
|
||||
|
@ -379,6 +393,17 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'messageRequestResponse') {
|
||||
notification = (
|
||||
<MessageRequestResponseNotification
|
||||
{...item.data}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
onOpenMessageRequestActionsConfirmation={
|
||||
onOpenMessageRequestActionsConfirmation
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Weird, yes, but the idea is to get a compile error when we aren't comprehensive
|
||||
// with our if/else checks above, but also log out the type we don't understand
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
import { assertDev } from '../../util/assert';
|
||||
import { isDirectConversation } from '../../util/whatTypeOfConversation';
|
||||
import * as log from '../../logging/log';
|
||||
import type { ConversationAttributesType } from '../../model-types.d';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import type { reportSpamJobQueue } from '../reportSpamJobQueue';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
export async function addReportSpamJob({
|
||||
conversation,
|
||||
|
@ -14,10 +14,7 @@ export async function addReportSpamJob({
|
|||
jobQueue,
|
||||
}: Readonly<{
|
||||
conversation: Readonly<
|
||||
Pick<
|
||||
ConversationAttributesType,
|
||||
'id' | 'type' | 'serviceId' | 'reportingToken'
|
||||
>
|
||||
Pick<ConversationType, 'id' | 'type' | 'serviceId' | 'reportingToken'>
|
||||
>;
|
||||
getMessageServerGuidsForSpam: (
|
||||
conversationId: string
|
||||
|
|
6
ts/model-types.d.ts
vendored
|
@ -32,6 +32,7 @@ import type { AnyPaymentEvent } from './types/Payment';
|
|||
|
||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||
import MemberRoleEnum = Proto.Member.Role;
|
||||
import type { MessageRequestResponseEvent } from './types/MessageRequestResponseEvent';
|
||||
|
||||
export type LastMessageStatus =
|
||||
| 'paused'
|
||||
|
@ -156,6 +157,7 @@ export type MessageAttributesType = {
|
|||
logger?: unknown;
|
||||
message?: unknown;
|
||||
messageTimer?: unknown;
|
||||
messageRequestResponseEvent?: MessageRequestResponseEvent;
|
||||
profileChange?: ProfileNameChangeType;
|
||||
payment?: AnyPaymentEvent;
|
||||
quote?: QuotedMessageType;
|
||||
|
@ -192,7 +194,8 @@ export type MessageAttributesType = {
|
|||
| 'universal-timer-notification'
|
||||
| 'contact-removed-notification'
|
||||
| 'title-transition-notification'
|
||||
| 'verified-change';
|
||||
| 'verified-change'
|
||||
| 'message-request-response-event';
|
||||
body?: string;
|
||||
attachments?: Array<AttachmentType>;
|
||||
preview?: Array<LinkPreviewType>;
|
||||
|
@ -359,6 +362,7 @@ export type ConversationAttributesType = {
|
|||
draftEditMessage?: DraftEditMessageType;
|
||||
hasPostedStory?: boolean;
|
||||
isArchived?: boolean;
|
||||
isReported?: boolean;
|
||||
name?: string;
|
||||
systemGivenName?: string;
|
||||
systemFamilyName?: string;
|
||||
|
|
|
@ -164,6 +164,7 @@ import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
|||
import OS from '../util/os/osMain';
|
||||
import { getMessageAuthorText } from '../util/getMessageAuthorText';
|
||||
import { downscaleOutgoingAttachment } from '../util/attachments';
|
||||
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -2115,8 +2116,38 @@ export class ConversationModel extends window.Backbone
|
|||
} while (messages.length > 0);
|
||||
}
|
||||
|
||||
async addMessageRequestResponseEventMessage(
|
||||
event: MessageRequestResponseEvent
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
id: generateGuid(),
|
||||
conversationId: this.id,
|
||||
type: 'message-request-response-event',
|
||||
sent_at: now,
|
||||
received_at: incrementMessageCounter(),
|
||||
received_at_ms: now,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.NotApplicable,
|
||||
timestamp: now,
|
||||
messageRequestResponseEvent: event,
|
||||
};
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
forceSave: true,
|
||||
});
|
||||
const model = new window.Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
});
|
||||
window.MessageCache.toMessageAttributes(model.attributes);
|
||||
this.trigger('newmessage', model);
|
||||
drop(this.updateLastMessage());
|
||||
}
|
||||
|
||||
async applyMessageRequestResponse(
|
||||
response: number,
|
||||
response: Proto.SyncMessage.MessageRequestResponse.Type,
|
||||
{ fromSync = false, viaStorageServiceSync = false, shouldSave = true } = {}
|
||||
): Promise<void> {
|
||||
try {
|
||||
|
@ -2127,11 +2158,84 @@ export class ConversationModel extends window.Backbone
|
|||
const didResponseChange = response !== currentMessageRequestState;
|
||||
const wasPreviouslyAccepted = this.getAccepted();
|
||||
|
||||
if (didResponseChange) {
|
||||
if (response === messageRequestEnum.ACCEPT) {
|
||||
drop(
|
||||
this.addMessageRequestResponseEventMessage(
|
||||
MessageRequestResponseEvent.ACCEPT
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
response === messageRequestEnum.BLOCK ||
|
||||
response === messageRequestEnum.BLOCK_AND_SPAM ||
|
||||
response === messageRequestEnum.BLOCK_AND_DELETE
|
||||
) {
|
||||
drop(
|
||||
this.addMessageRequestResponseEventMessage(
|
||||
MessageRequestResponseEvent.BLOCK
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
response === messageRequestEnum.SPAM ||
|
||||
response === messageRequestEnum.BLOCK_AND_SPAM
|
||||
) {
|
||||
drop(
|
||||
this.addMessageRequestResponseEventMessage(
|
||||
MessageRequestResponseEvent.SPAM
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply message request response locally
|
||||
this.set({
|
||||
messageRequestResponseType: response,
|
||||
});
|
||||
|
||||
const rejectConversation = async ({
|
||||
isBlock = false,
|
||||
isDelete = false,
|
||||
isSpam = false,
|
||||
}: {
|
||||
isBlock?: boolean;
|
||||
isDelete?: boolean;
|
||||
isSpam?: boolean;
|
||||
}) => {
|
||||
if (isBlock) {
|
||||
this.block({ viaStorageServiceSync });
|
||||
}
|
||||
|
||||
if (isBlock || isDelete) {
|
||||
this.disableProfileSharing({ viaStorageServiceSync });
|
||||
}
|
||||
|
||||
if (isDelete) {
|
||||
await this.destroyMessages();
|
||||
void this.updateLastMessage();
|
||||
}
|
||||
|
||||
if (isBlock || isDelete) {
|
||||
if (isLocalAction) {
|
||||
window.reduxActions.conversations.onConversationClosed(
|
||||
this.id,
|
||||
isBlock
|
||||
? 'blocked from message request'
|
||||
: 'deleted from message request'
|
||||
);
|
||||
|
||||
if (isGroupV2(this.attributes)) {
|
||||
await this.leaveGroupV2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSpam) {
|
||||
this.set({ isReported: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (response === messageRequestEnum.ACCEPT) {
|
||||
this.unblock({ viaStorageServiceSync });
|
||||
if (!viaStorageServiceSync) {
|
||||
|
@ -2188,53 +2292,15 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
}
|
||||
} else if (response === messageRequestEnum.BLOCK) {
|
||||
// Block locally, other devices should block upon receiving the sync message
|
||||
this.block({ viaStorageServiceSync });
|
||||
this.disableProfileSharing({ viaStorageServiceSync });
|
||||
|
||||
if (isLocalAction) {
|
||||
if (isGroupV2(this.attributes)) {
|
||||
await this.leaveGroupV2();
|
||||
}
|
||||
}
|
||||
await rejectConversation({ isBlock: true });
|
||||
} else if (response === messageRequestEnum.DELETE) {
|
||||
this.disableProfileSharing({ viaStorageServiceSync });
|
||||
|
||||
// Delete messages locally, other devices should delete upon receiving
|
||||
// the sync message
|
||||
await this.destroyMessages();
|
||||
void this.updateLastMessage();
|
||||
|
||||
if (isLocalAction) {
|
||||
window.reduxActions.conversations.onConversationClosed(
|
||||
this.id,
|
||||
'deleted from message request'
|
||||
);
|
||||
|
||||
if (isGroupV2(this.attributes)) {
|
||||
await this.leaveGroupV2();
|
||||
}
|
||||
}
|
||||
await rejectConversation({ isDelete: true });
|
||||
} else if (response === messageRequestEnum.BLOCK_AND_DELETE) {
|
||||
// Block locally, other devices should block upon receiving the sync message
|
||||
this.block({ viaStorageServiceSync });
|
||||
this.disableProfileSharing({ viaStorageServiceSync });
|
||||
|
||||
// Delete messages locally, other devices should delete upon receiving
|
||||
// the sync message
|
||||
await this.destroyMessages();
|
||||
void this.updateLastMessage();
|
||||
|
||||
if (isLocalAction) {
|
||||
window.reduxActions.conversations.onConversationClosed(
|
||||
this.id,
|
||||
'blocked and deleted from message request'
|
||||
);
|
||||
|
||||
if (isGroupV2(this.attributes)) {
|
||||
await this.leaveGroupV2();
|
||||
}
|
||||
}
|
||||
await rejectConversation({ isBlock: true, isDelete: true });
|
||||
} else if (response === messageRequestEnum.SPAM) {
|
||||
await rejectConversation({ isSpam: true });
|
||||
} else if (response === messageRequestEnum.BLOCK_AND_SPAM) {
|
||||
await rejectConversation({ isBlock: true, isSpam: true });
|
||||
}
|
||||
} finally {
|
||||
if (shouldSave) {
|
||||
|
@ -2489,40 +2555,6 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
}
|
||||
|
||||
async syncMessageRequestResponse(
|
||||
response: number,
|
||||
{ shouldSave = true } = {}
|
||||
): Promise<void> {
|
||||
// In GroupsV2, this may modify the server. We only want to continue if those
|
||||
// server updates were successful.
|
||||
await this.applyMessageRequestResponse(response, { shouldSave });
|
||||
|
||||
const groupId = this.getGroupIdBuffer();
|
||||
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
log.warn(
|
||||
'syncMessageRequestResponse: We are primary device; not sending message request sync'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await singleProtoJobQueue.add(
|
||||
MessageSender.getMessageRequestResponseSync({
|
||||
threadE164: this.get('e164'),
|
||||
threadAci: this.getAci(),
|
||||
groupId,
|
||||
type: response,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'syncMessageRequestResponse: Failed to queue sync message',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async safeGetVerified(): Promise<number> {
|
||||
const serviceId = this.getServiceId();
|
||||
if (!serviceId) {
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
|
||||
// State
|
||||
|
||||
export type AudioPlayerStateType = ReadonlyDeep<{
|
||||
export type AudioRecorderStateType = ReadonlyDeep<{
|
||||
recordingState: RecordingState;
|
||||
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
||||
}>;
|
||||
|
@ -211,16 +211,16 @@ function errorRecording(
|
|||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): AudioPlayerStateType {
|
||||
export function getEmptyState(): AudioRecorderStateType {
|
||||
return {
|
||||
recordingState: RecordingState.Idle,
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<AudioPlayerStateType> = getEmptyState(),
|
||||
state: Readonly<AudioRecorderStateType> = getEmptyState(),
|
||||
action: Readonly<AudioPlayerActionType>
|
||||
): AudioPlayerStateType {
|
||||
): AudioRecorderStateType {
|
||||
if (action.type === START_RECORDING) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -179,6 +179,10 @@ import {
|
|||
import type { ChangeNavTabActionType } from './nav';
|
||||
import { CHANGE_NAV_TAB, NavTab, actions as navActions } from './nav';
|
||||
import { sortByMessageOrder } from '../../types/ForwardDraft';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||
import { getConversationIdForLogging } from '../../util/idForLogging';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../../textsecure/SendMessage';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -228,6 +232,10 @@ export type DraftPreviewType = ReadonlyDeep<{
|
|||
bodyRanges?: HydratedBodyRangesType;
|
||||
}>;
|
||||
|
||||
export type ConversationRemovalStage = ReadonlyDeep<
|
||||
'justNotification' | 'messageRequest'
|
||||
>;
|
||||
|
||||
export type ConversationType = ReadonlyDeep<
|
||||
{
|
||||
id: string;
|
||||
|
@ -265,7 +273,9 @@ export type ConversationType = ReadonlyDeep<
|
|||
hideStory?: boolean;
|
||||
isArchived?: boolean;
|
||||
isBlocked?: boolean;
|
||||
removalStage?: 'justNotification' | 'messageRequest';
|
||||
isReported?: boolean;
|
||||
reportingToken?: string;
|
||||
removalStage?: ConversationRemovalStage;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
isPinned?: boolean;
|
||||
isUntrusted?: boolean;
|
||||
|
@ -1026,6 +1036,7 @@ export const actions = {
|
|||
acknowledgeGroupMemberNameCollisions,
|
||||
addMembersToGroup,
|
||||
approvePendingMembershipFromGroupV2,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
blockGroupLinkRequests,
|
||||
|
@ -3243,68 +3254,195 @@ function revokePendingMembershipsFromGroupV2(
|
|||
};
|
||||
}
|
||||
|
||||
async function syncMessageRequestResponse(
|
||||
conversationData: ConversationType,
|
||||
response: Proto.SyncMessage.MessageRequestResponse.Type,
|
||||
{ shouldSave = true } = {}
|
||||
): Promise<void> {
|
||||
const conversation = window.ConversationController.get(conversationData.id);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
`syncMessageRequestResponse: No conversation found for conversation ${conversationData.id}`
|
||||
);
|
||||
}
|
||||
|
||||
// In GroupsV2, this may modify the server. We only want to continue if those
|
||||
// server updates were successful.
|
||||
await conversation.applyMessageRequestResponse(response, { shouldSave });
|
||||
|
||||
const groupId = conversation.getGroupIdBuffer();
|
||||
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
log.warn(
|
||||
'syncMessageRequestResponse: We are primary device; not sending message request sync'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await singleProtoJobQueue.add(
|
||||
MessageSender.getMessageRequestResponseSync({
|
||||
threadE164: conversation.get('e164'),
|
||||
threadAci: conversation.getAci(),
|
||||
groupId,
|
||||
type: response,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'syncMessageRequestResponse: Failed to queue sync message',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getConversationForReportSpam(
|
||||
conversation: ConversationType
|
||||
): ConversationType | null {
|
||||
if (conversation.type === 'group') {
|
||||
const addedBy = getAddedByForOurPendingInvitation(conversation);
|
||||
if (addedBy == null) {
|
||||
log.error(
|
||||
`getConversationForReportSpam: No addedBy found for ${conversation.id}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return addedBy;
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
function reportSpam(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const conversationSelector = getConversationSelector(getState());
|
||||
const conversationOrGroup = conversationSelector(conversationId);
|
||||
if (!conversationOrGroup) {
|
||||
log.error(
|
||||
`reportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = getConversationForReportSpam(conversationOrGroup);
|
||||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
const idForLogging = getConversationIdForLogging(conversation);
|
||||
|
||||
drop(
|
||||
longRunningTaskWrapper({
|
||||
name: 'reportSpam',
|
||||
idForLogging,
|
||||
task: async () => {
|
||||
await Promise.all([
|
||||
syncMessageRequestResponse(conversation, messageRequestEnum.SPAM),
|
||||
addReportSpamJob({
|
||||
conversation,
|
||||
getMessageServerGuidsForSpam:
|
||||
window.Signal.Data.getMessageServerGuidsForSpam,
|
||||
jobQueue: reportSpamJobQueue,
|
||||
}),
|
||||
]);
|
||||
|
||||
dispatch({
|
||||
type: SHOW_TOAST,
|
||||
payload: {
|
||||
toastType: ToastType.ReportedSpam,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function blockAndReportSpam(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async dispatch => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
return async (dispatch, getState) => {
|
||||
const conversationSelector = getConversationSelector(getState());
|
||||
const conversationOrGroup = conversationSelector(conversationId);
|
||||
if (!conversationOrGroup) {
|
||||
log.error(
|
||||
`blockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationForSpam =
|
||||
getConversationForReportSpam(conversationOrGroup);
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
const idForLogging = conversation.idForLogging();
|
||||
const idForLogging = getConversationIdForLogging(conversationOrGroup);
|
||||
|
||||
void longRunningTaskWrapper({
|
||||
name: 'blockAndReportSpam',
|
||||
idForLogging,
|
||||
task: async () => {
|
||||
await Promise.all([
|
||||
conversation.syncMessageRequestResponse(messageRequestEnum.BLOCK),
|
||||
addReportSpamJob({
|
||||
conversation: conversation.attributes,
|
||||
getMessageServerGuidsForSpam:
|
||||
window.Signal.Data.getMessageServerGuidsForSpam,
|
||||
jobQueue: reportSpamJobQueue,
|
||||
}),
|
||||
]);
|
||||
drop(
|
||||
longRunningTaskWrapper({
|
||||
name: 'blockAndReportSpam',
|
||||
idForLogging,
|
||||
task: async () => {
|
||||
await Promise.all([
|
||||
syncMessageRequestResponse(
|
||||
conversationOrGroup,
|
||||
messageRequestEnum.BLOCK_AND_SPAM
|
||||
),
|
||||
conversationForSpam != null &&
|
||||
addReportSpamJob({
|
||||
conversation: conversationForSpam,
|
||||
getMessageServerGuidsForSpam:
|
||||
window.Signal.Data.getMessageServerGuidsForSpam,
|
||||
jobQueue: reportSpamJobQueue,
|
||||
}),
|
||||
]);
|
||||
|
||||
dispatch({
|
||||
type: SHOW_TOAST,
|
||||
payload: {
|
||||
toastType: ToastType.ReportedSpamAndBlocked,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: SHOW_TOAST,
|
||||
payload: {
|
||||
toastType: ToastType.ReportedSpamAndBlocked,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function acceptConversation(conversationId: string): NoopActionType {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'acceptConversation: Expected a conversation to be found. Doing nothing'
|
||||
function acceptConversation(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const conversationSelector = getConversationSelector(getState());
|
||||
const conversationOrGroup = conversationSelector(conversationId);
|
||||
if (!conversationOrGroup) {
|
||||
throw new Error(
|
||||
'acceptConversation: Expected a conversation to be found. Doing nothing'
|
||||
);
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
const idForLogging = getConversationIdForLogging(conversationOrGroup);
|
||||
|
||||
drop(
|
||||
longRunningTaskWrapper({
|
||||
name: 'acceptConversation',
|
||||
idForLogging,
|
||||
task: async () => {
|
||||
await syncMessageRequestResponse(
|
||||
conversationOrGroup,
|
||||
messageRequestEnum.ACCEPT
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
void longRunningTaskWrapper({
|
||||
name: 'acceptConversation',
|
||||
idForLogging: conversation.idForLogging(),
|
||||
task: conversation.syncMessageRequestResponse.bind(
|
||||
conversation,
|
||||
messageRequestEnum.ACCEPT
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3329,53 +3467,74 @@ function removeConversation(conversationId: string): ShowToastActionType {
|
|||
};
|
||||
}
|
||||
|
||||
function blockConversation(conversationId: string): NoopActionType {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'blockConversation: Expected a conversation to be found. Doing nothing'
|
||||
function blockConversation(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const conversationSelector = getConversationSelector(getState());
|
||||
const conversation = conversationSelector(conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'blockConversation: Expected a conversation to be found. Doing nothing'
|
||||
);
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
const idForLogging = getConversationIdForLogging(conversation);
|
||||
|
||||
drop(
|
||||
longRunningTaskWrapper({
|
||||
name: 'blockConversation',
|
||||
idForLogging,
|
||||
task: async () => {
|
||||
await syncMessageRequestResponse(
|
||||
conversation,
|
||||
messageRequestEnum.BLOCK
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
void longRunningTaskWrapper({
|
||||
name: 'blockConversation',
|
||||
idForLogging: conversation.idForLogging(),
|
||||
task: conversation.syncMessageRequestResponse.bind(
|
||||
conversation,
|
||||
messageRequestEnum.BLOCK
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function deleteConversation(conversationId: string): NoopActionType {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'deleteConversation: Expected a conversation to be found. Doing nothing'
|
||||
function deleteConversation(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const conversationSelector = getConversationSelector(getState());
|
||||
const conversation = conversationSelector(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'deleteConversation: Expected a conversation to be found. Doing nothing'
|
||||
);
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
const idForLogging = getConversationIdForLogging(conversation);
|
||||
|
||||
drop(
|
||||
longRunningTaskWrapper({
|
||||
name: 'deleteConversation',
|
||||
idForLogging,
|
||||
task: async () => {
|
||||
await syncMessageRequestResponse(
|
||||
conversation,
|
||||
messageRequestEnum.DELETE
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
void longRunningTaskWrapper({
|
||||
name: 'deleteConversation',
|
||||
idForLogging: conversation.idForLogging(),
|
||||
task: conversation.syncMessageRequestResponse.bind(
|
||||
conversation,
|
||||
messageRequestEnum.DELETE
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,9 @@ export const actions = {
|
|||
useEmoji,
|
||||
};
|
||||
|
||||
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
useBoundActions(actions);
|
||||
export const useEmojisActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function onUseEmoji({
|
||||
shortName,
|
||||
|
|
|
@ -42,6 +42,7 @@ import { SHOW_TOAST } from './toast';
|
|||
import type { ShowToastActionType } from './toast';
|
||||
import { isDownloaded } from '../../types/Attachment';
|
||||
import type { ButtonVariant } from '../../components/Button';
|
||||
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -58,6 +59,10 @@ export type ForwardMessagesPropsType = ReadonlyDeep<{
|
|||
messages: Array<ForwardMessagePropsType>;
|
||||
onForward?: () => void;
|
||||
}>;
|
||||
export type MessageRequestActionsConfirmationPropsType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
state: MessageRequestState;
|
||||
}>;
|
||||
export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
|
||||
promiseUuid: SingleServePromise.SingleServePromiseIdString;
|
||||
source?: SafetyNumberChangeSource;
|
||||
|
@ -101,6 +106,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
isSignalConnectionsVisible: boolean;
|
||||
isStoriesSettingsVisible: boolean;
|
||||
isWhatsNewVisible: boolean;
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
profileEditorHasError: boolean;
|
||||
profileEditorInitialEditState: ProfileEditorEditState | undefined;
|
||||
|
@ -144,6 +150,8 @@ const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW';
|
|||
const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
|
||||
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
|
||||
export const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL';
|
||||
const TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION =
|
||||
'globalModals/TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION';
|
||||
const SHOW_FORMATTING_WARNING_MODAL =
|
||||
'globalModals/SHOW_FORMATTING_WARNING_MODAL';
|
||||
const SHOW_SEND_EDIT_WARNING_MODAL =
|
||||
|
@ -316,6 +324,11 @@ export type ShowErrorModalActionType = ReadonlyDeep<{
|
|||
};
|
||||
}>;
|
||||
|
||||
type ToggleMessageRequestActionsConfirmationActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION;
|
||||
payload: MessageRequestActionsConfirmationPropsType | null;
|
||||
}>;
|
||||
|
||||
type CloseShortcutGuideModalActionType = ReadonlyDeep<{
|
||||
type: typeof CLOSE_SHORTCUT_GUIDE_MODAL;
|
||||
}>;
|
||||
|
@ -373,6 +386,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ShowContactModalActionType
|
||||
| ShowEditHistoryModalActionType
|
||||
| ShowErrorModalActionType
|
||||
| ToggleMessageRequestActionsConfirmationActionType
|
||||
| ShowFormattingWarningModalActionType
|
||||
| ShowSendAnywayDialogActionType
|
||||
| ShowSendEditWarningModalActionType
|
||||
|
@ -414,6 +428,7 @@ export const actions = {
|
|||
showContactModal,
|
||||
showEditHistoryModal,
|
||||
showErrorModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
showFormattingWarningModal,
|
||||
showSendEditWarningModal,
|
||||
showGV2MigrationDialog,
|
||||
|
@ -750,6 +765,18 @@ function showErrorModal({
|
|||
};
|
||||
}
|
||||
|
||||
function toggleMessageRequestActionsConfirmation(
|
||||
payload: {
|
||||
conversationId: string;
|
||||
state: MessageRequestState;
|
||||
} | null
|
||||
): ToggleMessageRequestActionsConfirmationActionType {
|
||||
return {
|
||||
type: TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function closeShortcutGuideModal(): CloseShortcutGuideModalActionType {
|
||||
return {
|
||||
type: CLOSE_SHORTCUT_GUIDE_MODAL,
|
||||
|
@ -908,6 +935,7 @@ export function getEmptyState(): GlobalModalsStateType {
|
|||
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
||||
profileEditorHasError: false,
|
||||
profileEditorInitialEditState: undefined,
|
||||
messageRequestActionsConfirmationProps: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1132,6 +1160,13 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION) {
|
||||
return {
|
||||
...state,
|
||||
messageRequestActionsConfirmationProps: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === CLOSE_SHORTCUT_GUIDE_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -101,8 +101,9 @@ export const actions = {
|
|||
selectDraftEmojiToBeReplaced,
|
||||
};
|
||||
|
||||
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
useBoundActions(actions);
|
||||
export const usePreferredReactionsActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function cancelCustomizePreferredReactionsModal(): CancelCustomizePreferredReactionsModalActionType {
|
||||
return { type: CANCEL_CUSTOMIZE_PREFERRED_REACTIONS_MODAL };
|
||||
|
|
|
@ -22,6 +22,8 @@ import { ERASE_STORAGE_SERVICE } from './user';
|
|||
import type { EraseStorageServiceStateAction } from './user';
|
||||
|
||||
import type { NoopActionType } from './noop';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
||||
const { getRecentStickers, updateStickerLastUsed } = dataInterface;
|
||||
|
||||
|
@ -154,6 +156,10 @@ export const actions = {
|
|||
useSticker,
|
||||
};
|
||||
|
||||
export const useStickersActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function removeStickerPack(id: string): StickerPackRemovedAction {
|
||||
return {
|
||||
type: 'stickers/REMOVE_STICKER_PACK',
|
||||
|
|
23
ts/state/selectors/audioRecorder.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { createSelector } from 'reselect';
|
||||
import type { StateType } from '../reducer';
|
||||
import type { AudioRecorderStateType } from '../ducks/audioRecorder';
|
||||
|
||||
export function getAudioRecorder(state: StateType): AudioRecorderStateType {
|
||||
return state.audioRecorder;
|
||||
}
|
||||
|
||||
export const getErrorDialogAudioRecorderType = createSelector(
|
||||
getAudioRecorder,
|
||||
audioRecorder => {
|
||||
return audioRecorder.errorDialogAudioRecorderType;
|
||||
}
|
||||
);
|
||||
|
||||
export const getRecordingState = createSelector(
|
||||
getAudioRecorder,
|
||||
audioRecorder => {
|
||||
return audioRecorder.recordingState;
|
||||
}
|
||||
);
|
|
@ -228,3 +228,17 @@ export const getNavTabsCollapsed = createSelector(
|
|||
getItems,
|
||||
(state: ItemsStateType): boolean => Boolean(state.navTabsCollapsed ?? false)
|
||||
);
|
||||
|
||||
export const getShowStickersIntroduction = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): boolean => {
|
||||
return state.showStickersIntroduction ?? false;
|
||||
}
|
||||
);
|
||||
|
||||
export const getShowStickerPickerHint = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): boolean => {
|
||||
return state.showStickerPickerHint ?? false;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -140,6 +140,7 @@ import { CallMode } from '../../types/Calling';
|
|||
import { CallDirection } from '../../types/CallDisposition';
|
||||
import { getCallIdFromEra } from '../../util/callDisposition';
|
||||
import { LONG_MESSAGE } from '../../types/MIME';
|
||||
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification';
|
||||
|
||||
export { isIncoming, isOutgoing, isStory };
|
||||
|
||||
|
@ -971,6 +972,14 @@ export function getPropsForBubble(
|
|||
};
|
||||
}
|
||||
|
||||
if (isMessageRequestResponse(message)) {
|
||||
return {
|
||||
type: 'messageRequestResponse',
|
||||
data: getPropsForMessageRequestResponse(message),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
const data = getPropsForMessage(message, options);
|
||||
|
||||
return {
|
||||
|
@ -1461,6 +1470,24 @@ function getPropsForProfileChange(
|
|||
} as ProfileChangeNotificationPropsType;
|
||||
}
|
||||
|
||||
// Message Request Response Event
|
||||
|
||||
export function isMessageRequestResponse(
|
||||
message: MessageAttributesType
|
||||
): boolean {
|
||||
return message.type === 'message-request-response-event';
|
||||
}
|
||||
|
||||
function getPropsForMessageRequestResponse(
|
||||
message: MessageAttributesType
|
||||
): MessageRequestResponseNotificationData {
|
||||
const { messageRequestResponseEvent } = message;
|
||||
if (!messageRequestResponseEvent) {
|
||||
throw new Error('getPropsForMessageRequestResponse: event is missing!');
|
||||
}
|
||||
return { messageRequestResponseEvent };
|
||||
}
|
||||
|
||||
// Universal Timer Notification
|
||||
|
||||
// Note: smart, so props not generated here
|
||||
|
|
|
@ -1,35 +1,27 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import type { Props as ComponentPropsType } from '../../components/CompositionArea';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CompositionArea } from '../../components/CompositionArea';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useContactNameData } from '../../components/conversation/ContactName';
|
||||
import type {
|
||||
DraftBodyRanges,
|
||||
HydratedBodyRangesType,
|
||||
} from '../../types/BodyRange';
|
||||
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
||||
import { dropNull } from '../../util/dropNull';
|
||||
import { hydrateRanges } from '../../types/BodyRange';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||
import { imageToBlurHash } from '../../util/imageToBlurHash';
|
||||
|
||||
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import type { StateType } from '../reducer';
|
||||
import {
|
||||
getErrorDialogAudioRecorderType,
|
||||
getRecordingState,
|
||||
} from '../selectors/audioRecorder';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import {
|
||||
getIntl,
|
||||
getPlatform,
|
||||
getTheme,
|
||||
getUserConversationId,
|
||||
} from '../selectors/user';
|
||||
import {
|
||||
getDefaultConversationColor,
|
||||
getEmojiSkinTone,
|
||||
getTextFormattingEnabled,
|
||||
} from '../selectors/items';
|
||||
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getGroupAdminsSelector,
|
||||
|
@ -38,71 +30,88 @@ import {
|
|||
getSelectedMessageIds,
|
||||
isMissingRequiredProfileSharing,
|
||||
} from '../selectors/conversations';
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import {
|
||||
getDefaultConversationColor,
|
||||
getEmojiSkinTone,
|
||||
getShowStickerPickerHint,
|
||||
getShowStickersIntroduction,
|
||||
getTextFormattingEnabled,
|
||||
} from '../selectors/items';
|
||||
import { getPropsForQuote } from '../selectors/message';
|
||||
import {
|
||||
getBlessedStickerPacks,
|
||||
getInstalledStickerPacks,
|
||||
getKnownStickerPacks,
|
||||
getReceivedStickerPacks,
|
||||
getRecentlyInstalledStickerPack,
|
||||
getRecentStickers,
|
||||
getRecentlyInstalledStickerPack,
|
||||
} from '../selectors/stickers';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
|
||||
import {
|
||||
getIntl,
|
||||
getPlatform,
|
||||
getTheme,
|
||||
getUserConversationId,
|
||||
} from '../selectors/user';
|
||||
import type { SmartCompositionRecordingProps } from './CompositionRecording';
|
||||
import { SmartCompositionRecording } from './CompositionRecording';
|
||||
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft';
|
||||
import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft';
|
||||
import { hydrateRanges } from '../../types/BodyRange';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useAudioRecorderActions } from '../ducks/audioRecorder';
|
||||
import { useEmojisActions } from '../ducks/emojis';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useStickersActions } from '../ducks/stickers';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
};
|
||||
function renderSmartCompositionRecording(
|
||||
recProps: SmartCompositionRecordingProps
|
||||
) {
|
||||
return <SmartCompositionRecording {...recProps} />;
|
||||
}
|
||||
|
||||
export type CompositionAreaPropsType = ExternalProps & ComponentPropsType;
|
||||
function renderSmartCompositionRecordingDraft(
|
||||
draftProps: SmartCompositionRecordingDraftProps
|
||||
) {
|
||||
return <SmartCompositionRecordingDraft {...draftProps} />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
const platform = getPlatform(state);
|
||||
|
||||
const shouldHidePopovers = getHasPanelOpen(state);
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
export function SmartCompositionArea({ id }: { id: string }): JSX.Element {
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const conversation = conversationSelector(id);
|
||||
if (!conversation) {
|
||||
throw new Error(`Conversation id ${id} not found!`);
|
||||
}
|
||||
strictAssert(conversation, `Conversation id ${id} not found!`);
|
||||
|
||||
const {
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
draftEditMessage,
|
||||
draftText,
|
||||
draftBodyRanges,
|
||||
} = conversation;
|
||||
|
||||
const receivedPacks = getReceivedStickerPacks(state);
|
||||
const installedPacks = getInstalledStickerPacks(state);
|
||||
const blessedPacks = getBlessedStickerPacks(state);
|
||||
const knownPacks = getKnownStickerPacks(state);
|
||||
|
||||
const installedPack = getRecentlyInstalledStickerPack(state);
|
||||
|
||||
const recentStickers = getRecentStickers(state);
|
||||
const showIntroduction = get(
|
||||
state.items,
|
||||
['showStickersIntroduction'],
|
||||
false
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const skinTone = useSelector(getEmojiSkinTone);
|
||||
const recentEmojis = useSelector(selectRecentEmojis);
|
||||
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
||||
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
|
||||
const lastEditableMessageId = useSelector(getLastEditableMessageId);
|
||||
const receivedPacks = useSelector(getReceivedStickerPacks);
|
||||
const installedPacks = useSelector(getInstalledStickerPacks);
|
||||
const blessedPacks = useSelector(getBlessedStickerPacks);
|
||||
const knownPacks = useSelector(getKnownStickerPacks);
|
||||
const platform = useSelector(getPlatform);
|
||||
const shouldHidePopovers = useSelector(getHasPanelOpen);
|
||||
const installedPack = useSelector(getRecentlyInstalledStickerPack);
|
||||
const recentStickers = useSelector(getRecentStickers);
|
||||
const showStickersIntroduction = useSelector(getShowStickersIntroduction);
|
||||
const showStickerPickerHint = useSelector(getShowStickerPickerHint);
|
||||
const recordingState = useSelector(getRecordingState);
|
||||
const errorDialogAudioRecorderType = useSelector(
|
||||
getErrorDialogAudioRecorderType
|
||||
);
|
||||
const showPickerHint = Boolean(
|
||||
get(state.items, ['showStickerPickerHint'], false) &&
|
||||
receivedPacks.length > 0
|
||||
const getGroupAdmins = useSelector(getGroupAdminsSelector);
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const composerStateForConversationIdSelector = useSelector(
|
||||
getComposerStateForConversationIdSelector
|
||||
);
|
||||
|
||||
const composerStateForConversationIdSelector =
|
||||
getComposerStateForConversationIdSelector(state);
|
||||
|
||||
const composerState = composerStateForConversationIdSelector(id);
|
||||
const { announcementsOnly, areWeAdmin, draftEditMessage, draftBodyRanges } =
|
||||
conversation;
|
||||
const {
|
||||
attachments: draftAttachments,
|
||||
focusCounter,
|
||||
|
@ -114,6 +123,34 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
shouldSendHighQualityAttachments,
|
||||
} = composerState;
|
||||
|
||||
const groupAdmins = useMemo(() => {
|
||||
return getGroupAdmins(id);
|
||||
}, [getGroupAdmins, id]);
|
||||
|
||||
const addedBy = useMemo(() => {
|
||||
if (conversation.type === 'group') {
|
||||
return getAddedByForOurPendingInvitation(conversation);
|
||||
}
|
||||
return null;
|
||||
}, [conversation]);
|
||||
|
||||
const conversationName = useContactNameData(conversation);
|
||||
strictAssert(conversationName, 'conversationName is required');
|
||||
const addedByName = useContactNameData(addedBy);
|
||||
|
||||
const hydratedDraftBodyRanges = useMemo(() => {
|
||||
return hydrateRanges(draftBodyRanges, conversationSelector);
|
||||
}, [conversationSelector, draftBodyRanges]);
|
||||
|
||||
const convertDraftBodyRangesIntoHydrated = useCallback(
|
||||
(
|
||||
bodyRanges: DraftBodyRanges | undefined
|
||||
): HydratedBodyRangesType | undefined => {
|
||||
return hydrateRanges(bodyRanges, conversationSelector);
|
||||
},
|
||||
[conversationSelector]
|
||||
);
|
||||
|
||||
let { quotedMessage } = composerState;
|
||||
if (!quotedMessage && draftEditMessage?.quote) {
|
||||
quotedMessage = {
|
||||
|
@ -122,117 +159,189 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
};
|
||||
}
|
||||
|
||||
const recentEmojis = selectRecentEmojis(state);
|
||||
|
||||
const selectedMessageIds = getSelectedMessageIds(state);
|
||||
|
||||
const isFormattingEnabled = getTextFormattingEnabled(state);
|
||||
|
||||
const lastEditableMessageId = getLastEditableMessageId(state);
|
||||
|
||||
const convertDraftBodyRangesIntoHydrated = (
|
||||
bodyRanges: DraftBodyRanges | undefined
|
||||
): HydratedBodyRangesType | undefined => {
|
||||
return hydrateRanges(bodyRanges, conversationSelector);
|
||||
};
|
||||
|
||||
return {
|
||||
// Base
|
||||
conversationId: id,
|
||||
draftEditMessage,
|
||||
focusCounter,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
isDisabled,
|
||||
isFormattingEnabled,
|
||||
lastEditableMessageId,
|
||||
messageCompositionId,
|
||||
platform,
|
||||
sendCounter,
|
||||
shouldHidePopovers,
|
||||
theme: getTheme(state),
|
||||
convertDraftBodyRangesIntoHydrated,
|
||||
|
||||
// AudioCapture
|
||||
errorDialogAudioRecorderType:
|
||||
state.audioRecorder.errorDialogAudioRecorderType,
|
||||
recordingState: state.audioRecorder.recordingState,
|
||||
// AttachmentsList
|
||||
draftAttachments,
|
||||
// MediaEditor
|
||||
imageToBlurHash,
|
||||
// MediaQualitySelector
|
||||
shouldSendHighQualityAttachments:
|
||||
shouldSendHighQualityAttachments !== undefined
|
||||
? shouldSendHighQualityAttachments
|
||||
: window.storage.get('sent-media-quality') === 'high',
|
||||
// StagedLinkPreview
|
||||
linkPreviewLoading,
|
||||
linkPreviewResult,
|
||||
// Quote
|
||||
quotedMessageId: quotedMessage?.quote?.messageId,
|
||||
quotedMessageProps: quotedMessage
|
||||
const quotedMessageProps = useSelector((state: StateType) => {
|
||||
return quotedMessage
|
||||
? getPropsForQuote(quotedMessage, {
|
||||
conversationSelector,
|
||||
ourConversationId: getUserConversationId(state),
|
||||
defaultConversationColor: getDefaultConversationColor(state),
|
||||
})
|
||||
: undefined,
|
||||
quotedMessageAuthorAci: quotedMessage?.quote?.authorAci,
|
||||
quotedMessageSentAt: quotedMessage?.quote?.id,
|
||||
// Emojis
|
||||
recentEmojis,
|
||||
skinTone: getEmojiSkinTone(state),
|
||||
// Stickers
|
||||
receivedPacks,
|
||||
installedPack,
|
||||
blessedPacks,
|
||||
knownPacks,
|
||||
installedPacks,
|
||||
recentStickers,
|
||||
showIntroduction,
|
||||
showPickerHint,
|
||||
// Message Requests
|
||||
...conversation,
|
||||
conversationType: conversation.type,
|
||||
isSMSOnly: Boolean(isConversationSMSOnly(conversation)),
|
||||
isSignalConversation: isSignalConversation(conversation),
|
||||
isFetchingUUID: conversation.isFetchingUUID,
|
||||
isMissingMandatoryProfileSharing:
|
||||
isMissingRequiredProfileSharing(conversation),
|
||||
// Groups
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
groupAdmins: getGroupAdminsSelector(state)(conversation.id),
|
||||
: undefined;
|
||||
});
|
||||
|
||||
draftText: dropNull(draftText),
|
||||
draftBodyRanges: hydrateRanges(draftBodyRanges, conversationSelector),
|
||||
renderSmartCompositionRecording: (
|
||||
recProps: SmartCompositionRecordingProps
|
||||
) => {
|
||||
return <SmartCompositionRecording {...recProps} />;
|
||||
},
|
||||
renderSmartCompositionRecordingDraft: (
|
||||
draftProps: SmartCompositionRecordingDraftProps
|
||||
) => {
|
||||
return <SmartCompositionRecordingDraft {...draftProps} />;
|
||||
const { putItem, removeItem } = useItemsActions();
|
||||
|
||||
const onSetSkinTone = useCallback(
|
||||
(tone: number) => {
|
||||
putItem('skinTone', tone);
|
||||
},
|
||||
[putItem]
|
||||
);
|
||||
|
||||
// Select Mode
|
||||
selectedMessageIds,
|
||||
};
|
||||
};
|
||||
const clearShowIntroduction = useCallback(() => {
|
||||
removeItem('showStickersIntroduction');
|
||||
}, [removeItem]);
|
||||
|
||||
const dispatchPropsMap = {
|
||||
...mapDispatchToProps,
|
||||
onSetSkinTone: (tone: number) => mapDispatchToProps.putItem('skinTone', tone),
|
||||
clearShowIntroduction: () =>
|
||||
mapDispatchToProps.removeItem('showStickersIntroduction'),
|
||||
clearShowPickerHint: () =>
|
||||
mapDispatchToProps.removeItem('showStickerPickerHint'),
|
||||
onPickEmoji: mapDispatchToProps.onUseEmoji,
|
||||
};
|
||||
const clearShowPickerHint = useCallback(() => {
|
||||
removeItem('showStickerPickerHint');
|
||||
}, [removeItem]);
|
||||
|
||||
const smart = connect(mapStateToProps, dispatchPropsMap);
|
||||
const {
|
||||
onTextTooLong,
|
||||
onCloseLinkPreview,
|
||||
addAttachment,
|
||||
removeAttachment,
|
||||
onClearAttachments,
|
||||
processAttachments,
|
||||
setMediaQualitySetting,
|
||||
setQuoteByMessageId,
|
||||
cancelJoinRequest,
|
||||
sendStickerMessage,
|
||||
sendEditedMessage,
|
||||
sendMultiMediaMessage,
|
||||
setComposerFocus,
|
||||
} = useComposerActions();
|
||||
const {
|
||||
pushPanelForConversation,
|
||||
discardEditMessage,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
toggleSelectMode,
|
||||
scrollToMessage,
|
||||
setMessageToEdit,
|
||||
showConversation,
|
||||
} = useConversationsActions();
|
||||
const { cancelRecording, completeRecording, startRecording, errorRecording } =
|
||||
useAudioRecorderActions();
|
||||
const { onUseEmoji } = useEmojisActions();
|
||||
const { showGV2MigrationDialog, toggleForwardMessagesModal } =
|
||||
useGlobalModalActions();
|
||||
const { clearInstalledStickerPack } = useStickersActions();
|
||||
const { showToast } = useToastActions();
|
||||
|
||||
export const SmartCompositionArea = smart(CompositionArea);
|
||||
return (
|
||||
<CompositionArea
|
||||
// Base
|
||||
conversationId={id}
|
||||
draftEditMessage={draftEditMessage ?? null}
|
||||
focusCounter={focusCounter}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
isDisabled={isDisabled}
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
lastEditableMessageId={lastEditableMessageId ?? null}
|
||||
messageCompositionId={messageCompositionId}
|
||||
platform={platform}
|
||||
sendCounter={sendCounter}
|
||||
shouldHidePopovers={shouldHidePopovers}
|
||||
theme={theme}
|
||||
convertDraftBodyRangesIntoHydrated={convertDraftBodyRangesIntoHydrated}
|
||||
onTextTooLong={onTextTooLong}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
discardEditMessage={discardEditMessage}
|
||||
onCloseLinkPreview={onCloseLinkPreview}
|
||||
// AudioCapture
|
||||
errorDialogAudioRecorderType={errorDialogAudioRecorderType ?? null}
|
||||
recordingState={recordingState}
|
||||
cancelRecording={cancelRecording}
|
||||
completeRecording={completeRecording}
|
||||
startRecording={startRecording}
|
||||
errorRecording={errorRecording}
|
||||
// AttachmentsList
|
||||
draftAttachments={draftAttachments}
|
||||
addAttachment={addAttachment}
|
||||
removeAttachment={removeAttachment}
|
||||
onClearAttachments={onClearAttachments}
|
||||
processAttachments={processAttachments}
|
||||
// MediaEditor
|
||||
imageToBlurHash={imageToBlurHash}
|
||||
// MediaQualitySelector
|
||||
shouldSendHighQualityAttachments={
|
||||
shouldSendHighQualityAttachments !== undefined
|
||||
? shouldSendHighQualityAttachments
|
||||
: window.storage.get('sent-media-quality') === 'high'
|
||||
}
|
||||
setMediaQualitySetting={setMediaQualitySetting}
|
||||
// StagedLinkPreview
|
||||
linkPreviewLoading={linkPreviewLoading}
|
||||
linkPreviewResult={linkPreviewResult ?? null}
|
||||
// Quote
|
||||
quotedMessageId={quotedMessage?.quote?.messageId ?? null}
|
||||
quotedMessageProps={quotedMessageProps ?? null}
|
||||
quotedMessageAuthorAci={quotedMessage?.quote?.authorAci ?? null}
|
||||
quotedMessageSentAt={quotedMessage?.quote?.id ?? null}
|
||||
setQuoteByMessageId={setQuoteByMessageId}
|
||||
// Emojis
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
onPickEmoji={onUseEmoji}
|
||||
// Stickers
|
||||
receivedPacks={receivedPacks}
|
||||
installedPack={installedPack}
|
||||
blessedPacks={blessedPacks}
|
||||
knownPacks={knownPacks}
|
||||
installedPacks={installedPacks}
|
||||
recentStickers={recentStickers}
|
||||
showIntroduction={showStickersIntroduction}
|
||||
showPickerHint={showStickerPickerHint}
|
||||
// Message Requests
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest ?? null}
|
||||
removalStage={conversation.removalStage ?? null}
|
||||
addedByName={addedByName}
|
||||
conversationName={conversationName}
|
||||
conversationType={conversation.type}
|
||||
isBlocked={conversation.isBlocked ?? false}
|
||||
isReported={conversation.isReported ?? false}
|
||||
isHidden={conversation.removalStage != null}
|
||||
isSMSOnly={Boolean(isConversationSMSOnly(conversation))}
|
||||
isSignalConversation={isSignalConversation(conversation)}
|
||||
isFetchingUUID={conversation.isFetchingUUID ?? null}
|
||||
isMissingMandatoryProfileSharing={isMissingRequiredProfileSharing(
|
||||
conversation
|
||||
)}
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
reportSpam={reportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
// Groups
|
||||
groupVersion={conversation.groupVersion ?? null}
|
||||
isGroupV1AndDisabled={conversation.isGroupV1AndDisabled ?? null}
|
||||
left={conversation.left ?? null}
|
||||
announcementsOnly={announcementsOnly ?? null}
|
||||
areWeAdmin={areWeAdmin ?? null}
|
||||
areWePending={conversation.areWePending ?? null}
|
||||
areWePendingApproval={conversation.areWePendingApproval ?? null}
|
||||
groupAdmins={groupAdmins}
|
||||
draftText={conversation.draftText ?? null}
|
||||
draftBodyRanges={hydratedDraftBodyRanges ?? null}
|
||||
renderSmartCompositionRecording={renderSmartCompositionRecording}
|
||||
renderSmartCompositionRecordingDraft={
|
||||
renderSmartCompositionRecordingDraft
|
||||
}
|
||||
showGV2MigrationDialog={showGV2MigrationDialog}
|
||||
cancelJoinRequest={cancelJoinRequest}
|
||||
sortedGroupMembers={conversation.sortedGroupMembers ?? null}
|
||||
// Select Mode
|
||||
selectedMessageIds={selectedMessageIds}
|
||||
toggleSelectMode={toggleSelectMode}
|
||||
toggleForwardMessagesModal={toggleForwardMessagesModal}
|
||||
// Dispatch
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
clearShowIntroduction={clearShowIntroduction}
|
||||
clearInstalledStickerPack={clearInstalledStickerPack}
|
||||
clearShowPickerHint={clearShowPickerHint}
|
||||
showToast={showToast}
|
||||
sendStickerMessage={sendStickerMessage}
|
||||
sendEditedMessage={sendEditedMessage}
|
||||
sendMultiMediaMessage={sendMultiMediaMessage}
|
||||
scrollToMessage={scrollToMessage}
|
||||
setComposerFocus={setComposerFocus}
|
||||
setMessageToEdit={setMessageToEdit}
|
||||
showConversation={showConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useSelector } from 'react-redux';
|
|||
import type { CompositionTextAreaProps } from '../../components/CompositionTextArea';
|
||||
import { CompositionTextArea } from '../../components/CompositionTextArea';
|
||||
import { getIntl, getPlatform } from '../selectors/user';
|
||||
import { useActions as useEmojiActions } from '../ducks/emojis';
|
||||
import { useEmojisActions as useEmojiActions } from '../ducks/emojis';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
|
|
|
@ -44,6 +44,7 @@ export function SmartContactSpoofingReviewDialog(
|
|||
|
||||
const {
|
||||
acceptConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
deleteConversation,
|
||||
|
@ -74,6 +75,7 @@ export function SmartContactSpoofingReviewDialog(
|
|||
const sharedProps = {
|
||||
...props,
|
||||
acceptConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
deleteConversation,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { pick } from 'lodash';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
|
@ -37,6 +37,8 @@ import { useStoriesActions } from '../ducks/stories';
|
|||
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||
import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall';
|
||||
import { useContactNameData } from '../../components/conversation/ContactName';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||
|
||||
export type OwnProps = {
|
||||
id: string;
|
||||
|
@ -108,6 +110,11 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
|
|||
setMuteExpiration,
|
||||
setPinned,
|
||||
toggleSelectMode,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
} = useConversationsActions();
|
||||
const {
|
||||
onOutgoingAudioCallInConversation,
|
||||
|
@ -129,6 +136,17 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
|
|||
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
||||
const isSelectMode = selectedMessageIds != null;
|
||||
|
||||
const addedBy = useMemo(() => {
|
||||
if (conversation.type === 'group') {
|
||||
return getAddedByForOurPendingInvitation(conversation);
|
||||
}
|
||||
return null;
|
||||
}, [conversation]);
|
||||
|
||||
const addedByName = useContactNameData(addedBy);
|
||||
const conversationName = useContactNameData(conversation);
|
||||
strictAssert(conversationName, 'conversationName is required');
|
||||
|
||||
return (
|
||||
<ConversationHeader
|
||||
{...pick(conversation, [
|
||||
|
@ -184,6 +202,18 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
|
|||
isSelectMode={isSelectMode}
|
||||
toggleSelectMode={toggleSelectMode}
|
||||
viewUserStories={viewUserStories}
|
||||
// MessageRequestActionsConfirmation
|
||||
addedByName={addedByName}
|
||||
conversationId={id}
|
||||
conversationType={conversation.type}
|
||||
conversationName={conversationName}
|
||||
isBlocked={conversation.isBlocked ?? false}
|
||||
isReported={conversation.isReported ?? false}
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
reportSpam={reportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useSelector } from 'react-redux';
|
|||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { useActions as usePreferredReactionsActions } from '../ducks/preferredReactions';
|
||||
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getEmojiSkinTone } from '../selectors/items';
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useRecentEmojis } from '../selectors/emojis';
|
||||
import { useActions as useEmojiActions } from '../ducks/emojis';
|
||||
import { useEmojisActions as useEmojiActions } from '../ducks/emojis';
|
||||
|
||||
import type { Props as EmojiPickerProps } from '../../components/emoji/EmojiPicker';
|
||||
import { EmojiPicker } from '../../components/emoji/EmojiPicker';
|
||||
|
|
|
@ -25,6 +25,7 @@ import { getConversationsStoppingSend } from '../selectors/conversations';
|
|||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
|
||||
import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation';
|
||||
|
||||
function renderEditHistoryMessagesModal(): JSX.Element {
|
||||
return <SmartEditHistoryMessagesModal />;
|
||||
|
@ -50,6 +51,10 @@ function renderForwardMessagesModal(): JSX.Element {
|
|||
return <SmartForwardMessagesModal />;
|
||||
}
|
||||
|
||||
function renderMessageRequestActionsConfirmation(): JSX.Element {
|
||||
return <SmartMessageRequestActionsConfirmation />;
|
||||
}
|
||||
|
||||
function renderStoriesSettings(): JSX.Element {
|
||||
return <SmartStoriesSettingsModal />;
|
||||
}
|
||||
|
@ -83,6 +88,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
errorModalProps,
|
||||
formattingWarningData,
|
||||
forwardMessagesProps,
|
||||
messageRequestActionsConfirmationProps,
|
||||
isAuthorizingArtCreator,
|
||||
isProfileEditorVisible,
|
||||
isShortcutGuideModalVisible,
|
||||
|
@ -163,6 +169,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
deleteMessagesProps={deleteMessagesProps}
|
||||
formattingWarningData={formattingWarningData}
|
||||
forwardMessagesProps={forwardMessagesProps}
|
||||
messageRequestActionsConfirmationProps={
|
||||
messageRequestActionsConfirmationProps
|
||||
}
|
||||
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
|
||||
hideUserNotFoundModal={hideUserNotFoundModal}
|
||||
hideWhatsNewModal={hideWhatsNewModal}
|
||||
|
@ -180,6 +189,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
renderErrorModal={renderErrorModal}
|
||||
renderDeleteMessagesModal={renderDeleteMessagesModal}
|
||||
renderForwardMessagesModal={renderForwardMessagesModal}
|
||||
renderMessageRequestActionsConfirmation={
|
||||
renderMessageRequestActionsConfirmation
|
||||
}
|
||||
renderProfileEditor={renderProfileEditor}
|
||||
renderUsernameOnboarding={renderUsernameOnboarding}
|
||||
renderSafetyNumber={renderSafetyNumber}
|
||||
|
|
84
ts/state/smart/MessageRequestActionsConfirmation.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getGlobalModalsState } from '../selectors/globalModals';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
} from '../../components/conversation/MessageRequestActionsConfirmation';
|
||||
import { useContactNameData } from '../../components/conversation/ContactName';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
|
||||
export function SmartMessageRequestActionsConfirmation(): JSX.Element | null {
|
||||
const i18n = useSelector(getIntl);
|
||||
const globalModals = useSelector(getGlobalModalsState);
|
||||
const { messageRequestActionsConfirmationProps } = globalModals;
|
||||
strictAssert(
|
||||
messageRequestActionsConfirmationProps,
|
||||
'messageRequestActionsConfirmationProps are required'
|
||||
);
|
||||
const { conversationId, state } = messageRequestActionsConfirmationProps;
|
||||
strictAssert(state !== MessageRequestState.default, 'state is required');
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const conversation = getConversation(conversationId);
|
||||
const addedBy = useMemo(() => {
|
||||
if (conversation.type === 'group') {
|
||||
return getAddedByForOurPendingInvitation(conversation);
|
||||
}
|
||||
return null;
|
||||
}, [conversation]);
|
||||
|
||||
const conversationName = useContactNameData(conversation);
|
||||
strictAssert(conversationName, 'conversationName is required');
|
||||
const addedByName = useContactNameData(addedBy);
|
||||
|
||||
const {
|
||||
acceptConversation,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
deleteConversation,
|
||||
} = useConversationsActions();
|
||||
const { toggleMessageRequestActionsConfirmation } = useGlobalModalActions();
|
||||
|
||||
const handleChangeState = useCallback(
|
||||
(nextState: MessageRequestState) => {
|
||||
if (nextState === MessageRequestState.default) {
|
||||
toggleMessageRequestActionsConfirmation(null);
|
||||
} else {
|
||||
toggleMessageRequestActionsConfirmation({
|
||||
conversationId,
|
||||
state: nextState,
|
||||
});
|
||||
}
|
||||
},
|
||||
[conversationId, toggleMessageRequestActionsConfirmation]
|
||||
);
|
||||
|
||||
return (
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
conversationId={conversation.id}
|
||||
conversationType={conversation.type}
|
||||
conversationName={conversationName}
|
||||
addedByName={addedByName}
|
||||
isBlocked={conversation.isBlocked ?? false}
|
||||
isReported={conversation.isReported ?? false}
|
||||
acceptConversation={acceptConversation}
|
||||
blockConversation={blockConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
state={state}
|
||||
onChangeState={handleChangeState}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
import * as React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useActions as usePreferredReactionsActions } from '../ducks/preferredReactions';
|
||||
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
|
|
@ -32,7 +32,7 @@ import {
|
|||
} from '../selectors/items';
|
||||
import { imageToBlurHash } from '../../util/imageToBlurHash';
|
||||
import { processAttachment } from '../../util/processAttachment';
|
||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
||||
import { useEmojisActions } from '../ducks/emojis';
|
||||
import { useAudioPlayerActions } from '../ducks/audioPlayer';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
|
@ -148,6 +148,7 @@ export function SmartStoryCreator(): JSX.Element | null {
|
|||
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
|
||||
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
||||
signalConnections={signalConnections}
|
||||
sortedGroupMembers={null}
|
||||
skinTone={skinTone}
|
||||
theme={ThemeType.dark}
|
||||
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
||||
|
|
|
@ -32,7 +32,7 @@ import { isSignalConversation } from '../../util/isSignalConversation';
|
|||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
|
||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
||||
import { useEmojisActions } from '../ducks/emojis';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useRecentEmojis } from '../selectors/emojis';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
|
|
|
@ -50,6 +50,7 @@ function renderItem({
|
|||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
conversationId,
|
||||
isBlocked,
|
||||
isOldestTimelineItem,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
|
@ -61,6 +62,7 @@ function renderItem({
|
|||
containerElementRef={containerElementRef}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
conversationId={conversationId}
|
||||
isBlocked={isBlocked}
|
||||
isOldestTimelineItem={isOldestTimelineItem}
|
||||
messageId={messageId}
|
||||
previousMessageId={previousMessageId}
|
||||
|
@ -163,6 +165,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
'isGroupV1AndDisabled',
|
||||
'typingContactIdTimestamps',
|
||||
]),
|
||||
isBlocked: conversation.isBlocked ?? false,
|
||||
isConversationSelected: state.conversations.selectedConversationId === id,
|
||||
isIncomingMessageRequest: Boolean(
|
||||
!conversation.acceptedMessageRequest &&
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { RefObject } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||
|
@ -35,11 +35,13 @@ import { isSameDay } from '../../util/timestamp';
|
|||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { renderReactionPicker } from './renderReactionPicker';
|
||||
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
|
||||
|
||||
export type SmartTimelineItemProps = {
|
||||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
conversationId: string;
|
||||
isBlocked: boolean;
|
||||
isOldestTimelineItem: boolean;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
|
@ -59,6 +61,7 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
|||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
conversationId,
|
||||
isBlocked,
|
||||
isOldestTimelineItem,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
|
@ -136,23 +139,27 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
|||
const {
|
||||
showContactModal,
|
||||
showEditHistoryModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
toggleDeleteMessagesModal,
|
||||
toggleForwardMessagesModal,
|
||||
toggleSafetyNumberModal,
|
||||
} = useGlobalModalActions();
|
||||
|
||||
const { checkForAccount } = useAccountsActions();
|
||||
|
||||
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
|
||||
|
||||
const { viewStory } = useStoriesActions();
|
||||
|
||||
const {
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
returnToActiveCall,
|
||||
} = useCallingActions();
|
||||
|
||||
const onOpenMessageRequestActionsConfirmation = useCallback(
|
||||
(state: MessageRequestState) => {
|
||||
toggleMessageRequestActionsConfirmation({ conversationId, state });
|
||||
},
|
||||
[conversationId, toggleMessageRequestActionsConfirmation]
|
||||
);
|
||||
|
||||
return (
|
||||
<TimelineItem
|
||||
item={item}
|
||||
|
@ -175,6 +182,7 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
|||
showEditHistoryModal={showEditHistoryModal}
|
||||
i18n={i18n}
|
||||
interactionMode={interactionMode}
|
||||
isBlocked={isBlocked}
|
||||
theme={theme}
|
||||
platform={platform}
|
||||
blockGroupLinkRequests={blockGroupLinkRequests}
|
||||
|
@ -188,6 +196,9 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
|||
pushPanelForConversation={pushPanelForConversation}
|
||||
reactToMessage={reactToMessage}
|
||||
copyMessageText={copyMessageText}
|
||||
onOpenMessageRequestActionsConfirmation={
|
||||
onOpenMessageRequestActionsConfirmation
|
||||
}
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||
|
|
|
@ -114,7 +114,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
|
|||
const item = leftPane
|
||||
.locator(
|
||||
'.module-conversation-list__item--contact-or-conversation' +
|
||||
`>> text=${LAST_MESSAGE}`
|
||||
'>> text="You accepted the message request"'
|
||||
)
|
||||
.first();
|
||||
await item.click({ timeout: 2 * MINUTE });
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Locator } from 'playwright';
|
||||
import { assert } from 'chai';
|
||||
import type { Locator, Page } from 'playwright';
|
||||
|
||||
export function bufferToUuid(buffer: Buffer): string {
|
||||
const hex = buffer.toString('hex');
|
||||
|
@ -32,3 +33,44 @@ export async function type(input: Locator, text: string): Promise<void> {
|
|||
// updated with the right value
|
||||
await input.locator(`:text("${currentValue}${text}")`).waitFor();
|
||||
}
|
||||
|
||||
export async function expectItemsWithText(
|
||||
items: Locator,
|
||||
expected: ReadonlyArray<string | RegExp>
|
||||
): Promise<void> {
|
||||
// Wait for each message to appear in case they're not all there yet
|
||||
for (const [index, message] of expected.entries()) {
|
||||
const nth = items.nth(index);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await nth.waitFor();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const text = await nth.innerText();
|
||||
const log = `Expect item at index ${index} to match`;
|
||||
if (typeof message === 'string') {
|
||||
assert.strictEqual(text, message, log);
|
||||
} else {
|
||||
assert.match(text, message, log);
|
||||
}
|
||||
}
|
||||
|
||||
const innerTexts = await items.allInnerTexts();
|
||||
assert.deepEqual(
|
||||
innerTexts.length,
|
||||
expected.length,
|
||||
`Expect correct number of items\nActual:\n${innerTexts
|
||||
.map(text => ` - "${text}"\n`)
|
||||
.join('')}\nExpected:\n${expected
|
||||
.map(text => ` - ${text.toString()}\n`)
|
||||
.join('')}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function expectSystemMessages(
|
||||
context: Page | Locator,
|
||||
expected: ReadonlyArray<string | RegExp>
|
||||
): Promise<void> {
|
||||
await expectItemsWithText(
|
||||
context.locator('.SystemMessage__contents'),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '../../util/libphonenumberInstance';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
import { expectSystemMessages } from '../helpers';
|
||||
|
||||
export const debug = createDebug('mock:test:gv2');
|
||||
|
||||
|
@ -114,11 +115,13 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
|
||||
debug('Checking that notifications are present');
|
||||
await window
|
||||
.locator(`"${first.profileName} invited you to the group."`)
|
||||
.locator(
|
||||
`.SystemMessage:has-text("${first.profileName} invited you to the group.")`
|
||||
)
|
||||
.waitFor();
|
||||
await window
|
||||
.locator(
|
||||
`"You accepted an invitation to the group from ${first.profileName}."`
|
||||
`.SystemMessage:has-text("You accepted an invitation to the group from ${first.profileName}.")`
|
||||
)
|
||||
.waitFor();
|
||||
|
||||
|
@ -130,7 +133,9 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
assert(group.getPendingMemberByServiceId(desktop.pni));
|
||||
|
||||
await window
|
||||
.locator(`"${second.profileName} invited you to the group."`)
|
||||
.locator(
|
||||
`.SystemMessage:has-text("${second.profileName} invited you to the group.")`
|
||||
)
|
||||
.waitFor();
|
||||
|
||||
debug('Verify that message request state is not visible');
|
||||
|
@ -179,11 +184,11 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
|
||||
debug('Declining');
|
||||
await conversationStack
|
||||
.locator('.module-message-request-actions button >> "Delete"')
|
||||
.locator('.module-message-request-actions button >> "Block"')
|
||||
.click();
|
||||
|
||||
debug('waiting for confirmation modal');
|
||||
await window.locator('.module-Modal button >> "Delete and Leave"').click();
|
||||
await window.locator('.module-Modal button >> "Block"').click();
|
||||
|
||||
group = await phone.waitForGroupUpdate(group);
|
||||
assert.strictEqual(group.revision, 2);
|
||||
|
@ -217,7 +222,9 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
|
||||
debug('Waiting for the PNI invite');
|
||||
await window
|
||||
.locator(`text=${first.profileName} invited you to the group.`)
|
||||
.locator(
|
||||
`.SystemMessage:has-text("${first.profileName} invited you to the group.")`
|
||||
)
|
||||
.waitFor();
|
||||
|
||||
debug('Inviting ACI from another contact');
|
||||
|
@ -229,7 +236,9 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
|
||||
debug('Waiting for the ACI invite');
|
||||
await window
|
||||
.locator(`text=${second.profileName} invited you to the group.`)
|
||||
.locator(
|
||||
`.SystemMessage:has-text("${second.profileName} invited you to the group.")`
|
||||
)
|
||||
.waitFor();
|
||||
|
||||
debug('Accepting');
|
||||
|
@ -240,8 +249,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
debug('Checking final notification');
|
||||
await window
|
||||
.locator(
|
||||
'.SystemMessage >> text=You accepted an invitation to the group from ' +
|
||||
`${second.profileName}.`
|
||||
`.SystemMessage:has-text("You accepted an invitation to the group from ${second.profileName}.")`
|
||||
)
|
||||
.waitFor();
|
||||
|
||||
|
@ -291,11 +299,11 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
|
||||
debug('Declining');
|
||||
await conversationStack
|
||||
.locator('.module-message-request-actions button >> "Delete"')
|
||||
.locator('.module-message-request-actions button >> "Block"')
|
||||
.click();
|
||||
|
||||
debug('waiting for confirmation modal');
|
||||
await window.locator('.module-Modal button >> "Delete and Leave"').click();
|
||||
await window.locator('.module-Modal button >> "Block"').click();
|
||||
|
||||
group = await phone.waitForGroupUpdate(group);
|
||||
assert.strictEqual(group.revision, 3);
|
||||
|
@ -347,13 +355,10 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
sendUpdateTo: [{ device: desktop }],
|
||||
});
|
||||
|
||||
await window
|
||||
.locator(
|
||||
'.SystemMessage >> ' +
|
||||
`text=${second.profileName} accepted an invitation to the group ` +
|
||||
`from ${first.profileName}.`
|
||||
)
|
||||
.waitFor();
|
||||
await expectSystemMessages(window, [
|
||||
'You were added to the group.',
|
||||
`${second.profileName} accepted an invitation to the group from ${first.profileName}.`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should display a e164 for a PNI invite', async () => {
|
||||
|
@ -398,7 +403,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
}
|
||||
const { e164 } = parsedE164;
|
||||
await window
|
||||
.locator(`.SystemMessage >> text=You invited ${e164} to the group`)
|
||||
.locator(`.SystemMessage:has-text("You invited ${e164} to the group")`)
|
||||
.waitFor();
|
||||
|
||||
debug('Accepting remote invite');
|
||||
|
@ -408,11 +413,10 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
|||
});
|
||||
|
||||
debug('Waiting for accept notification');
|
||||
await window
|
||||
.locator(
|
||||
'.SystemMessage >> ' +
|
||||
`text=${unknownPniContact.profileName} accepted your invitation to the group`
|
||||
)
|
||||
.waitFor();
|
||||
await expectSystemMessages(window, [
|
||||
'You were added to the group.',
|
||||
/^You invited .* to the group\.$/,
|
||||
`${unknownPniContact.profileName} accepted your invitation to the group.`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import { toUntaggedPni } from '../../types/ServiceId';
|
|||
import { MY_STORY_ID } from '../../types/Stories';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
import { expectSystemMessages } from '../helpers';
|
||||
|
||||
export const debug = createDebug('mock:test:merge');
|
||||
|
||||
|
@ -147,13 +148,9 @@ describe('pnp/merge', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||
|
||||
// No notifications
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(
|
||||
await notifications.count(),
|
||||
0,
|
||||
'notification count'
|
||||
);
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
]);
|
||||
}
|
||||
|
||||
if (withPNIMessage) {
|
||||
|
@ -210,20 +207,25 @@ describe('pnp/merge', function (this: Mocha.Suite) {
|
|||
'message count'
|
||||
);
|
||||
|
||||
// One notification - the merge
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(
|
||||
await notifications.count(),
|
||||
withPNIMessage ? 1 : 0,
|
||||
'notification count'
|
||||
);
|
||||
|
||||
if (withPNIMessage && !pniSignatureVerified) {
|
||||
const first = await notifications.first();
|
||||
assert.match(
|
||||
await first.innerText(),
|
||||
/Your message history with ACI Contact and their number .* has been merged./
|
||||
);
|
||||
if (withPNIMessage) {
|
||||
if (pniSignatureVerified) {
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
'You accepted the message request',
|
||||
/Your message history with ACI Contact and their number .* has been merged\./,
|
||||
]);
|
||||
} else {
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
'You accepted the message request',
|
||||
/Your message history with ACI Contact and their number .* has been merged\./,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
'You accepted the message request',
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import { MY_STORY_ID } from '../../types/Stories';
|
|||
import { toUntaggedPni } from '../../types/ServiceId';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
import { expectSystemMessages } from '../helpers';
|
||||
|
||||
export const debug = createDebug('mock:test:merge');
|
||||
|
||||
|
@ -143,12 +144,10 @@ describe('pnp/phone discovery', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||
|
||||
// One notification - the PhoneNumberDiscovery
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ACI Contact/);
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
/.* belongs to ACI Contact/,
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import * as durations from '../../util/durations';
|
|||
import { generatePni, toUntaggedPni } from '../../types/ServiceId';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
import { expectSystemMessages } from '../helpers';
|
||||
|
||||
export const debug = createDebug('mock:test:pni-change');
|
||||
|
||||
|
@ -97,8 +98,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||
|
||||
// No notifications
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
await expectSystemMessages(window, ['You accepted the message request']);
|
||||
}
|
||||
|
||||
debug('Send message to contactA');
|
||||
|
@ -165,11 +165,10 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||
|
||||
// Only a PhoneNumberDiscovery notification
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
/.* belongs to ContactA/,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -199,9 +198,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||
|
||||
// No notifications
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
await expectSystemMessages(window, ['You accepted the message request']);
|
||||
}
|
||||
|
||||
debug('Send message to contactA');
|
||||
|
@ -268,14 +265,11 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||
|
||||
// Two notifications - the safety number change and PhoneNumberDiscovery
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 2, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
|
||||
const second = await notifications.nth(1);
|
||||
assert.match(await second.innerText(), /Safety Number has changed/);
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
/.* belongs to ContactA/,
|
||||
/Safety Number has changed/,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -305,9 +299,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||
|
||||
// No notifications
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
await expectSystemMessages(window, ['You accepted the message request']);
|
||||
}
|
||||
|
||||
debug('Send message to contactA');
|
||||
|
@ -403,15 +395,12 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 2, 'message count');
|
||||
|
||||
// Two notifications - the safety number change and PhoneNumberDiscovery
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 2, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
|
||||
const second = await notifications.nth(1);
|
||||
assert.match(await second.innerText(), /Safety Number has changed/);
|
||||
// Three notifications - accepted, the safety number change and PhoneNumberDiscovery
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
/.* belongs to ContactA/,
|
||||
/Safety Number has changed/,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -442,8 +431,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||
|
||||
// No notifications
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
await expectSystemMessages(window, ['You accepted the message request']);
|
||||
}
|
||||
|
||||
debug('Send message to contactA');
|
||||
|
@ -563,11 +551,10 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 2, 'message count');
|
||||
|
||||
// Only a PhoneNumberDiscovery notification
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
/.* belongs to ContactA/,
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
RECEIPT_BATCHER_WAIT_MS,
|
||||
} from '../../types/Receipt';
|
||||
import { sleep } from '../../util/sleep';
|
||||
import { expectSystemMessages } from '../helpers';
|
||||
|
||||
export const debug = createDebug('mock:test:pni-signature');
|
||||
|
||||
|
@ -255,9 +256,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 4, 'message count');
|
||||
|
||||
// No notifications
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
await expectSystemMessages(window, ['You accepted the message request']);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -373,11 +372,10 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 3, 'messages');
|
||||
|
||||
// Title transition notification
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notifications');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /You started this chat with/);
|
||||
await expectSystemMessages(window, [
|
||||
'You accepted the message request',
|
||||
/You started this chat with/,
|
||||
]);
|
||||
|
||||
assert.isEmpty(await phone.getOrphanedStorageKeys());
|
||||
}
|
||||
|
|
|
@ -175,10 +175,7 @@ describe('pnp/send gv2 invite', function (this: Mocha.Suite) {
|
|||
await detailsHeader.locator('button >> "My group"').click();
|
||||
|
||||
const modal = window.locator('.module-Modal:has-text("Edit group")');
|
||||
|
||||
// Group title should be immediately focused.
|
||||
await modal.type(' (v2)');
|
||||
|
||||
await modal.locator('input').fill('My group (v2)');
|
||||
await modal.locator('button >> "Save"').click();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import { Job } from '../../../jobs/Job';
|
||||
import { generateAci } from '../../../types/ServiceId';
|
||||
|
||||
import { addReportSpamJob } from '../../../jobs/helpers/addReportSpamJob';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
describe('addReportSpamJob', () => {
|
||||
let getMessageServerGuidsForSpam: sinon.SinonStub;
|
||||
let jobQueue: { add: sinon.SinonStub };
|
||||
|
||||
const conversation = {
|
||||
id: 'convo',
|
||||
type: 'private' as const,
|
||||
serviceId: generateAci(),
|
||||
};
|
||||
const conversation: ConversationType = getDefaultConversation();
|
||||
|
||||
beforeEach(() => {
|
||||
getMessageServerGuidsForSpam = sinon.stub().resolves(['abc', 'xyz']);
|
||||
|
|
7
ts/types/MessageRequestResponseEvent.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
export enum MessageRequestResponseEvent {
|
||||
ACCEPT = 'ACCEPT',
|
||||
BLOCK = 'BLOCK',
|
||||
SPAM = 'SPAM',
|
||||
}
|
|
@ -44,6 +44,7 @@ export enum ToastType {
|
|||
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||
ReactionFailed = 'ReactionFailed',
|
||||
ReportedSpam = 'ReportedSpam',
|
||||
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
||||
StickerPackInstallFailed = 'StickerPackInstallFailed',
|
||||
StoryMuted = 'StoryMuted',
|
||||
|
@ -120,6 +121,7 @@ export type AnyToast =
|
|||
| { toastType: ToastType.OriginalMessageNotFound }
|
||||
| { toastType: ToastType.PinnedConversationsFull }
|
||||
| { toastType: ToastType.ReactionFailed }
|
||||
| { toastType: ToastType.ReportedSpam }
|
||||
| { toastType: ToastType.ReportedSpamAndBlocked }
|
||||
| { toastType: ToastType.StickerPackInstallFailed }
|
||||
| { toastType: ToastType.StoryMuted }
|
||||
|
|
17
ts/util/getAddedByForOurPendingInvitation.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export function getAddedByForOurPendingInvitation(
|
||||
conversation: ConversationType
|
||||
): ConversationType | null {
|
||||
const ourAci = window.storage.user.getCheckedAci();
|
||||
const ourPni = window.storage.user.getPni();
|
||||
const addedBy = conversation.pendingMemberships?.find(
|
||||
item => item.serviceId === ourAci || item.serviceId === ourPni
|
||||
)?.addedByUserId;
|
||||
if (addedBy == null) {
|
||||
return null;
|
||||
}
|
||||
return window.ConversationController.get(addedBy)?.format() ?? null;
|
||||
}
|
|
@ -177,6 +177,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
inboxPosition,
|
||||
isArchived: attributes.isArchived,
|
||||
isBlocked: isBlocked(attributes),
|
||||
reportingToken: attributes.reportingToken,
|
||||
removalStage: attributes.removalStage,
|
||||
isMe: isMe(attributes),
|
||||
isGroupV1AndDisabled: isGroupV1(attributes),
|
||||
|
|
|
@ -45,12 +45,15 @@ import {
|
|||
isTapToView,
|
||||
isUnsupportedMessage,
|
||||
isConversationMerge,
|
||||
isMessageRequestResponse,
|
||||
} from '../state/selectors/message';
|
||||
import {
|
||||
getContact,
|
||||
messageHasPaymentEvent,
|
||||
getPaymentEventNotificationText,
|
||||
} from '../messages/helpers';
|
||||
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
|
||||
function getNameForNumber(e164: string): string {
|
||||
const conversation = window.ConversationController.get(e164);
|
||||
|
@ -177,6 +180,34 @@ export function getNotificationDataForMessage(
|
|||
};
|
||||
}
|
||||
|
||||
if (isMessageRequestResponse(attributes)) {
|
||||
const { messageRequestResponseEvent: event } = attributes;
|
||||
strictAssert(
|
||||
event,
|
||||
'getNotificationData: isMessageRequestResponse true, but no messageRequestResponseEvent!'
|
||||
);
|
||||
let text: string;
|
||||
if (event === MessageRequestResponseEvent.ACCEPT) {
|
||||
text = window.i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Accepted'
|
||||
);
|
||||
} else if (event === MessageRequestResponseEvent.SPAM) {
|
||||
text = window.i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Reported'
|
||||
);
|
||||
} else if (event === MessageRequestResponseEvent.BLOCK) {
|
||||
text = window.i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Blocked'
|
||||
);
|
||||
} else {
|
||||
throw missingCaseError(event);
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
const { attachments = [] } = attributes;
|
||||
|
||||
if (isTapToView(attributes)) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '../messages/helpers';
|
||||
import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation';
|
||||
import { getE164 } from './getE164';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export function getMessageIdForLogging(
|
||||
message: Pick<
|
||||
|
@ -27,7 +28,7 @@ export function getMessageIdForLogging(
|
|||
}
|
||||
|
||||
export function getConversationIdForLogging(
|
||||
conversation: ConversationAttributesType
|
||||
conversation: ConversationAttributesType | ConversationType
|
||||
): string {
|
||||
if (isDirectConversation(conversation)) {
|
||||
const { serviceId, pni, id } = conversation;
|
||||
|
|
|
@ -3372,6 +3372,13 @@
|
|||
"updated": "2022-01-04T21:43:17.517Z",
|
||||
"reasonDetail": "Used to change the style in non-production builds."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/SafetyTipsModal.tsx",
|
||||
"line": " const scrollEndTimer = useRef<NodeJS.Timeout | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-03-08T01:48:15.330Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/Slider.tsx",
|
||||
|
|