Support for incoming gift badges
This commit is contained in:
parent
6b4bea6330
commit
0ba6a0926e
41 changed files with 1476 additions and 164 deletions
|
@ -2715,6 +2715,102 @@
|
||||||
"message": "This message was deleted.",
|
"message": "This message was deleted.",
|
||||||
"description": "Shown in a message's bubble when the message has been deleted for everyone."
|
"description": "Shown in a message's bubble when the message has been deleted for everyone."
|
||||||
},
|
},
|
||||||
|
"message--giftBadge--unopened": {
|
||||||
|
"message": "View this message on mobile to open it",
|
||||||
|
"description": "Shown in a message's bubble when you've received a gift badge from a contact"
|
||||||
|
},
|
||||||
|
"message--giftBadge--unopened--label": {
|
||||||
|
"message": "Gift",
|
||||||
|
"description": "Shown in a message's bubble when you've received a gift badge from a contact"
|
||||||
|
},
|
||||||
|
"message--giftBadge--unopened--toast--incoming": {
|
||||||
|
"message": "Check your phone to open gift",
|
||||||
|
"description": "Shown when you've clicked on an incoming gift badge you haven't yet redeemed"
|
||||||
|
},
|
||||||
|
"message--giftBadge--unopened--toast--outgoing": {
|
||||||
|
"message": "Check your phone to view your gift",
|
||||||
|
"description": "Shown when you've clicked on an outgoing gift badge"
|
||||||
|
},
|
||||||
|
"message--giftBadge--preview--unopened": {
|
||||||
|
"message": "You received a gift",
|
||||||
|
"description": "Shown to label the gift badge in notifications and the left pane"
|
||||||
|
},
|
||||||
|
"message--giftBadge--preview--redeemed": {
|
||||||
|
"message": "You redeemed a gift badge",
|
||||||
|
"description": "Shown to label the redeemed gift badge in notifications and the left pane"
|
||||||
|
},
|
||||||
|
"message--giftBadge--preview--sent": {
|
||||||
|
"message": "You sent a gift badge",
|
||||||
|
"description": "Shown to label a gift badge you've sent in notifications and the left pane"
|
||||||
|
},
|
||||||
|
"message--giftBadge": {
|
||||||
|
"message": "Gift Badge",
|
||||||
|
"description": "Shown to label the gift badge you've redeemed on another device"
|
||||||
|
},
|
||||||
|
"quote--giftBadge": {
|
||||||
|
"message": "Gift",
|
||||||
|
"description": "Shown to label a gift badge you've replied to"
|
||||||
|
},
|
||||||
|
"message--giftBadge--remaining--days": {
|
||||||
|
"message": "$days$ days remaining",
|
||||||
|
"description": "Describes how long remains for the gift badge you've redeemed on another device (only rendered for days > 1)",
|
||||||
|
"placeholders": {
|
||||||
|
"days": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "58"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message--giftBadge--remaining--hours": {
|
||||||
|
"message": "$hours$ hours remaining",
|
||||||
|
"description": "Describes how long remains for the gift badge you've redeemed on another device (only rendered for hours > 1)",
|
||||||
|
"placeholders": {
|
||||||
|
"hours": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message--giftBadge--remaining--minutes": {
|
||||||
|
"message": "$minutes$ minutes remaining",
|
||||||
|
"description": "Describes how long remains for the gift badge you've redeemed on another device (only rendered for minutes > 1)",
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "45"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message--giftBadge--remaining--one-minute": {
|
||||||
|
"message": "1 minute remaining",
|
||||||
|
"description": "Describes how long remains for the gift badge you've redeemed on another device"
|
||||||
|
},
|
||||||
|
"message--giftBadge--expired": {
|
||||||
|
"message": "Expired",
|
||||||
|
"description": "Shows that a gift badge is expired"
|
||||||
|
},
|
||||||
|
"message--giftBadge--view": {
|
||||||
|
"message": "View",
|
||||||
|
"description": "Shown when you've sent a gift badge to someone then opened it"
|
||||||
|
},
|
||||||
|
"message--giftBadge--redeemed": {
|
||||||
|
"message": "Redeemed",
|
||||||
|
"description": "Shown when you've redeemed the gift badge on another device"
|
||||||
|
},
|
||||||
|
"modal--giftBadge--title": {
|
||||||
|
"message": "Thanks for your support!",
|
||||||
|
"description": "The title of the outgoing gift badge detail dialog"
|
||||||
|
},
|
||||||
|
"modal--giftBadge--description": {
|
||||||
|
"message": "You've gifted a badge to $name$. When they accept, they'll be given a choice to show or hide their badge.",
|
||||||
|
"description": "The description of the outgoing gift badge detail dialog",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Paige Hall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"stickers--toast--InstallFailed": {
|
"stickers--toast--InstallFailed": {
|
||||||
"message": "Sticker pack could not be installed",
|
"message": "Sticker pack could not be installed",
|
||||||
"description": "Shown in a toast if the user attempts to install a sticker pack and it fails"
|
"description": "Shown in a toast if the user attempts to install a sticker pack and it fails"
|
||||||
|
|
16
images/gift-bow.svg
Normal file
16
images/gift-bow.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 60">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #2c6bed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-2" d="M78.738,36.229l-6.797-4.513c2.831-.994,5.188-2.288,6.311-3.947,.864-1.277,1.032-2.749,.473-4.147-3.227-8.067-7.676-15.12-13.223-20.963l-.889-.936-1.219,.424c-9.233,3.215-17.112,12.09-20.719,16.685-.825-.229-1.757-.292-2.677-.292s-1.852,.063-2.678,.291c-3.607-4.595-11.485-13.469-20.717-16.683l-1.219-.424-.889,.936C8.95,8.502,4.501,15.555,1.274,23.622c-.559,1.398-.391,2.871,.473,4.147,1.123,1.659,3.48,2.953,6.311,3.947l-6.797,4.513-1.052,.698,.178,1.25,2.573,18.069,.485,3.406,2.72-2.107c.786-.609,19.3-14.953,22.677-17.79,.925-.777,4.435-3.789,7.171-6.997,.981,.326,2.269,.482,3.986,.482s3.005-.156,3.986-.482c2.736,3.208,6.246,6.22,7.171,6.997,3.377,2.837,21.89,17.18,22.677,17.79l2.72,2.107,.485-3.406,2.573-18.069,.178-1.25-1.052-.698Z"/>
|
||||||
|
<path class="cls-1" d="M54.872,34.542c2.815,0,9.139-.572,14.602-2.063l8.158,5.417-2.573,18.069s-19.23-14.896-22.615-17.74c-1.637-1.375-3.599-3.174-5.349-5.013,1.802,.578,3.962,1.099,6.284,1.28,.433,.034,.935,.051,1.493,.051ZM4.941,55.964s19.23-14.896,22.615-17.74c1.637-1.375,3.599-3.174,5.349-5.013-1.802,.578-3.962,1.099-6.284,1.28-.433,.034-.935,.051-1.493,.051-2.815,0-9.139-.572-14.602-2.063l-8.158,5.417,2.573,18.069ZM43.241,21.846c-.097-.513-.154-1.306-3.241-1.306s-3.144,.793-3.241,1.306l-1.227,7.615c-.123,.649-.002,1.779,4.468,1.779s4.591-1.13,4.468-1.779l-1.227-7.615Zm33.628,2.519c-2.298-5.745-6.187-13.346-12.816-20.329-8.561,2.981-16.064,11.33-19.62,15.81,.369,.409,.646,.933,.774,1.629l.005,.026,.004,.027,1.223,7.594c.123,.685,.025,1.246-.156,1.693,1.893,.709,4.464,1.464,7.252,1.682,.384,.03,.833,.045,1.337,.045,6.927,0,24.139-2.821,21.997-8.176Zm-43.308,4.756l1.223-7.594,.012-.065,.004-.022c.125-.683,.398-1.199,.762-1.601-3.558-4.481-11.058-12.824-19.615-15.803C9.318,11.019,5.429,18.62,3.131,24.365c-2.142,5.355,15.07,8.176,21.997,8.176,.504,0,.953-.015,1.337-.045,2.788-.218,5.359-.972,7.252-1.682-.181-.447-.28-1.008-.156-1.693Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
19
images/gift-thumbnail.svg
Normal file
19
images/gift-thumbnail.svg
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #2c6bed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="cls-2" width="54" height="54"/>
|
||||||
|
<rect class="cls-1" y="24" width="54" height="6"/>
|
||||||
|
<rect class="cls-1" x="24" width="6" height="54"/>
|
||||||
|
<path class="cls-2" d="M42.511,31.491l-2.634-1.805c1.097-.398,2.01-.915,2.445-1.579,.335-.511,.4-1.1,.183-1.659-1.25-3.227-2.974-6.048-5.124-8.385l-.344-.374-.472,.17c-3.578,1.286-6.631,4.836-8.029,6.674-.32-.092-.681-.117-1.037-.117s-.718,.025-1.038,.116c-1.398-1.838-4.45-5.388-8.028-6.673l-.472-.17-.344,.374c-2.149,2.337-3.873,5.158-5.123,8.385-.217,.559-.151,1.148,.183,1.659,.435,.664,1.349,1.181,2.446,1.579l-2.634,1.805-.408,.279,.069,.5,.997,7.228,.188,1.362,1.054-.843c.305-.244,7.479-5.981,8.787-7.116,.358-.311,1.719-1.516,2.779-2.799,.38,.13,.879,.193,1.545,.193s1.164-.062,1.545-.193c1.06,1.283,2.42,2.488,2.779,2.799,1.309,1.135,8.482,6.872,8.787,7.116l1.054,.843,.188-1.362,.997-7.228,.069-.5-.408-.279h0Z"/>
|
||||||
|
<path class="cls-1" d="M33.263,30.817c1.091,0,3.541-.229,5.658-.825l3.161,2.167-.997,7.228s-7.452-5.958-8.763-7.096c-.634-.55-1.395-1.27-2.073-2.005,.698,.231,1.535,.44,2.435,.512,.168,.014,.362,.02,.579,.02h0Zm-19.348,8.568s7.452-5.958,8.763-7.096c.634-.55,1.395-1.27,2.073-2.005-.698,.231-1.535,.44-2.435,.512-.168,.014-.362,.02-.579,.02-1.091,0-3.541-.229-5.658-.825l-3.161,2.167,.997,7.228h0Zm14.841-13.648c-.038-.205-.06-.522-1.256-.522s-1.218,.317-1.256,.522l-.476,3.046c-.048,.26,0,.712,1.731,.712s1.779-.452,1.731-.712l-.475-3.046h0Zm13.031,1.008c-.89-2.298-2.397-5.338-4.966-8.132-3.317,1.192-6.225,4.532-7.603,6.324,.143,.164,.25,.373,.3,.652l.002,.01,.002,.011,.474,3.038c.048,.274,.01,.498-.061,.677,.734,.284,1.73,.586,2.81,.673,.149,.012,.323,.018,.518,.018,2.684,0,9.354-1.128,8.524-3.27h0Zm-16.782,1.902l.474-3.038,.005-.026,.002-.009c.048-.273,.154-.48,.295-.64-1.379-1.792-4.285-5.13-7.601-6.321-2.569,2.793-4.076,5.834-4.966,8.132-.83,2.142,5.84,3.27,8.524,3.27,.195,0,.369-.006,.518-.018,1.08-.087,2.077-.389,2.81-.673-.07-.179-.109-.403-.06-.677h0Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -128,6 +128,11 @@ message DataMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
message Quote {
|
message Quote {
|
||||||
|
enum Type {
|
||||||
|
NORMAL = 0;
|
||||||
|
GIFT_BADGE = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message QuotedAttachment {
|
message QuotedAttachment {
|
||||||
optional string contentType = 1;
|
optional string contentType = 1;
|
||||||
optional string fileName = 2;
|
optional string fileName = 2;
|
||||||
|
@ -140,6 +145,7 @@ message DataMessage {
|
||||||
optional string text = 3;
|
optional string text = 3;
|
||||||
repeated QuotedAttachment attachments = 4;
|
repeated QuotedAttachment attachments = 4;
|
||||||
repeated BodyRange bodyRanges = 6;
|
repeated BodyRange bodyRanges = 6;
|
||||||
|
optional Type type = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Contact {
|
message Contact {
|
||||||
|
@ -269,6 +275,10 @@ message DataMessage {
|
||||||
CURRENT = 7;
|
CURRENT = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GiftBadge {
|
||||||
|
optional bytes receiptCredentialPresentation = 1;
|
||||||
|
}
|
||||||
|
|
||||||
optional string body = 1;
|
optional string body = 1;
|
||||||
repeated AttachmentPointer attachments = 2;
|
repeated AttachmentPointer attachments = 2;
|
||||||
optional GroupContext group = 3;
|
optional GroupContext group = 3;
|
||||||
|
@ -289,6 +299,7 @@ message DataMessage {
|
||||||
optional GroupCallUpdate groupCallUpdate = 19;
|
optional GroupCallUpdate groupCallUpdate = 19;
|
||||||
reserved /* Payment payment */ 20;
|
reserved /* Payment payment */ 20;
|
||||||
optional StoryContext storyContext = 21;
|
optional StoryContext storyContext = 21;
|
||||||
|
optional GiftBadge giftBadge = 22;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NullMessage {
|
message NullMessage {
|
||||||
|
|
|
@ -301,6 +301,10 @@
|
||||||
max-width: 370px;
|
max-width: 370px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$message-padding-vertical: 8px;
|
||||||
|
$message-padding-horizontal: 12px;
|
||||||
|
|
||||||
.module-message__container {
|
.module-message__container {
|
||||||
$collapsed-border-radius: 4px;
|
$collapsed-border-radius: 4px;
|
||||||
|
|
||||||
|
@ -312,12 +316,11 @@
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// These should match the margins in .module-message__attachment-container.
|
|
||||||
padding: {
|
padding: {
|
||||||
left: 12px;
|
left: $message-padding-horizontal;
|
||||||
right: 12px;
|
right: $message-padding-horizontal;
|
||||||
top: 8px;
|
top: $message-padding-vertical;
|
||||||
bottom: 8px;
|
bottom: $message-padding-vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message--collapsed-above & {
|
.module-message--collapsed-above & {
|
||||||
|
@ -563,13 +566,11 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
// These should match the paddings from .module-message__container,
|
|
||||||
// effectively "undoing" that padding.
|
|
||||||
margin: {
|
margin: {
|
||||||
left: -12px;
|
left: -$message-padding-horizontal;
|
||||||
right: -12px;
|
right: -$message-padding-horizontal;
|
||||||
top: -8px;
|
top: -$message-padding-vertical;
|
||||||
bottom: -8px;
|
bottom: -$message-padding-vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
@ -596,10 +597,10 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
margin: {
|
margin: {
|
||||||
left: -12px;
|
left: -$message-padding-horizontal;
|
||||||
right: -12px;
|
right: -$message-padding-horizontal;
|
||||||
top: -9px;
|
top: -$message-padding-vertical - 1px;
|
||||||
bottom: -5px;
|
bottom: -$message-padding-vertical + 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--with-content-below {
|
&--with-content-below {
|
||||||
|
@ -787,12 +788,12 @@
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
margin-left: -12px;
|
margin-left: -$message-padding-horizontal;
|
||||||
margin-right: -12px;
|
margin-right: -$message-padding-horizontal;
|
||||||
width: calc(100% + 24px);
|
width: calc(100% + 24px);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
margin-top: -8px;
|
margin-top: -$message-padding-vertical;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -808,7 +809,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__link-preview__content {
|
.module-message__link-preview__content {
|
||||||
padding: 8px 12px;
|
padding: $message-padding-vertical $message-padding-horizontal;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
@ -1219,10 +1220,10 @@
|
||||||
|
|
||||||
@include font-body-2-bold;
|
@include font-body-2-bold;
|
||||||
|
|
||||||
margin-top: 8px;
|
margin-top: $message-padding-vertical;
|
||||||
margin-bottom: -8px;
|
margin-bottom: -$message-padding-vertical;
|
||||||
margin-left: -12px;
|
margin-left: -$message-padding-horizontal;
|
||||||
margin-right: -12px;
|
margin-right: -$message-padding-horizontal;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -1267,6 +1268,301 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__unopened-gift-badge__container {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__unopened-gift-badge {
|
||||||
|
width: 240px;
|
||||||
|
height: 132px;
|
||||||
|
background-color: $color-ultramarine;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin: {
|
||||||
|
left: -$message-padding-horizontal;
|
||||||
|
right: -$message-padding-horizontal;
|
||||||
|
top: -$message-padding-vertical;
|
||||||
|
bottom: $message-padding-vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__unopened-gift-badge--outgoing {
|
||||||
|
@include light-theme {
|
||||||
|
border-bottom: 1px solid $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
border-bottom: 1px solid $color-gray-95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__unopened-gift-badge__ribbon-horizontal {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
.module-message__unopened-gift-badge__ribbon-vertical {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 16px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
.module-message__unopened-gift-badge__bow {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
// Centered
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
// For proper alignment with the ribbons
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
// 75.26px by 51.93px in Figma, but there's a buffer in the SVG file
|
||||||
|
width: 81px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__unopened-gift-badge__text {
|
||||||
|
@include font-body-2;
|
||||||
|
}
|
||||||
|
.module-message__unopened-gift-badge__text--incoming {
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.module-message__unopened-gift-badge__container
|
||||||
|
.module-message__text--incoming {
|
||||||
|
@include font-body-2;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.module-message__unopened-gift-badge__container
|
||||||
|
.module-message__text--outgoing {
|
||||||
|
@include font-body-2;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__redeemed-gift-badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
height: 64px;
|
||||||
|
width: 64px;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&--missing-incoming {
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-15;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--missing-outgoing {
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-white-alpha-20;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-white-alpha-20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-top: 19px;
|
||||||
|
}
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
@include font-body-1;
|
||||||
|
}
|
||||||
|
&__remaining {
|
||||||
|
@include font-subtitle;
|
||||||
|
|
||||||
|
&--incoming {
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-75;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--outgoing {
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
@include button-reset;
|
||||||
|
@include button-secondary;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 216px;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--incoming {
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-90;
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
background-color: $color-gray-62;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabling hover
|
||||||
|
&:hover {
|
||||||
|
@include mouse-mode {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-mouse-mode {
|
||||||
|
background-color: $color-gray-62;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outgoing {
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-90;
|
||||||
|
background-color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-90;
|
||||||
|
background-color: $color-white-alpha-80;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
@include mouse-mode {
|
||||||
|
background-color: $color-white-alpha-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-mouse-mode {
|
||||||
|
background-color: $color-white-alpha-90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
@include keyboard-mode {
|
||||||
|
box-shadow: 0px 0px 0px 3px $color-ultramarine-light;
|
||||||
|
}
|
||||||
|
@include dark-keyboard-mode {
|
||||||
|
box-shadow: 0px 0px 0px 3px $color-ultramarine-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
// We need to include all four here for specificity precedence
|
||||||
|
|
||||||
|
@include mouse-mode {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
@include dark-mouse-mode {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
@include dark-keyboard-mode {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon-check {
|
||||||
|
height: 19px;
|
||||||
|
width: 19px;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&--incoming {
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/check-circle-outline-24.svg',
|
||||||
|
$color-gray-90
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/check-circle-outline-24.svg',
|
||||||
|
$color-gray-05
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outgoing {
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/check-circle-outline-24.svg',
|
||||||
|
$color-gray-90
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/check-circle-outline-24.svg',
|
||||||
|
$color-gray-90
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__typing-container {
|
.module-message__typing-container {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ $color-gray-20: #c6c6c6;
|
||||||
$color-gray-25: #b9b9b9;
|
$color-gray-25: #b9b9b9;
|
||||||
$color-gray-45: #848484;
|
$color-gray-45: #848484;
|
||||||
$color-gray-60: #5e5e5e;
|
$color-gray-60: #5e5e5e;
|
||||||
|
$color-gray-62: #545454;
|
||||||
$color-gray-65: #4a4a4a;
|
$color-gray-65: #4a4a4a;
|
||||||
$color-gray-75: #3b3b3b;
|
$color-gray-75: #3b3b3b;
|
||||||
$color-gray-80: #2e2e2e;
|
$color-gray-80: #2e2e2e;
|
||||||
|
|
46
stylesheets/components/OutgoingGiftBadgeModal.scss
Normal file
46
stylesheets/components/OutgoingGiftBadgeModal.scss
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.OutgoingGiftBadgeModal {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
width: 420px;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include font-title-2;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
&__description {
|
||||||
|
@include font-body-1;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 328px;
|
||||||
|
}
|
||||||
|
&__badge {
|
||||||
|
margin-top: 34px;
|
||||||
|
height: 160px;
|
||||||
|
width: 160px;
|
||||||
|
|
||||||
|
&--missing {
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__badge-summary {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
@include font-body-1-bold;
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,7 +63,6 @@
|
||||||
.module-quote--outgoing {
|
.module-quote--outgoing {
|
||||||
border-left-color: $color-steel;
|
border-left-color: $color-steel;
|
||||||
background-color: $color-steel;
|
background-color: $color-steel;
|
||||||
margin-top: -4px;
|
|
||||||
|
|
||||||
// To preserve contrast
|
// To preserve contrast
|
||||||
@include keyboard-mode {
|
@include keyboard-mode {
|
||||||
|
@ -126,14 +125,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-quote--curve-top-left {
|
|
||||||
border-top-left-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-quote--curve-top-right {
|
|
||||||
border-top-right-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-quote__primary {
|
.module-quote__primary {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
@ -265,6 +256,18 @@
|
||||||
flex: 0 0 54px;
|
flex: 0 0 54px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 54px;
|
width: 54px;
|
||||||
|
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-quote__icon-container__outgoing-gift-badge {
|
||||||
|
@include light-theme {
|
||||||
|
border: 1px solid $color-white;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
border: 1px solid $color-white-alpha-80;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-quote__icon-container__inner {
|
.module-quote__icon-container__inner {
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
@import './components/MessageDetail.scss';
|
@import './components/MessageDetail.scss';
|
||||||
@import './components/Modal.scss';
|
@import './components/Modal.scss';
|
||||||
@import './components/MyStories.scss';
|
@import './components/MyStories.scss';
|
||||||
|
@import './components/OutgoingGiftBadgeModal.scss';
|
||||||
@import './components/PermissionsPopup.scss';
|
@import './components/PermissionsPopup.scss';
|
||||||
@import './components/Preferences.scss';
|
@import './components/Preferences.scss';
|
||||||
@import './components/ProfileEditor.scss';
|
@import './components/ProfileEditor.scss';
|
||||||
|
|
|
@ -2096,6 +2096,7 @@ export async function startApp(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
server.registerCapabilities({
|
server.registerCapabilities({
|
||||||
announcementGroup: true,
|
announcementGroup: true,
|
||||||
|
giftBadges: true,
|
||||||
'gv2-3': true,
|
'gv2-3': true,
|
||||||
'gv1-migration': true,
|
'gv1-migration': true,
|
||||||
senderKey: true,
|
senderKey: true,
|
||||||
|
|
|
@ -23,6 +23,96 @@ const badgeFromServerSchema = z.object({
|
||||||
visible: z.boolean().optional(),
|
visible: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /v1/subscription/boost/badges
|
||||||
|
const boostBadgesFromServerSchema = z.object({
|
||||||
|
levels: z.record(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
badge: z.unknown(),
|
||||||
|
})
|
||||||
|
.or(z.undefined())
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function parseBoostBadgeListFromServer(
|
||||||
|
value: unknown,
|
||||||
|
updatesUrl: string
|
||||||
|
): Record<string, BadgeType> {
|
||||||
|
const result: Record<string, BadgeType> = {};
|
||||||
|
|
||||||
|
const parseResult = boostBadgesFromServerSchema.safeParse(value);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
log.warn(
|
||||||
|
'parseBoostBadgeListFromServer: server response was invalid:',
|
||||||
|
parseResult.error.format()
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
'parseBoostBadgeListFromServer: Failed to parse server response'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boostBadges = parseResult.data;
|
||||||
|
Object.keys(boostBadges.levels).forEach(level => {
|
||||||
|
const item = boostBadges.levels[level];
|
||||||
|
if (!item) {
|
||||||
|
log.warn(`parseBoostBadgeListFromServer: level ${level} had no badge`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseBadgeFromServer(item.badge, updatesUrl);
|
||||||
|
|
||||||
|
if (parsed) {
|
||||||
|
result[`BOOST-${level}`] = parsed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBadgeFromServer(
|
||||||
|
value: unknown,
|
||||||
|
updatesUrl: string
|
||||||
|
): BadgeType | undefined {
|
||||||
|
const parseResult = badgeFromServerSchema.safeParse(value);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
log.warn(
|
||||||
|
'parseBadgeFromServer: badge was invalid:',
|
||||||
|
parseResult.error.format()
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
category,
|
||||||
|
description: descriptionTemplate,
|
||||||
|
expiration,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
svg,
|
||||||
|
svgs,
|
||||||
|
visible,
|
||||||
|
} = parseResult.data;
|
||||||
|
const images = parseImages(svgs, svg, updatesUrl);
|
||||||
|
if (images.length !== 4) {
|
||||||
|
log.warn('Got invalid number of SVGs from the server');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
category: parseBadgeCategory(category),
|
||||||
|
name,
|
||||||
|
descriptionTemplate,
|
||||||
|
images,
|
||||||
|
...(isNormalNumber(expiration) && typeof visible === 'boolean'
|
||||||
|
? {
|
||||||
|
expiresAt: expiration * 1000,
|
||||||
|
isVisible: visible,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function parseBadgesFromServer(
|
export function parseBadgesFromServer(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
updatesUrl: string
|
updatesUrl: string
|
||||||
|
@ -36,45 +126,13 @@ export function parseBadgesFromServer(
|
||||||
const numberOfBadgesToParse = Math.min(value.length, MAX_BADGES);
|
const numberOfBadgesToParse = Math.min(value.length, MAX_BADGES);
|
||||||
for (let i = 0; i < numberOfBadgesToParse; i += 1) {
|
for (let i = 0; i < numberOfBadgesToParse; i += 1) {
|
||||||
const item = value[i];
|
const item = value[i];
|
||||||
|
const parsed = parseBadgeFromServer(item, updatesUrl);
|
||||||
|
|
||||||
const parseResult = badgeFromServerSchema.safeParse(item);
|
if (!parsed) {
|
||||||
if (!parseResult.success) {
|
|
||||||
log.warn(
|
|
||||||
'parseBadgesFromServer got an invalid item',
|
|
||||||
parseResult.error.format()
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
result.push(parsed);
|
||||||
category,
|
|
||||||
description: descriptionTemplate,
|
|
||||||
expiration,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
svg,
|
|
||||||
svgs,
|
|
||||||
visible,
|
|
||||||
} = parseResult.data;
|
|
||||||
const images = parseImages(svgs, svg, updatesUrl);
|
|
||||||
if (images.length !== 4) {
|
|
||||||
log.warn('Got invalid number of SVGs from the server');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
id,
|
|
||||||
category: parseBadgeCategory(category),
|
|
||||||
name,
|
|
||||||
descriptionTemplate,
|
|
||||||
images,
|
|
||||||
...(isNormalNumber(expiration) && typeof visible === 'boolean'
|
|
||||||
? {
|
|
||||||
expiresAt: expiration * 1000,
|
|
||||||
isVisible: visible,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -191,6 +191,7 @@ story.add('Quote', () => (
|
||||||
quotedMessageProps: {
|
quotedMessageProps: {
|
||||||
text: 'something',
|
text: 'something',
|
||||||
conversationColor: ConversationColors[10],
|
conversationColor: ConversationColors[10],
|
||||||
|
isGiftBadge: false,
|
||||||
isViewOnce: false,
|
isViewOnce: false,
|
||||||
referencedMessageNotFound: false,
|
referencedMessageNotFound: false,
|
||||||
authorTitle: 'Someone',
|
authorTitle: 'Someone',
|
||||||
|
|
57
ts/components/OutgoingGiftBadgeModal.stories.tsx
Normal file
57
ts/components/OutgoingGiftBadgeModal.stories.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text } from '@storybook/addon-knobs';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import type { PropsType } from './OutgoingGiftBadgeModal';
|
||||||
|
import { OutgoingGiftBadgeModal } from './OutgoingGiftBadgeModal';
|
||||||
|
|
||||||
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
import { BadgeCategory } from '../badges/BadgeCategory';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const getPreferredBadge = () => ({
|
||||||
|
category: BadgeCategory.Donor,
|
||||||
|
descriptionTemplate: 'This is a description of the badge',
|
||||||
|
id: 'BOOST-3',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
transparent: {
|
||||||
|
localPath: '/fixtures/orange-heart.svg',
|
||||||
|
url: 'http://someplace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: 'heart',
|
||||||
|
});
|
||||||
|
|
||||||
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
recipientTitle: text(
|
||||||
|
'recipientTitle',
|
||||||
|
overrideProps.recipientTitle || 'Default Name'
|
||||||
|
),
|
||||||
|
badgeId: text('badgeId', overrideProps.badgeId || 'heart'),
|
||||||
|
getPreferredBadge,
|
||||||
|
hideOutgoingGiftBadgeModal: action('hideOutgoingGiftBadgeModal'),
|
||||||
|
i18n,
|
||||||
|
});
|
||||||
|
|
||||||
|
const story = storiesOf('Components/OutgoingGiftBadgeModal', module);
|
||||||
|
|
||||||
|
story.add('Normal', () => {
|
||||||
|
return <OutgoingGiftBadgeModal {...createProps()} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Missing badge', () => {
|
||||||
|
const props = {
|
||||||
|
...createProps(),
|
||||||
|
getPreferredBadge: () => undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <OutgoingGiftBadgeModal {...props} />;
|
||||||
|
});
|
77
ts/components/OutgoingGiftBadgeModal.tsx
Normal file
77
ts/components/OutgoingGiftBadgeModal.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { BadgeImageTheme } from '../badges/BadgeImageTheme';
|
||||||
|
|
||||||
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
const CLASS_NAME = 'OutgoingGiftBadgeModal';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
recipientTitle: string;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
badgeId: string;
|
||||||
|
hideOutgoingGiftBadgeModal: () => unknown;
|
||||||
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OutgoingGiftBadgeModal = ({
|
||||||
|
recipientTitle,
|
||||||
|
i18n,
|
||||||
|
badgeId,
|
||||||
|
hideOutgoingGiftBadgeModal,
|
||||||
|
getPreferredBadge,
|
||||||
|
}: PropsType): JSX.Element => {
|
||||||
|
const badge = getPreferredBadge([{ id: badgeId }]);
|
||||||
|
const badgeSize = 140;
|
||||||
|
const badgeImagePath = getBadgeImageFileLocalPath(
|
||||||
|
badge,
|
||||||
|
badgeSize,
|
||||||
|
BadgeImageTheme.Transparent
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeElement = badge ? (
|
||||||
|
<img
|
||||||
|
className={`${CLASS_NAME}__badge`}
|
||||||
|
src={badgeImagePath}
|
||||||
|
alt={badge.name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`${CLASS_NAME}__badge`,
|
||||||
|
`${CLASS_NAME}__badge--missing`
|
||||||
|
)}
|
||||||
|
aria-label={i18n('giftBadge--missing')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
i18n={i18n}
|
||||||
|
moduleClassName={`${CLASS_NAME}__container`}
|
||||||
|
onClose={hideOutgoingGiftBadgeModal}
|
||||||
|
hasXButton
|
||||||
|
useFocusTrap
|
||||||
|
>
|
||||||
|
<div className={CLASS_NAME}>
|
||||||
|
<div className={`${CLASS_NAME}__title`}>
|
||||||
|
{i18n('modal--giftBadge--title')}
|
||||||
|
</div>
|
||||||
|
<div className={`${CLASS_NAME}__description`}>
|
||||||
|
{i18n('modal--giftBadge--description', { name: recipientTitle })}
|
||||||
|
</div>
|
||||||
|
{badgeElement}
|
||||||
|
<div className={`${CLASS_NAME}__badge-summary`}>
|
||||||
|
{i18n('message--giftBadge')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -143,6 +143,7 @@ export const StoryViewsNRepliesModal = ({
|
||||||
conversationColor="ultramarine"
|
conversationColor="ultramarine"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isFromMe={false}
|
isFromMe={false}
|
||||||
|
isGiftBadge={false}
|
||||||
isStoryReply
|
isStoryReply
|
||||||
isViewOnce={false}
|
isViewOnce={false}
|
||||||
moduleClassName="StoryViewsNRepliesModal__quote"
|
moduleClassName="StoryViewsNRepliesModal__quote"
|
||||||
|
|
24
ts/components/ToastCannotOpenGiftBadge.tsx
Normal file
24
ts/components/ToastCannotOpenGiftBadge.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { Toast } from './Toast';
|
||||||
|
|
||||||
|
export type ToastPropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
isIncoming: boolean;
|
||||||
|
onClose: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToastCannotOpenGiftBadge = ({
|
||||||
|
i18n,
|
||||||
|
isIncoming,
|
||||||
|
onClose,
|
||||||
|
}: ToastPropsType): JSX.Element => {
|
||||||
|
const key = `message--giftBadge--unopened--toast--${
|
||||||
|
isIncoming ? 'incoming' : 'outgoing'
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return <Toast onClose={onClose}>{i18n(key)}</Toast>;
|
||||||
|
};
|
|
@ -12,7 +12,7 @@ import { SignalService } from '../../protobuf';
|
||||||
import { ConversationColors } from '../../types/Colors';
|
import { ConversationColors } from '../../types/Colors';
|
||||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
import type { Props, AudioAttachmentProps } from './Message';
|
import type { Props, AudioAttachmentProps } from './Message';
|
||||||
import { TextDirection, Message } from './Message';
|
import { GiftBadgeStates, Message, TextDirection } from './Message';
|
||||||
import {
|
import {
|
||||||
AUDIO_MP3,
|
AUDIO_MP3,
|
||||||
IMAGE_JPEG,
|
IMAGE_JPEG,
|
||||||
|
@ -30,7 +30,7 @@ import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { pngUrl } from '../../storybook/Fixtures';
|
import { pngUrl } from '../../storybook/Fixtures';
|
||||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||||
import { WidthBreakpoint } from '../_util';
|
import { WidthBreakpoint } from '../_util';
|
||||||
import { MINUTE } from '../../util/durations';
|
import { DAY, HOUR, MINUTE, SECOND } from '../../util/durations';
|
||||||
import { ContactFormType } from '../../types/EmbeddedContact';
|
import { ContactFormType } from '../../types/EmbeddedContact';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -40,6 +40,7 @@ import {
|
||||||
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
|
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
|
||||||
import { ThemeType } from '../../types/Util';
|
import { ThemeType } from '../../types/Util';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
|
import { BadgeCategory } from '../../badges/BadgeCategory';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -119,6 +120,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
conversationColor:
|
conversationColor:
|
||||||
overrideProps.conversationColor ||
|
overrideProps.conversationColor ||
|
||||||
select('conversationColor', ConversationColors, ConversationColors[0]),
|
select('conversationColor', ConversationColors, ConversationColors[0]),
|
||||||
|
conversationTitle:
|
||||||
|
overrideProps.conversationTitle ||
|
||||||
|
text('conversationTitle', 'Conversation Title'),
|
||||||
conversationId: text('conversationId', overrideProps.conversationId || ''),
|
conversationId: text('conversationId', overrideProps.conversationId || ''),
|
||||||
conversationType: overrideProps.conversationType || 'direct',
|
conversationType: overrideProps.conversationType || 'direct',
|
||||||
contact: overrideProps.contact,
|
contact: overrideProps.contact,
|
||||||
|
@ -138,8 +142,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
number('expirationTimestamp', overrideProps.expirationTimestamp || 0) ||
|
number('expirationTimestamp', overrideProps.expirationTimestamp || 0) ||
|
||||||
undefined,
|
undefined,
|
||||||
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
|
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
|
||||||
|
giftBadge: overrideProps.giftBadge,
|
||||||
i18n,
|
i18n,
|
||||||
id: text('id', overrideProps.id || ''),
|
id: text('id', overrideProps.id || 'random-message-id'),
|
||||||
renderingContext: 'storybook',
|
renderingContext: 'storybook',
|
||||||
interactionMode: overrideProps.interactionMode || 'keyboard',
|
interactionMode: overrideProps.interactionMode || 'keyboard',
|
||||||
isSticker: isBoolean(overrideProps.isSticker)
|
isSticker: isBoolean(overrideProps.isSticker)
|
||||||
|
@ -159,6 +164,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
markViewed: action('markViewed'),
|
markViewed: action('markViewed'),
|
||||||
messageExpanded: action('messageExpanded'),
|
messageExpanded: action('messageExpanded'),
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
|
openGiftBadge: action('openGiftBadge'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
reactions: overrideProps.reactions,
|
reactions: overrideProps.reactions,
|
||||||
|
@ -1218,6 +1224,7 @@ story.add('Other File Type', () => {
|
||||||
contentType: stringToMIMEType('text/plain'),
|
contentType: stringToMIMEType('text/plain'),
|
||||||
fileName: 'my-resume.txt',
|
fileName: 'my-resume.txt',
|
||||||
url: 'my-resume.txt',
|
url: 'my-resume.txt',
|
||||||
|
fileSize: '10MB',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
|
@ -1233,6 +1240,7 @@ story.add('Other File Type with Caption', () => {
|
||||||
contentType: stringToMIMEType('text/plain'),
|
contentType: stringToMIMEType('text/plain'),
|
||||||
fileName: 'my-resume.txt',
|
fileName: 'my-resume.txt',
|
||||||
url: 'my-resume.txt',
|
url: 'my-resume.txt',
|
||||||
|
fileSize: '10MB',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
|
@ -1250,6 +1258,7 @@ story.add('Other File Type with Long Filename', () => {
|
||||||
fileName:
|
fileName:
|
||||||
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
|
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
|
||||||
url: 'a2/a2334324darewer4234',
|
url: 'a2/a2334324darewer4234',
|
||||||
|
fileSize: '10MB',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
|
@ -1714,3 +1723,101 @@ story.add('EmbeddedContact: Loading Avatar', () => {
|
||||||
});
|
});
|
||||||
return renderBothDirections(props);
|
return renderBothDirections(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Unopened', () => {
|
||||||
|
const props = createProps({
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Unopened,
|
||||||
|
expiration: Date.now() + DAY * 30,
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPreferredBadge = () => ({
|
||||||
|
category: BadgeCategory.Donor,
|
||||||
|
descriptionTemplate: 'This is a description of the badge',
|
||||||
|
id: 'BOOST-3',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
transparent: {
|
||||||
|
localPath: '/fixtures/orange-heart.svg',
|
||||||
|
url: 'http://someplace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: 'heart',
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Redeemed (30 days)', () => {
|
||||||
|
const props = createProps({
|
||||||
|
getPreferredBadge,
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
expiration: Date.now() + DAY * 30 + SECOND,
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Redeemed (24 hours)', () => {
|
||||||
|
const props = createProps({
|
||||||
|
getPreferredBadge,
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
expiration: Date.now() + DAY + SECOND,
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Redeemed (60 minutes)', () => {
|
||||||
|
const props = createProps({
|
||||||
|
getPreferredBadge,
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
expiration: Date.now() + HOUR + SECOND,
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Redeemed (1 minute)', () => {
|
||||||
|
const props = createProps({
|
||||||
|
getPreferredBadge,
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
expiration: Date.now() + MINUTE + SECOND,
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Redeemed (expired)', () => {
|
||||||
|
const props = createProps({
|
||||||
|
getPreferredBadge,
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
expiration: Date.now(),
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge: Missing Badge', () => {
|
||||||
|
const props = createProps({
|
||||||
|
getPreferredBadge: () => undefined,
|
||||||
|
giftBadge: {
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
expiration: Date.now() + MINUTE + SECOND,
|
||||||
|
level: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { ReactNode, RefObject } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM, { createPortal } from 'react-dom';
|
import ReactDOM, { createPortal } from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import getDirection from 'direction';
|
||||||
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
||||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
@ -41,6 +42,7 @@ import { LinkPreviewDate } from './LinkPreviewDate';
|
||||||
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||||
import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseFullSizeLinkPreviewImage';
|
import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseFullSizeLinkPreviewImage';
|
||||||
import { WidthBreakpoint } from '../_util';
|
import { WidthBreakpoint } from '../_util';
|
||||||
|
import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
import type { AttachmentType } from '../../types/Attachment';
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
|
@ -69,6 +71,7 @@ import type {
|
||||||
LocalizerType,
|
LocalizerType,
|
||||||
ThemeType,
|
ThemeType,
|
||||||
} from '../../types/Util';
|
} from '../../types/Util';
|
||||||
|
|
||||||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
|
||||||
import type {
|
import type {
|
||||||
ContactNameColorType,
|
ContactNameColorType,
|
||||||
|
@ -84,6 +87,9 @@ import { offsetDistanceModifier } from '../../util/popperUtil';
|
||||||
import * as KeyboardLayout from '../../services/keyboardLayout';
|
import * as KeyboardLayout from '../../services/keyboardLayout';
|
||||||
import { StopPropagation } from '../StopPropagation';
|
import { StopPropagation } from '../StopPropagation';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
|
import { DAY, HOUR, MINUTE, SECOND } from '../../util/durations';
|
||||||
|
import { BadgeImageTheme } from '../../badges/BadgeImageTheme';
|
||||||
|
import { getBadgeImageFileLocalPath } from '../../badges/getBadgeImageFileLocalPath';
|
||||||
|
|
||||||
type Trigger = {
|
type Trigger = {
|
||||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
@ -116,6 +122,7 @@ const SENT_STATUSES = new Set<MessageStatusType>([
|
||||||
'sent',
|
'sent',
|
||||||
'viewed',
|
'viewed',
|
||||||
]);
|
]);
|
||||||
|
const GIFT_BADGE_UPDATE_INTERVAL = 30 * SECOND;
|
||||||
|
|
||||||
enum MetadataPlacement {
|
enum MetadataPlacement {
|
||||||
NotRendered,
|
NotRendered,
|
||||||
|
@ -171,11 +178,22 @@ export type AudioAttachmentProps = {
|
||||||
onFirstPlayed(): void;
|
onFirstPlayed(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum GiftBadgeStates {
|
||||||
|
Unopened = 'Unopened',
|
||||||
|
Redeemed = 'Redeemed',
|
||||||
|
}
|
||||||
|
export type GiftBadgeType = {
|
||||||
|
level: number;
|
||||||
|
expiration: number;
|
||||||
|
state: GiftBadgeStates.Redeemed | GiftBadgeStates.Unopened;
|
||||||
|
};
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
id: string;
|
id: string;
|
||||||
renderingContext: string;
|
renderingContext: string;
|
||||||
contactNameColor?: ContactNameColorType;
|
contactNameColor?: ContactNameColorType;
|
||||||
conversationColor: ConversationColorType;
|
conversationColor: ConversationColorType;
|
||||||
|
conversationTitle: string;
|
||||||
customColor?: CustomColorType;
|
customColor?: CustomColorType;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
|
@ -207,6 +225,7 @@ export type PropsData = {
|
||||||
reducedMotion?: boolean;
|
reducedMotion?: boolean;
|
||||||
conversationType: ConversationTypeType;
|
conversationType: ConversationTypeType;
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
|
giftBadge?: GiftBadgeType;
|
||||||
quote?: {
|
quote?: {
|
||||||
conversationColor: ConversationColorType;
|
conversationColor: ConversationColorType;
|
||||||
customColor?: CustomColorType;
|
customColor?: CustomColorType;
|
||||||
|
@ -222,6 +241,7 @@ export type PropsData = {
|
||||||
bodyRanges?: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
referencedMessageNotFound: boolean;
|
referencedMessageNotFound: boolean;
|
||||||
isViewOnce: boolean;
|
isViewOnce: boolean;
|
||||||
|
isGiftBadge: boolean;
|
||||||
};
|
};
|
||||||
storyReplyContext?: {
|
storyReplyContext?: {
|
||||||
authorTitle: string;
|
authorTitle: string;
|
||||||
|
@ -299,6 +319,7 @@ export type PropsActions = {
|
||||||
|
|
||||||
startConversation: (e164: string, uuid: UUIDStringType) => void;
|
startConversation: (e164: string, uuid: UUIDStringType) => void;
|
||||||
openConversation: (conversationId: string, messageId?: string) => void;
|
openConversation: (conversationId: string, messageId?: string) => void;
|
||||||
|
openGiftBadge: (messageId: string) => void;
|
||||||
showContactDetail: (options: {
|
showContactDetail: (options: {
|
||||||
contact: EmbeddedContactType;
|
contact: EmbeddedContactType;
|
||||||
signalAccount?: {
|
signalAccount?: {
|
||||||
|
@ -357,6 +378,9 @@ type State = {
|
||||||
reactionViewerRoot: HTMLDivElement | null;
|
reactionViewerRoot: HTMLDivElement | null;
|
||||||
reactionPickerRoot: HTMLDivElement | null;
|
reactionPickerRoot: HTMLDivElement | null;
|
||||||
|
|
||||||
|
giftBadgeCounter: number | null;
|
||||||
|
showOutgoingGiftBadgeModal: boolean;
|
||||||
|
|
||||||
hasDeleteForEveryoneTimerExpired: boolean;
|
hasDeleteForEveryoneTimerExpired: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -374,6 +398,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
public expirationCheckInterval: NodeJS.Timeout | undefined;
|
public expirationCheckInterval: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
public giftBadgeInterval: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
public expiredTimeout: NodeJS.Timeout | undefined;
|
public expiredTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
public selectedTimeout: NodeJS.Timeout | undefined;
|
public selectedTimeout: NodeJS.Timeout | undefined;
|
||||||
|
@ -396,6 +422,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
reactionViewerRoot: null,
|
reactionViewerRoot: null,
|
||||||
reactionPickerRoot: null,
|
reactionPickerRoot: null,
|
||||||
|
|
||||||
|
giftBadgeCounter: null,
|
||||||
|
showOutgoingGiftBadgeModal: false,
|
||||||
|
|
||||||
hasDeleteForEveryoneTimerExpired:
|
hasDeleteForEveryoneTimerExpired:
|
||||||
this.getTimeRemainingForDeleteForEveryone() <= 0,
|
this.getTimeRemainingForDeleteForEveryone() <= 0,
|
||||||
};
|
};
|
||||||
|
@ -490,6 +519,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
this.startSelectedTimer();
|
this.startSelectedTimer();
|
||||||
this.startDeleteForEveryoneTimerIfApplicable();
|
this.startDeleteForEveryoneTimerIfApplicable();
|
||||||
|
this.startGiftBadgeInterval();
|
||||||
|
|
||||||
const { isSelected } = this.props;
|
const { isSelected } = this.props;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
@ -519,6 +549,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
clearTimeoutIfNecessary(this.expirationCheckInterval);
|
clearTimeoutIfNecessary(this.expirationCheckInterval);
|
||||||
clearTimeoutIfNecessary(this.expiredTimeout);
|
clearTimeoutIfNecessary(this.expiredTimeout);
|
||||||
clearTimeoutIfNecessary(this.deleteForEveryoneTimeout);
|
clearTimeoutIfNecessary(this.deleteForEveryoneTimeout);
|
||||||
|
clearTimeoutIfNecessary(this.giftBadgeInterval);
|
||||||
this.toggleReactionViewer(true);
|
this.toggleReactionViewer(true);
|
||||||
this.toggleReactionPicker(true);
|
this.toggleReactionPicker(true);
|
||||||
}
|
}
|
||||||
|
@ -559,6 +590,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
|
giftBadge,
|
||||||
|
i18n,
|
||||||
shouldHideMetadata,
|
shouldHideMetadata,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
|
@ -576,6 +609,17 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return MetadataPlacement.NotRendered;
|
return MetadataPlacement.NotRendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (giftBadge) {
|
||||||
|
const description = i18n('message--giftBadge--unopened');
|
||||||
|
const isDescriptionRTL = getDirection(description) === 'rtl';
|
||||||
|
|
||||||
|
if (giftBadge.state === GiftBadgeStates.Unopened && !isDescriptionRTL) {
|
||||||
|
return MetadataPlacement.InlineWithText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetadataPlacement.Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
if (!text && !deletedForEveryone) {
|
if (!text && !deletedForEveryone) {
|
||||||
return isAudio(attachments)
|
return isAudio(attachments)
|
||||||
? MetadataPlacement.RenderedByMessageAudioComponent
|
? MetadataPlacement.RenderedByMessageAudioComponent
|
||||||
|
@ -635,6 +679,24 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public startGiftBadgeInterval(): void {
|
||||||
|
const { giftBadge } = this.props;
|
||||||
|
|
||||||
|
if (!giftBadge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.giftBadgeInterval = setInterval(() => {
|
||||||
|
this.updateGiftBadgeCounter();
|
||||||
|
}, GIFT_BADGE_UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateGiftBadgeCounter(): void {
|
||||||
|
this.setState((state: State) => ({
|
||||||
|
giftBadgeCounter: (state.giftBadgeCounter || 0) + 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private getTimeRemainingForDeleteForEveryone(): number {
|
private getTimeRemainingForDeleteForEveryone(): number {
|
||||||
const { timestamp } = this.props;
|
const { timestamp } = this.props;
|
||||||
return Math.max(timestamp - Date.now() + THREE_HOURS, 0);
|
return Math.max(timestamp - Date.now() + THREE_HOURS, 0);
|
||||||
|
@ -1054,17 +1116,17 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
public renderPreview(): JSX.Element | null {
|
public renderPreview(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
id,
|
|
||||||
attachments,
|
attachments,
|
||||||
conversationType,
|
conversationType,
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
id,
|
||||||
|
kickOffAttachmentDownload,
|
||||||
openLink,
|
openLink,
|
||||||
previews,
|
previews,
|
||||||
quote,
|
quote,
|
||||||
shouldCollapseAbove,
|
shouldCollapseAbove,
|
||||||
theme,
|
theme,
|
||||||
kickOffAttachmentDownload,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Attachments take precedence over Link Previews
|
// Attachments take precedence over Link Previews
|
||||||
|
@ -1205,6 +1267,188 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public renderGiftBadge(): JSX.Element | null {
|
||||||
|
const { conversationTitle, direction, getPreferredBadge, giftBadge, i18n } =
|
||||||
|
this.props;
|
||||||
|
const { showOutgoingGiftBadgeModal } = this.state;
|
||||||
|
if (!giftBadge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (giftBadge.state === GiftBadgeStates.Unopened) {
|
||||||
|
const description = i18n('message--giftBadge--unopened');
|
||||||
|
const isRTL = getDirection(description) === 'rtl';
|
||||||
|
const { metadataWidth } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-message__unopened-gift-badge__container">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-message__unopened-gift-badge',
|
||||||
|
`module-message__unopened-gift-badge--${direction}`
|
||||||
|
)}
|
||||||
|
aria-label={i18n('message--giftBadge--unopened--label')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="module-message__unopened-gift-badge__ribbon-horizontal"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="module-message__unopened-gift-badge__ribbon-vertical"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="module-message__unopened-gift-badge__bow"
|
||||||
|
src="images/gift-bow.svg"
|
||||||
|
alt=""
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-message__unopened-gift-badge__text',
|
||||||
|
`module-message__unopened-gift-badge__text--${direction}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-message__text',
|
||||||
|
`module-message__text--${direction}`
|
||||||
|
)}
|
||||||
|
dir={isRTL ? 'rtl' : undefined}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
{this.getMetadataPlacement() ===
|
||||||
|
MetadataPlacement.InlineWithText && (
|
||||||
|
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{this.renderMetadata()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (giftBadge.state === GiftBadgeStates.Redeemed) {
|
||||||
|
const badgeId = `BOOST-${giftBadge.level}`;
|
||||||
|
const badgeSize = 64;
|
||||||
|
const badge = getPreferredBadge([{ id: badgeId }]);
|
||||||
|
const badgeImagePath = getBadgeImageFileLocalPath(
|
||||||
|
badge,
|
||||||
|
badgeSize,
|
||||||
|
BadgeImageTheme.Transparent
|
||||||
|
);
|
||||||
|
|
||||||
|
let remaining: string;
|
||||||
|
const duration = giftBadge.expiration - Date.now();
|
||||||
|
|
||||||
|
const remainingDays = Math.floor(duration / DAY);
|
||||||
|
const remainingHours = Math.floor(duration / HOUR);
|
||||||
|
const remainingMinutes = Math.floor(duration / MINUTE);
|
||||||
|
|
||||||
|
if (remainingDays > 1) {
|
||||||
|
remaining = i18n('message--giftBadge--remaining--days', {
|
||||||
|
days: remainingDays,
|
||||||
|
});
|
||||||
|
} else if (remainingHours > 1) {
|
||||||
|
remaining = i18n('message--giftBadge--remaining--hours', {
|
||||||
|
hours: remainingHours,
|
||||||
|
});
|
||||||
|
} else if (remainingMinutes > 1) {
|
||||||
|
remaining = i18n('message--giftBadge--remaining--minutes', {
|
||||||
|
minutes: remainingMinutes,
|
||||||
|
});
|
||||||
|
} else if (remainingMinutes === 1) {
|
||||||
|
remaining = i18n('message--giftBadge--remaining--one-minute');
|
||||||
|
} else {
|
||||||
|
remaining = i18n('message--giftBadge--expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasSent = direction === 'outgoing';
|
||||||
|
const buttonContents = wasSent ? (
|
||||||
|
i18n('message--giftBadge--view')
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'module-message__redeemed-gift-badge__icon-check',
|
||||||
|
`module-message__redeemed-gift-badge__icon-check--${direction}`
|
||||||
|
)}
|
||||||
|
/>{' '}
|
||||||
|
{i18n('message--giftBadge--redeemed')}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeElement = badge ? (
|
||||||
|
<img
|
||||||
|
className="module-message__redeemed-gift-badge__badge"
|
||||||
|
src={badgeImagePath}
|
||||||
|
alt={badge.name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-message__redeemed-gift-badge__badge',
|
||||||
|
`module-message__redeemed-gift-badge__badge--missing-${direction}`
|
||||||
|
)}
|
||||||
|
aria-label={i18n('giftBadge--missing')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-message__redeemed-gift-badge__container">
|
||||||
|
<div className="module-message__redeemed-gift-badge">
|
||||||
|
{badgeElement}
|
||||||
|
<div className="module-message__redeemed-gift-badge__text">
|
||||||
|
<div className="module-message__redeemed-gift-badge__title">
|
||||||
|
{i18n('message--giftBadge')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-message__redeemed-gift-badge__remaining',
|
||||||
|
`module-message__redeemed-gift-badge__remaining--${direction}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{remaining}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'module-message__redeemed-gift-badge__button',
|
||||||
|
`module-message__redeemed-gift-badge__button--${direction}`
|
||||||
|
)}
|
||||||
|
disabled={!wasSent}
|
||||||
|
onClick={
|
||||||
|
wasSent
|
||||||
|
? () => this.setState({ showOutgoingGiftBadgeModal: true })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div className="module-message__redeemed-gift-badge__button__text">
|
||||||
|
{buttonContents}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{this.renderMetadata()}
|
||||||
|
{showOutgoingGiftBadgeModal ? (
|
||||||
|
<OutgoingGiftBadgeModal
|
||||||
|
i18n={i18n}
|
||||||
|
recipientTitle={conversationTitle}
|
||||||
|
badgeId={badgeId}
|
||||||
|
getPreferredBadge={getPreferredBadge}
|
||||||
|
hideOutgoingGiftBadgeModal={() =>
|
||||||
|
this.setState({ showOutgoingGiftBadgeModal: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw missingCaseError(giftBadge.state);
|
||||||
|
}
|
||||||
|
|
||||||
public renderQuote(): JSX.Element | null {
|
public renderQuote(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
conversationColor,
|
conversationColor,
|
||||||
|
@ -1216,14 +1460,13 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
id,
|
id,
|
||||||
quote,
|
quote,
|
||||||
scrollToQuotedMessage,
|
scrollToQuotedMessage,
|
||||||
shouldCollapseAbove,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isViewOnce, referencedMessageNotFound } = quote;
|
const { isGiftBadge, isViewOnce, referencedMessageNotFound } = quote;
|
||||||
|
|
||||||
const clickHandler = disableScroll
|
const clickHandler = disableScroll
|
||||||
? undefined
|
? undefined
|
||||||
|
@ -1236,19 +1479,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const isIncoming = direction === 'incoming';
|
const isIncoming = direction === 'incoming';
|
||||||
|
|
||||||
let curveTopLeft: boolean;
|
|
||||||
let curveTopRight: boolean;
|
|
||||||
if (this.shouldRenderAuthor()) {
|
|
||||||
curveTopLeft = false;
|
|
||||||
curveTopRight = false;
|
|
||||||
} else if (isIncoming) {
|
|
||||||
curveTopLeft = !shouldCollapseAbove;
|
|
||||||
curveTopRight = true;
|
|
||||||
} else {
|
|
||||||
curveTopLeft = true;
|
|
||||||
curveTopRight = !shouldCollapseAbove;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Quote
|
<Quote
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -1260,9 +1490,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
bodyRanges={quote.bodyRanges}
|
bodyRanges={quote.bodyRanges}
|
||||||
conversationColor={conversationColor}
|
conversationColor={conversationColor}
|
||||||
customColor={customColor}
|
customColor={customColor}
|
||||||
curveTopLeft={curveTopLeft}
|
|
||||||
curveTopRight={curveTopRight}
|
|
||||||
isViewOnce={isViewOnce}
|
isViewOnce={isViewOnce}
|
||||||
|
isGiftBadge={isGiftBadge}
|
||||||
referencedMessageNotFound={referencedMessageNotFound}
|
referencedMessageNotFound={referencedMessageNotFound}
|
||||||
isFromMe={quote.isFromMe}
|
isFromMe={quote.isFromMe}
|
||||||
doubleCheckMissingQuoteReference={() =>
|
doubleCheckMissingQuoteReference={() =>
|
||||||
|
@ -1279,7 +1508,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
storyReplyContext,
|
storyReplyContext,
|
||||||
shouldCollapseAbove,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!storyReplyContext) {
|
if (!storyReplyContext) {
|
||||||
|
@ -1288,19 +1516,6 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const isIncoming = direction === 'incoming';
|
const isIncoming = direction === 'incoming';
|
||||||
|
|
||||||
let curveTopLeft: boolean;
|
|
||||||
let curveTopRight: boolean;
|
|
||||||
if (this.shouldRenderAuthor()) {
|
|
||||||
curveTopLeft = false;
|
|
||||||
curveTopRight = false;
|
|
||||||
} else if (isIncoming) {
|
|
||||||
curveTopLeft = !shouldCollapseAbove;
|
|
||||||
curveTopRight = true;
|
|
||||||
} else {
|
|
||||||
curveTopLeft = true;
|
|
||||||
curveTopRight = !shouldCollapseAbove;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{storyReplyContext.emoji && (
|
{storyReplyContext.emoji && (
|
||||||
|
@ -1311,11 +1526,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<Quote
|
<Quote
|
||||||
authorTitle={storyReplyContext.authorTitle}
|
authorTitle={storyReplyContext.authorTitle}
|
||||||
conversationColor={conversationColor}
|
conversationColor={conversationColor}
|
||||||
curveTopLeft={curveTopLeft}
|
|
||||||
curveTopRight={curveTopRight}
|
|
||||||
customColor={customColor}
|
customColor={customColor}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isFromMe={storyReplyContext.isFromMe}
|
isFromMe={storyReplyContext.isFromMe}
|
||||||
|
isGiftBadge={false}
|
||||||
isIncoming={isIncoming}
|
isIncoming={isIncoming}
|
||||||
isStoryReply
|
isStoryReply
|
||||||
isViewOnce={false}
|
isViewOnce={false}
|
||||||
|
@ -1757,6 +1971,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
|
giftBadge,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isSticker,
|
isSticker,
|
||||||
|
@ -1769,7 +1984,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
text,
|
text,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const canForward = !isTapToView && !deletedForEveryone && !contact;
|
const canForward =
|
||||||
|
!isTapToView && !deletedForEveryone && !giftBadge && !contact;
|
||||||
const multipleAttachments = attachments && attachments.length > 1;
|
const multipleAttachments = attachments && attachments.length > 1;
|
||||||
|
|
||||||
const shouldShowAdditional =
|
const shouldShowAdditional =
|
||||||
|
@ -1934,7 +2150,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWidth(): number | undefined {
|
public getWidth(): number | undefined {
|
||||||
const { attachments, isSticker, previews } = this.props;
|
const { attachments, giftBadge, isSticker, previews } = this.props;
|
||||||
|
|
||||||
|
if (giftBadge) {
|
||||||
|
return 240;
|
||||||
|
}
|
||||||
|
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
if (isGIF(attachments)) {
|
if (isGIF(attachments)) {
|
||||||
|
@ -2370,7 +2590,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderContents(): JSX.Element | null {
|
public renderContents(): JSX.Element | null {
|
||||||
const { isTapToView, deletedForEveryone } = this.props;
|
const { giftBadge, isTapToView, deletedForEveryone } = this.props;
|
||||||
|
|
||||||
if (deletedForEveryone) {
|
if (deletedForEveryone) {
|
||||||
return (
|
return (
|
||||||
|
@ -2381,6 +2601,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (giftBadge) {
|
||||||
|
return this.renderGiftBadge();
|
||||||
|
}
|
||||||
|
|
||||||
if (isTapToView) {
|
if (isTapToView) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -2412,11 +2636,13 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
contact,
|
contact,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
direction,
|
direction,
|
||||||
|
giftBadge,
|
||||||
id,
|
id,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
openGiftBadge,
|
||||||
showContactDetail,
|
showContactDetail,
|
||||||
showVisualAttachment,
|
showVisualAttachment,
|
||||||
showExpiredIncomingTapToViewToast,
|
showExpiredIncomingTapToViewToast,
|
||||||
|
@ -2426,6 +2652,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const isAttachmentPending = this.isAttachmentPending();
|
const isAttachmentPending = this.isAttachmentPending();
|
||||||
|
|
||||||
|
if (giftBadge && giftBadge.state === GiftBadgeStates.Unopened) {
|
||||||
|
openGiftBadge(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isTapToView) {
|
if (isTapToView) {
|
||||||
if (isAttachmentPending) {
|
if (isAttachmentPending) {
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -2621,6 +2852,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
customColor,
|
customColor,
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
|
giftBadge,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
|
@ -2632,7 +2864,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const isAttachmentPending = this.isAttachmentPending();
|
const isAttachmentPending = this.isAttachmentPending();
|
||||||
|
|
||||||
const width = this.getWidth();
|
const width = this.getWidth();
|
||||||
const isShowingImage = this.isShowingImage();
|
const shouldUseWidth = Boolean(giftBadge || this.isShowingImage());
|
||||||
|
|
||||||
const isEmojiOnly = this.canRenderStickerLikeEmoji();
|
const isEmojiOnly = this.canRenderStickerLikeEmoji();
|
||||||
const isStickerLike = isSticker || isEmojiOnly;
|
const isStickerLike = isSticker || isEmojiOnly;
|
||||||
|
@ -2673,7 +2905,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
const containerStyles = {
|
const containerStyles = {
|
||||||
width: isShowingImage ? width : undefined,
|
width: shouldUseWidth ? width : undefined,
|
||||||
};
|
};
|
||||||
if (!isStickerLike && !deletedForEveryone && direction === 'outgoing') {
|
if (!isStickerLike && !deletedForEveryone && direction === 'outgoing') {
|
||||||
Object.assign(containerStyles, getCustomColorStyle(customColor));
|
Object.assign(containerStyles, getCustomColorStyle(customColor));
|
||||||
|
|
|
@ -36,6 +36,7 @@ const defaultMessage: MessageDataPropsType = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'my-convo',
|
conversationId: 'my-convo',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'direct',
|
conversationType: 'direct',
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
id: 'my-message',
|
id: 'my-message',
|
||||||
|
@ -81,6 +82,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
markViewed: action('markViewed'),
|
markViewed: action('markViewed'),
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
|
openGiftBadge: action('openGiftBadge'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
|
|
|
@ -73,6 +73,7 @@ export type PropsBackboneActions = Pick<
|
||||||
| 'markAttachmentAsCorrupted'
|
| 'markAttachmentAsCorrupted'
|
||||||
| 'markViewed'
|
| 'markViewed'
|
||||||
| 'openConversation'
|
| 'openConversation'
|
||||||
|
| 'openGiftBadge'
|
||||||
| 'openLink'
|
| 'openLink'
|
||||||
| 'reactToMessage'
|
| 'reactToMessage'
|
||||||
| 'renderAudioAttachment'
|
| 'renderAudioAttachment'
|
||||||
|
@ -284,6 +285,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed,
|
markViewed,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
openGiftBadge,
|
||||||
openLink,
|
openLink,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
renderAudioAttachment,
|
renderAudioAttachment,
|
||||||
|
@ -339,6 +341,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
markViewed={markViewed}
|
markViewed={markViewed}
|
||||||
messageExpanded={noop}
|
messageExpanded={noop}
|
||||||
openConversation={openConversation}
|
openConversation={openConversation}
|
||||||
|
openGiftBadge={openGiftBadge}
|
||||||
openLink={openLink}
|
openLink={openLink}
|
||||||
reactToMessage={reactToMessage}
|
reactToMessage={reactToMessage}
|
||||||
renderAudioAttachment={renderAudioAttachment}
|
renderAudioAttachment={renderAudioAttachment}
|
||||||
|
|
|
@ -49,6 +49,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversationId',
|
conversationId: 'conversationId',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'direct', // override
|
conversationType: 'direct', // override
|
||||||
deleteMessage: action('default--deleteMessage'),
|
deleteMessage: action('default--deleteMessage'),
|
||||||
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
|
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
|
||||||
|
@ -70,6 +71,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
markViewed: action('default--markViewed'),
|
markViewed: action('default--markViewed'),
|
||||||
messageExpanded: action('default--message-expanded'),
|
messageExpanded: action('default--message-expanded'),
|
||||||
openConversation: action('default--openConversation'),
|
openConversation: action('default--openConversation'),
|
||||||
|
openGiftBadge: action('openGiftBadge'),
|
||||||
openLink: action('default--openLink'),
|
openLink: action('default--openLink'),
|
||||||
previews: [],
|
previews: [],
|
||||||
reactToMessage: action('default--reactToMessage'),
|
reactToMessage: action('default--reactToMessage'),
|
||||||
|
@ -110,6 +112,7 @@ const renderInMessage = ({
|
||||||
isFromMe,
|
isFromMe,
|
||||||
rawAttachment,
|
rawAttachment,
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
|
isGiftBadge,
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
text: quoteText,
|
text: quoteText,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
@ -123,6 +126,7 @@ const renderInMessage = ({
|
||||||
isFromMe,
|
isFromMe,
|
||||||
rawAttachment,
|
rawAttachment,
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
|
isGiftBadge,
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
sentAt: Date.now() - 30 * 1000,
|
sentAt: Date.now() - 30 * 1000,
|
||||||
text: quoteText,
|
text: quoteText,
|
||||||
|
@ -139,7 +143,10 @@ const renderInMessage = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
authorTitle: text(
|
||||||
|
'authorTitle',
|
||||||
|
overrideProps.authorTitle || 'Default Sender'
|
||||||
|
),
|
||||||
conversationColor: overrideProps.conversationColor || 'forest',
|
conversationColor: overrideProps.conversationColor || 'forest',
|
||||||
doubleCheckMissingQuoteReference:
|
doubleCheckMissingQuoteReference:
|
||||||
overrideProps.doubleCheckMissingQuoteReference ||
|
overrideProps.doubleCheckMissingQuoteReference ||
|
||||||
|
@ -154,6 +161,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
'referencedMessageNotFound',
|
'referencedMessageNotFound',
|
||||||
overrideProps.referencedMessageNotFound || false
|
overrideProps.referencedMessageNotFound || false
|
||||||
),
|
),
|
||||||
|
isGiftBadge: boolean('isGiftBadge', overrideProps.isGiftBadge || false),
|
||||||
isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false),
|
isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false),
|
||||||
text: text(
|
text: text(
|
||||||
'text',
|
'text',
|
||||||
|
@ -338,6 +346,15 @@ story.add('Video Tap-to-View', () => {
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Gift Badge', () => {
|
||||||
|
const props = createProps({
|
||||||
|
text: '',
|
||||||
|
isGiftBadge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderInMessage(props);
|
||||||
|
});
|
||||||
|
|
||||||
story.add('Audio Only', () => {
|
story.add('Audio Only', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
|
|
|
@ -26,8 +26,6 @@ import { getCustomColorStyle } from '../../util/getCustomColorStyle';
|
||||||
export type Props = {
|
export type Props = {
|
||||||
authorTitle: string;
|
authorTitle: string;
|
||||||
conversationColor: ConversationColorType;
|
conversationColor: ConversationColorType;
|
||||||
curveTopLeft?: boolean;
|
|
||||||
curveTopRight?: boolean;
|
|
||||||
customColor?: CustomColorType;
|
customColor?: CustomColorType;
|
||||||
bodyRanges?: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -39,6 +37,7 @@ export type Props = {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
text: string;
|
text: string;
|
||||||
rawAttachment?: QuotedAttachmentType;
|
rawAttachment?: QuotedAttachmentType;
|
||||||
|
isGiftBadge: boolean;
|
||||||
isViewOnce: boolean;
|
isViewOnce: boolean;
|
||||||
reactionEmoji?: string;
|
reactionEmoji?: string;
|
||||||
referencedMessageNotFound: boolean;
|
referencedMessageNotFound: boolean;
|
||||||
|
@ -62,6 +61,10 @@ function validateQuote(quote: Props): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (quote.isGiftBadge) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (quote.text) {
|
if (quote.text) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -178,7 +181,12 @@ export class Quote extends React.Component<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public renderImage(url: string, icon?: string): JSX.Element {
|
public renderImage(
|
||||||
|
url: string,
|
||||||
|
icon: string | undefined,
|
||||||
|
isGiftBadge?: boolean
|
||||||
|
): JSX.Element {
|
||||||
|
const { isIncoming } = this.props;
|
||||||
const iconElement = icon ? (
|
const iconElement = icon ? (
|
||||||
<div className={this.getClassName('__icon-container__inner')}>
|
<div className={this.getClassName('__icon-container__inner')}>
|
||||||
<div
|
<div
|
||||||
|
@ -196,7 +204,12 @@ export class Quote extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThumbnailImage
|
<ThumbnailImage
|
||||||
className={this.getClassName('__icon-container')}
|
className={classNames(
|
||||||
|
this.getClassName('__icon-container'),
|
||||||
|
isIncoming === false &&
|
||||||
|
isGiftBadge &&
|
||||||
|
this.getClassName('__icon-container__outgoing-gift-badge')
|
||||||
|
)}
|
||||||
src={url}
|
src={url}
|
||||||
onError={this.handleImageError}
|
onError={this.handleImageError}
|
||||||
>
|
>
|
||||||
|
@ -261,10 +274,14 @@ export class Quote extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderIconContainer(): JSX.Element | null {
|
public renderIconContainer(): JSX.Element | null {
|
||||||
const { rawAttachment, isViewOnce, i18n } = this.props;
|
const { isGiftBadge, isViewOnce, i18n, rawAttachment } = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
const attachment = getAttachment(rawAttachment);
|
const attachment = getAttachment(rawAttachment);
|
||||||
|
|
||||||
|
if (isGiftBadge) {
|
||||||
|
return this.renderImage('images/gift-thumbnail.svg', undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -295,7 +312,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
||||||
return url && !imageBroken
|
return url && !imageBroken
|
||||||
? this.renderImage(url)
|
? this.renderImage(url, undefined)
|
||||||
: this.renderIcon('image');
|
: this.renderIcon('image');
|
||||||
}
|
}
|
||||||
if (MIME.isAudio(contentType)) {
|
if (MIME.isAudio(contentType)) {
|
||||||
|
@ -306,8 +323,15 @@ export class Quote extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderText(): JSX.Element | null {
|
public renderText(): JSX.Element | null {
|
||||||
const { bodyRanges, i18n, text, rawAttachment, isIncoming, isViewOnce } =
|
const {
|
||||||
this.props;
|
bodyRanges,
|
||||||
|
isGiftBadge,
|
||||||
|
i18n,
|
||||||
|
text,
|
||||||
|
rawAttachment,
|
||||||
|
isIncoming,
|
||||||
|
isViewOnce,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
const quoteText = bodyRanges
|
const quoteText = bodyRanges
|
||||||
|
@ -334,18 +358,22 @@ export class Quote extends React.Component<Props, State> {
|
||||||
|
|
||||||
const attachment = getAttachment(rawAttachment);
|
const attachment = getAttachment(rawAttachment);
|
||||||
|
|
||||||
if (!attachment) {
|
let typeLabel;
|
||||||
|
|
||||||
|
if (isGiftBadge) {
|
||||||
|
typeLabel = i18n('quote--giftBadge');
|
||||||
|
} else if (attachment) {
|
||||||
|
const { contentType, isVoiceMessage } = attachment;
|
||||||
|
typeLabel = getTypeLabel({
|
||||||
|
i18n,
|
||||||
|
isViewOnce,
|
||||||
|
contentType,
|
||||||
|
isVoiceMessage,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { contentType, isVoiceMessage } = attachment;
|
|
||||||
|
|
||||||
const typeLabel = getTypeLabel({
|
|
||||||
i18n,
|
|
||||||
isViewOnce,
|
|
||||||
contentType,
|
|
||||||
isVoiceMessage,
|
|
||||||
});
|
|
||||||
if (typeLabel) {
|
if (typeLabel) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -476,8 +504,6 @@ export class Quote extends React.Component<Props, State> {
|
||||||
public override render(): JSX.Element | null {
|
public override render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
conversationColor,
|
conversationColor,
|
||||||
curveTopLeft,
|
|
||||||
curveTopRight,
|
|
||||||
customColor,
|
customColor,
|
||||||
isIncoming,
|
isIncoming,
|
||||||
onClick,
|
onClick,
|
||||||
|
@ -506,9 +532,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
: this.getClassName(`--outgoing-${conversationColor}`),
|
: this.getClassName(`--outgoing-${conversationColor}`),
|
||||||
!onClick && this.getClassName('--no-click'),
|
!onClick && this.getClassName('--no-click'),
|
||||||
referencedMessageNotFound &&
|
referencedMessageNotFound &&
|
||||||
this.getClassName('--with-reference-warning'),
|
this.getClassName('--with-reference-warning')
|
||||||
curveTopLeft && this.getClassName('--curve-top-left'),
|
|
||||||
curveTopRight && this.getClassName('--curve-top-right')
|
|
||||||
)}
|
)}
|
||||||
style={{ ...getCustomColorStyle(customColor, true) }}
|
style={{ ...getCustomColorStyle(customColor, true) }}
|
||||||
>
|
>
|
||||||
|
|
|
@ -55,6 +55,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'forest',
|
conversationColor: 'forest',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
id: 'id-1',
|
id: 'id-1',
|
||||||
|
@ -80,6 +81,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'forest',
|
conversationColor: 'forest',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
id: 'id-2',
|
id: 'id-2',
|
||||||
|
@ -119,6 +121,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
id: 'id-3',
|
id: 'id-3',
|
||||||
|
@ -219,6 +222,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'plum',
|
conversationColor: 'plum',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'outgoing',
|
direction: 'outgoing',
|
||||||
id: 'id-6',
|
id: 'id-6',
|
||||||
|
@ -245,6 +249,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'outgoing',
|
direction: 'outgoing',
|
||||||
id: 'id-7',
|
id: 'id-7',
|
||||||
|
@ -271,6 +276,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'outgoing',
|
direction: 'outgoing',
|
||||||
id: 'id-8',
|
id: 'id-8',
|
||||||
|
@ -297,6 +303,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'outgoing',
|
direction: 'outgoing',
|
||||||
id: 'id-9',
|
id: 'id-9',
|
||||||
|
@ -323,6 +330,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canRetryDeleteForEveryone: true,
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationTitle: 'Conversation Title',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
direction: 'outgoing',
|
direction: 'outgoing',
|
||||||
id: 'id-10',
|
id: 'id-10',
|
||||||
|
@ -379,6 +387,7 @@ const actions = () => ({
|
||||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
|
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
|
openGiftBadge: action('openGiftBadge'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
'showExpiredIncomingTapToViewToast'
|
'showExpiredIncomingTapToViewToast'
|
||||||
|
|
|
@ -248,6 +248,7 @@ const getActions = createSelector(
|
||||||
'deleteMessageForEveryone',
|
'deleteMessageForEveryone',
|
||||||
'showMessageDetail',
|
'showMessageDetail',
|
||||||
'openConversation',
|
'openConversation',
|
||||||
|
'openGiftBadge',
|
||||||
'showContactDetail',
|
'showContactDetail',
|
||||||
'showContactModal',
|
'showContactModal',
|
||||||
'kickOffAttachmentDownload',
|
'kickOffAttachmentDownload',
|
||||||
|
|
|
@ -75,6 +75,7 @@ const getDefaultProps = () => ({
|
||||||
messageExpanded: action('messageExpanded'),
|
messageExpanded: action('messageExpanded'),
|
||||||
showMessageDetail: action('showMessageDetail'),
|
showMessageDetail: action('showMessageDetail'),
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
|
openGiftBadge: action('openGiftBadge'),
|
||||||
showContactDetail: action('showContactDetail'),
|
showContactDetail: action('showContactDetail'),
|
||||||
showContactModal: action('showContactModal'),
|
showContactModal: action('showContactModal'),
|
||||||
showForwardMessageModal: action('showForwardMessageModal'),
|
showForwardMessageModal: action('showForwardMessageModal'),
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { markViewed } from '../services/MessageUpdater';
|
||||||
import { isIncoming, isStory } from '../state/selectors/message';
|
import { isIncoming, isStory } from '../state/selectors/message';
|
||||||
import { notificationService } from '../services/notifications';
|
import { notificationService } from '../services/notifications';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
|
|
||||||
export type ViewSyncAttributesType = {
|
export type ViewSyncAttributesType = {
|
||||||
senderId: string;
|
senderId: string;
|
||||||
|
@ -92,6 +93,16 @@ export class ViewSyncs extends Collection {
|
||||||
message.set(markViewed(message.attributes, sync.get('viewedAt')));
|
message.set(markViewed(message.attributes, sync.get('viewedAt')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const giftBadge = message.get('giftBadge');
|
||||||
|
if (giftBadge) {
|
||||||
|
message.set({
|
||||||
|
giftBadge: {
|
||||||
|
...giftBadge,
|
||||||
|
state: GiftBadgeStates.Redeemed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.remove(sync);
|
this.remove(sync);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
|
|
27
ts/model-types.d.ts
vendored
27
ts/model-types.d.ts
vendored
|
@ -4,29 +4,20 @@
|
||||||
import * as Backbone from 'backbone';
|
import * as Backbone from 'backbone';
|
||||||
|
|
||||||
import { GroupV2ChangeType } from './groups';
|
import { GroupV2ChangeType } from './groups';
|
||||||
import { LocalizerType, BodyRangeType, BodyRangesType } from './types/Util';
|
import { BodyRangeType, BodyRangesType } from './types/Util';
|
||||||
import { CallHistoryDetailsFromDiskType } from './types/Calling';
|
import { CallHistoryDetailsFromDiskType } from './types/Calling';
|
||||||
import { CustomColorType } from './types/Colors';
|
import { CustomColorType } from './types/Colors';
|
||||||
import { DeviceType } from './textsecure/Types';
|
import { DeviceType } from './textsecure/Types';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
|
||||||
import { SendMessageChallengeData } from './textsecure/Errors';
|
import { SendMessageChallengeData } from './textsecure/Errors';
|
||||||
import { UserMessage } from './types/Message';
|
|
||||||
import { MessageModel } from './models/messages';
|
import { MessageModel } from './models/messages';
|
||||||
import { ConversationModel } from './models/conversations';
|
import { ConversationModel } from './models/conversations';
|
||||||
import { ProfileNameChangeType } from './util/getStringForProfileChange';
|
import { ProfileNameChangeType } from './util/getStringForProfileChange';
|
||||||
import { CapabilitiesType } from './textsecure/WebAPI';
|
import { CapabilitiesType } from './textsecure/WebAPI';
|
||||||
import { ReadStatus } from './messages/MessageReadStatus';
|
import { ReadStatus } from './messages/MessageReadStatus';
|
||||||
import {
|
import { SendStateByConversationId } from './messages/MessageSendState';
|
||||||
SendState,
|
|
||||||
SendStateByConversationId,
|
|
||||||
} from './messages/MessageSendState';
|
|
||||||
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
||||||
import { ConversationColorType } from './types/Colors';
|
import { ConversationColorType } from './types/Colors';
|
||||||
import {
|
import { AttachmentDraftType, AttachmentType } from './types/Attachment';
|
||||||
AttachmentDraftType,
|
|
||||||
AttachmentType,
|
|
||||||
ThumbnailType,
|
|
||||||
} from './types/Attachment';
|
|
||||||
import { EmbeddedContactType } from './types/EmbeddedContact';
|
import { EmbeddedContactType } from './types/EmbeddedContact';
|
||||||
import { SignalService as Proto } from './protobuf';
|
import { SignalService as Proto } from './protobuf';
|
||||||
import { AvatarDataType } from './types/Avatar';
|
import { AvatarDataType } from './types/Avatar';
|
||||||
|
@ -36,6 +27,7 @@ import { ReactionSource } from './reactions/ReactionSource';
|
||||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||||
import MemberRoleEnum = Proto.Member.Role;
|
import MemberRoleEnum = Proto.Member.Role;
|
||||||
import { SeenStatus } from './MessageSeenStatus';
|
import { SeenStatus } from './MessageSeenStatus';
|
||||||
|
import { GiftBadgeStates } from './components/conversation/Message';
|
||||||
|
|
||||||
export type WhatIsThis = any;
|
export type WhatIsThis = any;
|
||||||
|
|
||||||
|
@ -80,10 +72,11 @@ export type QuotedMessageType = {
|
||||||
authorUuid?: string;
|
authorUuid?: string;
|
||||||
bodyRanges?: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
id: number;
|
id: number;
|
||||||
referencedMessageNotFound: boolean;
|
isGiftBadge?: boolean;
|
||||||
isViewOnce: boolean;
|
isViewOnce: boolean;
|
||||||
text?: string;
|
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
referencedMessageNotFound: boolean;
|
||||||
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StoryReplyContextType = {
|
type StoryReplyContextType = {
|
||||||
|
@ -187,6 +180,12 @@ export type MessageAttributesType = {
|
||||||
contact?: Array<EmbeddedContactType>;
|
contact?: Array<EmbeddedContactType>;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
storyReactionEmoji?: string;
|
storyReactionEmoji?: string;
|
||||||
|
giftBadge?: {
|
||||||
|
expiration: number;
|
||||||
|
level: number;
|
||||||
|
receiptCredentialPresentation: string;
|
||||||
|
state: GiftBadgeStates;
|
||||||
|
};
|
||||||
|
|
||||||
expirationTimerUpdate?: {
|
expirationTimerUpdate?: {
|
||||||
expireTimer: number;
|
expireTimer: number;
|
||||||
|
|
|
@ -93,6 +93,7 @@ import { SignalService as Proto } from '../protobuf';
|
||||||
import {
|
import {
|
||||||
getMessagePropStatus,
|
getMessagePropStatus,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
|
isGiftBadge,
|
||||||
isIncoming,
|
isIncoming,
|
||||||
isStory,
|
isStory,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
|
@ -1818,7 +1819,6 @@ export class ConversationModel extends window.Backbone
|
||||||
const { customColor, customColorId } = this.getCustomColorData();
|
const { customColor, customColorId } = this.getCustomColorData();
|
||||||
|
|
||||||
// TODO: DESKTOP-720
|
// TODO: DESKTOP-720
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
uuid: this.get('uuid'),
|
uuid: this.get('uuid'),
|
||||||
|
@ -1832,6 +1832,7 @@ export class ConversationModel extends window.Backbone
|
||||||
aboutText: this.get('about'),
|
aboutText: this.get('about'),
|
||||||
aboutEmoji: this.get('aboutEmoji'),
|
aboutEmoji: this.get('aboutEmoji'),
|
||||||
acceptedMessageRequest: this.getAccepted(),
|
acceptedMessageRequest: this.getAccepted(),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
activeAt: this.get('active_at')!,
|
activeAt: this.get('active_at')!,
|
||||||
areWePending: Boolean(
|
areWePending: Boolean(
|
||||||
ourConversationId && this.isMemberPending(ourConversationId)
|
ourConversationId && this.isMemberPending(ourConversationId)
|
||||||
|
@ -1857,14 +1858,14 @@ export class ConversationModel extends window.Backbone
|
||||||
draftPreview,
|
draftPreview,
|
||||||
draftText,
|
draftText,
|
||||||
familyName: this.get('profileFamilyName'),
|
familyName: this.get('profileFamilyName'),
|
||||||
firstName: this.get('profileName')!,
|
firstName: this.get('profileName'),
|
||||||
groupDescription: this.get('description'),
|
groupDescription: this.get('description'),
|
||||||
groupVersion,
|
groupVersion,
|
||||||
groupId: this.get('groupId'),
|
groupId: this.get('groupId'),
|
||||||
groupLink: this.getGroupLink(),
|
groupLink: this.getGroupLink(),
|
||||||
hideStory: Boolean(this.get('hideStory')),
|
hideStory: Boolean(this.get('hideStory')),
|
||||||
inboxPosition,
|
inboxPosition,
|
||||||
isArchived: this.get('isArchived')!,
|
isArchived: this.get('isArchived'),
|
||||||
isBlocked: this.isBlocked(),
|
isBlocked: this.isBlocked(),
|
||||||
isMe: isMe(this.attributes),
|
isMe: isMe(this.attributes),
|
||||||
isGroupV1AndDisabled: this.isGroupV1AndDisabled(),
|
isGroupV1AndDisabled: this.isGroupV1AndDisabled(),
|
||||||
|
@ -1873,9 +1874,10 @@ export class ConversationModel extends window.Backbone
|
||||||
isVerified: this.isVerified(),
|
isVerified: this.isVerified(),
|
||||||
isFetchingUUID: this.isFetchingUUID,
|
isFetchingUUID: this.isFetchingUUID,
|
||||||
lastMessage,
|
lastMessage,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
lastUpdated: this.get('timestamp')!,
|
lastUpdated: this.get('timestamp')!,
|
||||||
left: Boolean(this.get('left')),
|
left: Boolean(this.get('left')),
|
||||||
markedUnread: this.get('markedUnread')!,
|
markedUnread: this.get('markedUnread'),
|
||||||
membersCount: this.getMembersCount(),
|
membersCount: this.getMembersCount(),
|
||||||
memberships: this.getMemberships(),
|
memberships: this.getMemberships(),
|
||||||
messageCount: this.get('messageCount') || 0,
|
messageCount: this.get('messageCount') || 0,
|
||||||
|
@ -1891,23 +1893,23 @@ export class ConversationModel extends window.Backbone
|
||||||
announcementsOnly: Boolean(this.get('announcementsOnly')),
|
announcementsOnly: Boolean(this.get('announcementsOnly')),
|
||||||
announcementsOnlyReady: this.canBeAnnouncementGroup(),
|
announcementsOnlyReady: this.canBeAnnouncementGroup(),
|
||||||
expireTimer: this.get('expireTimer'),
|
expireTimer: this.get('expireTimer'),
|
||||||
muteExpiresAt: this.get('muteExpiresAt')!,
|
muteExpiresAt: this.get('muteExpiresAt'),
|
||||||
dontNotifyForMentionsIfMuted: this.get('dontNotifyForMentionsIfMuted'),
|
dontNotifyForMentionsIfMuted: this.get('dontNotifyForMentionsIfMuted'),
|
||||||
name: this.get('name')!,
|
name: this.get('name'),
|
||||||
phoneNumber: this.getNumber()!,
|
phoneNumber: this.getNumber(),
|
||||||
profileName: this.getProfileName()!,
|
profileName: this.getProfileName(),
|
||||||
profileSharing: this.get('profileSharing'),
|
profileSharing: this.get('profileSharing'),
|
||||||
publicParams: this.get('publicParams'),
|
publicParams: this.get('publicParams'),
|
||||||
secretParams: this.get('secretParams'),
|
secretParams: this.get('secretParams'),
|
||||||
shouldShowDraft,
|
shouldShowDraft,
|
||||||
sortedGroupMembers,
|
sortedGroupMembers,
|
||||||
timestamp,
|
timestamp,
|
||||||
title: this.getTitle()!,
|
title: this.getTitle(),
|
||||||
typingContactId: typingMostRecent?.senderId,
|
typingContactId: typingMostRecent?.senderId,
|
||||||
searchableTitle: isMe(this.attributes)
|
searchableTitle: isMe(this.attributes)
|
||||||
? window.i18n('noteToSelf')
|
? window.i18n('noteToSelf')
|
||||||
: this.getTitle(),
|
: this.getTitle(),
|
||||||
unreadCount: this.get('unreadCount')! || 0,
|
unreadCount: this.get('unreadCount') || 0,
|
||||||
...(isDirectConversation(this.attributes)
|
...(isDirectConversation(this.attributes)
|
||||||
? {
|
? {
|
||||||
type: 'direct' as const,
|
type: 'direct' as const,
|
||||||
|
@ -1920,7 +1922,6 @@ export class ConversationModel extends window.Backbone
|
||||||
sharedGroupNames: [],
|
sharedGroupNames: [],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateE164(e164?: string | null): void {
|
updateE164(e164?: string | null): void {
|
||||||
|
@ -3762,6 +3763,7 @@ export class ConversationModel extends window.Backbone
|
||||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||||
id: quotedMessage.get('sent_at'),
|
id: quotedMessage.get('sent_at'),
|
||||||
isViewOnce: isTapToView(quotedMessage.attributes),
|
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||||
|
isGiftBadge: isGiftBadge(quotedMessage.attributes),
|
||||||
messageId: quotedMessage.get('id'),
|
messageId: quotedMessage.get('id'),
|
||||||
referencedMessageNotFound: false,
|
referencedMessageNotFound: false,
|
||||||
text: body || embeddedContactName,
|
text: body || embeddedContactName,
|
||||||
|
|
|
@ -38,6 +38,7 @@ import type {
|
||||||
} from '../textsecure/Types.d';
|
} from '../textsecure/Types.d';
|
||||||
import { SendMessageProtoError } from '../textsecure/Errors';
|
import { SendMessageProtoError } from '../textsecure/Errors';
|
||||||
import * as expirationTimer from '../util/expirationTimer';
|
import * as expirationTimer from '../util/expirationTimer';
|
||||||
|
import { getUserLanguages } from '../util/userLanguages';
|
||||||
|
|
||||||
import type { ReactionType } from '../types/Reactions';
|
import type { ReactionType } from '../types/Reactions';
|
||||||
import { UUID, UUIDKind } from '../types/UUID';
|
import { UUID, UUIDKind } from '../types/UUID';
|
||||||
|
@ -86,6 +87,7 @@ import {
|
||||||
isDeliveryIssue,
|
isDeliveryIssue,
|
||||||
isEndSession,
|
isEndSession,
|
||||||
isExpirationTimerUpdate,
|
isExpirationTimerUpdate,
|
||||||
|
isGiftBadge,
|
||||||
isGroupUpdate,
|
isGroupUpdate,
|
||||||
isGroupV1Migration,
|
isGroupV1Migration,
|
||||||
isGroupV2Change,
|
isGroupV2Change,
|
||||||
|
@ -153,6 +155,8 @@ import { shouldShowStoriesView } from '../state/selectors/stories';
|
||||||
import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage';
|
import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage';
|
||||||
import { SeenStatus } from '../MessageSeenStatus';
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
import { isNewReactionReplacingPrevious } from '../reactions/util';
|
import { isNewReactionReplacingPrevious } from '../reactions/util';
|
||||||
|
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
|
||||||
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
@ -762,6 +766,26 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const giftBadge = this.get('giftBadge');
|
||||||
|
if (giftBadge) {
|
||||||
|
const emoji = '🎁';
|
||||||
|
|
||||||
|
if (isIncoming(this.attributes)) {
|
||||||
|
return {
|
||||||
|
emoji,
|
||||||
|
text: window.i18n('message--giftBadge--preview--sent'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
emoji,
|
||||||
|
text:
|
||||||
|
giftBadge.state === GiftBadgeStates.Unopened
|
||||||
|
? window.i18n('message--giftBadge--preview--unopened')
|
||||||
|
: window.i18n('message--giftBadge--preview--redeemed'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
return { text: body };
|
return { text: body };
|
||||||
}
|
}
|
||||||
|
@ -1093,6 +1117,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const isCallHistoryValue = isCallHistory(attributes);
|
const isCallHistoryValue = isCallHistory(attributes);
|
||||||
const isChatSessionRefreshedValue = isChatSessionRefreshed(attributes);
|
const isChatSessionRefreshedValue = isChatSessionRefreshed(attributes);
|
||||||
const isDeliveryIssueValue = isDeliveryIssue(attributes);
|
const isDeliveryIssueValue = isDeliveryIssue(attributes);
|
||||||
|
const isGiftBadgeValue = isGiftBadge(attributes);
|
||||||
const isGroupUpdateValue = isGroupUpdate(attributes);
|
const isGroupUpdateValue = isGroupUpdate(attributes);
|
||||||
const isGroupV2ChangeValue = isGroupV2Change(attributes);
|
const isGroupV2ChangeValue = isGroupV2Change(attributes);
|
||||||
const isEndSessionValue = isEndSession(attributes);
|
const isEndSessionValue = isEndSession(attributes);
|
||||||
|
@ -1124,6 +1149,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
isCallHistoryValue ||
|
isCallHistoryValue ||
|
||||||
isChatSessionRefreshedValue ||
|
isChatSessionRefreshedValue ||
|
||||||
isDeliveryIssueValue ||
|
isDeliveryIssueValue ||
|
||||||
|
isGiftBadgeValue ||
|
||||||
isGroupUpdateValue ||
|
isGroupUpdateValue ||
|
||||||
isGroupV2ChangeValue ||
|
isGroupV2ChangeValue ||
|
||||||
isEndSessionValue ||
|
isEndSessionValue ||
|
||||||
|
@ -1812,6 +1838,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
// Just placeholder values for the fields
|
// Just placeholder values for the fields
|
||||||
referencedMessageNotFound: false,
|
referencedMessageNotFound: false,
|
||||||
|
isGiftBadge: quote.type === Proto.DataMessage.Quote.Type.GIFT_BADGE,
|
||||||
isViewOnce: false,
|
isViewOnce: false,
|
||||||
messageId: '',
|
messageId: '',
|
||||||
};
|
};
|
||||||
|
@ -1869,6 +1896,23 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMessageAGiftBadge = isGiftBadge(originalMessage.attributes);
|
||||||
|
if (isMessageAGiftBadge !== quote.isGiftBadge) {
|
||||||
|
log.warn(
|
||||||
|
`copyQuoteContentFromOriginal: Quote.isGiftBadge: ${quote.isGiftBadge}, isGiftBadge(message): ${isMessageAGiftBadge}`
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
quote.isGiftBadge = isMessageAGiftBadge;
|
||||||
|
}
|
||||||
|
if (isMessageAGiftBadge) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
quote.text = undefined;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
quote.attachments = [];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
quote.isViewOnce = false;
|
quote.isViewOnce = false;
|
||||||
|
|
||||||
|
@ -2310,6 +2354,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
decrypted_at: now,
|
decrypted_at: now,
|
||||||
errors: [],
|
errors: [],
|
||||||
flags: dataMessage.flags,
|
flags: dataMessage.flags,
|
||||||
|
giftBadge: initialMessage.giftBadge,
|
||||||
hasAttachments: dataMessage.hasAttachments,
|
hasAttachments: dataMessage.hasAttachments,
|
||||||
hasFileAttachments: dataMessage.hasFileAttachments,
|
hasFileAttachments: dataMessage.hasFileAttachments,
|
||||||
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
|
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
|
||||||
|
@ -2612,9 +2657,50 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
conversation.incrementMessageCount();
|
conversation.incrementMessageCount();
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
|
||||||
|
const reduxState = window.reduxStore.getState();
|
||||||
|
|
||||||
|
const giftBadge = message.get('giftBadge');
|
||||||
|
if (giftBadge) {
|
||||||
|
const { level } = giftBadge;
|
||||||
|
const existingBadgesById = reduxState.badges.byId;
|
||||||
|
|
||||||
|
const badgeId = `BOOST-${level}`;
|
||||||
|
if (!existingBadgesById[badgeId]) {
|
||||||
|
const { updatesUrl } = window.SignalContext.config;
|
||||||
|
strictAssert(
|
||||||
|
typeof updatesUrl === 'string',
|
||||||
|
'getProfile: expected updatesUrl to be a defined string'
|
||||||
|
);
|
||||||
|
const userLanguages = getUserLanguages(
|
||||||
|
navigator.languages,
|
||||||
|
window.getLocale()
|
||||||
|
);
|
||||||
|
const response =
|
||||||
|
await window.textsecure.messaging.server.getBoostBadgesFromServer(
|
||||||
|
userLanguages
|
||||||
|
);
|
||||||
|
const boostBadges = parseBoostBadgeListFromServer(
|
||||||
|
response,
|
||||||
|
updatesUrl
|
||||||
|
);
|
||||||
|
const badge = boostBadges[badgeId];
|
||||||
|
if (!badge) {
|
||||||
|
log.error(
|
||||||
|
`handleDataMessage: gift badge ${badgeId} not found on server`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await window.reduxActions.badges.updateOrCreate([
|
||||||
|
{
|
||||||
|
...badge,
|
||||||
|
id: badgeId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only queue attachments for downloads if this is a story or
|
// Only queue attachments for downloads if this is a story or
|
||||||
// outgoing message or we've accepted the conversation
|
// outgoing message or we've accepted the conversation
|
||||||
const reduxState = window.reduxStore.getState();
|
|
||||||
const attachments = this.get('attachments') || [];
|
const attachments = this.get('attachments') || [];
|
||||||
|
|
||||||
let queueStoryForDownload = false;
|
let queueStoryForDownload = false;
|
||||||
|
|
|
@ -502,6 +502,7 @@ export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
authorUuid,
|
authorUuid,
|
||||||
id: sentAt,
|
id: sentAt,
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
|
isGiftBadge: isTargetGiftBadge,
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
text = '',
|
text = '',
|
||||||
} = quote;
|
} = quote;
|
||||||
|
@ -534,6 +535,7 @@ export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
rawAttachment: firstAttachment
|
rawAttachment: firstAttachment
|
||||||
? processQuoteAttachment(firstAttachment)
|
? processQuoteAttachment(firstAttachment)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
isGiftBadge: Boolean(isTargetGiftBadge),
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
sentAt: Number(sentAt),
|
sentAt: Number(sentAt),
|
||||||
|
@ -569,6 +571,7 @@ type ShallowPropsType = Pick<
|
||||||
| 'contactNameColor'
|
| 'contactNameColor'
|
||||||
| 'conversationColor'
|
| 'conversationColor'
|
||||||
| 'conversationId'
|
| 'conversationId'
|
||||||
|
| 'conversationTitle'
|
||||||
| 'conversationType'
|
| 'conversationType'
|
||||||
| 'customColor'
|
| 'customColor'
|
||||||
| 'deletedForEveryone'
|
| 'deletedForEveryone'
|
||||||
|
@ -576,6 +579,7 @@ type ShallowPropsType = Pick<
|
||||||
| 'displayLimit'
|
| 'displayLimit'
|
||||||
| 'expirationLength'
|
| 'expirationLength'
|
||||||
| 'expirationTimestamp'
|
| 'expirationTimestamp'
|
||||||
|
| 'giftBadge'
|
||||||
| 'id'
|
| 'id'
|
||||||
| 'isBlocked'
|
| 'isBlocked'
|
||||||
| 'isMessageRequestAccepted'
|
| 'isMessageRequestAccepted'
|
||||||
|
@ -654,6 +658,7 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
contactNameColor,
|
contactNameColor,
|
||||||
conversationColor,
|
conversationColor,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
conversationTitle: conversation.title,
|
||||||
conversationType: isGroup ? 'group' : 'direct',
|
conversationType: isGroup ? 'group' : 'direct',
|
||||||
customColor,
|
customColor,
|
||||||
deletedForEveryone: message.deletedForEveryone || false,
|
deletedForEveryone: message.deletedForEveryone || false,
|
||||||
|
@ -661,6 +666,7 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
displayLimit: message.displayLimit,
|
displayLimit: message.displayLimit,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
|
giftBadge: message.giftBadge,
|
||||||
id: message.id,
|
id: message.id,
|
||||||
isBlocked: conversation.isBlocked || false,
|
isBlocked: conversation.isBlocked || false,
|
||||||
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
||||||
|
@ -1080,6 +1086,14 @@ function getPropsForVerificationNotification(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gift Badge
|
||||||
|
|
||||||
|
export function isGiftBadge(
|
||||||
|
message: Pick<MessageWithUIFieldsType, 'giftBadge'>
|
||||||
|
): boolean {
|
||||||
|
return Boolean(message.giftBadge);
|
||||||
|
}
|
||||||
|
|
||||||
// Group Update (V1)
|
// Group Update (V1)
|
||||||
|
|
||||||
export function isGroupUpdate(
|
export function isGroupUpdate(
|
||||||
|
|
|
@ -45,6 +45,7 @@ const mapStateToProps = (
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed,
|
markViewed,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
openGiftBadge,
|
||||||
openLink,
|
openLink,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
@ -89,6 +90,7 @@ const mapStateToProps = (
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed,
|
markViewed,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
openGiftBadge,
|
||||||
openLink,
|
openLink,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
renderAudioAttachment,
|
renderAudioAttachment,
|
||||||
|
|
|
@ -83,6 +83,7 @@ export type TimelinePropsType = ExternalProps &
|
||||||
| 'onDelete'
|
| 'onDelete'
|
||||||
| 'onUnblock'
|
| 'onUnblock'
|
||||||
| 'openConversation'
|
| 'openConversation'
|
||||||
|
| 'openGiftBadge'
|
||||||
| 'openLink'
|
| 'openLink'
|
||||||
| 'reactToMessage'
|
| 'reactToMessage'
|
||||||
| 'removeMember'
|
| 'removeMember'
|
||||||
|
|
|
@ -102,8 +102,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
conversationId,
|
conversationId,
|
||||||
conversationColor: conversation?.conversationColor,
|
conversationColor: conversation.conversationColor,
|
||||||
customColor: conversation?.customColor,
|
customColor: conversation.customColor,
|
||||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||||
isNextItemCallingNotification,
|
isNextItemCallingNotification,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
|
|
@ -200,6 +200,7 @@ describe('processDataMessage', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bodyRanges: [],
|
bodyRanges: [],
|
||||||
|
type: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ describe('both/state/ducks/composer', () => {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
id: 456,
|
id: 456,
|
||||||
isViewOnce: false,
|
isViewOnce: false,
|
||||||
|
isGiftBadge: false,
|
||||||
messageId: '789',
|
messageId: '789',
|
||||||
referencedMessageNotFound: false,
|
referencedMessageNotFound: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -118,11 +118,12 @@ export type StickerType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuoteType = {
|
export type QuoteType = {
|
||||||
id?: number;
|
|
||||||
authorUuid?: string;
|
|
||||||
text?: string;
|
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
|
authorUuid?: string;
|
||||||
bodyRanges?: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
|
id?: number;
|
||||||
|
isGiftBadge?: boolean;
|
||||||
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReactionType = {
|
export type ReactionType = {
|
||||||
|
@ -494,6 +495,12 @@ class Message {
|
||||||
proto.quote = new Quote();
|
proto.quote = new Quote();
|
||||||
const { quote } = proto;
|
const { quote } = proto;
|
||||||
|
|
||||||
|
if (this.quote.isGiftBadge) {
|
||||||
|
quote.type = Proto.DataMessage.Quote.Type.GIFT_BADGE;
|
||||||
|
} else {
|
||||||
|
quote.type = Proto.DataMessage.Quote.Type.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
quote.id =
|
quote.id =
|
||||||
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
|
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
|
||||||
quote.authorUuid = this.quote.authorUuid || null;
|
quote.authorUuid = this.quote.authorUuid || null;
|
||||||
|
|
10
ts/textsecure/Types.d.ts
vendored
10
ts/textsecure/Types.d.ts
vendored
|
@ -5,6 +5,7 @@ import type { SignalService as Proto } from '../protobuf';
|
||||||
import type { IncomingWebSocketRequest } from './WebsocketResources';
|
import type { IncomingWebSocketRequest } from './WebsocketResources';
|
||||||
import type { UUID } from '../types/UUID';
|
import type { UUID } from '../types/UUID';
|
||||||
import type { TextAttachmentType } from '../types/Attachment';
|
import type { TextAttachmentType } from '../types/Attachment';
|
||||||
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
|
@ -143,6 +144,7 @@ export type ProcessedQuote = {
|
||||||
text?: string;
|
text?: string;
|
||||||
attachments: ReadonlyArray<ProcessedQuoteAttachment>;
|
attachments: ReadonlyArray<ProcessedQuoteAttachment>;
|
||||||
bodyRanges: ReadonlyArray<Proto.DataMessage.IBodyRange>;
|
bodyRanges: ReadonlyArray<Proto.DataMessage.IBodyRange>;
|
||||||
|
type: Proto.DataMessage.Quote.Type;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProcessedAvatar = {
|
export type ProcessedAvatar = {
|
||||||
|
@ -186,6 +188,13 @@ export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate;
|
||||||
|
|
||||||
export type ProcessedStoryContext = Proto.DataMessage.IStoryContext;
|
export type ProcessedStoryContext = Proto.DataMessage.IStoryContext;
|
||||||
|
|
||||||
|
export type ProcessedGiftBadge = {
|
||||||
|
receiptCredentialPresentation: string;
|
||||||
|
level: number;
|
||||||
|
expiration: number;
|
||||||
|
state: GiftBadgeStates;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProcessedDataMessage = {
|
export type ProcessedDataMessage = {
|
||||||
body?: string;
|
body?: string;
|
||||||
attachments: ReadonlyArray<ProcessedAttachment>;
|
attachments: ReadonlyArray<ProcessedAttachment>;
|
||||||
|
@ -207,6 +216,7 @@ export type ProcessedDataMessage = {
|
||||||
bodyRanges?: ReadonlyArray<ProcessedBodyRange>;
|
bodyRanges?: ReadonlyArray<ProcessedBodyRange>;
|
||||||
groupCallUpdate?: ProcessedGroupCallUpdate;
|
groupCallUpdate?: ProcessedGroupCallUpdate;
|
||||||
storyContext?: ProcessedStoryContext;
|
storyContext?: ProcessedStoryContext;
|
||||||
|
giftBadge?: ProcessedGiftBadge;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProcessedUnidentifiedDeliveryStatus = Omit<
|
export type ProcessedUnidentifiedDeliveryStatus = Omit<
|
||||||
|
|
|
@ -527,6 +527,7 @@ const URL_CALLS = {
|
||||||
accountExistence: 'v1/accounts/account',
|
accountExistence: 'v1/accounts/account',
|
||||||
attachmentId: 'v2/attachments/form/upload',
|
attachmentId: 'v2/attachments/form/upload',
|
||||||
attestation: 'v1/attestation',
|
attestation: 'v1/attestation',
|
||||||
|
boostBadges: 'v1/subscription/boost/badges',
|
||||||
challenge: 'v1/challenge',
|
challenge: 'v1/challenge',
|
||||||
config: 'v1/config',
|
config: 'v1/config',
|
||||||
deliveryCert: 'v1/certificate/delivery',
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
|
@ -660,6 +661,7 @@ export type WebAPIConnectType = {
|
||||||
|
|
||||||
export type CapabilitiesType = {
|
export type CapabilitiesType = {
|
||||||
announcementGroup: boolean;
|
announcementGroup: boolean;
|
||||||
|
giftBadges: boolean;
|
||||||
'gv1-migration': boolean;
|
'gv1-migration': boolean;
|
||||||
senderKey: boolean;
|
senderKey: boolean;
|
||||||
changeNumber: boolean;
|
changeNumber: boolean;
|
||||||
|
@ -667,6 +669,7 @@ export type CapabilitiesType = {
|
||||||
};
|
};
|
||||||
export type CapabilitiesUploadType = {
|
export type CapabilitiesUploadType = {
|
||||||
announcementGroup: true;
|
announcementGroup: true;
|
||||||
|
giftBadges: true;
|
||||||
'gv2-3': true;
|
'gv2-3': true;
|
||||||
'gv1-migration': true;
|
'gv1-migration': true;
|
||||||
senderKey: true;
|
senderKey: true;
|
||||||
|
@ -864,6 +867,9 @@ export type WebAPIType = {
|
||||||
options: GetProfileUnauthOptionsType
|
options: GetProfileUnauthOptionsType
|
||||||
) => Promise<ProfileType>;
|
) => Promise<ProfileType>;
|
||||||
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
|
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
|
||||||
|
getBoostBadgesFromServer: (
|
||||||
|
userLanguages: ReadonlyArray<string>
|
||||||
|
) => Promise<unknown>;
|
||||||
getProvisioningResource: (
|
getProvisioningResource: (
|
||||||
handler: IRequestHandler
|
handler: IRequestHandler
|
||||||
) => Promise<WebSocketResource>;
|
) => Promise<WebSocketResource>;
|
||||||
|
@ -1186,6 +1192,7 @@ export function initialize({
|
||||||
getProfileForUsername,
|
getProfileForUsername,
|
||||||
getProfileUnauth,
|
getProfileUnauth,
|
||||||
getBadgeImageFile,
|
getBadgeImageFile,
|
||||||
|
getBoostBadgesFromServer,
|
||||||
getProvisioningResource,
|
getProvisioningResource,
|
||||||
getSenderCertificate,
|
getSenderCertificate,
|
||||||
getSticker,
|
getSticker,
|
||||||
|
@ -1630,6 +1637,19 @@ export function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getBoostBadgesFromServer(
|
||||||
|
userLanguages: ReadonlyArray<string>
|
||||||
|
): Promise<unknown> {
|
||||||
|
return _ajax({
|
||||||
|
call: 'boostBadges',
|
||||||
|
httpType: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getAvatar(path: string) {
|
async function getAvatar(path: string) {
|
||||||
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
|
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
|
||||||
// attachment CDN, it uses our self-signed certificate, so we pass it in.
|
// attachment CDN, it uses our self-signed certificate, so we pass it in.
|
||||||
|
@ -1744,6 +1764,7 @@ export function initialize({
|
||||||
) {
|
) {
|
||||||
const capabilities: CapabilitiesUploadType = {
|
const capabilities: CapabilitiesUploadType = {
|
||||||
announcementGroup: true,
|
announcementGroup: true,
|
||||||
|
giftBadges: true,
|
||||||
'gv2-3': true,
|
'gv2-3': true,
|
||||||
'gv1-migration': true,
|
'gv1-migration': true,
|
||||||
senderKey: true,
|
senderKey: true,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
|
import { ReceiptCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
|
||||||
|
|
||||||
import { assert, strictAssert } from '../util/assert';
|
import { assert, strictAssert } from '../util/assert';
|
||||||
import { dropNull, shallowDropNull } from '../util/dropNull';
|
import { dropNull, shallowDropNull } from '../util/dropNull';
|
||||||
|
@ -21,8 +22,10 @@ import type {
|
||||||
ProcessedSticker,
|
ProcessedSticker,
|
||||||
ProcessedReaction,
|
ProcessedReaction,
|
||||||
ProcessedDelete,
|
ProcessedDelete,
|
||||||
|
ProcessedGiftBadge,
|
||||||
} from './Types.d';
|
} from './Types.d';
|
||||||
import { WarnOnlyError } from './Errors';
|
import { WarnOnlyError } from './Errors';
|
||||||
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
|
|
||||||
const FLAGS = Proto.DataMessage.Flags;
|
const FLAGS = Proto.DataMessage.Flags;
|
||||||
export const ATTACHMENT_MAX = 32;
|
export const ATTACHMENT_MAX = 32;
|
||||||
|
@ -130,6 +133,7 @@ export function processQuote(
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
bodyRanges: quote.bodyRanges ?? [],
|
bodyRanges: quote.bodyRanges ?? [],
|
||||||
|
type: quote.type || Proto.DataMessage.Quote.Type.NORMAL,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,6 +231,32 @@ export function processDelete(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function processGiftBadge(
|
||||||
|
timestamp: number,
|
||||||
|
giftBadge: Proto.DataMessage.IGiftBadge | null | undefined
|
||||||
|
): ProcessedGiftBadge | undefined {
|
||||||
|
if (
|
||||||
|
!giftBadge ||
|
||||||
|
!giftBadge.receiptCredentialPresentation ||
|
||||||
|
giftBadge.receiptCredentialPresentation.length === 0
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receipt = new ReceiptCredentialPresentation(
|
||||||
|
Buffer.from(giftBadge.receiptCredentialPresentation)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
expiration: timestamp + Number(receipt.getReceiptExpirationTime()),
|
||||||
|
level: Number(receipt.getReceiptLevel()),
|
||||||
|
receiptCredentialPresentation: Bytes.toBase64(
|
||||||
|
giftBadge.receiptCredentialPresentation
|
||||||
|
),
|
||||||
|
state: GiftBadgeStates.Unopened,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function processDataMessage(
|
export async function processDataMessage(
|
||||||
message: Proto.IDataMessage,
|
message: Proto.IDataMessage,
|
||||||
envelopeTimestamp: number
|
envelopeTimestamp: number
|
||||||
|
@ -276,6 +306,7 @@ export async function processDataMessage(
|
||||||
bodyRanges: message.bodyRanges ?? [],
|
bodyRanges: message.bodyRanges ?? [],
|
||||||
groupCallUpdate: dropNull(message.groupCallUpdate),
|
groupCallUpdate: dropNull(message.groupCallUpdate),
|
||||||
storyContext: dropNull(message.storyContext),
|
storyContext: dropNull(message.storyContext),
|
||||||
|
giftBadge: processGiftBadge(timestamp, message.giftBadge),
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEndSession = Boolean(result.flags & FLAGS.END_SESSION);
|
const isEndSession = Boolean(result.flags & FLAGS.END_SESSION);
|
||||||
|
|
|
@ -9,6 +9,10 @@ import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequ
|
||||||
import type { ToastBlocked } from '../components/ToastBlocked';
|
import type { ToastBlocked } from '../components/ToastBlocked';
|
||||||
import type { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
import type { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||||
import type { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments';
|
import type { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments';
|
||||||
|
import type {
|
||||||
|
ToastCannotOpenGiftBadge,
|
||||||
|
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
|
||||||
|
} from '../components/ToastCannotOpenGiftBadge';
|
||||||
import type { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall';
|
import type { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall';
|
||||||
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
|
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
|
||||||
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
|
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
|
||||||
|
@ -60,6 +64,10 @@ export function showToast(
|
||||||
Toast: typeof ToastCannotMixImageAndNonImageAttachments
|
Toast: typeof ToastCannotMixImageAndNonImageAttachments
|
||||||
): void;
|
): void;
|
||||||
export function showToast(Toast: typeof ToastCannotStartGroupCall): void;
|
export function showToast(Toast: typeof ToastCannotStartGroupCall): void;
|
||||||
|
export function showToast(
|
||||||
|
Toast: typeof ToastCannotOpenGiftBadge,
|
||||||
|
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
|
||||||
|
): void;
|
||||||
export function showToast(Toast: typeof ToastCaptchaFailed): void;
|
export function showToast(Toast: typeof ToastCaptchaFailed): void;
|
||||||
export function showToast(Toast: typeof ToastCaptchaSolved): void;
|
export function showToast(Toast: typeof ToastCaptchaSolved): void;
|
||||||
export function showToast(
|
export function showToast(
|
||||||
|
|
|
@ -97,6 +97,7 @@ import { ToastReportedSpamAndBlocked } from '../components/ToastReportedSpamAndB
|
||||||
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
||||||
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
|
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
|
||||||
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
|
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
|
||||||
|
import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge';
|
||||||
import { autoScale } from '../util/handleImageAttachment';
|
import { autoScale } from '../util/handleImageAttachment';
|
||||||
import { copyGroupLink } from '../util/copyGroupLink';
|
import { copyGroupLink } from '../util/copyGroupLink';
|
||||||
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
||||||
|
@ -163,6 +164,7 @@ type MessageActionsType = {
|
||||||
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
|
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
|
||||||
markViewed: (messageId: string) => unknown;
|
markViewed: (messageId: string) => unknown;
|
||||||
openConversation: (conversationId: string, messageId?: string) => unknown;
|
openConversation: (conversationId: string, messageId?: string) => unknown;
|
||||||
|
openGiftBadge: (messageId: string) => unknown;
|
||||||
openLink: (url: string) => unknown;
|
openLink: (url: string) => unknown;
|
||||||
reactToMessage: (
|
reactToMessage: (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
|
@ -859,6 +861,17 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const showIdentity = (conversationId: string) => {
|
const showIdentity = (conversationId: string) => {
|
||||||
this.showSafetyNumber(conversationId);
|
this.showSafetyNumber(conversationId);
|
||||||
};
|
};
|
||||||
|
const openGiftBadge = (messageId: string): void => {
|
||||||
|
const message = window.MessageController.getById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(`openGiftBadge: Message ${messageId} missing!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(ToastCannotOpenGiftBadge, {
|
||||||
|
isIncoming: isIncoming(message.attributes),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const openLink = openLinkInWebBrowser;
|
const openLink = openLinkInWebBrowser;
|
||||||
const downloadNewVersion = () => {
|
const downloadNewVersion = () => {
|
||||||
openLinkInWebBrowser('https://signal.org/download');
|
openLinkInWebBrowser('https://signal.org/download');
|
||||||
|
@ -888,6 +901,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
markViewed: onMarkViewed,
|
markViewed: onMarkViewed,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
openGiftBadge,
|
||||||
openLink,
|
openLink,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
|
Loading…
Reference in a new issue