Spam Reporting UI changes
|
@ -116,7 +116,6 @@ window.SignalContext = {
|
||||||
|
|
||||||
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
|
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
|
||||||
getPreferredSystemLocales: () => ['en'],
|
getPreferredSystemLocales: () => ['en'],
|
||||||
getResolvedMessagesLocaleDirection: () => 'ltr',
|
|
||||||
getLocaleOverride: () => null,
|
getLocaleOverride: () => null,
|
||||||
getLocaleDisplayNames: () => ({ en: { en: 'English' } }),
|
getLocaleDisplayNames: () => ({ en: { en: 'English' } }),
|
||||||
};
|
};
|
||||||
|
@ -133,6 +132,9 @@ const withGlobalTypesProvider = (Story, context) => {
|
||||||
const mode = context.globals.mode;
|
const mode = context.globals.mode;
|
||||||
const direction = context.globals.direction ?? 'auto';
|
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
|
// Adding it to the body as well so that we can cover modals and other
|
||||||
// components that are rendered outside of this decorator container
|
// components that are rendered outside of this decorator container
|
||||||
if (theme === 'light') {
|
if (theme === 'light') {
|
||||||
|
|
|
@ -427,6 +427,26 @@
|
||||||
"messageformat": "Select messages",
|
"messageformat": "Select messages",
|
||||||
"description": "Shown in menu for conversation, allows the user to start selecting multiple messages in the conversation"
|
"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": {
|
"icu:ContactListItem__menu": {
|
||||||
"messageformat": "Manage Contact",
|
"messageformat": "Manage Contact",
|
||||||
"description": "Shown as aria label for context menu for a contact"
|
"description": "Shown as aria label for context menu for a contact"
|
||||||
|
@ -3394,6 +3414,62 @@
|
||||||
"messageformat": "All",
|
"messageformat": "All",
|
||||||
"description": "Shown in reaction viewer as the title for the 'all' category"
|
"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": {
|
"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.",
|
"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"
|
"description": "Shown as the message for a message request in a direct message"
|
||||||
|
@ -3458,6 +3534,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.",
|
"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"
|
"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": {
|
"icu:MessageRequests--delete": {
|
||||||
"messageformat": "Delete",
|
"messageformat": "Delete",
|
||||||
"description": "Shown as a button to let the user delete any message request"
|
"description": "Shown as a button to let the user delete any message request"
|
||||||
|
@ -5242,6 +5354,10 @@
|
||||||
"messageformat": "Learn more",
|
"messageformat": "Learn more",
|
||||||
"description": "Shown on the message request warning. Clicking this button will open a dialog with more information"
|
"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": {
|
"icu:MessageRequestWarning__dialog__details": {
|
||||||
"messageformat": "You have no groups in common with this person. Review requests carefully before accepting to avoid unwanted messages.",
|
"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"
|
"description": "Shown in the message request warning dialog. Gives more information about message requests"
|
||||||
|
@ -6332,6 +6448,26 @@
|
||||||
"messageformat": "Check your primary device for this payment’s status",
|
"messageformat": "Check your primary device for this payment’s status",
|
||||||
"description": "Payment event notification check device label"
|
"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": {
|
"icu:SignalConnectionsModal__title": {
|
||||||
"messageformat": "Signal Connections",
|
"messageformat": "Signal Connections",
|
||||||
"description": "The phrase/term: '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 |
|
@ -550,6 +550,8 @@ message SyncMessage {
|
||||||
DELETE = 2;
|
DELETE = 2;
|
||||||
BLOCK = 3;
|
BLOCK = 3;
|
||||||
BLOCK_AND_DELETE = 4;
|
BLOCK_AND_DELETE = 4;
|
||||||
|
SPAM = 5;
|
||||||
|
BLOCK_AND_SPAM = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional string threadE164 = 1;
|
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 {
|
&__membership {
|
||||||
@include font-body-2;
|
@include font-body-2;
|
||||||
user-select: none;
|
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'
|
'../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 {
|
&--error {
|
||||||
|
|
|
@ -114,6 +114,7 @@
|
||||||
@import './components/MediaQualitySelector.scss';
|
@import './components/MediaQualitySelector.scss';
|
||||||
@import './components/MessageAudio.scss';
|
@import './components/MessageAudio.scss';
|
||||||
@import './components/MessageBody.scss';
|
@import './components/MessageBody.scss';
|
||||||
|
@import './components/MessageRequestActionsConfirmation.scss';
|
||||||
@import './components/MessageTextRenderer.scss';
|
@import './components/MessageTextRenderer.scss';
|
||||||
@import './components/MessageDetail.scss';
|
@import './components/MessageDetail.scss';
|
||||||
@import './components/MiniPlayer.scss';
|
@import './components/MiniPlayer.scss';
|
||||||
|
@ -133,6 +134,7 @@
|
||||||
@import './components/SafetyNumberChangeDialog.scss';
|
@import './components/SafetyNumberChangeDialog.scss';
|
||||||
@import './components/SafetyNumberOnboarding.scss';
|
@import './components/SafetyNumberOnboarding.scss';
|
||||||
@import './components/SafetyNumberViewer.scss';
|
@import './components/SafetyNumberViewer.scss';
|
||||||
|
@import './components/SafetyTipsModal.scss';
|
||||||
@import './components/ScrollDownButton.scss';
|
@import './components/ScrollDownButton.scss';
|
||||||
@import './components/SearchInput.scss';
|
@import './components/SearchInput.scss';
|
||||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default {
|
||||||
blockConversation: action('blockConversation'),
|
blockConversation: action('blockConversation'),
|
||||||
blockAndReportSpam: action('blockAndReportSpam'),
|
blockAndReportSpam: action('blockAndReportSpam'),
|
||||||
deleteConversation: action('deleteConversation'),
|
deleteConversation: action('deleteConversation'),
|
||||||
title: '',
|
conversationName: getDefaultConversation(),
|
||||||
// GroupV1 Disabled Actions
|
// GroupV1 Disabled Actions
|
||||||
showGV2MigrationDialog: action('showGV2MigrationDialog'),
|
showGV2MigrationDialog: action('showGV2MigrationDialog'),
|
||||||
// GroupV2
|
// GroupV2
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019 Signal Messenger, LLC
|
// Copyright 2019 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 classNames from 'classnames';
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ import type { AciString } from '../types/ServiceId';
|
||||||
import { AudioCapture } from './conversation/AudioCapture';
|
import { AudioCapture } from './conversation/AudioCapture';
|
||||||
import { CompositionUpload } from './CompositionUpload';
|
import { CompositionUpload } from './CompositionUpload';
|
||||||
import type {
|
import type {
|
||||||
|
ConversationRemovalStage,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
PushPanelForConversationActionType,
|
PushPanelForConversationActionType,
|
||||||
ShowConversationType,
|
ShowConversationType,
|
||||||
|
@ -73,16 +74,16 @@ import type { ShowToastAction } from '../state/ducks/toast';
|
||||||
import type { DraftEditMessageType } from '../model-types.d';
|
import type { DraftEditMessageType } from '../model-types.d';
|
||||||
|
|
||||||
export type OwnProps = Readonly<{
|
export type OwnProps = Readonly<{
|
||||||
acceptedMessageRequest?: boolean;
|
acceptedMessageRequest: boolean | null;
|
||||||
removalStage?: 'justNotification' | 'messageRequest';
|
removalStage: ConversationRemovalStage | null;
|
||||||
addAttachment: (
|
addAttachment: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
attachment: InMemoryAttachmentDraftType
|
attachment: InMemoryAttachmentDraftType
|
||||||
) => unknown;
|
) => unknown;
|
||||||
announcementsOnly?: boolean;
|
announcementsOnly: boolean | null;
|
||||||
areWeAdmin?: boolean;
|
areWeAdmin: boolean | null;
|
||||||
areWePending?: boolean;
|
areWePending: boolean | null;
|
||||||
areWePendingApproval?: boolean;
|
areWePendingApproval: boolean | null;
|
||||||
cancelRecording: () => unknown;
|
cancelRecording: () => unknown;
|
||||||
completeRecording: (
|
completeRecording: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
@ -93,29 +94,29 @@ export type OwnProps = Readonly<{
|
||||||
) => HydratedBodyRangesType | undefined;
|
) => HydratedBodyRangesType | undefined;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
discardEditMessage: (id: string) => unknown;
|
discardEditMessage: (id: string) => unknown;
|
||||||
draftEditMessage?: DraftEditMessageType;
|
draftEditMessage: DraftEditMessageType | null;
|
||||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||||
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType | null;
|
||||||
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||||||
focusCounter: number;
|
focusCounter: number;
|
||||||
groupAdmins: Array<ConversationType>;
|
groupAdmins: Array<ConversationType>;
|
||||||
groupVersion?: 1 | 2;
|
groupVersion: 1 | 2 | null;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
imageToBlurHash: typeof imageToBlurHash;
|
imageToBlurHash: typeof imageToBlurHash;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isFetchingUUID?: boolean;
|
isFetchingUUID: boolean | null;
|
||||||
isFormattingEnabled: boolean;
|
isFormattingEnabled: boolean;
|
||||||
isGroupV1AndDisabled?: boolean;
|
isGroupV1AndDisabled: boolean | null;
|
||||||
isMissingMandatoryProfileSharing?: boolean;
|
isMissingMandatoryProfileSharing: boolean | null;
|
||||||
isSignalConversation?: boolean;
|
isSignalConversation: boolean | null;
|
||||||
lastEditableMessageId?: string;
|
lastEditableMessageId: string | null;
|
||||||
recordingState: RecordingState;
|
recordingState: RecordingState;
|
||||||
messageCompositionId: string;
|
messageCompositionId: string;
|
||||||
shouldHidePopovers?: boolean;
|
shouldHidePopovers: boolean | null;
|
||||||
isSMSOnly?: boolean;
|
isSMSOnly: boolean | null;
|
||||||
left?: boolean;
|
left: boolean | null;
|
||||||
linkPreviewLoading: boolean;
|
linkPreviewLoading: boolean;
|
||||||
linkPreviewResult?: LinkPreviewType;
|
linkPreviewResult: LinkPreviewType | null;
|
||||||
onClearAttachments(conversationId: string): unknown;
|
onClearAttachments(conversationId: string): unknown;
|
||||||
onCloseLinkPreview(conversationId: string): unknown;
|
onCloseLinkPreview(conversationId: string): unknown;
|
||||||
platform: string;
|
platform: string;
|
||||||
|
@ -149,15 +150,15 @@ export type OwnProps = Readonly<{
|
||||||
voiceNoteAttachment?: InMemoryAttachmentDraftType;
|
voiceNoteAttachment?: InMemoryAttachmentDraftType;
|
||||||
}
|
}
|
||||||
): unknown;
|
): unknown;
|
||||||
quotedMessageId?: string;
|
quotedMessageId: string | null;
|
||||||
quotedMessageProps?: ReadonlyDeep<
|
quotedMessageProps: null | ReadonlyDeep<
|
||||||
Omit<
|
Omit<
|
||||||
QuoteProps,
|
QuoteProps,
|
||||||
'i18n' | 'onClick' | 'onClose' | 'withContentAbove' | 'isCompose'
|
'i18n' | 'onClick' | 'onClose' | 'withContentAbove' | 'isCompose'
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
quotedMessageAuthorAci?: AciString;
|
quotedMessageAuthorAci: AciString | null;
|
||||||
quotedMessageSentAt?: number;
|
quotedMessageSentAt: number | null;
|
||||||
|
|
||||||
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||||
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
||||||
|
@ -210,6 +211,7 @@ export type Props = Pick<
|
||||||
| 'blessedPacks'
|
| 'blessedPacks'
|
||||||
| 'recentStickers'
|
| 'recentStickers'
|
||||||
| 'clearInstalledStickerPack'
|
| 'clearInstalledStickerPack'
|
||||||
|
| 'showIntroduction'
|
||||||
| 'clearShowIntroduction'
|
| 'clearShowIntroduction'
|
||||||
| 'showPickerHint'
|
| 'showPickerHint'
|
||||||
| 'clearShowPickerHint'
|
| 'clearShowPickerHint'
|
||||||
|
@ -220,7 +222,7 @@ export type Props = Pick<
|
||||||
pushPanelForConversation: PushPanelForConversationActionType;
|
pushPanelForConversation: PushPanelForConversationActionType;
|
||||||
} & OwnProps;
|
} & OwnProps;
|
||||||
|
|
||||||
export function CompositionArea({
|
export const CompositionArea = memo(function CompositionArea({
|
||||||
// Base props
|
// Base props
|
||||||
addAttachment,
|
addAttachment,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -291,6 +293,7 @@ export function CompositionArea({
|
||||||
recentStickers,
|
recentStickers,
|
||||||
clearInstalledStickerPack,
|
clearInstalledStickerPack,
|
||||||
sendStickerMessage,
|
sendStickerMessage,
|
||||||
|
showIntroduction,
|
||||||
clearShowIntroduction,
|
clearShowIntroduction,
|
||||||
showPickerHint,
|
showPickerHint,
|
||||||
clearShowPickerHint,
|
clearShowPickerHint,
|
||||||
|
@ -301,14 +304,18 @@ export function CompositionArea({
|
||||||
conversationType,
|
conversationType,
|
||||||
groupVersion,
|
groupVersion,
|
||||||
isBlocked,
|
isBlocked,
|
||||||
|
isHidden,
|
||||||
|
isReported,
|
||||||
isMissingMandatoryProfileSharing,
|
isMissingMandatoryProfileSharing,
|
||||||
left,
|
left,
|
||||||
removalStage,
|
removalStage,
|
||||||
acceptConversation,
|
acceptConversation,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
|
reportSpam,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
deleteConversation,
|
deleteConversation,
|
||||||
title,
|
conversationName,
|
||||||
|
addedByName,
|
||||||
// GroupV1 Disabled Actions
|
// GroupV1 Disabled Actions
|
||||||
isGroupV1AndDisabled,
|
isGroupV1AndDisabled,
|
||||||
showGV2MigrationDialog,
|
showGV2MigrationDialog,
|
||||||
|
@ -356,8 +363,8 @@ export function CompositionArea({
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
message,
|
message,
|
||||||
// sent timestamp for the quote
|
// sent timestamp for the quote
|
||||||
quoteSentAt: quotedMessageSentAt,
|
quoteSentAt: quotedMessageSentAt ?? undefined,
|
||||||
quoteAuthorAci: quotedMessageAuthorAci,
|
quoteAuthorAci: quotedMessageAuthorAci ?? undefined,
|
||||||
targetMessageId: editedMessageId,
|
targetMessageId: editedMessageId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -469,12 +476,7 @@ export function CompositionArea({
|
||||||
) {
|
) {
|
||||||
inputApiRef.current.reset();
|
inputApiRef.current.reset();
|
||||||
}
|
}
|
||||||
}, [
|
}, [messageCompositionId, sendCounter, previousMessageCompositionId, previousSendCounter]);
|
||||||
messageCompositionId,
|
|
||||||
sendCounter,
|
|
||||||
previousMessageCompositionId,
|
|
||||||
previousSendCounter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const insertEmoji = useCallback(
|
const insertEmoji = useCallback(
|
||||||
(e: EmojiPickDataType) => {
|
(e: EmojiPickDataType) => {
|
||||||
|
@ -504,7 +506,7 @@ export function CompositionArea({
|
||||||
|
|
||||||
inputApiRef.current?.setContents(
|
inputApiRef.current?.setContents(
|
||||||
draftEditMessageBody ?? '',
|
draftEditMessageBody ?? '',
|
||||||
draftBodyRanges,
|
draftBodyRanges ?? undefined,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}, [draftBodyRanges, draftEditMessageBody, hasEditDraftChanged]);
|
}, [draftBodyRanges, draftEditMessageBody, hasEditDraftChanged]);
|
||||||
|
@ -520,7 +522,11 @@ export function CompositionArea({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
inputApiRef.current?.setContents(draftText, draftBodyRanges, true);
|
inputApiRef.current?.setContents(
|
||||||
|
draftText,
|
||||||
|
draftBodyRanges ?? undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
}, [conversationId, draftBodyRanges, draftText, previousConversationId]);
|
}, [conversationId, draftBodyRanges, draftText, previousConversationId]);
|
||||||
|
|
||||||
const handleToggleLarge = useCallback(() => {
|
const handleToggleLarge = useCallback(() => {
|
||||||
|
@ -637,6 +643,7 @@ export function CompositionArea({
|
||||||
onPickSticker={(packId, stickerId) =>
|
onPickSticker={(packId, stickerId) =>
|
||||||
sendStickerMessage(conversationId, { packId, stickerId })
|
sendStickerMessage(conversationId, { packId, stickerId })
|
||||||
}
|
}
|
||||||
|
showIntroduction={showIntroduction}
|
||||||
clearShowIntroduction={clearShowIntroduction}
|
clearShowIntroduction={clearShowIntroduction}
|
||||||
showPickerHint={showPickerHint}
|
showPickerHint={showPickerHint}
|
||||||
clearShowPickerHint={clearShowPickerHint}
|
clearShowPickerHint={clearShowPickerHint}
|
||||||
|
@ -735,16 +742,19 @@ export function CompositionArea({
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<MessageRequestActions
|
<MessageRequestActions
|
||||||
acceptConversation={acceptConversation}
|
addedByName={addedByName}
|
||||||
blockAndReportSpam={blockAndReportSpam}
|
|
||||||
blockConversation={blockConversation}
|
|
||||||
conversationId={conversationId}
|
|
||||||
conversationType={conversationType}
|
conversationType={conversationType}
|
||||||
deleteConversation={deleteConversation}
|
conversationId={conversationId}
|
||||||
|
conversationName={conversationName}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isBlocked={isBlocked}
|
isBlocked={isBlocked}
|
||||||
isHidden={removalStage !== undefined}
|
isHidden={isHidden}
|
||||||
title={title}
|
isReported={isReported}
|
||||||
|
acceptConversation={acceptConversation}
|
||||||
|
reportSpam={reportSpam}
|
||||||
|
blockAndReportSpam={blockAndReportSpam}
|
||||||
|
blockConversation={blockConversation}
|
||||||
|
deleteConversation={deleteConversation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -788,14 +798,18 @@ export function CompositionArea({
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<MandatoryProfileSharingActions
|
<MandatoryProfileSharingActions
|
||||||
acceptConversation={acceptConversation}
|
addedByName={addedByName}
|
||||||
blockAndReportSpam={blockAndReportSpam}
|
|
||||||
blockConversation={blockConversation}
|
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
conversationType={conversationType}
|
conversationType={conversationType}
|
||||||
deleteConversation={deleteConversation}
|
conversationName={conversationName}
|
||||||
i18n={i18n}
|
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}
|
platform={platform}
|
||||||
sendCounter={sendCounter}
|
sendCounter={sendCounter}
|
||||||
shouldHidePopovers={shouldHidePopovers}
|
shouldHidePopovers={shouldHidePopovers}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone ?? null}
|
||||||
sortedGroupMembers={sortedGroupMembers}
|
sortedGroupMembers={sortedGroupMembers}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
|
@ -1031,4 +1045,4 @@ export function CompositionArea({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
@ -21,30 +21,38 @@ export default {
|
||||||
args: {},
|
args: {},
|
||||||
} satisfies Meta<Props>;
|
} satisfies Meta<Props>;
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const useProps = (overrideProps: Partial<Props> = {}): Props => {
|
||||||
i18n,
|
const conversation = getDefaultConversation();
|
||||||
disabled: overrideProps.disabled ?? false,
|
return {
|
||||||
draftText: overrideProps.draftText || undefined,
|
i18n,
|
||||||
draftBodyRanges: overrideProps.draftBodyRanges || [],
|
conversationId: conversation.id,
|
||||||
clearQuotedMessage: action('clearQuotedMessage'),
|
disabled: overrideProps.disabled ?? false,
|
||||||
getPreferredBadge: () => undefined,
|
draftText: overrideProps.draftText ?? null,
|
||||||
getQuotedMessage: action('getQuotedMessage'),
|
draftEditMessage: overrideProps.draftEditMessage ?? null,
|
||||||
isFormattingEnabled:
|
draftBodyRanges: overrideProps.draftBodyRanges || [],
|
||||||
overrideProps.isFormattingEnabled === false
|
clearQuotedMessage: action('clearQuotedMessage'),
|
||||||
? overrideProps.isFormattingEnabled
|
getPreferredBadge: () => undefined,
|
||||||
: true,
|
getQuotedMessage: action('getQuotedMessage'),
|
||||||
large: overrideProps.large ?? false,
|
isFormattingEnabled:
|
||||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
overrideProps.isFormattingEnabled === false
|
||||||
onEditorStateChange: action('onEditorStateChange'),
|
? overrideProps.isFormattingEnabled
|
||||||
onPickEmoji: action('onPickEmoji'),
|
: true,
|
||||||
onSubmit: action('onSubmit'),
|
large: overrideProps.large ?? false,
|
||||||
onTextTooLong: action('onTextTooLong'),
|
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||||
platform: 'darwin',
|
onEditorStateChange: action('onEditorStateChange'),
|
||||||
sendCounter: 0,
|
onPickEmoji: action('onPickEmoji'),
|
||||||
sortedGroupMembers: overrideProps.sortedGroupMembers ?? [],
|
onSubmit: action('onSubmit'),
|
||||||
skinTone: overrideProps.skinTone ?? undefined,
|
onTextTooLong: action('onTextTooLong'),
|
||||||
theme: React.useContext(StorybookThemeContext),
|
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 {
|
export function Default(): JSX.Element {
|
||||||
const props = useProps();
|
const props = useProps();
|
||||||
|
|
|
@ -96,22 +96,22 @@ export type InputApi = {
|
||||||
|
|
||||||
export type Props = Readonly<{
|
export type Props = Readonly<{
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
conversationId?: string;
|
conversationId: string | null;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
draftEditMessage?: DraftEditMessageType;
|
draftEditMessage: DraftEditMessageType | null;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
large?: boolean;
|
large: boolean | null;
|
||||||
inputApi?: React.MutableRefObject<InputApi | undefined>;
|
inputApi: React.MutableRefObject<InputApi | undefined> | null;
|
||||||
isFormattingEnabled: boolean;
|
isFormattingEnabled: boolean;
|
||||||
sendCounter: number;
|
sendCounter: number;
|
||||||
skinTone?: EmojiPickDataType['skinTone'];
|
skinTone: NonNullable<EmojiPickDataType['skinTone']> | null;
|
||||||
draftText?: string;
|
draftText: string | null;
|
||||||
draftBodyRanges?: HydratedBodyRangesType;
|
draftBodyRanges: HydratedBodyRangesType | null;
|
||||||
moduleClassName?: string;
|
moduleClassName?: string;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
sortedGroupMembers?: ReadonlyArray<ConversationType>;
|
sortedGroupMembers: ReadonlyArray<ConversationType> | null;
|
||||||
scrollerRef?: React.RefObject<HTMLDivElement>;
|
scrollerRef?: React.RefObject<HTMLDivElement>;
|
||||||
onDirtyChange?(dirty: boolean): unknown;
|
onDirtyChange?(dirty: boolean): unknown;
|
||||||
onEditorStateChange?(options: {
|
onEditorStateChange?(options: {
|
||||||
|
@ -132,11 +132,11 @@ export type Props = Readonly<{
|
||||||
): unknown;
|
): unknown;
|
||||||
onScroll?: (ev: React.UIEvent<HTMLElement>) => void;
|
onScroll?: (ev: React.UIEvent<HTMLElement>) => void;
|
||||||
platform: string;
|
platform: string;
|
||||||
shouldHidePopovers?: boolean;
|
shouldHidePopovers: boolean | null;
|
||||||
getQuotedMessage?(): unknown;
|
getQuotedMessage?(): unknown;
|
||||||
clearQuotedMessage?(): unknown;
|
clearQuotedMessage?(): unknown;
|
||||||
linkPreviewLoading?: boolean;
|
linkPreviewLoading?: boolean;
|
||||||
linkPreviewResult?: LinkPreviewType;
|
linkPreviewResult: LinkPreviewType | null;
|
||||||
onCloseLinkPreview?(conversationId: string): unknown;
|
onCloseLinkPreview?(conversationId: string): unknown;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -562,7 +562,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
onEditorStateChange({
|
onEditorStateChange({
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
caretLocation: selection ? selection.index : undefined,
|
caretLocation: selection ? selection.index : undefined,
|
||||||
conversationId,
|
conversationId: conversationId ?? undefined,
|
||||||
messageText: text,
|
messageText: text,
|
||||||
sendCounter,
|
sendCounter,
|
||||||
});
|
});
|
||||||
|
@ -612,7 +612,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const emojiCompletion = emojiCompletionRef.current;
|
const emojiCompletion = emojiCompletionRef.current;
|
||||||
|
|
||||||
if (emojiCompletion === undefined || skinTone === undefined) {
|
if (emojiCompletion == null || skinTone == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
import * as grapheme from '../util/grapheme';
|
import * as grapheme from '../util/grapheme';
|
||||||
|
|
||||||
export type CompositionTextAreaProps = {
|
export type CompositionTextAreaProps = {
|
||||||
bodyRanges?: HydratedBodyRangesType;
|
bodyRanges: HydratedBodyRangesType | null;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isFormattingEnabled: boolean;
|
isFormattingEnabled: boolean;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
@ -153,6 +153,17 @@ export function CompositionTextArea({
|
||||||
scrollerRef={scrollerRef}
|
scrollerRef={scrollerRef}
|
||||||
sendCounter={0}
|
sendCounter={0}
|
||||||
theme={theme}
|
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">
|
<div className="CompositionTextArea__emoji">
|
||||||
<EmojiButton
|
<EmojiButton
|
||||||
|
|
|
@ -470,7 +470,7 @@ function ForwardMessageEditor({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<RenderCompositionTextArea
|
<RenderCompositionTextArea
|
||||||
bodyRanges={draft.bodyRanges}
|
bodyRanges={draft.bodyRanges ?? null}
|
||||||
draftText={draft.messageBody ?? ''}
|
draftText={draft.messageBody ?? ''}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
EditHistoryMessagesType,
|
EditHistoryMessagesType,
|
||||||
FormattingWarningDataType,
|
FormattingWarningDataType,
|
||||||
ForwardMessagesPropsType,
|
ForwardMessagesPropsType,
|
||||||
|
MessageRequestActionsConfirmationPropsType,
|
||||||
SafetyNumberChangedBlockingDataType,
|
SafetyNumberChangedBlockingDataType,
|
||||||
SendEditWarningDataType,
|
SendEditWarningDataType,
|
||||||
UserNotFoundModalStateType,
|
UserNotFoundModalStateType,
|
||||||
|
@ -59,6 +60,9 @@ export type PropsType = {
|
||||||
// ForwardMessageModal
|
// ForwardMessageModal
|
||||||
forwardMessagesProps: ForwardMessagesPropsType | undefined;
|
forwardMessagesProps: ForwardMessagesPropsType | undefined;
|
||||||
renderForwardMessagesModal: () => JSX.Element;
|
renderForwardMessagesModal: () => JSX.Element;
|
||||||
|
// MessageRequestActionsConfirmation
|
||||||
|
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||||
|
renderMessageRequestActionsConfirmation: () => JSX.Element;
|
||||||
// ProfileEditor
|
// ProfileEditor
|
||||||
isProfileEditorVisible: boolean;
|
isProfileEditorVisible: boolean;
|
||||||
renderProfileEditor: () => JSX.Element;
|
renderProfileEditor: () => JSX.Element;
|
||||||
|
@ -130,6 +134,9 @@ export function GlobalModalContainer({
|
||||||
// ForwardMessageModal
|
// ForwardMessageModal
|
||||||
forwardMessagesProps,
|
forwardMessagesProps,
|
||||||
renderForwardMessagesModal,
|
renderForwardMessagesModal,
|
||||||
|
// MessageRequestActionsConfirmation
|
||||||
|
messageRequestActionsConfirmationProps,
|
||||||
|
renderMessageRequestActionsConfirmation,
|
||||||
// ProfileEditor
|
// ProfileEditor
|
||||||
isProfileEditorVisible,
|
isProfileEditorVisible,
|
||||||
renderProfileEditor,
|
renderProfileEditor,
|
||||||
|
@ -223,6 +230,10 @@ export function GlobalModalContainer({
|
||||||
return renderForwardMessagesModal();
|
return renderForwardMessagesModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (messageRequestActionsConfirmationProps) {
|
||||||
|
return renderMessageRequestActionsConfirmation();
|
||||||
|
}
|
||||||
|
|
||||||
if (isProfileEditorVisible) {
|
if (isProfileEditorVisible) {
|
||||||
return renderProfileEditor();
|
return renderProfileEditor();
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,13 +176,12 @@ export function MediaEditor({
|
||||||
const [isEmojiPopperOpen, setEmojiPopperOpen] = useState<boolean>(false);
|
const [isEmojiPopperOpen, setEmojiPopperOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const [caption, setCaption] = useState(draftText ?? '');
|
const [caption, setCaption] = useState(draftText ?? '');
|
||||||
const [captionBodyRanges, setCaptionBodyRanges] = useState<
|
const [captionBodyRanges, setCaptionBodyRanges] =
|
||||||
DraftBodyRanges | undefined
|
useState<DraftBodyRanges | null>(draftBodyRanges);
|
||||||
>(draftBodyRanges);
|
|
||||||
|
|
||||||
const conversationSelector = useSelector(getConversationSelector);
|
const conversationSelector = useSelector(getConversationSelector);
|
||||||
const hydratedBodyRanges = useMemo(
|
const hydratedBodyRanges = useMemo(
|
||||||
() => hydrateRanges(captionBodyRanges, conversationSelector),
|
() => hydrateRanges(captionBodyRanges ?? undefined, conversationSelector),
|
||||||
[captionBodyRanges, conversationSelector]
|
[captionBodyRanges, conversationSelector]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1297,7 +1296,7 @@ export function MediaEditor({
|
||||||
<div className="MediaEditor__tools--input dark-theme">
|
<div className="MediaEditor__tools--input dark-theme">
|
||||||
<CompositionInput
|
<CompositionInput
|
||||||
draftText={caption}
|
draftText={caption}
|
||||||
draftBodyRanges={hydratedBodyRanges}
|
draftBodyRanges={hydratedBodyRanges ?? null}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
inputApi={inputApiRef}
|
inputApi={inputApiRef}
|
||||||
|
@ -1308,6 +1307,7 @@ export function MediaEditor({
|
||||||
setCaptionBodyRanges(bodyRanges);
|
setCaptionBodyRanges(bodyRanges);
|
||||||
setCaption(messageText);
|
setCaption(messageText);
|
||||||
}}
|
}}
|
||||||
|
skinTone={skinTone ?? null}
|
||||||
onPickEmoji={onPickEmoji}
|
onPickEmoji={onPickEmoji}
|
||||||
onSubmit={noop}
|
onSubmit={noop}
|
||||||
onTextTooLong={onTextTooLong}
|
onTextTooLong={onTextTooLong}
|
||||||
|
@ -1316,6 +1316,16 @@ export function MediaEditor({
|
||||||
sendCounter={0}
|
sendCounter={0}
|
||||||
sortedGroupMembers={sortedGroupMembers}
|
sortedGroupMembers={sortedGroupMembers}
|
||||||
theme={ThemeType.dark}
|
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
|
<EmojiButton
|
||||||
className="StoryViewsNRepliesModal__emoji-button"
|
className="StoryViewsNRepliesModal__emoji-button"
|
||||||
|
@ -1394,7 +1404,7 @@ export function MediaEditor({
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
data,
|
data,
|
||||||
caption: caption !== '' ? caption : undefined,
|
caption: caption !== '' ? caption : undefined,
|
||||||
captionBodyRanges,
|
captionBodyRanges: captionBodyRanges ?? undefined,
|
||||||
blurHash,
|
blurHash,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import { animated } from '@react-spring/web';
|
import { animated } from '@react-spring/web';
|
||||||
|
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { ModalHost } from './ModalHost';
|
import { ModalHost } from './ModalHost';
|
||||||
import type { Theme } from '../util/theme';
|
import type { Theme } from '../util/theme';
|
||||||
|
@ -37,6 +38,7 @@ type PropsType = {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
useFocusTrap?: boolean;
|
useFocusTrap?: boolean;
|
||||||
padded?: boolean;
|
padded?: boolean;
|
||||||
|
['aria-describedby']?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModalPropsType = PropsType & {
|
export type ModalPropsType = PropsType & {
|
||||||
|
@ -65,6 +67,7 @@ export function Modal({
|
||||||
hasFooterDivider = false,
|
hasFooterDivider = false,
|
||||||
noTransform = false,
|
noTransform = false,
|
||||||
padded = true,
|
padded = true,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
}: Readonly<ModalPropsType>): JSX.Element | null {
|
}: Readonly<ModalPropsType>): JSX.Element | null {
|
||||||
const { close, isClosed, modalStyles, overlayStyles } = useAnimated(
|
const { close, isClosed, modalStyles, overlayStyles } = useAnimated(
|
||||||
onClose,
|
onClose,
|
||||||
|
@ -132,6 +135,7 @@ export function Modal({
|
||||||
padded={padded}
|
padded={padded}
|
||||||
hasHeaderDivider={hasHeaderDivider}
|
hasHeaderDivider={hasHeaderDivider}
|
||||||
hasFooterDivider={hasFooterDivider}
|
hasFooterDivider={hasFooterDivider}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ModalPage>
|
</ModalPage>
|
||||||
|
@ -173,6 +177,7 @@ export function ModalPage({
|
||||||
padded = true,
|
padded = true,
|
||||||
hasHeaderDivider = false,
|
hasHeaderDivider = false,
|
||||||
hasFooterDivider = false,
|
hasFooterDivider = false,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
}: ModalPageProps): JSX.Element {
|
}: ModalPageProps): JSX.Element {
|
||||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
@ -188,6 +193,8 @@ export function ModalPage({
|
||||||
);
|
);
|
||||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||||
|
|
||||||
|
const [id] = useState(() => uuid());
|
||||||
|
|
||||||
useScrollObserver(bodyRef, bodyInnerRef, scroll => {
|
useScrollObserver(bodyRef, bodyInnerRef, scroll => {
|
||||||
setScrolled(isScrolled(scroll));
|
setScrolled(isScrolled(scroll));
|
||||||
setScrolledToBottom(isScrolledToBottom(scroll));
|
setScrolledToBottom(isScrolledToBottom(scroll));
|
||||||
|
@ -198,7 +205,7 @@ export function ModalPage({
|
||||||
<>
|
<>
|
||||||
{/* We don't want the click event to propagate to its container node. */}
|
{/* We don't want the click event to propagate to its container node. */}
|
||||||
{/* eslint-disable-next-line max-len */}
|
{/* 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
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
getClassName(''),
|
getClassName(''),
|
||||||
|
@ -209,6 +216,10 @@ export function ModalPage({
|
||||||
hasFooterDivider && getClassName('--footer-divider')
|
hasFooterDivider && getClassName('--footer-divider')
|
||||||
)}
|
)}
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
|
role="dialog"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-labelledby={title ? `${id}-title` : undefined}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
@ -234,6 +245,7 @@ export function ModalPage({
|
||||||
)}
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
<h1
|
<h1
|
||||||
|
id={`${id}-title`}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
getClassName('__title'),
|
getClassName('__title'),
|
||||||
hasXButton ? getClassName('__title--with-x-button') : null
|
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<
|
Pick<
|
||||||
MediaEditorPropsType,
|
MediaEditorPropsType,
|
||||||
'isFormattingEnabled' | 'onPickEmoji' | 'onTextTooLong' | 'platform'
|
| 'isFormattingEnabled'
|
||||||
|
| 'onPickEmoji'
|
||||||
|
| 'onTextTooLong'
|
||||||
|
| 'platform'
|
||||||
|
| 'sortedGroupMembers'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function StoryCreator({
|
export function StoryCreator({
|
||||||
|
@ -139,6 +143,7 @@ export function StoryCreator({
|
||||||
setMyStoriesToAllSignalConnections,
|
setMyStoriesToAllSignalConnections,
|
||||||
signalConnections,
|
signalConnections,
|
||||||
skinTone,
|
skinTone,
|
||||||
|
sortedGroupMembers,
|
||||||
theme,
|
theme,
|
||||||
toggleGroupsForStorySend,
|
toggleGroupsForStorySend,
|
||||||
toggleSignalConnectionsModal,
|
toggleSignalConnectionsModal,
|
||||||
|
@ -272,6 +277,9 @@ export function StoryCreator({
|
||||||
platform={platform}
|
platform={platform}
|
||||||
recentStickers={recentStickers}
|
recentStickers={recentStickers}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
|
sortedGroupMembers={sortedGroupMembers}
|
||||||
|
draftText={null}
|
||||||
|
draftBodyRanges={null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!file && (
|
{!file && (
|
||||||
|
|
|
@ -258,8 +258,15 @@ export function StoryViewsNRepliesModal({
|
||||||
}
|
}
|
||||||
platform={platform}
|
platform={platform}
|
||||||
sendCounter={0}
|
sendCounter={0}
|
||||||
sortedGroupMembers={sortedGroupMembers}
|
skinTone={skinTone ?? null}
|
||||||
|
sortedGroupMembers={sortedGroupMembers ?? null}
|
||||||
theme={ThemeType.dark}
|
theme={ThemeType.dark}
|
||||||
|
conversationId={null}
|
||||||
|
draftBodyRanges={null}
|
||||||
|
draftEditMessage={null}
|
||||||
|
large={null}
|
||||||
|
shouldHidePopovers={null}
|
||||||
|
linkPreviewResult={null}
|
||||||
>
|
>
|
||||||
<EmojiButton
|
<EmojiButton
|
||||||
className="StoryViewsNRepliesModal__emoji-button"
|
className="StoryViewsNRepliesModal__emoji-button"
|
||||||
|
|
|
@ -121,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast {
|
||||||
return { toastType: ToastType.PinnedConversationsFull };
|
return { toastType: ToastType.PinnedConversationsFull };
|
||||||
case ToastType.ReactionFailed:
|
case ToastType.ReactionFailed:
|
||||||
return { toastType: ToastType.ReactionFailed };
|
return { toastType: ToastType.ReactionFailed };
|
||||||
|
case ToastType.ReportedSpam:
|
||||||
|
return { toastType: ToastType.ReportedSpam };
|
||||||
case ToastType.ReportedSpamAndBlocked:
|
case ToastType.ReportedSpamAndBlocked:
|
||||||
return { toastType: ToastType.ReportedSpamAndBlocked };
|
return { toastType: ToastType.ReportedSpamAndBlocked };
|
||||||
case ToastType.StickerPackInstallFailed:
|
case ToastType.StickerPackInstallFailed:
|
||||||
|
|
|
@ -371,6 +371,14 @@ export function renderToast({
|
||||||
return <Toast onClose={hideToast}>{i18n('icu:Reactions--error')}</Toast>;
|
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) {
|
if (toastType === ToastType.ReportedSpamAndBlocked) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
|
|
|
@ -1,21 +1,47 @@
|
||||||
// Copyright 2018 Signal Messenger, LLC
|
// Copyright 2018 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Emojify } from './Emojify';
|
import { Emojify } from './Emojify';
|
||||||
import type { ContactNameColorType } from '../../types/Colors';
|
import type { ContactNameColorType } from '../../types/Colors';
|
||||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
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;
|
contactNameColor?: ContactNameColorType;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
isSignalConversation?: boolean;
|
isSignalConversation?: boolean;
|
||||||
isMe?: 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;
|
module?: string;
|
||||||
preferFirstName?: boolean;
|
preferFirstName?: boolean;
|
||||||
title: string;
|
|
||||||
onClick?: VoidFunction;
|
onClick?: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
||||||
|
|
||||||
const getCommonProps = () => ({
|
const getCommonProps = () => ({
|
||||||
acceptConversation: action('acceptConversation'),
|
acceptConversation: action('acceptConversation'),
|
||||||
|
reportSpam: action('reportSpam'),
|
||||||
blockAndReportSpam: action('blockAndReportSpam'),
|
blockAndReportSpam: action('blockAndReportSpam'),
|
||||||
blockConversation: action('blockConversation'),
|
blockConversation: action('blockConversation'),
|
||||||
conversationId: 'some-conversation-id',
|
conversationId: 'some-conversation-id',
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type ReviewPropsType = Readonly<
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
acceptConversation: (conversationId: string) => unknown;
|
acceptConversation: (conversationId: string) => unknown;
|
||||||
|
reportSpam: (conversationId: string) => unknown;
|
||||||
blockAndReportSpam: (conversationId: string) => unknown;
|
blockAndReportSpam: (conversationId: string) => unknown;
|
||||||
blockConversation: (conversationId: string) => unknown;
|
blockConversation: (conversationId: string) => unknown;
|
||||||
deleteConversation: (conversationId: string) => unknown;
|
deleteConversation: (conversationId: string) => unknown;
|
||||||
|
@ -75,6 +76,7 @@ enum ConfirmationStateType {
|
||||||
export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
||||||
const {
|
const {
|
||||||
acceptConversation,
|
acceptConversation,
|
||||||
|
reportSpam,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -111,19 +113,23 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
||||||
case ConfirmationStateType.ConfirmingBlock:
|
case ConfirmationStateType.ConfirmingBlock:
|
||||||
return (
|
return (
|
||||||
<MessageRequestActionsConfirmation
|
<MessageRequestActionsConfirmation
|
||||||
acceptConversation={acceptConversation}
|
addedByName={affectedConversation}
|
||||||
blockAndReportSpam={blockAndReportSpam}
|
|
||||||
blockConversation={blockConversation}
|
|
||||||
conversationId={affectedConversation.id}
|
conversationId={affectedConversation.id}
|
||||||
conversationType="direct"
|
conversationType={affectedConversation.type}
|
||||||
deleteConversation={deleteConversation}
|
conversationName={affectedConversation}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
title={affectedConversation.title}
|
isBlocked={affectedConversation.isBlocked ?? false}
|
||||||
|
isReported={affectedConversation.isReported ?? false}
|
||||||
state={
|
state={
|
||||||
type === ConfirmationStateType.ConfirmingDelete
|
type === ConfirmationStateType.ConfirmingDelete
|
||||||
? MessageRequestState.deleting
|
? MessageRequestState.deleting
|
||||||
: MessageRequestState.blocking
|
: MessageRequestState.blocking
|
||||||
}
|
}
|
||||||
|
acceptConversation={acceptConversation}
|
||||||
|
reportSpam={reportSpam}
|
||||||
|
blockAndReportSpam={blockAndReportSpam}
|
||||||
|
blockConversation={blockConversation}
|
||||||
|
deleteConversation={deleteConversation}
|
||||||
onChangeState={messageRequestState => {
|
onChangeState={messageRequestState => {
|
||||||
switch (messageRequestState) {
|
switch (messageRequestState) {
|
||||||
case MessageRequestState.blocking:
|
case MessageRequestState.blocking:
|
||||||
|
@ -138,10 +144,12 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
||||||
affectedConversation,
|
affectedConversation,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case MessageRequestState.reportingAndMaybeBlocking:
|
||||||
|
case MessageRequestState.acceptedOptions:
|
||||||
case MessageRequestState.unblocking:
|
case MessageRequestState.unblocking:
|
||||||
assertDev(
|
assertDev(
|
||||||
false,
|
false,
|
||||||
'Got unexpected MessageRequestState.unblocking state. Clearing confiration state'
|
`Got unexpected MessageRequestState.${MessageRequestState[messageRequestState]} state. Clearing confiration state`
|
||||||
);
|
);
|
||||||
setConfirmationState(undefined);
|
setConfirmationState(undefined);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -29,8 +29,15 @@ type ItemsType = Array<{
|
||||||
props: Omit<ComponentProps<typeof ConversationHeader>, 'theme'>;
|
props: Omit<ComponentProps<typeof ConversationHeader>, 'theme'>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
const commonConversation = getDefaultConversation();
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
...getDefaultConversation(),
|
...commonConversation,
|
||||||
|
conversationId: commonConversation.id,
|
||||||
|
conversationType: commonConversation.type,
|
||||||
|
conversationName: commonConversation,
|
||||||
|
addedByName: null,
|
||||||
|
isBlocked: commonConversation.isBlocked ?? false,
|
||||||
|
isReported: commonConversation.isReported ?? false,
|
||||||
|
|
||||||
cannotLeaveBecauseYouAreLastAdmin: false,
|
cannotLeaveBecauseYouAreLastAdmin: false,
|
||||||
showBackButton: false,
|
showBackButton: false,
|
||||||
|
@ -59,6 +66,12 @@ const commonProps = {
|
||||||
setMuteExpiration: action('onSetMuteNotifications'),
|
setMuteExpiration: action('onSetMuteNotifications'),
|
||||||
setPinned: action('setPinned'),
|
setPinned: action('setPinned'),
|
||||||
viewUserStories: action('viewUserStories'),
|
viewUserStories: action('viewUserStories'),
|
||||||
|
|
||||||
|
acceptConversation: action('acceptConversation'),
|
||||||
|
blockAndReportSpam: action('blockAndReportSpam'),
|
||||||
|
blockConversation: action('blockConversation'),
|
||||||
|
reportSpam: action('reportSpam'),
|
||||||
|
deleteConversation: action('deleteConversation'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PrivateConvo(): JSX.Element {
|
export function PrivateConvo(): JSX.Element {
|
||||||
|
|
|
@ -41,6 +41,12 @@ import { PanelType } from '../../types/Panels';
|
||||||
import { UserText } from '../UserText';
|
import { UserText } from '../UserText';
|
||||||
import { Alert } from '../Alert';
|
import { Alert } from '../Alert';
|
||||||
import { SizeObserver } from '../../hooks/useSizeObserver';
|
import { SizeObserver } from '../../hooks/useSizeObserver';
|
||||||
|
import type { MessageRequestActionsConfirmationBaseProps } from './MessageRequestActionsConfirmation';
|
||||||
|
import {
|
||||||
|
MessageRequestActionsConfirmation,
|
||||||
|
MessageRequestState,
|
||||||
|
} from './MessageRequestActionsConfirmation';
|
||||||
|
import type { ContactNameData } from './ContactName';
|
||||||
|
|
||||||
export enum OutgoingCallButtonStyle {
|
export enum OutgoingCallButtonStyle {
|
||||||
None,
|
None,
|
||||||
|
@ -60,6 +66,8 @@ export type PropsDataType = {
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
isSignalConversation?: boolean;
|
isSignalConversation?: boolean;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
|
addedByName: ContactNameData | null;
|
||||||
|
conversationName: ContactNameData;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
| 'acceptedMessageRequest'
|
| 'acceptedMessageRequest'
|
||||||
|
@ -72,6 +80,8 @@ export type PropsDataType = {
|
||||||
| 'groupVersion'
|
| 'groupVersion'
|
||||||
| 'id'
|
| 'id'
|
||||||
| 'isArchived'
|
| 'isArchived'
|
||||||
|
| 'isBlocked'
|
||||||
|
| 'isReported'
|
||||||
| 'isMe'
|
| 'isMe'
|
||||||
| 'isPinned'
|
| 'isPinned'
|
||||||
| 'isVerified'
|
| 'isVerified'
|
||||||
|
@ -81,6 +91,7 @@ export type PropsDataType = {
|
||||||
| 'name'
|
| 'name'
|
||||||
| 'phoneNumber'
|
| 'phoneNumber'
|
||||||
| 'profileName'
|
| 'profileName'
|
||||||
|
| 'removalStage'
|
||||||
| 'sharedGroupNames'
|
| 'sharedGroupNames'
|
||||||
| 'title'
|
| 'title'
|
||||||
| 'type'
|
| 'type'
|
||||||
|
@ -106,7 +117,7 @@ export type PropsActionsType = {
|
||||||
setMuteExpiration: (conversationId: string, seconds: number) => void;
|
setMuteExpiration: (conversationId: string, seconds: number) => void;
|
||||||
setPinned: (conversationId: string, value: boolean) => void;
|
setPinned: (conversationId: string, value: boolean) => void;
|
||||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||||
};
|
} & MessageRequestActionsConfirmationBaseProps;
|
||||||
|
|
||||||
export type PropsHousekeepingType = {
|
export type PropsHousekeepingType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -127,6 +138,7 @@ type StateType = {
|
||||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: boolean;
|
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: boolean;
|
||||||
isNarrow: boolean;
|
isNarrow: boolean;
|
||||||
modalState: ModalState;
|
modalState: ModalState;
|
||||||
|
messageRequestState: MessageRequestState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
||||||
|
@ -149,6 +161,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false,
|
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false,
|
||||||
isNarrow: false,
|
isNarrow: false,
|
||||||
modalState: ModalState.NothingOpen,
|
modalState: ModalState.NothingOpen,
|
||||||
|
messageRequestState: MessageRequestState.default,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.menuTriggerRef = React.createRef();
|
this.menuTriggerRef = React.createRef();
|
||||||
|
@ -156,6 +169,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
this.showMenuBound = this.showMenu.bind(this);
|
this.showMenuBound = this.showMenu.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleMessageRequestStateChange = (
|
||||||
|
state: MessageRequestState
|
||||||
|
): void => {
|
||||||
|
this.setState({ messageRequestState: state });
|
||||||
|
};
|
||||||
|
|
||||||
private showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
|
private showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
|
||||||
if (this.menuTriggerRef.current) {
|
if (this.menuTriggerRef.current) {
|
||||||
this.menuTriggerRef.current.handleContextClick(event);
|
this.menuTriggerRef.current.handleContextClick(event);
|
||||||
|
@ -328,6 +347,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
private renderMenu(triggerId: string): ReactNode {
|
private renderMenu(triggerId: string): ReactNode {
|
||||||
const {
|
const {
|
||||||
|
acceptConversation,
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
canChangeTimer,
|
canChangeTimer,
|
||||||
cannotLeaveBecauseYouAreLastAdmin,
|
cannotLeaveBecauseYouAreLastAdmin,
|
||||||
|
@ -336,6 +356,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isArchived,
|
isArchived,
|
||||||
|
isBlocked,
|
||||||
isMissingMandatoryProfileSharing,
|
isMissingMandatoryProfileSharing,
|
||||||
isPinned,
|
isPinned,
|
||||||
isSignalConversation,
|
isSignalConversation,
|
||||||
|
@ -431,6 +452,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
{i18n('icu:archiveConversation')}
|
{i18n('icu:archiveConversation')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
this.setState({ hasDeleteMessagesConfirmation: true })
|
this.setState({ hasDeleteMessagesConfirmation: true })
|
||||||
|
@ -491,98 +513,164 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<ContextMenu id={triggerId} rtl={isRTL}>
|
<ContextMenu id={triggerId} rtl={isRTL}>
|
||||||
{disableTimerChanges ? null : (
|
{!acceptedMessageRequest && (
|
||||||
<SubMenu hoverDelay={1} title={disappearingTitle} rtl={!isRTL}>
|
<>
|
||||||
{expireDurations}
|
{!isBlocked && (
|
||||||
</SubMenu>
|
<MenuItem
|
||||||
)}
|
onClick={() => {
|
||||||
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
this.setState({
|
||||||
{muteOptions.map(item => (
|
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
|
<MenuItem
|
||||||
key={item.name}
|
|
||||||
disabled={item.disabled}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMuteExpiration(id, item.value);
|
this.setState({
|
||||||
|
messageRequestState:
|
||||||
|
MessageRequestState.reportingAndMaybeBlocking,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.name}
|
{i18n('icu:ConversationHeader__MenuItem--ReportSpam')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
<MenuItem
|
||||||
</SubMenu>
|
onClick={() => {
|
||||||
{!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) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
|
messageRequestState: MessageRequestState.deleting,
|
||||||
});
|
});
|
||||||
} else {
|
}}
|
||||||
this.setState({ hasLeaveGroupConfirmation: true });
|
>
|
||||||
}
|
{i18n('icu:ConversationHeader__MenuItem--DeleteChat')}
|
||||||
}}
|
</MenuItem>
|
||||||
>
|
</>
|
||||||
{i18n(
|
)}
|
||||||
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
|
{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>,
|
</ContextMenu>,
|
||||||
document.body
|
document.body
|
||||||
|
@ -751,25 +839,35 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
public override render(): ReactNode {
|
public override render(): ReactNode {
|
||||||
const {
|
const {
|
||||||
|
addedByName,
|
||||||
announcementsOnly,
|
announcementsOnly,
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
|
conversationName,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
hasPanelShowing,
|
hasPanelShowing,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
|
isBlocked,
|
||||||
|
isReported,
|
||||||
isSMSOnly,
|
isSMSOnly,
|
||||||
isSignalConversation,
|
isSignalConversation,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
outgoingCallButtonStyle,
|
outgoingCallButtonStyle,
|
||||||
setDisappearingMessages,
|
setDisappearingMessages,
|
||||||
|
type,
|
||||||
|
acceptConversation,
|
||||||
|
blockAndReportSpam,
|
||||||
|
blockConversation,
|
||||||
|
reportSpam,
|
||||||
|
deleteConversation,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (hasPanelShowing) {
|
if (hasPanelShowing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isNarrow, modalState } = this.state;
|
const { isNarrow, modalState, messageRequestState } = this.state;
|
||||||
const triggerId = `conversation-${id}`;
|
const triggerId = `conversation-${id}`;
|
||||||
|
|
||||||
let modalNode: ReactNode;
|
let modalNode: ReactNode;
|
||||||
|
@ -829,6 +927,22 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
{this.renderSearchButton()}
|
{this.renderSearchButton()}
|
||||||
{this.renderMoreButton(triggerId)}
|
{this.renderMoreButton(triggerId)}
|
||||||
{this.renderMenu(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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SizeObserver>
|
</SizeObserver>
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { StoryViewModeType } from '../../types/Stories';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||||
|
import { Button, ButtonVariant } from '../Button';
|
||||||
|
import { SafetyTipsModal } from '../SafetyTipsModal';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
about?: string;
|
about?: string;
|
||||||
|
@ -42,6 +44,7 @@ const renderMembershipRow = ({
|
||||||
i18n,
|
i18n,
|
||||||
isMe,
|
isMe,
|
||||||
onClickMessageRequestWarning,
|
onClickMessageRequestWarning,
|
||||||
|
onToggleSafetyTips,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
|
@ -54,6 +57,7 @@ const renderMembershipRow = ({
|
||||||
> &
|
> &
|
||||||
Required<Pick<Props, 'sharedGroupNames'>> & {
|
Required<Pick<Props, 'sharedGroupNames'>> & {
|
||||||
onClickMessageRequestWarning: () => void;
|
onClickMessageRequestWarning: () => void;
|
||||||
|
onToggleSafetyTips: (showSafetyTips: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
if (conversationType !== 'direct') {
|
if (conversationType !== 'direct') {
|
||||||
return null;
|
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) {
|
if (sharedGroupNames.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className="module-conversation-hero__membership">
|
<div className="module-conversation-hero__membership">
|
||||||
|
@ -76,6 +94,7 @@ const renderMembershipRow = ({
|
||||||
nameClassName="module-conversation-hero__membership__name"
|
nameClassName="module-conversation-hero__membership__name"
|
||||||
sharedGroupNames={sharedGroupNames}
|
sharedGroupNames={sharedGroupNames}
|
||||||
/>
|
/>
|
||||||
|
{safetyTipsButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -86,6 +105,7 @@ const renderMembershipRow = ({
|
||||||
return (
|
return (
|
||||||
<div className="module-conversation-hero__membership">
|
<div className="module-conversation-hero__membership">
|
||||||
{i18n('icu:no-groups-in-common')}
|
{i18n('icu:no-groups-in-common')}
|
||||||
|
{safetyTipsButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -107,6 +127,7 @@ const renderMembershipRow = ({
|
||||||
{i18n('icu:MessageRequestWarning__learn-more')}
|
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{safetyTipsButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -136,6 +157,7 @@ export function ConversationHero({
|
||||||
viewUserStories,
|
viewUserStories,
|
||||||
toggleAboutContactModal,
|
toggleAboutContactModal,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
|
const [isShowingSafetyTips, setIsShowingSafetyTips] = useState(false);
|
||||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const closeMessageRequestWarning = () => {
|
const closeMessageRequestWarning = () => {
|
||||||
|
@ -248,6 +270,9 @@ export function ConversationHero({
|
||||||
onClickMessageRequestWarning() {
|
onClickMessageRequestWarning() {
|
||||||
setIsShowingMessageRequestWarning(true);
|
setIsShowingMessageRequestWarning(true);
|
||||||
},
|
},
|
||||||
|
onToggleSafetyTips(showSafetyTips: boolean) {
|
||||||
|
setIsShowingSafetyTips(showSafetyTips);
|
||||||
|
},
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
})}
|
})}
|
||||||
|
@ -277,6 +302,15 @@ export function ConversationHero({
|
||||||
{i18n('icu:MessageRequestWarning__dialog__details')}
|
{i18n('icu:MessageRequestWarning__dialog__details')}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isShowingSafetyTips && (
|
||||||
|
<SafetyTipsModal
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => {
|
||||||
|
setIsShowingSafetyTips(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
/* eslint-enable no-nested-ternary */
|
/* eslint-enable no-nested-ternary */
|
||||||
|
|
|
@ -8,9 +8,17 @@ import type { Props } from './MandatoryProfileSharingActions';
|
||||||
import { MandatoryProfileSharingActions } from './MandatoryProfileSharingActions';
|
import { MandatoryProfileSharingActions } from './MandatoryProfileSharingActions';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import {
|
||||||
|
getDefaultConversation,
|
||||||
|
getDefaultGroup,
|
||||||
|
} from '../../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
conversationType: 'direct' | 'group';
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/Conversation/MandatoryProfileSharingActions',
|
title: 'Components/Conversation/MandatoryProfileSharingActions',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
@ -20,34 +28,43 @@ export default {
|
||||||
options: ['direct', 'group'],
|
options: ['direct', 'group'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
firstName: { control: { type: 'text' } },
|
|
||||||
title: { control: { type: 'text' } },
|
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
conversationId: '123',
|
|
||||||
i18n,
|
|
||||||
conversationType: 'direct',
|
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 (
|
return (
|
||||||
<div style={{ width: '480px' }}>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Direct(args: Props): JSX.Element {
|
||||||
|
return <Example {...args} conversationType="direct" />;
|
||||||
|
}
|
||||||
|
|
||||||
export function Group(args: Props): JSX.Element {
|
export function Group(args: Props): JSX.Element {
|
||||||
return (
|
return <Example {...args} conversationType="group" />;
|
||||||
<div style={{ width: '480px' }}>
|
|
||||||
<MandatoryProfileSharingActions {...args} conversationType="group" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { PropsType as ContactNameProps } from './ContactName';
|
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { Button, ButtonVariant } from '../Button';
|
import { Button, ButtonVariant } from '../Button';
|
||||||
import type { Props as MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
import type { MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||||
import {
|
import {
|
||||||
MessageRequestActionsConfirmation,
|
MessageRequestActionsConfirmation,
|
||||||
MessageRequestState,
|
MessageRequestState,
|
||||||
|
@ -15,17 +14,20 @@ import type { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
firstName?: string;
|
} & Pick<
|
||||||
} & Omit<ContactNameProps, 'module'> &
|
MessageRequestActionsConfirmationProps,
|
||||||
Pick<
|
| 'addedByName'
|
||||||
MessageRequestActionsConfirmationProps,
|
| 'conversationId'
|
||||||
| 'acceptConversation'
|
| 'conversationType'
|
||||||
| 'blockAndReportSpam'
|
| 'conversationName'
|
||||||
| 'blockConversation'
|
| 'isBlocked'
|
||||||
| 'conversationId'
|
| 'isReported'
|
||||||
| 'conversationType'
|
| 'acceptConversation'
|
||||||
| 'deleteConversation'
|
| 'reportSpam'
|
||||||
>;
|
| 'blockAndReportSpam'
|
||||||
|
| 'blockConversation'
|
||||||
|
| 'deleteConversation'
|
||||||
|
>;
|
||||||
|
|
||||||
const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
||||||
<a
|
<a
|
||||||
|
@ -39,15 +41,18 @@ const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export function MandatoryProfileSharingActions({
|
export function MandatoryProfileSharingActions({
|
||||||
acceptConversation,
|
addedByName,
|
||||||
blockAndReportSpam,
|
|
||||||
blockConversation,
|
|
||||||
conversationId,
|
conversationId,
|
||||||
conversationType,
|
conversationType,
|
||||||
deleteConversation,
|
conversationName,
|
||||||
firstName,
|
|
||||||
i18n,
|
i18n,
|
||||||
title,
|
isBlocked,
|
||||||
|
isReported,
|
||||||
|
acceptConversation,
|
||||||
|
reportSpam,
|
||||||
|
blockAndReportSpam,
|
||||||
|
blockConversation,
|
||||||
|
deleteConversation,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@ export function MandatoryProfileSharingActions({
|
||||||
key="name"
|
key="name"
|
||||||
className="module-message-request-actions__message__name"
|
className="module-message-request-actions__message__name"
|
||||||
>
|
>
|
||||||
<ContactName firstName={firstName} title={title} preferFirstName />
|
<ContactName {...conversationName} preferFirstName />
|
||||||
</strong>
|
</strong>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -64,19 +69,23 @@ export function MandatoryProfileSharingActions({
|
||||||
<>
|
<>
|
||||||
{mrState !== MessageRequestState.default ? (
|
{mrState !== MessageRequestState.default ? (
|
||||||
<MessageRequestActionsConfirmation
|
<MessageRequestActionsConfirmation
|
||||||
|
addedByName={addedByName}
|
||||||
|
conversationId={conversationId}
|
||||||
|
conversationType={conversationType}
|
||||||
|
conversationName={conversationName}
|
||||||
|
i18n={i18n}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
isReported={isReported}
|
||||||
|
state={mrState}
|
||||||
acceptConversation={() => {
|
acceptConversation={() => {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Should not be able to unblock from MandatoryProfileSharingActions'
|
'Should not be able to unblock from MandatoryProfileSharingActions'
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
blockConversation={blockConversation}
|
blockConversation={blockConversation}
|
||||||
conversationId={conversationId}
|
|
||||||
deleteConversation={deleteConversation}
|
deleteConversation={deleteConversation}
|
||||||
i18n={i18n}
|
reportSpam={reportSpam}
|
||||||
blockAndReportSpam={blockAndReportSpam}
|
blockAndReportSpam={blockAndReportSpam}
|
||||||
title={title}
|
|
||||||
conversationType={conversationType}
|
|
||||||
state={mrState}
|
|
||||||
onChangeState={setMrState}
|
onChangeState={setMrState}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -4,13 +4,23 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import type { Meta } from '@storybook/react';
|
import type { Meta } from '@storybook/react';
|
||||||
import type { Props } from './MessageRequestActions';
|
|
||||||
import { MessageRequestActions } from './MessageRequestActions';
|
import { MessageRequestActions } from './MessageRequestActions';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import {
|
||||||
|
getDefaultConversation,
|
||||||
|
getDefaultGroup,
|
||||||
|
} from '../../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
conversationType: 'direct' | 'group';
|
||||||
|
isBlocked: boolean;
|
||||||
|
isHidden: boolean;
|
||||||
|
isReported: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/Conversation/MessageRequestActions',
|
title: 'Components/Conversation/MessageRequestActions',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
@ -20,19 +30,9 @@ export default {
|
||||||
options: ['direct', 'group'],
|
options: ['direct', 'group'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
firstName: { control: { type: 'text' } },
|
|
||||||
title: { control: { type: 'text' } },
|
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
conversationId: '123',
|
|
||||||
i18n,
|
|
||||||
conversationType: 'direct',
|
conversationType: 'direct',
|
||||||
firstName: 'Cayce',
|
|
||||||
title: 'Cayce Bollard',
|
|
||||||
acceptConversation: action('acceptConversation'),
|
|
||||||
blockAndReportSpam: action('blockAndReportSpam'),
|
|
||||||
blockConversation: action('blockConversation'),
|
|
||||||
deleteConversation: action('deleteConversation'),
|
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story: React.ComponentType): JSX.Element => {
|
(Story: React.ComponentType): JSX.Element => {
|
||||||
|
@ -43,20 +43,62 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} satisfies Meta<Props>;
|
} satisfies Meta<Args>;
|
||||||
|
|
||||||
export function Direct(args: Props): JSX.Element {
|
function Example(args: Args): JSX.Element {
|
||||||
return <MessageRequestActions {...args} />;
|
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 {
|
export function Direct(args: Args): JSX.Element {
|
||||||
return <MessageRequestActions {...args} isBlocked />;
|
return <Example {...args} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Group(args: Props): JSX.Element {
|
export function DirectBlocked(args: Args): JSX.Element {
|
||||||
return <MessageRequestActions {...args} conversationType="group" />;
|
return <Example {...args} isBlocked />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupBlocked(args: Props): JSX.Element {
|
export function DirectReported(args: Args): JSX.Element {
|
||||||
return <MessageRequestActions {...args} conversationType="group" isBlocked />;
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { PropsType as ContactNameProps } from './ContactName';
|
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { Button, ButtonVariant } from '../Button';
|
import { Button, ButtonVariant } from '../Button';
|
||||||
import type { Props as MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
import type { MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||||
import {
|
import {
|
||||||
MessageRequestActionsConfirmation,
|
MessageRequestActionsConfirmation,
|
||||||
MessageRequestState,
|
MessageRequestState,
|
||||||
} from './MessageRequestActionsConfirmation';
|
} from './MessageRequestActionsConfirmation';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isHidden?: boolean;
|
isHidden: boolean | null;
|
||||||
} & Omit<ContactNameProps, 'module'> &
|
} & Omit<
|
||||||
Omit<
|
MessageRequestActionsConfirmationProps,
|
||||||
MessageRequestActionsConfirmationProps,
|
'i18n' | 'state' | 'onChangeState'
|
||||||
'i18n' | 'state' | 'onChangeState'
|
>;
|
||||||
>;
|
|
||||||
|
|
||||||
export function MessageRequestActions({
|
export function MessageRequestActions({
|
||||||
|
addedByName,
|
||||||
|
conversationId,
|
||||||
|
conversationType,
|
||||||
|
conversationName,
|
||||||
|
i18n,
|
||||||
|
isBlocked,
|
||||||
|
isHidden,
|
||||||
|
isReported,
|
||||||
acceptConversation,
|
acceptConversation,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
conversationId,
|
reportSpam,
|
||||||
conversationType,
|
|
||||||
deleteConversation,
|
deleteConversation,
|
||||||
firstName,
|
|
||||||
i18n,
|
|
||||||
isHidden,
|
|
||||||
isBlocked,
|
|
||||||
title,
|
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||||
|
|
||||||
const name = (
|
const nameValue =
|
||||||
<strong
|
conversationType === 'direct' ? conversationName : addedByName;
|
||||||
key="name"
|
|
||||||
className="module-message-request-actions__message__name"
|
|
||||||
>
|
|
||||||
<ContactName firstName={firstName} title={title} preferFirstName />
|
|
||||||
</strong>
|
|
||||||
);
|
|
||||||
|
|
||||||
let message: JSX.Element | undefined;
|
let message: JSX.Element | undefined;
|
||||||
if (conversationType === 'direct') {
|
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) {
|
if (isBlocked) {
|
||||||
message = (
|
message = (
|
||||||
<Intl
|
<Intl
|
||||||
|
@ -87,39 +92,26 @@ export function MessageRequestActions({
|
||||||
<>
|
<>
|
||||||
{mrState !== MessageRequestState.default ? (
|
{mrState !== MessageRequestState.default ? (
|
||||||
<MessageRequestActionsConfirmation
|
<MessageRequestActionsConfirmation
|
||||||
|
addedByName={addedByName}
|
||||||
|
conversationId={conversationId}
|
||||||
|
conversationType={conversationType}
|
||||||
|
conversationName={conversationName}
|
||||||
|
i18n={i18n}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
isReported={isReported}
|
||||||
|
state={mrState}
|
||||||
acceptConversation={acceptConversation}
|
acceptConversation={acceptConversation}
|
||||||
blockAndReportSpam={blockAndReportSpam}
|
blockAndReportSpam={blockAndReportSpam}
|
||||||
blockConversation={blockConversation}
|
blockConversation={blockConversation}
|
||||||
conversationId={conversationId}
|
reportSpam={reportSpam}
|
||||||
conversationType={conversationType}
|
|
||||||
deleteConversation={deleteConversation}
|
deleteConversation={deleteConversation}
|
||||||
i18n={i18n}
|
|
||||||
onChangeState={setMrState}
|
onChangeState={setMrState}
|
||||||
state={mrState}
|
|
||||||
title={title}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="module-message-request-actions">
|
<div className="module-message-request-actions">
|
||||||
<p className="module-message-request-actions__message">{message}</p>
|
<p className="module-message-request-actions__message">{message}</p>
|
||||||
<div className="module-message-request-actions__buttons">
|
<div className="module-message-request-actions__buttons">
|
||||||
<Button
|
{!isBlocked && (
|
||||||
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>
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMrState(MessageRequestState.blocking);
|
setMrState(MessageRequestState.blocking);
|
||||||
|
@ -129,6 +121,36 @@ export function MessageRequestActions({
|
||||||
{i18n('icu:MessageRequests--block')}
|
{i18n('icu:MessageRequests--block')}
|
||||||
</Button>
|
</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 ? (
|
{!isBlocked ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => acceptConversation(conversationId)}
|
onClick={() => acceptConversation(conversationId)}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { PropsType as ContactNameProps } from './ContactName';
|
import type { ContactNameData } from './ContactName';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
|
@ -12,38 +12,53 @@ export enum MessageRequestState {
|
||||||
blocking,
|
blocking,
|
||||||
deleting,
|
deleting,
|
||||||
unblocking,
|
unblocking,
|
||||||
|
reportingAndMaybeBlocking,
|
||||||
|
acceptedOptions,
|
||||||
default,
|
default,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Props = {
|
export type MessageRequestActionsConfirmationBaseProps = {
|
||||||
acceptConversation(conversationId: string): unknown;
|
addedByName: ContactNameData | null;
|
||||||
blockAndReportSpam(conversationId: string): unknown;
|
|
||||||
blockConversation(conversationId: string): unknown;
|
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
conversationType: 'group' | 'direct';
|
conversationType: 'group' | 'direct';
|
||||||
deleteConversation(conversationId: string): unknown;
|
conversationName: ContactNameData;
|
||||||
i18n: LocalizerType;
|
isBlocked: boolean;
|
||||||
isBlocked?: boolean;
|
isReported: boolean;
|
||||||
onChangeState(state: MessageRequestState): unknown;
|
acceptConversation(conversationId: string): void;
|
||||||
state: MessageRequestState;
|
blockAndReportSpam(conversationId: string): void;
|
||||||
} & Omit<ContactNameProps, 'module'>;
|
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({
|
export function MessageRequestActionsConfirmation({
|
||||||
|
addedByName,
|
||||||
|
conversationId,
|
||||||
|
conversationType,
|
||||||
|
conversationName,
|
||||||
|
i18n,
|
||||||
|
isBlocked,
|
||||||
|
state,
|
||||||
acceptConversation,
|
acceptConversation,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
conversationId,
|
reportSpam,
|
||||||
conversationType,
|
|
||||||
deleteConversation,
|
deleteConversation,
|
||||||
i18n,
|
|
||||||
onChangeState,
|
onChangeState,
|
||||||
state,
|
}: MessageRequestActionsConfirmationProps): JSX.Element | null {
|
||||||
title,
|
|
||||||
}: Props): JSX.Element | null {
|
|
||||||
if (state === MessageRequestState.blocking) {
|
if (state === MessageRequestState.blocking) {
|
||||||
return (
|
return (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
key="messageRequestActionsConfirmation.blocking"
|
||||||
dialogName="messageRequestActionsConfirmation.blocking"
|
dialogName="messageRequestActionsConfirmation.blocking"
|
||||||
|
moduleClassName="MessageRequestActionsConfirmation"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onChangeState(MessageRequestState.default);
|
onChangeState(MessageRequestState.default);
|
||||||
|
@ -54,7 +69,13 @@ export function MessageRequestActionsConfirmation({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MessageRequests--block-direct-confirm-title"
|
id="icu:MessageRequests--block-direct-confirm-title"
|
||||||
components={{
|
components={{
|
||||||
title: <ContactName key="name" title={title} />,
|
title: (
|
||||||
|
<ContactName
|
||||||
|
key="name"
|
||||||
|
{...conversationName}
|
||||||
|
preferFirstName
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -62,21 +83,18 @@ export function MessageRequestActionsConfirmation({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MessageRequests--block-group-confirm-title"
|
id="icu:MessageRequests--block-group-confirm-title"
|
||||||
components={{
|
components={{
|
||||||
title: <ContactName key="name" title={title} />,
|
title: (
|
||||||
|
<ContactName
|
||||||
|
key="name"
|
||||||
|
{...conversationName}
|
||||||
|
preferFirstName
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
actions={[
|
actions={[
|
||||||
...(conversationType === 'direct'
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
text: i18n('icu:MessageRequests--block-and-report-spam'),
|
|
||||||
action: () => blockAndReportSpam(conversationId),
|
|
||||||
style: 'negative' as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
text: i18n('icu:MessageRequests--block'),
|
text: i18n('icu:MessageRequests--block'),
|
||||||
action: () => blockConversation(conversationId),
|
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) {
|
if (state === MessageRequestState.unblocking) {
|
||||||
return (
|
return (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
key="messageRequestActionsConfirmation.unblocking"
|
||||||
dialogName="messageRequestActionsConfirmation.unblocking"
|
dialogName="messageRequestActionsConfirmation.unblocking"
|
||||||
|
moduleClassName="MessageRequestActionsConfirmation"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onChangeState(MessageRequestState.default);
|
onChangeState(MessageRequestState.default);
|
||||||
|
@ -104,7 +174,9 @@ export function MessageRequestActionsConfirmation({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MessageRequests--unblock-direct-confirm-title"
|
id="icu:MessageRequests--unblock-direct-confirm-title"
|
||||||
components={{
|
components={{
|
||||||
name: <ContactName key="name" title={title} />,
|
name: (
|
||||||
|
<ContactName key="name" {...conversationName} preferFirstName />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -126,7 +198,9 @@ export function MessageRequestActionsConfirmation({
|
||||||
if (state === MessageRequestState.deleting) {
|
if (state === MessageRequestState.deleting) {
|
||||||
return (
|
return (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
key="messageRequestActionsConfirmation.deleting"
|
||||||
dialogName="messageRequestActionsConfirmation.deleting"
|
dialogName="messageRequestActionsConfirmation.deleting"
|
||||||
|
moduleClassName="MessageRequestActionsConfirmation"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onChangeState(MessageRequestState.default);
|
onChangeState(MessageRequestState.default);
|
||||||
|
@ -142,7 +216,13 @@ export function MessageRequestActionsConfirmation({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MessageRequests--delete-group-confirm-title"
|
id="icu:MessageRequests--delete-group-confirm-title"
|
||||||
components={{
|
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;
|
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-incoming'
|
||||||
| 'audio-missed'
|
| 'audio-missed'
|
||||||
| 'audio-outgoing'
|
| 'audio-outgoing'
|
||||||
|
| 'block'
|
||||||
| 'group'
|
| 'group'
|
||||||
| 'group-access'
|
| 'group-access'
|
||||||
| 'group-add'
|
| 'group-add'
|
||||||
|
@ -30,6 +31,7 @@ export type PropsType = {
|
||||||
| 'phone'
|
| 'phone'
|
||||||
| 'profile'
|
| 'profile'
|
||||||
| 'safety-number'
|
| 'safety-number'
|
||||||
|
| 'spam'
|
||||||
| 'session-refresh'
|
| 'session-refresh'
|
||||||
| 'thread'
|
| 'thread'
|
||||||
| 'timer'
|
| 'timer'
|
||||||
|
|
|
@ -335,6 +335,10 @@ const actions = () => ({
|
||||||
viewStory: action('viewStory'),
|
viewStory: action('viewStory'),
|
||||||
|
|
||||||
onReplyToMessage: action('onReplyToMessage'),
|
onReplyToMessage: action('onReplyToMessage'),
|
||||||
|
|
||||||
|
onOpenMessageRequestActionsConfirmation: action(
|
||||||
|
'onOpenMessageRequestActionsConfirmation'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = ({
|
const renderItem = ({
|
||||||
|
@ -350,6 +354,7 @@ const renderItem = ({
|
||||||
getPreferredBadge={() => undefined}
|
getPreferredBadge={() => undefined}
|
||||||
id=""
|
id=""
|
||||||
isTargeted={false}
|
isTargeted={false}
|
||||||
|
isBlocked={false}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode="keyboard"
|
interactionMode="keyboard"
|
||||||
isNextItemCallingNotification={false}
|
isNextItemCallingNotification={false}
|
||||||
|
@ -442,6 +447,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
getTimestampForMessage: Date.now,
|
getTimestampForMessage: Date.now,
|
||||||
haveNewest: overrideProps.haveNewest ?? false,
|
haveNewest: overrideProps.haveNewest ?? false,
|
||||||
haveOldest: overrideProps.haveOldest ?? false,
|
haveOldest: overrideProps.haveOldest ?? false,
|
||||||
|
isBlocked: false,
|
||||||
isConversationSelected: true,
|
isConversationSelected: true,
|
||||||
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
|
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
|
||||||
items: overrideProps.items ?? Object.keys(items),
|
items: overrideProps.items ?? Object.keys(items),
|
||||||
|
|
|
@ -81,6 +81,7 @@ export type PropsDataType = {
|
||||||
|
|
||||||
type PropsHousekeepingType = {
|
type PropsHousekeepingType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
isBlocked: boolean;
|
||||||
isConversationSelected: boolean;
|
isConversationSelected: boolean;
|
||||||
isGroupV1AndDisabled?: boolean;
|
isGroupV1AndDisabled?: boolean;
|
||||||
isIncomingMessageRequest: boolean;
|
isIncomingMessageRequest: boolean;
|
||||||
|
@ -121,6 +122,7 @@ type PropsHousekeepingType = {
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
isBlocked: boolean;
|
||||||
isOldestTimelineItem: boolean;
|
isOldestTimelineItem: boolean;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
nextMessageId: undefined | string;
|
nextMessageId: undefined | string;
|
||||||
|
@ -786,6 +788,7 @@ export class Timeline extends React.Component<
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
invitedContactsForNewlyCreatedGroup,
|
invitedContactsForNewlyCreatedGroup,
|
||||||
|
isBlocked,
|
||||||
isConversationSelected,
|
isConversationSelected,
|
||||||
isGroupV1AndDisabled,
|
isGroupV1AndDisabled,
|
||||||
items,
|
items,
|
||||||
|
@ -928,6 +931,7 @@ export class Timeline extends React.Component<
|
||||||
containerElementRef: this.containerRef,
|
containerElementRef: this.containerRef,
|
||||||
containerWidthBreakpoint: widthBreakpoint,
|
containerWidthBreakpoint: widthBreakpoint,
|
||||||
conversationId: id,
|
conversationId: id,
|
||||||
|
isBlocked,
|
||||||
isOldestTimelineItem: haveOldest && itemIndex === 0,
|
isOldestTimelineItem: haveOldest && itemIndex === 0,
|
||||||
messageId,
|
messageId,
|
||||||
nextMessageId,
|
nextMessageId,
|
||||||
|
|
|
@ -59,6 +59,7 @@ const getDefaultProps = () => ({
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isNextItemCallingNotification: false,
|
isNextItemCallingNotification: false,
|
||||||
isTargeted: false,
|
isTargeted: false,
|
||||||
|
isBlocked: false,
|
||||||
interactionMode: 'keyboard' as const,
|
interactionMode: 'keyboard' as const,
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
|
@ -118,6 +119,9 @@ const getDefaultProps = () => ({
|
||||||
viewStory: action('viewStory'),
|
viewStory: action('viewStory'),
|
||||||
|
|
||||||
onReplyToMessage: action('onReplyToMessage'),
|
onReplyToMessage: action('onReplyToMessage'),
|
||||||
|
onOpenMessageRequestActionsConfirmation: action(
|
||||||
|
'onOpenMessageRequestActionsConfirmation'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -56,6 +56,11 @@ import type { PropsDataType as PhoneNumberDiscoveryNotificationPropsType } from
|
||||||
import { PhoneNumberDiscoveryNotification } from './PhoneNumberDiscoveryNotification';
|
import { PhoneNumberDiscoveryNotification } from './PhoneNumberDiscoveryNotification';
|
||||||
import { SystemMessage } from './SystemMessage';
|
import { SystemMessage } from './SystemMessage';
|
||||||
import { TimelineMessage } from './TimelineMessage';
|
import { TimelineMessage } from './TimelineMessage';
|
||||||
|
import {
|
||||||
|
MessageRequestResponseNotification,
|
||||||
|
type MessageRequestResponseNotificationData,
|
||||||
|
} from './MessageRequestResponseNotification';
|
||||||
|
import type { MessageRequestState } from './MessageRequestActionsConfirmation';
|
||||||
|
|
||||||
type CallHistoryType = {
|
type CallHistoryType = {
|
||||||
type: 'callHistory';
|
type: 'callHistory';
|
||||||
|
@ -137,6 +142,10 @@ type PaymentEventType = {
|
||||||
type: 'paymentEvent';
|
type: 'paymentEvent';
|
||||||
data: Omit<PaymentEventNotificationPropsType, 'i18n'>;
|
data: Omit<PaymentEventNotificationPropsType, 'i18n'>;
|
||||||
};
|
};
|
||||||
|
type MessageRequestResponseNotificationType = {
|
||||||
|
type: 'messageRequestResponse';
|
||||||
|
data: MessageRequestResponseNotificationData;
|
||||||
|
};
|
||||||
|
|
||||||
export type TimelineItemType = (
|
export type TimelineItemType = (
|
||||||
| CallHistoryType
|
| CallHistoryType
|
||||||
|
@ -159,6 +168,7 @@ export type TimelineItemType = (
|
||||||
| UnsupportedMessageType
|
| UnsupportedMessageType
|
||||||
| VerificationNotificationType
|
| VerificationNotificationType
|
||||||
| PaymentEventType
|
| PaymentEventType
|
||||||
|
| MessageRequestResponseNotificationType
|
||||||
) & { timestamp: number };
|
) & { timestamp: number };
|
||||||
|
|
||||||
type PropsLocalType = {
|
type PropsLocalType = {
|
||||||
|
@ -166,10 +176,12 @@ type PropsLocalType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
id: string;
|
id: string;
|
||||||
|
isBlocked: boolean;
|
||||||
isNextItemCallingNotification: boolean;
|
isNextItemCallingNotification: boolean;
|
||||||
isTargeted: boolean;
|
isTargeted: boolean;
|
||||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
shouldRenderDateHeader: boolean;
|
shouldRenderDateHeader: boolean;
|
||||||
|
onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void;
|
||||||
platform: string;
|
platform: string;
|
||||||
renderContact: SmartContactRendererType<JSX.Element>;
|
renderContact: SmartContactRendererType<JSX.Element>;
|
||||||
renderUniversalTimerNotification: () => JSX.Element;
|
renderUniversalTimerNotification: () => JSX.Element;
|
||||||
|
@ -203,9 +215,11 @@ export const TimelineItem = memo(function TimelineItem({
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
|
isBlocked,
|
||||||
isNextItemCallingNotification,
|
isNextItemCallingNotification,
|
||||||
isTargeted,
|
isTargeted,
|
||||||
item,
|
item,
|
||||||
|
onOpenMessageRequestActionsConfirmation,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
platform,
|
platform,
|
||||||
|
@ -379,6 +393,17 @@ export const TimelineItem = memo(function TimelineItem({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (item.type === 'messageRequestResponse') {
|
||||||
|
notification = (
|
||||||
|
<MessageRequestResponseNotification
|
||||||
|
{...item.data}
|
||||||
|
i18n={i18n}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
onOpenMessageRequestActionsConfirmation={
|
||||||
|
onOpenMessageRequestActionsConfirmation
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Weird, yes, but the idea is to get a compile error when we aren't comprehensive
|
// 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
|
// 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 { assertDev } from '../../util/assert';
|
||||||
import { isDirectConversation } from '../../util/whatTypeOfConversation';
|
import { isDirectConversation } from '../../util/whatTypeOfConversation';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import type { ConversationAttributesType } from '../../model-types.d';
|
|
||||||
import { isAciString } from '../../util/isAciString';
|
import { isAciString } from '../../util/isAciString';
|
||||||
import type { reportSpamJobQueue } from '../reportSpamJobQueue';
|
import type { reportSpamJobQueue } from '../reportSpamJobQueue';
|
||||||
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
|
||||||
export async function addReportSpamJob({
|
export async function addReportSpamJob({
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -14,10 +14,7 @@ export async function addReportSpamJob({
|
||||||
jobQueue,
|
jobQueue,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
conversation: Readonly<
|
conversation: Readonly<
|
||||||
Pick<
|
Pick<ConversationType, 'id' | 'type' | 'serviceId' | 'reportingToken'>
|
||||||
ConversationAttributesType,
|
|
||||||
'id' | 'type' | 'serviceId' | 'reportingToken'
|
|
||||||
>
|
|
||||||
>;
|
>;
|
||||||
getMessageServerGuidsForSpam: (
|
getMessageServerGuidsForSpam: (
|
||||||
conversationId: string
|
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 AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||||
import MemberRoleEnum = Proto.Member.Role;
|
import MemberRoleEnum = Proto.Member.Role;
|
||||||
|
import type { MessageRequestResponseEvent } from './types/MessageRequestResponseEvent';
|
||||||
|
|
||||||
export type LastMessageStatus =
|
export type LastMessageStatus =
|
||||||
| 'paused'
|
| 'paused'
|
||||||
|
@ -156,6 +157,7 @@ export type MessageAttributesType = {
|
||||||
logger?: unknown;
|
logger?: unknown;
|
||||||
message?: unknown;
|
message?: unknown;
|
||||||
messageTimer?: unknown;
|
messageTimer?: unknown;
|
||||||
|
messageRequestResponseEvent?: MessageRequestResponseEvent;
|
||||||
profileChange?: ProfileNameChangeType;
|
profileChange?: ProfileNameChangeType;
|
||||||
payment?: AnyPaymentEvent;
|
payment?: AnyPaymentEvent;
|
||||||
quote?: QuotedMessageType;
|
quote?: QuotedMessageType;
|
||||||
|
@ -192,7 +194,8 @@ export type MessageAttributesType = {
|
||||||
| 'universal-timer-notification'
|
| 'universal-timer-notification'
|
||||||
| 'contact-removed-notification'
|
| 'contact-removed-notification'
|
||||||
| 'title-transition-notification'
|
| 'title-transition-notification'
|
||||||
| 'verified-change';
|
| 'verified-change'
|
||||||
|
| 'message-request-response-event';
|
||||||
body?: string;
|
body?: string;
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
preview?: Array<LinkPreviewType>;
|
preview?: Array<LinkPreviewType>;
|
||||||
|
@ -359,6 +362,7 @@ export type ConversationAttributesType = {
|
||||||
draftEditMessage?: DraftEditMessageType;
|
draftEditMessage?: DraftEditMessageType;
|
||||||
hasPostedStory?: boolean;
|
hasPostedStory?: boolean;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
|
isReported?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
systemGivenName?: string;
|
systemGivenName?: string;
|
||||||
systemFamilyName?: string;
|
systemFamilyName?: string;
|
||||||
|
|
|
@ -164,6 +164,7 @@ import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
import OS from '../util/os/osMain';
|
import OS from '../util/os/osMain';
|
||||||
import { getMessageAuthorText } from '../util/getMessageAuthorText';
|
import { getMessageAuthorText } from '../util/getMessageAuthorText';
|
||||||
import { downscaleOutgoingAttachment } from '../util/attachments';
|
import { downscaleOutgoingAttachment } from '../util/attachments';
|
||||||
|
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -2118,8 +2119,38 @@ export class ConversationModel extends window.Backbone
|
||||||
} while (messages.length > 0);
|
} 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(
|
async applyMessageRequestResponse(
|
||||||
response: number,
|
response: Proto.SyncMessage.MessageRequestResponse.Type,
|
||||||
{ fromSync = false, viaStorageServiceSync = false, shouldSave = true } = {}
|
{ fromSync = false, viaStorageServiceSync = false, shouldSave = true } = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
@ -2130,11 +2161,84 @@ export class ConversationModel extends window.Backbone
|
||||||
const didResponseChange = response !== currentMessageRequestState;
|
const didResponseChange = response !== currentMessageRequestState;
|
||||||
const wasPreviouslyAccepted = this.getAccepted();
|
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
|
// Apply message request response locally
|
||||||
this.set({
|
this.set({
|
||||||
messageRequestResponseType: response,
|
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) {
|
if (response === messageRequestEnum.ACCEPT) {
|
||||||
this.unblock({ viaStorageServiceSync });
|
this.unblock({ viaStorageServiceSync });
|
||||||
if (!viaStorageServiceSync) {
|
if (!viaStorageServiceSync) {
|
||||||
|
@ -2191,53 +2295,15 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (response === messageRequestEnum.BLOCK) {
|
} else if (response === messageRequestEnum.BLOCK) {
|
||||||
// Block locally, other devices should block upon receiving the sync message
|
await rejectConversation({ isBlock: true });
|
||||||
this.block({ viaStorageServiceSync });
|
|
||||||
this.disableProfileSharing({ viaStorageServiceSync });
|
|
||||||
|
|
||||||
if (isLocalAction) {
|
|
||||||
if (isGroupV2(this.attributes)) {
|
|
||||||
await this.leaveGroupV2();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (response === messageRequestEnum.DELETE) {
|
} else if (response === messageRequestEnum.DELETE) {
|
||||||
this.disableProfileSharing({ viaStorageServiceSync });
|
await rejectConversation({ isDelete: true });
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (response === messageRequestEnum.BLOCK_AND_DELETE) {
|
} else if (response === messageRequestEnum.BLOCK_AND_DELETE) {
|
||||||
// Block locally, other devices should block upon receiving the sync message
|
await rejectConversation({ isBlock: true, isDelete: true });
|
||||||
this.block({ viaStorageServiceSync });
|
} else if (response === messageRequestEnum.SPAM) {
|
||||||
this.disableProfileSharing({ viaStorageServiceSync });
|
await rejectConversation({ isSpam: true });
|
||||||
|
} else if (response === messageRequestEnum.BLOCK_AND_SPAM) {
|
||||||
// Delete messages locally, other devices should delete upon receiving
|
await rejectConversation({ isBlock: true, isSpam: true });
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldSave) {
|
if (shouldSave) {
|
||||||
|
@ -2492,40 +2558,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> {
|
async safeGetVerified(): Promise<number> {
|
||||||
const serviceId = this.getServiceId();
|
const serviceId = this.getServiceId();
|
||||||
if (!serviceId) {
|
if (!serviceId) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type AudioPlayerStateType = ReadonlyDeep<{
|
export type AudioRecorderStateType = ReadonlyDeep<{
|
||||||
recordingState: RecordingState;
|
recordingState: RecordingState;
|
||||||
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
||||||
}>;
|
}>;
|
||||||
|
@ -211,16 +211,16 @@ function errorRecording(
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
export function getEmptyState(): AudioPlayerStateType {
|
export function getEmptyState(): AudioRecorderStateType {
|
||||||
return {
|
return {
|
||||||
recordingState: RecordingState.Idle,
|
recordingState: RecordingState.Idle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: Readonly<AudioPlayerStateType> = getEmptyState(),
|
state: Readonly<AudioRecorderStateType> = getEmptyState(),
|
||||||
action: Readonly<AudioPlayerActionType>
|
action: Readonly<AudioPlayerActionType>
|
||||||
): AudioPlayerStateType {
|
): AudioRecorderStateType {
|
||||||
if (action.type === START_RECORDING) {
|
if (action.type === START_RECORDING) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -179,6 +179,10 @@ import {
|
||||||
import type { ChangeNavTabActionType } from './nav';
|
import type { ChangeNavTabActionType } from './nav';
|
||||||
import { CHANGE_NAV_TAB, NavTab, actions as navActions } from './nav';
|
import { CHANGE_NAV_TAB, NavTab, actions as navActions } from './nav';
|
||||||
import { sortByMessageOrder } from '../../types/ForwardDraft';
|
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
|
// State
|
||||||
|
|
||||||
|
@ -228,6 +232,10 @@ export type DraftPreviewType = ReadonlyDeep<{
|
||||||
bodyRanges?: HydratedBodyRangesType;
|
bodyRanges?: HydratedBodyRangesType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ConversationRemovalStage = ReadonlyDeep<
|
||||||
|
'justNotification' | 'messageRequest'
|
||||||
|
>;
|
||||||
|
|
||||||
export type ConversationType = ReadonlyDeep<
|
export type ConversationType = ReadonlyDeep<
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -265,7 +273,9 @@ export type ConversationType = ReadonlyDeep<
|
||||||
hideStory?: boolean;
|
hideStory?: boolean;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isBlocked?: boolean;
|
isBlocked?: boolean;
|
||||||
removalStage?: 'justNotification' | 'messageRequest';
|
isReported?: boolean;
|
||||||
|
reportingToken?: string;
|
||||||
|
removalStage?: ConversationRemovalStage;
|
||||||
isGroupV1AndDisabled?: boolean;
|
isGroupV1AndDisabled?: boolean;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
isUntrusted?: boolean;
|
isUntrusted?: boolean;
|
||||||
|
@ -1026,6 +1036,7 @@ export const actions = {
|
||||||
acknowledgeGroupMemberNameCollisions,
|
acknowledgeGroupMemberNameCollisions,
|
||||||
addMembersToGroup,
|
addMembersToGroup,
|
||||||
approvePendingMembershipFromGroupV2,
|
approvePendingMembershipFromGroupV2,
|
||||||
|
reportSpam,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
blockGroupLinkRequests,
|
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(
|
function blockAndReportSpam(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||||
return async dispatch => {
|
return async (dispatch, getState) => {
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversationSelector = getConversationSelector(getState());
|
||||||
if (!conversation) {
|
const conversationOrGroup = conversationSelector(conversationId);
|
||||||
|
if (!conversationOrGroup) {
|
||||||
log.error(
|
log.error(
|
||||||
`blockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
|
`blockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const conversationForSpam =
|
||||||
|
getConversationForReportSpam(conversationOrGroup);
|
||||||
|
|
||||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||||
const idForLogging = conversation.idForLogging();
|
const idForLogging = getConversationIdForLogging(conversationOrGroup);
|
||||||
|
|
||||||
void longRunningTaskWrapper({
|
drop(
|
||||||
name: 'blockAndReportSpam',
|
longRunningTaskWrapper({
|
||||||
idForLogging,
|
name: 'blockAndReportSpam',
|
||||||
task: async () => {
|
idForLogging,
|
||||||
await Promise.all([
|
task: async () => {
|
||||||
conversation.syncMessageRequestResponse(messageRequestEnum.BLOCK),
|
await Promise.all([
|
||||||
addReportSpamJob({
|
syncMessageRequestResponse(
|
||||||
conversation: conversation.attributes,
|
conversationOrGroup,
|
||||||
getMessageServerGuidsForSpam:
|
messageRequestEnum.BLOCK_AND_SPAM
|
||||||
window.Signal.Data.getMessageServerGuidsForSpam,
|
),
|
||||||
jobQueue: reportSpamJobQueue,
|
conversationForSpam != null &&
|
||||||
}),
|
addReportSpamJob({
|
||||||
]);
|
conversation: conversationForSpam,
|
||||||
|
getMessageServerGuidsForSpam:
|
||||||
|
window.Signal.Data.getMessageServerGuidsForSpam,
|
||||||
|
jobQueue: reportSpamJobQueue,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SHOW_TOAST,
|
type: SHOW_TOAST,
|
||||||
payload: {
|
payload: {
|
||||||
toastType: ToastType.ReportedSpamAndBlocked,
|
toastType: ToastType.ReportedSpamAndBlocked,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function acceptConversation(conversationId: string): NoopActionType {
|
function acceptConversation(
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
conversationId: string
|
||||||
if (!conversation) {
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
throw new Error(
|
return async (dispatch, getState) => {
|
||||||
'acceptConversation: Expected a conversation to be found. Doing nothing'
|
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;
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
void longRunningTaskWrapper({
|
payload: null,
|
||||||
name: 'acceptConversation',
|
});
|
||||||
idForLogging: conversation.idForLogging(),
|
|
||||||
task: conversation.syncMessageRequestResponse.bind(
|
|
||||||
conversation,
|
|
||||||
messageRequestEnum.ACCEPT
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'NOOP',
|
|
||||||
payload: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3329,53 +3467,74 @@ function removeConversation(conversationId: string): ShowToastActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockConversation(conversationId: string): NoopActionType {
|
function blockConversation(
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
conversationId: string
|
||||||
if (!conversation) {
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
throw new Error(
|
return (dispatch, getState) => {
|
||||||
'blockConversation: Expected a conversation to be found. Doing nothing'
|
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;
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
void longRunningTaskWrapper({
|
payload: null,
|
||||||
name: 'blockConversation',
|
});
|
||||||
idForLogging: conversation.idForLogging(),
|
|
||||||
task: conversation.syncMessageRequestResponse.bind(
|
|
||||||
conversation,
|
|
||||||
messageRequestEnum.BLOCK
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'NOOP',
|
|
||||||
payload: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteConversation(conversationId: string): NoopActionType {
|
function deleteConversation(
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
conversationId: string
|
||||||
if (!conversation) {
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
throw new Error(
|
return (dispatch, getState) => {
|
||||||
'deleteConversation: Expected a conversation to be found. Doing nothing'
|
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;
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
void longRunningTaskWrapper({
|
payload: null,
|
||||||
name: 'deleteConversation',
|
});
|
||||||
idForLogging: conversation.idForLogging(),
|
|
||||||
task: conversation.syncMessageRequestResponse.bind(
|
|
||||||
conversation,
|
|
||||||
messageRequestEnum.DELETE
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'NOOP',
|
|
||||||
payload: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,9 @@ export const actions = {
|
||||||
useEmoji,
|
useEmoji,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
export const useEmojisActions = (): BoundActionCreatorsMapObject<
|
||||||
useBoundActions(actions);
|
typeof actions
|
||||||
|
> => useBoundActions(actions);
|
||||||
|
|
||||||
function onUseEmoji({
|
function onUseEmoji({
|
||||||
shortName,
|
shortName,
|
||||||
|
|
|
@ -42,6 +42,7 @@ import { SHOW_TOAST } from './toast';
|
||||||
import type { ShowToastActionType } from './toast';
|
import type { ShowToastActionType } from './toast';
|
||||||
import { isDownloaded } from '../../types/Attachment';
|
import { isDownloaded } from '../../types/Attachment';
|
||||||
import type { ButtonVariant } from '../../components/Button';
|
import type { ButtonVariant } from '../../components/Button';
|
||||||
|
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -58,6 +59,10 @@ export type ForwardMessagesPropsType = ReadonlyDeep<{
|
||||||
messages: Array<ForwardMessagePropsType>;
|
messages: Array<ForwardMessagePropsType>;
|
||||||
onForward?: () => void;
|
onForward?: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
export type MessageRequestActionsConfirmationPropsType = ReadonlyDeep<{
|
||||||
|
conversationId: string;
|
||||||
|
state: MessageRequestState;
|
||||||
|
}>;
|
||||||
export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
|
export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
|
||||||
promiseUuid: SingleServePromise.SingleServePromiseIdString;
|
promiseUuid: SingleServePromise.SingleServePromiseIdString;
|
||||||
source?: SafetyNumberChangeSource;
|
source?: SafetyNumberChangeSource;
|
||||||
|
@ -101,6 +106,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
||||||
isSignalConnectionsVisible: boolean;
|
isSignalConnectionsVisible: boolean;
|
||||||
isStoriesSettingsVisible: boolean;
|
isStoriesSettingsVisible: boolean;
|
||||||
isWhatsNewVisible: boolean;
|
isWhatsNewVisible: boolean;
|
||||||
|
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||||
usernameOnboardingState: UsernameOnboardingState;
|
usernameOnboardingState: UsernameOnboardingState;
|
||||||
profileEditorHasError: boolean;
|
profileEditorHasError: boolean;
|
||||||
profileEditorInitialEditState: ProfileEditorEditState | undefined;
|
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_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
|
||||||
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
|
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
|
||||||
export const SHOW_ERROR_MODAL = 'globalModals/SHOW_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 =
|
const SHOW_FORMATTING_WARNING_MODAL =
|
||||||
'globalModals/SHOW_FORMATTING_WARNING_MODAL';
|
'globalModals/SHOW_FORMATTING_WARNING_MODAL';
|
||||||
const SHOW_SEND_EDIT_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 CloseShortcutGuideModalActionType = ReadonlyDeep<{
|
||||||
type: typeof CLOSE_SHORTCUT_GUIDE_MODAL;
|
type: typeof CLOSE_SHORTCUT_GUIDE_MODAL;
|
||||||
}>;
|
}>;
|
||||||
|
@ -373,6 +386,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
||||||
| ShowContactModalActionType
|
| ShowContactModalActionType
|
||||||
| ShowEditHistoryModalActionType
|
| ShowEditHistoryModalActionType
|
||||||
| ShowErrorModalActionType
|
| ShowErrorModalActionType
|
||||||
|
| ToggleMessageRequestActionsConfirmationActionType
|
||||||
| ShowFormattingWarningModalActionType
|
| ShowFormattingWarningModalActionType
|
||||||
| ShowSendAnywayDialogActionType
|
| ShowSendAnywayDialogActionType
|
||||||
| ShowSendEditWarningModalActionType
|
| ShowSendEditWarningModalActionType
|
||||||
|
@ -414,6 +428,7 @@ export const actions = {
|
||||||
showContactModal,
|
showContactModal,
|
||||||
showEditHistoryModal,
|
showEditHistoryModal,
|
||||||
showErrorModal,
|
showErrorModal,
|
||||||
|
toggleMessageRequestActionsConfirmation,
|
||||||
showFormattingWarningModal,
|
showFormattingWarningModal,
|
||||||
showSendEditWarningModal,
|
showSendEditWarningModal,
|
||||||
showGV2MigrationDialog,
|
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 {
|
function closeShortcutGuideModal(): CloseShortcutGuideModalActionType {
|
||||||
return {
|
return {
|
||||||
type: CLOSE_SHORTCUT_GUIDE_MODAL,
|
type: CLOSE_SHORTCUT_GUIDE_MODAL,
|
||||||
|
@ -908,6 +935,7 @@ export function getEmptyState(): GlobalModalsStateType {
|
||||||
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
||||||
profileEditorHasError: false,
|
profileEditorHasError: false,
|
||||||
profileEditorInitialEditState: undefined,
|
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) {
|
if (action.type === CLOSE_SHORTCUT_GUIDE_MODAL) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -101,8 +101,9 @@ export const actions = {
|
||||||
selectDraftEmojiToBeReplaced,
|
selectDraftEmojiToBeReplaced,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
export const usePreferredReactionsActions = (): BoundActionCreatorsMapObject<
|
||||||
useBoundActions(actions);
|
typeof actions
|
||||||
|
> => useBoundActions(actions);
|
||||||
|
|
||||||
function cancelCustomizePreferredReactionsModal(): CancelCustomizePreferredReactionsModalActionType {
|
function cancelCustomizePreferredReactionsModal(): CancelCustomizePreferredReactionsModalActionType {
|
||||||
return { type: CANCEL_CUSTOMIZE_PREFERRED_REACTIONS_MODAL };
|
return { type: CANCEL_CUSTOMIZE_PREFERRED_REACTIONS_MODAL };
|
||||||
|
|
|
@ -22,6 +22,8 @@ import { ERASE_STORAGE_SERVICE } from './user';
|
||||||
import type { EraseStorageServiceStateAction } from './user';
|
import type { EraseStorageServiceStateAction } from './user';
|
||||||
|
|
||||||
import type { NoopActionType } from './noop';
|
import type { NoopActionType } from './noop';
|
||||||
|
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||||
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
const { getRecentStickers, updateStickerLastUsed } = dataInterface;
|
const { getRecentStickers, updateStickerLastUsed } = dataInterface;
|
||||||
|
|
||||||
|
@ -154,6 +156,10 @@ export const actions = {
|
||||||
useSticker,
|
useSticker,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useStickersActions = (): BoundActionCreatorsMapObject<
|
||||||
|
typeof actions
|
||||||
|
> => useBoundActions(actions);
|
||||||
|
|
||||||
function removeStickerPack(id: string): StickerPackRemovedAction {
|
function removeStickerPack(id: string): StickerPackRemovedAction {
|
||||||
return {
|
return {
|
||||||
type: 'stickers/REMOVE_STICKER_PACK',
|
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,
|
getItems,
|
||||||
(state: ItemsStateType): boolean => Boolean(state.navTabsCollapsed ?? false)
|
(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 { CallDirection } from '../../types/CallDisposition';
|
||||||
import { getCallIdFromEra } from '../../util/callDisposition';
|
import { getCallIdFromEra } from '../../util/callDisposition';
|
||||||
import { LONG_MESSAGE } from '../../types/MIME';
|
import { LONG_MESSAGE } from '../../types/MIME';
|
||||||
|
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification';
|
||||||
|
|
||||||
export { isIncoming, isOutgoing, isStory };
|
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);
|
const data = getPropsForMessage(message, options);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1461,6 +1470,24 @@ function getPropsForProfileChange(
|
||||||
} as ProfileChangeNotificationPropsType;
|
} 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
|
// Universal Timer Notification
|
||||||
|
|
||||||
// Note: smart, so props not generated here
|
// Note: smart, so props not generated here
|
||||||
|
|
|
@ -1,35 +1,27 @@
|
||||||
// Copyright 2019 Signal Messenger, LLC
|
// Copyright 2019 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { get } from 'lodash';
|
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
|
||||||
import type { Props as ComponentPropsType } from '../../components/CompositionArea';
|
|
||||||
import { CompositionArea } from '../../components/CompositionArea';
|
import { CompositionArea } from '../../components/CompositionArea';
|
||||||
import type { StateType } from '../reducer';
|
import { useContactNameData } from '../../components/conversation/ContactName';
|
||||||
import type {
|
import type {
|
||||||
DraftBodyRanges,
|
DraftBodyRanges,
|
||||||
HydratedBodyRangesType,
|
HydratedBodyRangesType,
|
||||||
} from '../../types/BodyRange';
|
} from '../../types/BodyRange';
|
||||||
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
import { hydrateRanges } from '../../types/BodyRange';
|
||||||
import { dropNull } from '../../util/dropNull';
|
import { strictAssert } from '../../util/assert';
|
||||||
|
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||||
import { imageToBlurHash } from '../../util/imageToBlurHash';
|
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 { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import { selectRecentEmojis } from '../selectors/emojis';
|
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
|
||||||
import {
|
|
||||||
getIntl,
|
|
||||||
getPlatform,
|
|
||||||
getTheme,
|
|
||||||
getUserConversationId,
|
|
||||||
} from '../selectors/user';
|
|
||||||
import {
|
|
||||||
getDefaultConversationColor,
|
|
||||||
getEmojiSkinTone,
|
|
||||||
getTextFormattingEnabled,
|
|
||||||
} from '../selectors/items';
|
|
||||||
import {
|
import {
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getGroupAdminsSelector,
|
getGroupAdminsSelector,
|
||||||
|
@ -38,71 +30,88 @@ import {
|
||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
isMissingRequiredProfileSharing,
|
isMissingRequiredProfileSharing,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
|
import { selectRecentEmojis } from '../selectors/emojis';
|
||||||
|
import {
|
||||||
|
getDefaultConversationColor,
|
||||||
|
getEmojiSkinTone,
|
||||||
|
getShowStickerPickerHint,
|
||||||
|
getShowStickersIntroduction,
|
||||||
|
getTextFormattingEnabled,
|
||||||
|
} from '../selectors/items';
|
||||||
import { getPropsForQuote } from '../selectors/message';
|
import { getPropsForQuote } from '../selectors/message';
|
||||||
import {
|
import {
|
||||||
getBlessedStickerPacks,
|
getBlessedStickerPacks,
|
||||||
getInstalledStickerPacks,
|
getInstalledStickerPacks,
|
||||||
getKnownStickerPacks,
|
getKnownStickerPacks,
|
||||||
getReceivedStickerPacks,
|
getReceivedStickerPacks,
|
||||||
getRecentlyInstalledStickerPack,
|
|
||||||
getRecentStickers,
|
getRecentStickers,
|
||||||
|
getRecentlyInstalledStickerPack,
|
||||||
} from '../selectors/stickers';
|
} from '../selectors/stickers';
|
||||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
import {
|
||||||
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
|
getIntl,
|
||||||
|
getPlatform,
|
||||||
|
getTheme,
|
||||||
|
getUserConversationId,
|
||||||
|
} from '../selectors/user';
|
||||||
import type { SmartCompositionRecordingProps } from './CompositionRecording';
|
import type { SmartCompositionRecordingProps } from './CompositionRecording';
|
||||||
import { SmartCompositionRecording } from './CompositionRecording';
|
import { SmartCompositionRecording } from './CompositionRecording';
|
||||||
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft';
|
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft';
|
||||||
import { SmartCompositionRecordingDraft } 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 = {
|
function renderSmartCompositionRecording(
|
||||||
id: string;
|
recProps: SmartCompositionRecordingProps
|
||||||
};
|
) {
|
||||||
|
return <SmartCompositionRecording {...recProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
export type CompositionAreaPropsType = ExternalProps & ComponentPropsType;
|
function renderSmartCompositionRecordingDraft(
|
||||||
|
draftProps: SmartCompositionRecordingDraftProps
|
||||||
|
) {
|
||||||
|
return <SmartCompositionRecordingDraft {...draftProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
export function SmartCompositionArea({ id }: { id: string }): JSX.Element {
|
||||||
const { id } = props;
|
const conversationSelector = useSelector(getConversationSelector);
|
||||||
const platform = getPlatform(state);
|
|
||||||
|
|
||||||
const shouldHidePopovers = getHasPanelOpen(state);
|
|
||||||
|
|
||||||
const conversationSelector = getConversationSelector(state);
|
|
||||||
const conversation = conversationSelector(id);
|
const conversation = conversationSelector(id);
|
||||||
if (!conversation) {
|
strictAssert(conversation, `Conversation id ${id} not found!`);
|
||||||
throw new Error(`Conversation id ${id} not found!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const i18n = useSelector(getIntl);
|
||||||
announcementsOnly,
|
const theme = useSelector(getTheme);
|
||||||
areWeAdmin,
|
const skinTone = useSelector(getEmojiSkinTone);
|
||||||
draftEditMessage,
|
const recentEmojis = useSelector(selectRecentEmojis);
|
||||||
draftText,
|
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
||||||
draftBodyRanges,
|
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
|
||||||
} = conversation;
|
const lastEditableMessageId = useSelector(getLastEditableMessageId);
|
||||||
|
const receivedPacks = useSelector(getReceivedStickerPacks);
|
||||||
const receivedPacks = getReceivedStickerPacks(state);
|
const installedPacks = useSelector(getInstalledStickerPacks);
|
||||||
const installedPacks = getInstalledStickerPacks(state);
|
const blessedPacks = useSelector(getBlessedStickerPacks);
|
||||||
const blessedPacks = getBlessedStickerPacks(state);
|
const knownPacks = useSelector(getKnownStickerPacks);
|
||||||
const knownPacks = getKnownStickerPacks(state);
|
const platform = useSelector(getPlatform);
|
||||||
|
const shouldHidePopovers = useSelector(getHasPanelOpen);
|
||||||
const installedPack = getRecentlyInstalledStickerPack(state);
|
const installedPack = useSelector(getRecentlyInstalledStickerPack);
|
||||||
|
const recentStickers = useSelector(getRecentStickers);
|
||||||
const recentStickers = getRecentStickers(state);
|
const showStickersIntroduction = useSelector(getShowStickersIntroduction);
|
||||||
const showIntroduction = get(
|
const showStickerPickerHint = useSelector(getShowStickerPickerHint);
|
||||||
state.items,
|
const recordingState = useSelector(getRecordingState);
|
||||||
['showStickersIntroduction'],
|
const errorDialogAudioRecorderType = useSelector(
|
||||||
false
|
getErrorDialogAudioRecorderType
|
||||||
);
|
);
|
||||||
const showPickerHint = Boolean(
|
const getGroupAdmins = useSelector(getGroupAdminsSelector);
|
||||||
get(state.items, ['showStickerPickerHint'], false) &&
|
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||||
receivedPacks.length > 0
|
const composerStateForConversationIdSelector = useSelector(
|
||||||
|
getComposerStateForConversationIdSelector
|
||||||
);
|
);
|
||||||
|
|
||||||
const composerStateForConversationIdSelector =
|
|
||||||
getComposerStateForConversationIdSelector(state);
|
|
||||||
|
|
||||||
const composerState = composerStateForConversationIdSelector(id);
|
const composerState = composerStateForConversationIdSelector(id);
|
||||||
|
const { announcementsOnly, areWeAdmin, draftEditMessage, draftBodyRanges } =
|
||||||
|
conversation;
|
||||||
const {
|
const {
|
||||||
attachments: draftAttachments,
|
attachments: draftAttachments,
|
||||||
focusCounter,
|
focusCounter,
|
||||||
|
@ -114,6 +123,34 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
shouldSendHighQualityAttachments,
|
shouldSendHighQualityAttachments,
|
||||||
} = composerState;
|
} = 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;
|
let { quotedMessage } = composerState;
|
||||||
if (!quotedMessage && draftEditMessage?.quote) {
|
if (!quotedMessage && draftEditMessage?.quote) {
|
||||||
quotedMessage = {
|
quotedMessage = {
|
||||||
|
@ -122,117 +159,189 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const recentEmojis = selectRecentEmojis(state);
|
const quotedMessageProps = useSelector((state: StateType) => {
|
||||||
|
return quotedMessage
|
||||||
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
|
|
||||||
? getPropsForQuote(quotedMessage, {
|
? getPropsForQuote(quotedMessage, {
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
ourConversationId: getUserConversationId(state),
|
ourConversationId: getUserConversationId(state),
|
||||||
defaultConversationColor: getDefaultConversationColor(state),
|
defaultConversationColor: getDefaultConversationColor(state),
|
||||||
})
|
})
|
||||||
: undefined,
|
: 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),
|
|
||||||
|
|
||||||
draftText: dropNull(draftText),
|
const { putItem, removeItem } = useItemsActions();
|
||||||
draftBodyRanges: hydrateRanges(draftBodyRanges, conversationSelector),
|
|
||||||
renderSmartCompositionRecording: (
|
const onSetSkinTone = useCallback(
|
||||||
recProps: SmartCompositionRecordingProps
|
(tone: number) => {
|
||||||
) => {
|
putItem('skinTone', tone);
|
||||||
return <SmartCompositionRecording {...recProps} />;
|
|
||||||
},
|
|
||||||
renderSmartCompositionRecordingDraft: (
|
|
||||||
draftProps: SmartCompositionRecordingDraftProps
|
|
||||||
) => {
|
|
||||||
return <SmartCompositionRecordingDraft {...draftProps} />;
|
|
||||||
},
|
},
|
||||||
|
[putItem]
|
||||||
|
);
|
||||||
|
|
||||||
// Select Mode
|
const clearShowIntroduction = useCallback(() => {
|
||||||
selectedMessageIds,
|
removeItem('showStickersIntroduction');
|
||||||
};
|
}, [removeItem]);
|
||||||
};
|
|
||||||
|
|
||||||
const dispatchPropsMap = {
|
const clearShowPickerHint = useCallback(() => {
|
||||||
...mapDispatchToProps,
|
removeItem('showStickerPickerHint');
|
||||||
onSetSkinTone: (tone: number) => mapDispatchToProps.putItem('skinTone', tone),
|
}, [removeItem]);
|
||||||
clearShowIntroduction: () =>
|
|
||||||
mapDispatchToProps.removeItem('showStickersIntroduction'),
|
|
||||||
clearShowPickerHint: () =>
|
|
||||||
mapDispatchToProps.removeItem('showStickerPickerHint'),
|
|
||||||
onPickEmoji: mapDispatchToProps.onUseEmoji,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 type { CompositionTextAreaProps } from '../../components/CompositionTextArea';
|
||||||
import { CompositionTextArea } from '../../components/CompositionTextArea';
|
import { CompositionTextArea } from '../../components/CompositionTextArea';
|
||||||
import { getIntl, getPlatform } from '../selectors/user';
|
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 { useItemsActions } from '../ducks/items';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import { useComposerActions } from '../ducks/composer';
|
import { useComposerActions } from '../ducks/composer';
|
||||||
|
|
|
@ -44,6 +44,7 @@ export function SmartContactSpoofingReviewDialog(
|
||||||
|
|
||||||
const {
|
const {
|
||||||
acceptConversation,
|
acceptConversation,
|
||||||
|
reportSpam,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
deleteConversation,
|
deleteConversation,
|
||||||
|
@ -74,6 +75,7 @@ export function SmartContactSpoofingReviewDialog(
|
||||||
const sharedProps = {
|
const sharedProps = {
|
||||||
...props,
|
...props,
|
||||||
acceptConversation,
|
acceptConversation,
|
||||||
|
reportSpam,
|
||||||
blockAndReportSpam,
|
blockAndReportSpam,
|
||||||
blockConversation,
|
blockConversation,
|
||||||
deleteConversation,
|
deleteConversation,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import type { ConversationType } from '../ducks/conversations';
|
import type { ConversationType } from '../ducks/conversations';
|
||||||
|
@ -37,6 +37,8 @@ import { useStoriesActions } from '../ducks/stories';
|
||||||
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
|
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
|
||||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||||
import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall';
|
import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall';
|
||||||
|
import { useContactNameData } from '../../components/conversation/ContactName';
|
||||||
|
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -108,6 +110,11 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
|
||||||
setMuteExpiration,
|
setMuteExpiration,
|
||||||
setPinned,
|
setPinned,
|
||||||
toggleSelectMode,
|
toggleSelectMode,
|
||||||
|
acceptConversation,
|
||||||
|
blockAndReportSpam,
|
||||||
|
blockConversation,
|
||||||
|
reportSpam,
|
||||||
|
deleteConversation,
|
||||||
} = useConversationsActions();
|
} = useConversationsActions();
|
||||||
const {
|
const {
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
|
@ -129,6 +136,17 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
|
||||||
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
||||||
const isSelectMode = selectedMessageIds != null;
|
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 (
|
return (
|
||||||
<ConversationHeader
|
<ConversationHeader
|
||||||
{...pick(conversation, [
|
{...pick(conversation, [
|
||||||
|
@ -184,6 +202,18 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
|
||||||
isSelectMode={isSelectMode}
|
isSelectMode={isSelectMode}
|
||||||
toggleSelectMode={toggleSelectMode}
|
toggleSelectMode={toggleSelectMode}
|
||||||
viewUserStories={viewUserStories}
|
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 { StateType } from '../reducer';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import { useActions as usePreferredReactionsActions } from '../ducks/preferredReactions';
|
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
|
||||||
import { useItemsActions } from '../ducks/items';
|
import { useItemsActions } from '../ducks/items';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getEmojiSkinTone } from '../selectors/items';
|
import { getEmojiSkinTone } from '../selectors/items';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import { useRecentEmojis } from '../selectors/emojis';
|
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 type { Props as EmojiPickerProps } from '../../components/emoji/EmojiPicker';
|
||||||
import { EmojiPicker } 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 { getIntl, getTheme } from '../selectors/user';
|
||||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||||
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
|
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
|
||||||
|
import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation';
|
||||||
|
|
||||||
function renderEditHistoryMessagesModal(): JSX.Element {
|
function renderEditHistoryMessagesModal(): JSX.Element {
|
||||||
return <SmartEditHistoryMessagesModal />;
|
return <SmartEditHistoryMessagesModal />;
|
||||||
|
@ -50,6 +51,10 @@ function renderForwardMessagesModal(): JSX.Element {
|
||||||
return <SmartForwardMessagesModal />;
|
return <SmartForwardMessagesModal />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMessageRequestActionsConfirmation(): JSX.Element {
|
||||||
|
return <SmartMessageRequestActionsConfirmation />;
|
||||||
|
}
|
||||||
|
|
||||||
function renderStoriesSettings(): JSX.Element {
|
function renderStoriesSettings(): JSX.Element {
|
||||||
return <SmartStoriesSettingsModal />;
|
return <SmartStoriesSettingsModal />;
|
||||||
}
|
}
|
||||||
|
@ -83,6 +88,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
||||||
errorModalProps,
|
errorModalProps,
|
||||||
formattingWarningData,
|
formattingWarningData,
|
||||||
forwardMessagesProps,
|
forwardMessagesProps,
|
||||||
|
messageRequestActionsConfirmationProps,
|
||||||
isAuthorizingArtCreator,
|
isAuthorizingArtCreator,
|
||||||
isProfileEditorVisible,
|
isProfileEditorVisible,
|
||||||
isShortcutGuideModalVisible,
|
isShortcutGuideModalVisible,
|
||||||
|
@ -163,6 +169,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
||||||
deleteMessagesProps={deleteMessagesProps}
|
deleteMessagesProps={deleteMessagesProps}
|
||||||
formattingWarningData={formattingWarningData}
|
formattingWarningData={formattingWarningData}
|
||||||
forwardMessagesProps={forwardMessagesProps}
|
forwardMessagesProps={forwardMessagesProps}
|
||||||
|
messageRequestActionsConfirmationProps={
|
||||||
|
messageRequestActionsConfirmationProps
|
||||||
|
}
|
||||||
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
|
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
|
||||||
hideUserNotFoundModal={hideUserNotFoundModal}
|
hideUserNotFoundModal={hideUserNotFoundModal}
|
||||||
hideWhatsNewModal={hideWhatsNewModal}
|
hideWhatsNewModal={hideWhatsNewModal}
|
||||||
|
@ -180,6 +189,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
||||||
renderErrorModal={renderErrorModal}
|
renderErrorModal={renderErrorModal}
|
||||||
renderDeleteMessagesModal={renderDeleteMessagesModal}
|
renderDeleteMessagesModal={renderDeleteMessagesModal}
|
||||||
renderForwardMessagesModal={renderForwardMessagesModal}
|
renderForwardMessagesModal={renderForwardMessagesModal}
|
||||||
|
renderMessageRequestActionsConfirmation={
|
||||||
|
renderMessageRequestActionsConfirmation
|
||||||
|
}
|
||||||
renderProfileEditor={renderProfileEditor}
|
renderProfileEditor={renderProfileEditor}
|
||||||
renderUsernameOnboarding={renderUsernameOnboarding}
|
renderUsernameOnboarding={renderUsernameOnboarding}
|
||||||
renderSafetyNumber={renderSafetyNumber}
|
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 * as React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import { useActions as usePreferredReactionsActions } from '../ducks/preferredReactions';
|
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
|
||||||
import { useItemsActions } from '../ducks/items';
|
import { useItemsActions } from '../ducks/items';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {
|
||||||
} from '../selectors/items';
|
} from '../selectors/items';
|
||||||
import { imageToBlurHash } from '../../util/imageToBlurHash';
|
import { imageToBlurHash } from '../../util/imageToBlurHash';
|
||||||
import { processAttachment } from '../../util/processAttachment';
|
import { processAttachment } from '../../util/processAttachment';
|
||||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
import { useEmojisActions } from '../ducks/emojis';
|
||||||
import { useAudioPlayerActions } from '../ducks/audioPlayer';
|
import { useAudioPlayerActions } from '../ducks/audioPlayer';
|
||||||
import { useComposerActions } from '../ducks/composer';
|
import { useComposerActions } from '../ducks/composer';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
|
@ -148,6 +148,7 @@ export function SmartStoryCreator(): JSX.Element | null {
|
||||||
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
|
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
|
||||||
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
|
||||||
signalConnections={signalConnections}
|
signalConnections={signalConnections}
|
||||||
|
sortedGroupMembers={null}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
theme={ThemeType.dark}
|
theme={ThemeType.dark}
|
||||||
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
toggleGroupsForStorySend={toggleGroupsForStorySend}
|
||||||
|
|
|
@ -32,7 +32,7 @@ import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
|
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
|
||||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
import { useEmojisActions } from '../ducks/emojis';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import { useRecentEmojis } from '../selectors/emojis';
|
import { useRecentEmojis } from '../selectors/emojis';
|
||||||
import { useItemsActions } from '../ducks/items';
|
import { useItemsActions } from '../ducks/items';
|
||||||
|
|
|
@ -50,6 +50,7 @@ function renderItem({
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
isBlocked,
|
||||||
isOldestTimelineItem,
|
isOldestTimelineItem,
|
||||||
messageId,
|
messageId,
|
||||||
nextMessageId,
|
nextMessageId,
|
||||||
|
@ -61,6 +62,7 @@ function renderItem({
|
||||||
containerElementRef={containerElementRef}
|
containerElementRef={containerElementRef}
|
||||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
|
isBlocked={isBlocked}
|
||||||
isOldestTimelineItem={isOldestTimelineItem}
|
isOldestTimelineItem={isOldestTimelineItem}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
previousMessageId={previousMessageId}
|
previousMessageId={previousMessageId}
|
||||||
|
@ -163,6 +165,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
'isGroupV1AndDisabled',
|
'isGroupV1AndDisabled',
|
||||||
'typingContactIdTimestamps',
|
'typingContactIdTimestamps',
|
||||||
]),
|
]),
|
||||||
|
isBlocked: conversation.isBlocked ?? false,
|
||||||
isConversationSelected: state.conversations.selectedConversationId === id,
|
isConversationSelected: state.conversations.selectedConversationId === id,
|
||||||
isIncomingMessageRequest: Boolean(
|
isIncomingMessageRequest: Boolean(
|
||||||
!conversation.acceptedMessageRequest &&
|
!conversation.acceptedMessageRequest &&
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||||
|
@ -35,11 +35,13 @@ import { isSameDay } from '../../util/timestamp';
|
||||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { renderReactionPicker } from './renderReactionPicker';
|
import { renderReactionPicker } from './renderReactionPicker';
|
||||||
|
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
|
||||||
|
|
||||||
export type SmartTimelineItemProps = {
|
export type SmartTimelineItemProps = {
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
isBlocked: boolean;
|
||||||
isOldestTimelineItem: boolean;
|
isOldestTimelineItem: boolean;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
nextMessageId: undefined | string;
|
nextMessageId: undefined | string;
|
||||||
|
@ -59,6 +61,7 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
isBlocked,
|
||||||
isOldestTimelineItem,
|
isOldestTimelineItem,
|
||||||
messageId,
|
messageId,
|
||||||
nextMessageId,
|
nextMessageId,
|
||||||
|
@ -136,23 +139,27 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
showContactModal,
|
showContactModal,
|
||||||
showEditHistoryModal,
|
showEditHistoryModal,
|
||||||
|
toggleMessageRequestActionsConfirmation,
|
||||||
toggleDeleteMessagesModal,
|
toggleDeleteMessagesModal,
|
||||||
toggleForwardMessagesModal,
|
toggleForwardMessagesModal,
|
||||||
toggleSafetyNumberModal,
|
toggleSafetyNumberModal,
|
||||||
} = useGlobalModalActions();
|
} = useGlobalModalActions();
|
||||||
|
|
||||||
const { checkForAccount } = useAccountsActions();
|
const { checkForAccount } = useAccountsActions();
|
||||||
|
|
||||||
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
|
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
|
||||||
|
|
||||||
const { viewStory } = useStoriesActions();
|
const { viewStory } = useStoriesActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
} = useCallingActions();
|
} = useCallingActions();
|
||||||
|
|
||||||
|
const onOpenMessageRequestActionsConfirmation = useCallback(
|
||||||
|
(state: MessageRequestState) => {
|
||||||
|
toggleMessageRequestActionsConfirmation({ conversationId, state });
|
||||||
|
},
|
||||||
|
[conversationId, toggleMessageRequestActionsConfirmation]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -175,6 +182,7 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
||||||
showEditHistoryModal={showEditHistoryModal}
|
showEditHistoryModal={showEditHistoryModal}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode={interactionMode}
|
interactionMode={interactionMode}
|
||||||
|
isBlocked={isBlocked}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
platform={platform}
|
platform={platform}
|
||||||
blockGroupLinkRequests={blockGroupLinkRequests}
|
blockGroupLinkRequests={blockGroupLinkRequests}
|
||||||
|
@ -188,6 +196,9 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
||||||
pushPanelForConversation={pushPanelForConversation}
|
pushPanelForConversation={pushPanelForConversation}
|
||||||
reactToMessage={reactToMessage}
|
reactToMessage={reactToMessage}
|
||||||
copyMessageText={copyMessageText}
|
copyMessageText={copyMessageText}
|
||||||
|
onOpenMessageRequestActionsConfirmation={
|
||||||
|
onOpenMessageRequestActionsConfirmation
|
||||||
|
}
|
||||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||||
retryDeleteForEveryone={retryDeleteForEveryone}
|
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||||
|
|
|
@ -114,7 +114,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
|
||||||
const item = leftPane
|
const item = leftPane
|
||||||
.locator(
|
.locator(
|
||||||
'.module-conversation-list__item--contact-or-conversation' +
|
'.module-conversation-list__item--contact-or-conversation' +
|
||||||
`>> text=${LAST_MESSAGE}`
|
'>> text="You accepted the message request"'
|
||||||
)
|
)
|
||||||
.first();
|
.first();
|
||||||
await item.click({ timeout: 2 * MINUTE });
|
await item.click({ timeout: 2 * MINUTE });
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright 2023 Signal Messenger, LLC
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 {
|
export function bufferToUuid(buffer: Buffer): string {
|
||||||
const hex = buffer.toString('hex');
|
const hex = buffer.toString('hex');
|
||||||
|
@ -32,3 +33,44 @@ export async function type(input: Locator, text: string): Promise<void> {
|
||||||
// updated with the right value
|
// updated with the right value
|
||||||
await input.locator(`:text("${currentValue}${text}")`).waitFor();
|
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';
|
} from '../../util/libphonenumberInstance';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
import type { App } from '../bootstrap';
|
import type { App } from '../bootstrap';
|
||||||
|
import { expectSystemMessages } from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:gv2');
|
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');
|
debug('Checking that notifications are present');
|
||||||
await window
|
await window
|
||||||
.locator(`"${first.profileName} invited you to the group."`)
|
.locator(
|
||||||
|
`.SystemMessage:has-text("${first.profileName} invited you to the group.")`
|
||||||
|
)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
await window
|
await window
|
||||||
.locator(
|
.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();
|
.waitFor();
|
||||||
|
|
||||||
|
@ -130,7 +133,9 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
assert(group.getPendingMemberByServiceId(desktop.pni));
|
assert(group.getPendingMemberByServiceId(desktop.pni));
|
||||||
|
|
||||||
await window
|
await window
|
||||||
.locator(`"${second.profileName} invited you to the group."`)
|
.locator(
|
||||||
|
`.SystemMessage:has-text("${second.profileName} invited you to the group.")`
|
||||||
|
)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
debug('Verify that message request state is not visible');
|
debug('Verify that message request state is not visible');
|
||||||
|
@ -179,11 +184,11 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
debug('Declining');
|
debug('Declining');
|
||||||
await conversationStack
|
await conversationStack
|
||||||
.locator('.module-message-request-actions button >> "Delete"')
|
.locator('.module-message-request-actions button >> "Block"')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
debug('waiting for confirmation modal');
|
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);
|
group = await phone.waitForGroupUpdate(group);
|
||||||
assert.strictEqual(group.revision, 2);
|
assert.strictEqual(group.revision, 2);
|
||||||
|
@ -217,7 +222,9 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
debug('Waiting for the PNI invite');
|
debug('Waiting for the PNI invite');
|
||||||
await window
|
await window
|
||||||
.locator(`text=${first.profileName} invited you to the group.`)
|
.locator(
|
||||||
|
`.SystemMessage:has-text("${first.profileName} invited you to the group.")`
|
||||||
|
)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
debug('Inviting ACI from another contact');
|
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');
|
debug('Waiting for the ACI invite');
|
||||||
await window
|
await window
|
||||||
.locator(`text=${second.profileName} invited you to the group.`)
|
.locator(
|
||||||
|
`.SystemMessage:has-text("${second.profileName} invited you to the group.")`
|
||||||
|
)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
debug('Accepting');
|
debug('Accepting');
|
||||||
|
@ -240,8 +249,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
debug('Checking final notification');
|
debug('Checking final notification');
|
||||||
await window
|
await window
|
||||||
.locator(
|
.locator(
|
||||||
'.SystemMessage >> text=You accepted an invitation to the group from ' +
|
`.SystemMessage:has-text("You accepted an invitation to the group from ${second.profileName}.")`
|
||||||
`${second.profileName}.`
|
|
||||||
)
|
)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
|
@ -291,11 +299,11 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
debug('Declining');
|
debug('Declining');
|
||||||
await conversationStack
|
await conversationStack
|
||||||
.locator('.module-message-request-actions button >> "Delete"')
|
.locator('.module-message-request-actions button >> "Block"')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
debug('waiting for confirmation modal');
|
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);
|
group = await phone.waitForGroupUpdate(group);
|
||||||
assert.strictEqual(group.revision, 3);
|
assert.strictEqual(group.revision, 3);
|
||||||
|
@ -347,13 +355,10 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
sendUpdateTo: [{ device: desktop }],
|
sendUpdateTo: [{ device: desktop }],
|
||||||
});
|
});
|
||||||
|
|
||||||
await window
|
await expectSystemMessages(window, [
|
||||||
.locator(
|
'You were added to the group.',
|
||||||
'.SystemMessage >> ' +
|
`${second.profileName} accepted an invitation to the group from ${first.profileName}.`,
|
||||||
`text=${second.profileName} accepted an invitation to the group ` +
|
]);
|
||||||
`from ${first.profileName}.`
|
|
||||||
)
|
|
||||||
.waitFor();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a e164 for a PNI invite', async () => {
|
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;
|
const { e164 } = parsedE164;
|
||||||
await window
|
await window
|
||||||
.locator(`.SystemMessage >> text=You invited ${e164} to the group`)
|
.locator(`.SystemMessage:has-text("You invited ${e164} to the group")`)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
debug('Accepting remote invite');
|
debug('Accepting remote invite');
|
||||||
|
@ -408,11 +413,10 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) {
|
||||||
});
|
});
|
||||||
|
|
||||||
debug('Waiting for accept notification');
|
debug('Waiting for accept notification');
|
||||||
await window
|
await expectSystemMessages(window, [
|
||||||
.locator(
|
'You were added to the group.',
|
||||||
'.SystemMessage >> ' +
|
/^You invited .* to the group\.$/,
|
||||||
`text=${unknownPniContact.profileName} accepted your invitation to the group`
|
`${unknownPniContact.profileName} accepted your invitation to the group.`,
|
||||||
)
|
]);
|
||||||
.waitFor();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { toUntaggedPni } from '../../types/ServiceId';
|
||||||
import { MY_STORY_ID } from '../../types/Stories';
|
import { MY_STORY_ID } from '../../types/Stories';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
import type { App } from '../bootstrap';
|
import type { App } from '../bootstrap';
|
||||||
|
import { expectSystemMessages } from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:merge');
|
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');
|
const messages = window.locator('.module-message__text');
|
||||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||||
|
|
||||||
// No notifications
|
await expectSystemMessages(window, [
|
||||||
const notifications = window.locator('.SystemMessage');
|
'You accepted the message request',
|
||||||
assert.strictEqual(
|
]);
|
||||||
await notifications.count(),
|
|
||||||
0,
|
|
||||||
'notification count'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withPNIMessage) {
|
if (withPNIMessage) {
|
||||||
|
@ -210,20 +207,25 @@ describe('pnp/merge', function (this: Mocha.Suite) {
|
||||||
'message count'
|
'message count'
|
||||||
);
|
);
|
||||||
|
|
||||||
// One notification - the merge
|
if (withPNIMessage) {
|
||||||
const notifications = window.locator('.SystemMessage');
|
if (pniSignatureVerified) {
|
||||||
assert.strictEqual(
|
await expectSystemMessages(window, [
|
||||||
await notifications.count(),
|
'You accepted the message request',
|
||||||
withPNIMessage ? 1 : 0,
|
'You accepted the message request',
|
||||||
'notification count'
|
/Your message history with ACI Contact and their number .* has been merged\./,
|
||||||
);
|
]);
|
||||||
|
} else {
|
||||||
if (withPNIMessage && !pniSignatureVerified) {
|
await expectSystemMessages(window, [
|
||||||
const first = await notifications.first();
|
'You accepted the message request',
|
||||||
assert.match(
|
'You accepted the message request',
|
||||||
await first.innerText(),
|
/Your message history with ACI Contact and their number .* has been merged\./,
|
||||||
/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 { toUntaggedPni } from '../../types/ServiceId';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
import type { App } from '../bootstrap';
|
import type { App } from '../bootstrap';
|
||||||
|
import { expectSystemMessages } from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:merge');
|
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');
|
const messages = window.locator('.module-message__text');
|
||||||
assert.strictEqual(await messages.count(), 1, 'message count');
|
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||||
|
|
||||||
// One notification - the PhoneNumberDiscovery
|
await expectSystemMessages(window, [
|
||||||
const notifications = window.locator('.SystemMessage');
|
'You accepted the message request',
|
||||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
/.* belongs to ACI Contact/,
|
||||||
|
]);
|
||||||
const first = await notifications.first();
|
|
||||||
assert.match(await first.innerText(), /.* belongs to ACI Contact/);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import * as durations from '../../util/durations';
|
||||||
import { generatePni, toUntaggedPni } from '../../types/ServiceId';
|
import { generatePni, toUntaggedPni } from '../../types/ServiceId';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
import type { App } from '../bootstrap';
|
import type { App } from '../bootstrap';
|
||||||
|
import { expectSystemMessages } from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:pni-change');
|
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');
|
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||||
|
|
||||||
// No notifications
|
// No notifications
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, ['You accepted the message request']);
|
||||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Send message to contactA');
|
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');
|
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||||
|
|
||||||
// Only a PhoneNumberDiscovery notification
|
// Only a PhoneNumberDiscovery notification
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, [
|
||||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
'You accepted the message request',
|
||||||
|
/.* belongs to ContactA/,
|
||||||
const first = await notifications.first();
|
]);
|
||||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -199,9 +198,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
||||||
const messages = window.locator('.module-message__text');
|
const messages = window.locator('.module-message__text');
|
||||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||||
|
|
||||||
// No notifications
|
await expectSystemMessages(window, ['You accepted the message request']);
|
||||||
const notifications = window.locator('.SystemMessage');
|
|
||||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Send message to contactA');
|
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');
|
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||||
|
|
||||||
// Two notifications - the safety number change and PhoneNumberDiscovery
|
// Two notifications - the safety number change and PhoneNumberDiscovery
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, [
|
||||||
assert.strictEqual(await notifications.count(), 2, 'notification count');
|
'You accepted the message request',
|
||||||
|
/.* belongs to ContactA/,
|
||||||
const first = await notifications.first();
|
/Safety Number has changed/,
|
||||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
]);
|
||||||
|
|
||||||
const second = await notifications.nth(1);
|
|
||||||
assert.match(await second.innerText(), /Safety Number has changed/);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -305,9 +299,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
||||||
const messages = window.locator('.module-message__text');
|
const messages = window.locator('.module-message__text');
|
||||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||||
|
|
||||||
// No notifications
|
await expectSystemMessages(window, ['You accepted the message request']);
|
||||||
const notifications = window.locator('.SystemMessage');
|
|
||||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Send message to contactA');
|
debug('Send message to contactA');
|
||||||
|
@ -403,15 +395,12 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
||||||
const messages = window.locator('.module-message__text');
|
const messages = window.locator('.module-message__text');
|
||||||
assert.strictEqual(await messages.count(), 2, 'message count');
|
assert.strictEqual(await messages.count(), 2, 'message count');
|
||||||
|
|
||||||
// Two notifications - the safety number change and PhoneNumberDiscovery
|
// Three notifications - accepted, the safety number change and PhoneNumberDiscovery
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, [
|
||||||
assert.strictEqual(await notifications.count(), 2, 'notification count');
|
'You accepted the message request',
|
||||||
|
/.* belongs to ContactA/,
|
||||||
const first = await notifications.first();
|
/Safety Number has changed/,
|
||||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
]);
|
||||||
|
|
||||||
const second = await notifications.nth(1);
|
|
||||||
assert.match(await second.innerText(), /Safety Number has changed/);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -442,8 +431,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
||||||
assert.strictEqual(await messages.count(), 0, 'message count');
|
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||||
|
|
||||||
// No notifications
|
// No notifications
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, ['You accepted the message request']);
|
||||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Send message to contactA');
|
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');
|
assert.strictEqual(await messages.count(), 2, 'message count');
|
||||||
|
|
||||||
// Only a PhoneNumberDiscovery notification
|
// Only a PhoneNumberDiscovery notification
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, [
|
||||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
'You accepted the message request',
|
||||||
|
/.* belongs to ContactA/,
|
||||||
const first = await notifications.first();
|
]);
|
||||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
RECEIPT_BATCHER_WAIT_MS,
|
RECEIPT_BATCHER_WAIT_MS,
|
||||||
} from '../../types/Receipt';
|
} from '../../types/Receipt';
|
||||||
import { sleep } from '../../util/sleep';
|
import { sleep } from '../../util/sleep';
|
||||||
|
import { expectSystemMessages } from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:pni-signature');
|
export const debug = createDebug('mock:test:pni-signature');
|
||||||
|
|
||||||
|
@ -235,9 +236,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
const messages = window.locator('.module-message__text');
|
const messages = window.locator('.module-message__text');
|
||||||
assert.strictEqual(await messages.count(), 4, 'message count');
|
assert.strictEqual(await messages.count(), 4, 'message count');
|
||||||
|
|
||||||
// No notifications
|
await expectSystemMessages(window, ['You accepted the message request']);
|
||||||
const notifications = window.locator('.SystemMessage');
|
|
||||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -424,11 +423,10 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
assert.strictEqual(await messages.count(), 3, 'messages');
|
assert.strictEqual(await messages.count(), 3, 'messages');
|
||||||
|
|
||||||
// Title transition notification
|
// Title transition notification
|
||||||
const notifications = window.locator('.SystemMessage');
|
await expectSystemMessages(window, [
|
||||||
assert.strictEqual(await notifications.count(), 1, 'notifications');
|
'You accepted the message request',
|
||||||
|
/You started this chat with/,
|
||||||
const first = await notifications.first();
|
]);
|
||||||
assert.match(await first.innerText(), /You started this chat with/);
|
|
||||||
|
|
||||||
assert.isEmpty(await phone.getOrphanedStorageKeys());
|
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();
|
await detailsHeader.locator('button >> "My group"').click();
|
||||||
|
|
||||||
const modal = window.locator('.module-Modal:has-text("Edit group")');
|
const modal = window.locator('.module-Modal:has-text("Edit group")');
|
||||||
|
await modal.locator('input').fill('My group (v2)');
|
||||||
// Group title should be immediately focused.
|
|
||||||
await modal.type(' (v2)');
|
|
||||||
|
|
||||||
await modal.locator('button >> "Save"').click();
|
await modal.locator('button >> "Save"').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { Job } from '../../../jobs/Job';
|
import { Job } from '../../../jobs/Job';
|
||||||
import { generateAci } from '../../../types/ServiceId';
|
|
||||||
|
|
||||||
import { addReportSpamJob } from '../../../jobs/helpers/addReportSpamJob';
|
import { addReportSpamJob } from '../../../jobs/helpers/addReportSpamJob';
|
||||||
|
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||||
|
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
describe('addReportSpamJob', () => {
|
describe('addReportSpamJob', () => {
|
||||||
let getMessageServerGuidsForSpam: sinon.SinonStub;
|
let getMessageServerGuidsForSpam: sinon.SinonStub;
|
||||||
let jobQueue: { add: sinon.SinonStub };
|
let jobQueue: { add: sinon.SinonStub };
|
||||||
|
|
||||||
const conversation = {
|
const conversation: ConversationType = getDefaultConversation();
|
||||||
id: 'convo',
|
|
||||||
type: 'private' as const,
|
|
||||||
serviceId: generateAci(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getMessageServerGuidsForSpam = sinon.stub().resolves(['abc', 'xyz']);
|
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',
|
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
||||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||||
ReactionFailed = 'ReactionFailed',
|
ReactionFailed = 'ReactionFailed',
|
||||||
|
ReportedSpam = 'ReportedSpam',
|
||||||
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
||||||
StickerPackInstallFailed = 'StickerPackInstallFailed',
|
StickerPackInstallFailed = 'StickerPackInstallFailed',
|
||||||
StoryMuted = 'StoryMuted',
|
StoryMuted = 'StoryMuted',
|
||||||
|
@ -120,6 +121,7 @@ export type AnyToast =
|
||||||
| { toastType: ToastType.OriginalMessageNotFound }
|
| { toastType: ToastType.OriginalMessageNotFound }
|
||||||
| { toastType: ToastType.PinnedConversationsFull }
|
| { toastType: ToastType.PinnedConversationsFull }
|
||||||
| { toastType: ToastType.ReactionFailed }
|
| { toastType: ToastType.ReactionFailed }
|
||||||
|
| { toastType: ToastType.ReportedSpam }
|
||||||
| { toastType: ToastType.ReportedSpamAndBlocked }
|
| { toastType: ToastType.ReportedSpamAndBlocked }
|
||||||
| { toastType: ToastType.StickerPackInstallFailed }
|
| { toastType: ToastType.StickerPackInstallFailed }
|
||||||
| { toastType: ToastType.StoryMuted }
|
| { 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,
|
inboxPosition,
|
||||||
isArchived: attributes.isArchived,
|
isArchived: attributes.isArchived,
|
||||||
isBlocked: isBlocked(attributes),
|
isBlocked: isBlocked(attributes),
|
||||||
|
reportingToken: attributes.reportingToken,
|
||||||
removalStage: attributes.removalStage,
|
removalStage: attributes.removalStage,
|
||||||
isMe: isMe(attributes),
|
isMe: isMe(attributes),
|
||||||
isGroupV1AndDisabled: isGroupV1(attributes),
|
isGroupV1AndDisabled: isGroupV1(attributes),
|
||||||
|
|
|
@ -45,12 +45,15 @@ import {
|
||||||
isTapToView,
|
isTapToView,
|
||||||
isUnsupportedMessage,
|
isUnsupportedMessage,
|
||||||
isConversationMerge,
|
isConversationMerge,
|
||||||
|
isMessageRequestResponse,
|
||||||
} from '../state/selectors/message';
|
} from '../state/selectors/message';
|
||||||
import {
|
import {
|
||||||
getContact,
|
getContact,
|
||||||
messageHasPaymentEvent,
|
messageHasPaymentEvent,
|
||||||
getPaymentEventNotificationText,
|
getPaymentEventNotificationText,
|
||||||
} from '../messages/helpers';
|
} from '../messages/helpers';
|
||||||
|
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
|
||||||
|
import { missingCaseError } from './missingCaseError';
|
||||||
|
|
||||||
function getNameForNumber(e164: string): string {
|
function getNameForNumber(e164: string): string {
|
||||||
const conversation = window.ConversationController.get(e164);
|
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;
|
const { attachments = [] } = attributes;
|
||||||
|
|
||||||
if (isTapToView(attributes)) {
|
if (isTapToView(attributes)) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from '../messages/helpers';
|
} from '../messages/helpers';
|
||||||
import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation';
|
import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation';
|
||||||
import { getE164 } from './getE164';
|
import { getE164 } from './getE164';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
export function getMessageIdForLogging(
|
export function getMessageIdForLogging(
|
||||||
message: Pick<
|
message: Pick<
|
||||||
|
@ -27,7 +28,7 @@ export function getMessageIdForLogging(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConversationIdForLogging(
|
export function getConversationIdForLogging(
|
||||||
conversation: ConversationAttributesType
|
conversation: ConversationAttributesType | ConversationType
|
||||||
): string {
|
): string {
|
||||||
if (isDirectConversation(conversation)) {
|
if (isDirectConversation(conversation)) {
|
||||||
const { serviceId, pni, id } = conversation;
|
const { serviceId, pni, id } = conversation;
|
||||||
|
|
|
@ -3372,6 +3372,13 @@
|
||||||
"updated": "2022-01-04T21:43:17.517Z",
|
"updated": "2022-01-04T21:43:17.517Z",
|
||||||
"reasonDetail": "Used to change the style in non-production builds."
|
"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",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/Slider.tsx",
|
"path": "ts/components/Slider.tsx",
|
||||||
|
|