From a114e4e210a5de111c81f546bae218c1b39d0cad Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 16 May 2022 12:54:38 -0700 Subject: [PATCH] Gift Badges: A few tweaks --- stylesheets/_modules.scss | 146 +++++++++--------- ts/badges/parseBadgesFromServer.ts | 2 +- .../conversation/Message.stories.tsx | 27 ++-- ts/components/conversation/Message.tsx | 13 +- ts/components/conversation/Quote.stories.tsx | 2 +- ts/components/conversation/Quote.tsx | 2 +- ts/messageModifiers/ViewSyncs.ts | 4 +- ts/model-types.d.ts | 1 + ts/models/messages.ts | 55 +++---- ts/textsecure/Types.d.ts | 5 +- ts/textsecure/processDataMessage.ts | 1 + 11 files changed, 134 insertions(+), 124 deletions(-) diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 0c56b85b6154..476da70fde9d 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1268,11 +1268,6 @@ $message-padding-horizontal: 12px; } } -.module-message__unopened-gift-badge__container { - cursor: pointer; - user-select: none; -} - .module-message__unopened-gift-badge { width: 240px; height: 132px; @@ -1285,82 +1280,89 @@ $message-padding-horizontal: 12px; 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; + &--outgoing { + @include light-theme { + border-bottom: 1px solid $color-white-alpha-80; + } + @include dark-theme { + border-bottom: 1px solid $color-gray-95; + } } - @include dark-theme { - border-bottom: 1px solid $color-gray-95; + + &__container { + cursor: default; + user-select: none; } -} -.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; + &__ribbon-horizontal { + position: absolute; + left: 0; + right: 0; + height: 16px; + top: 50%; + transform: translateY(-50%); + background-color: $color-white; } - @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; + &__ribbon-vertical { + position: absolute; + top: 0; + bottom: 0; + width: 16px; + left: 50%; + transform: translateX(-50%); + background-color: $color-white; } - @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; + &__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; } - @include dark-theme { - color: $color-white-alpha-80; + + &__text { + @include font-body-2-italic; + + &--incoming { + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + } + } + + &__container .module-message__text--incoming { + @include font-body-2-italic; + + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + } + &__container .module-message__text--outgoing { + @include font-body-2-italic; + + @include light-theme { + color: $color-white-alpha-80; + } + @include dark-theme { + color: $color-white-alpha-80; + } } } diff --git a/ts/badges/parseBadgesFromServer.ts b/ts/badges/parseBadgesFromServer.ts index b9067990d9f8..73bbfaa295a4 100644 --- a/ts/badges/parseBadgesFromServer.ts +++ b/ts/badges/parseBadgesFromServer.ts @@ -62,7 +62,7 @@ export function parseBoostBadgeListFromServer( const parsed = parseBadgeFromServer(item.badge, updatesUrl); if (parsed) { - result[`BOOST-${level}`] = parsed; + result[level] = parsed; } }); diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index aaee08b52d9e..7d9bb6092273 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -1727,9 +1727,10 @@ story.add('EmbeddedContact: Loading Avatar', () => { story.add('Gift Badge: Unopened', () => { const props = createProps({ giftBadge: { - state: GiftBadgeStates.Unopened, + id: 'GIFT', expiration: Date.now() + DAY * 30, level: 3, + state: GiftBadgeStates.Unopened, }, }); return renderBothDirections(props); @@ -1738,7 +1739,7 @@ story.add('Gift Badge: Unopened', () => { const getPreferredBadge = () => ({ category: BadgeCategory.Donor, descriptionTemplate: 'This is a description of the badge', - id: 'BOOST-3', + id: 'GIFT', images: [ { transparent: { @@ -1754,9 +1755,10 @@ story.add('Gift Badge: Redeemed (30 days)', () => { const props = createProps({ getPreferredBadge, giftBadge: { - state: GiftBadgeStates.Redeemed, expiration: Date.now() + DAY * 30 + SECOND, + id: 'GIFT', level: 3, + state: GiftBadgeStates.Redeemed, }, }); return renderBothDirections(props); @@ -1766,21 +1768,23 @@ story.add('Gift Badge: Redeemed (24 hours)', () => { const props = createProps({ getPreferredBadge, giftBadge: { - state: GiftBadgeStates.Redeemed, expiration: Date.now() + DAY + SECOND, + id: 'GIFT', level: 3, + state: GiftBadgeStates.Redeemed, }, }); return renderBothDirections(props); }); -story.add('Gift Badge: Redeemed (60 minutes)', () => { +story.add('Gift Badge: Opened (60 minutes)', () => { const props = createProps({ getPreferredBadge, giftBadge: { - state: GiftBadgeStates.Redeemed, expiration: Date.now() + HOUR + SECOND, + id: 'GIFT', level: 3, + state: GiftBadgeStates.Opened, }, }); return renderBothDirections(props); @@ -1790,21 +1794,23 @@ story.add('Gift Badge: Redeemed (1 minute)', () => { const props = createProps({ getPreferredBadge, giftBadge: { - state: GiftBadgeStates.Redeemed, expiration: Date.now() + MINUTE + SECOND, + id: 'GIFT', level: 3, + state: GiftBadgeStates.Redeemed, }, }); return renderBothDirections(props); }); -story.add('Gift Badge: Redeemed (expired)', () => { +story.add('Gift Badge: Opened (expired)', () => { const props = createProps({ getPreferredBadge, giftBadge: { - state: GiftBadgeStates.Redeemed, expiration: Date.now(), + id: 'GIFT', level: 3, + state: GiftBadgeStates.Opened, }, }); return renderBothDirections(props); @@ -1814,9 +1820,10 @@ story.add('Gift Badge: Missing Badge', () => { const props = createProps({ getPreferredBadge: () => undefined, giftBadge: { - state: GiftBadgeStates.Redeemed, expiration: Date.now() + MINUTE + SECOND, + id: 'MISSING', level: 3, + state: GiftBadgeStates.Redeemed, }, }); return renderBothDirections(props); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 8e760e906d3c..30379c5dfa42 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -180,12 +180,14 @@ export type AudioAttachmentProps = { export enum GiftBadgeStates { Unopened = 'Unopened', + Opened = 'Opened', Redeemed = 'Redeemed', } export type GiftBadgeType = { - level: number; expiration: number; - state: GiftBadgeStates.Redeemed | GiftBadgeStates.Unopened; + id: string | undefined; + level: number; + state: GiftBadgeStates; }; export type PropsData = { @@ -1329,8 +1331,11 @@ export class Message extends React.PureComponent { ); } - if (giftBadge.state === GiftBadgeStates.Redeemed) { - const badgeId = `BOOST-${giftBadge.level}`; + if ( + giftBadge.state === GiftBadgeStates.Redeemed || + giftBadge.state === GiftBadgeStates.Opened + ) { + const badgeId = giftBadge.id || `BOOST-${giftBadge.level}`; const badgeSize = 64; const badge = getPreferredBadge([{ id: badgeId }]); const badgeImagePath = getBadgeImageFileLocalPath( diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index b037de3c651d..426cf47ce68c 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -348,7 +348,7 @@ story.add('Video Tap-to-View', () => { story.add('Gift Badge', () => { const props = createProps({ - text: '', + text: "Some text which shouldn't be rendered", isGiftBadge: true, }); diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index c6567d8c2053..bb412c5872f1 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -333,7 +333,7 @@ export class Quote extends React.Component { isViewOnce, } = this.props; - if (text) { + if (text && !isGiftBadge) { const quoteText = bodyRanges ? getTextWithMentions(bodyRanges, text) : text; diff --git a/ts/messageModifiers/ViewSyncs.ts b/ts/messageModifiers/ViewSyncs.ts index 3186066d8bd9..cad692a12467 100644 --- a/ts/messageModifiers/ViewSyncs.ts +++ b/ts/messageModifiers/ViewSyncs.ts @@ -98,7 +98,9 @@ export class ViewSyncs extends Collection { message.set({ giftBadge: { ...giftBadge, - state: GiftBadgeStates.Redeemed, + state: isIncoming(message.attributes) + ? GiftBadgeStates.Redeemed + : GiftBadgeStates.Opened, }, }); } diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 5d21bffd0aa9..4bfbe2bedd71 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -183,6 +183,7 @@ export type MessageAttributesType = { giftBadge?: { expiration: number; level: number; + id: string | undefined; receiptCredentialPresentation: string; state: GiftBadgeStates; }; diff --git a/ts/models/messages.ts b/ts/models/messages.ts index de2b7d95e1c0..0d600c72452b 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -2682,40 +2682,31 @@ export class MessageModel extends window.Backbone.Model { 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 { 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 userLanguages = getUserLanguages( - navigator.languages, - window.getLocale() + const boostBadgesByLevel = parseBoostBadgeListFromServer( + response, + updatesUrl + ); + const badge = boostBadgesByLevel[level]; + if (!badge) { + log.error( + `handleDataMessage: gift badge with level ${level} not found on server` ); - 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, - }, - ]); - } + } else { + await window.reduxActions.badges.updateOrCreate([badge]); + giftBadge.id = badge.id; } } diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 13a05c7fe54f..7ea6c322d1a3 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -189,9 +189,10 @@ export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate; export type ProcessedStoryContext = Proto.DataMessage.IStoryContext; export type ProcessedGiftBadge = { - receiptCredentialPresentation: string; - level: number; expiration: number; + id: string | undefined; + level: number; + receiptCredentialPresentation: string; state: GiftBadgeStates; }; diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index 3f40fda2aded..0ae38528b95a 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -249,6 +249,7 @@ export function processGiftBadge( return { expiration: timestamp + Number(receipt.getReceiptExpirationTime()), + id: undefined, level: Number(receipt.getReceiptLevel()), receiptCredentialPresentation: Bytes.toBase64( giftBadge.receiptCredentialPresentation