Refresh the sticker pack manager

This commit is contained in:
Josh Perez 2022-12-13 19:06:15 -05:00 committed by GitHub
parent fdfc0539a3
commit 94875efaf6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 550 additions and 627 deletions

View file

@ -2352,8 +2352,16 @@
"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"
}, },
"stickers--StickerManager--title": {
"message": "Stickers",
"description": "Title for the sticker manager"
},
"stickers--StickerManager--Available": {
"message": "Available",
"description": "Shown in the sticker pack manager as a tab for available stickers"
},
"stickers--StickerManager--InstalledPacks": { "stickers--StickerManager--InstalledPacks": {
"message": "Installed Stickers", "message": "Installed",
"description": "Shown in the sticker pack manager above your installed sticker packs." "description": "Shown in the sticker pack manager above your installed sticker packs."
}, },
"stickers--StickerManager--InstalledPacks--Empty": { "stickers--StickerManager--InstalledPacks--Empty": {

View file

@ -2713,6 +2713,10 @@ button.ConversationDetails__action-button {
vertical-align: middle; vertical-align: middle;
} }
.module-image--hidden {
visibility: hidden;
}
.module-image--tap-to-play, .module-image--tap-to-play,
.module-image--not-downloaded { .module-image--not-downloaded {
align-items: center; align-items: center;
@ -5741,421 +5745,6 @@ button.module-image__border-overlay:focus {
} }
} }
// Module: StickerManager
.module-sticker-manager {
padding: 0 16px;
outline: none;
}
.module-sticker-manager__text {
height: 18px;
letter-spacing: 0px;
line-height: 18px;
@include light-theme() {
color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-25;
}
&--heading {
@include font-body-1-bold;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-gray-05;
}
}
}
.module-sticker-manager__empty {
display: flex;
justify-content: center;
align-items: center;
height: 64px;
border-radius: 8px;
@include light-theme {
background: $color-gray-02;
color: $color-gray-60;
}
@include dark-theme {
background: $color-gray-90;
color: $color-gray-25;
}
}
%blessed-sticker-pack-icon {
height: 14px;
width: 14px;
border-radius: 8px;
background-color: $color-white;
display: inline-block;
vertical-align: middle;
margin: {
left: 5px;
bottom: 2px;
}
position: relative;
&::before {
content: '';
display: block;
width: 16px;
height: 16px;
position: absolute;
top: -1px;
left: -1px;
@include light-theme {
@include color-svg(
'../images/icons/v2/check-circle-solid-24.svg',
$color-accent-blue
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/check-circle-solid-24.svg',
$color-accent-blue
);
}
}
}
.module-sticker-manager__pack-row {
@include button-reset;
display: flex;
flex-direction: row;
padding: 16px;
@include light-theme {
& + & {
border-top: 1px solid $color-gray-15;
}
}
@include dark-theme {
& + & {
border-top: 1px solid $color-gray-75;
}
}
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-ultramarine;
}
}
&__cover {
width: 48px;
height: 48px;
object-fit: contain;
}
&__cover-placeholder {
width: 48px;
height: 48px;
background: $color-gray-05;
}
&__meta {
flex-grow: 1;
display: flex;
flex-direction: column;
&:not(:first-child) {
padding: 0 12px;
}
&__title {
flex: 1;
}
&__author {
flex: 1;
@include light-theme() {
color: $color-gray-45;
}
@include dark-theme() {
color: $color-gray-25;
}
}
&__blessed-icon {
@extend %blessed-sticker-pack-icon;
}
}
&__controls {
flex-shrink: 1;
display: flex;
justify-content: center;
align-items: center;
&__button {
background: none;
border: 0;
&--menu {
&::after {
content: '';
display: block;
min-width: 24px;
min-height: 24px;
@include light-theme {
@include color-svg(
'../images/icons/v2/more-horiz-24.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/more-horiz-24.svg',
$color-gray-25
);
}
}
}
}
}
}
.module-sticker-manager__install-button {
background: none;
border: 0;
color: $color-gray-90;
@include font-body-1-bold;
height: 24px;
background: $color-gray-05;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 12px;
@include dark-theme {
color: $color-gray-05;
background: $color-gray-75;
}
@include mouse-mode {
outline: none;
}
&--blue {
@include light-theme {
background: $color-ultramarine;
color: $color-white;
}
@include dark-theme {
background: $color-ultramarine-light;
color: $color-white;
}
}
}
.module-sticker-manager__preview-modal {
&__overlay {
background: $color-black-alpha-40;
position: fixed;
left: 0;
top: 0;
width: var(--window-width);
height: var(--window-height);
display: flex;
justify-content: center;
align-items: center;
z-index: $z-index-popup-overlay;
}
&__container {
position: relative;
border-radius: 8px;
box-shadow: 0 4px 12px 0 $color-black-alpha-20;
width: 440px;
height: 360px;
overflow: hidden;
display: flex;
flex-direction: column;
@include light-theme {
background: $color-white;
}
@include dark-theme {
background: $color-gray-75;
}
&__error {
color: $color-accent-red;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
width: 100%;
height: 100%;
padding: 0 80px 30px 80px;
@include font-body-1-bold;
}
&__header {
display: flex;
flex-direction: row;
flex-shrink: 0;
height: 36px;
padding: 0 8px 0 16px;
justify-content: space-between;
align-items: center;
&__text {
@include font-body-1-bold;
color: $color-gray-90;
@include dark-theme {
color: $color-gray-05;
}
}
&__close-button {
border: none;
width: 20px;
height: 20px;
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
}
}
}
&__sticker-grid {
width: 100%;
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
overflow-y: auto;
overflow-x: hidden;
padding: 0 16px;
&::after {
content: '';
display: block;
height: 80px;
grid-column: 1 / span 4;
}
&__cell {
width: 96px;
height: 96px;
display: flex;
justify-content: center;
align-items: center;
&__image {
width: 100%;
height: 100%;
object-fit: contain;
}
&--placeholder {
border-radius: 4px;
@include light-theme() {
background: $color-gray-05;
}
@include dark-theme() {
background: $color-gray-60;
}
}
}
}
&__meta-overlay {
border-radius: 4px;
width: 408px;
height: 52px;
position: absolute;
left: 16px;
bottom: 16px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
@include light-theme {
background: $color-gray-05;
}
@include dark-theme {
background: $color-gray-60;
}
&__info {
display: flex;
flex-direction: column;
justify-content: center;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
&__title {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
@include font-body-1-bold;
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-gray-05;
}
}
&__author {
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@include light-theme {
color: $color-gray-45;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__blessed-icon {
@extend %blessed-sticker-pack-icon;
}
}
&__install {
flex-shrink: 0;
overflow: hidden;
}
}
}
}
// Module: Sticker button (launches the sticker picker) // Module: Sticker button (launches the sticker picker)
.sticker-button-wrapper { .sticker-button-wrapper {

View file

@ -0,0 +1,323 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-sticker-manager {
padding: 0 16px;
outline: none;
}
.module-sticker-manager__text {
height: 18px;
letter-spacing: 0px;
line-height: 18px;
padding-left: 8px;
@include light-theme() {
color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-25;
}
&--heading {
@include font-body-1-bold;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-gray-05;
}
}
}
.module-sticker-manager__empty {
display: flex;
justify-content: center;
align-items: center;
height: 64px;
border-radius: 8px;
@include light-theme {
background: $color-gray-02;
color: $color-gray-60;
}
@include dark-theme {
background: $color-gray-90;
color: $color-gray-25;
}
}
%blessed-sticker-pack-icon {
height: 14px;
width: 14px;
border-radius: 8px;
background-color: $color-white;
display: inline-block;
vertical-align: middle;
margin: {
left: 5px;
bottom: 2px;
}
position: relative;
&::before {
content: '';
display: block;
width: 16px;
height: 16px;
position: absolute;
top: -1px;
left: -1px;
@include light-theme {
@include color-svg(
'../images/icons/v2/check-circle-solid-24.svg',
$color-accent-blue
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/check-circle-solid-24.svg',
$color-accent-blue
);
}
}
}
.module-sticker-manager__pack-row {
@include button-reset;
display: flex;
flex-direction: row;
padding: 16px;
padding-left: 8px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-ultramarine;
}
}
&__cover {
width: 48px;
height: 48px;
object-fit: contain;
}
&__cover-placeholder {
width: 48px;
height: 48px;
background: $color-gray-05;
}
&__meta {
flex-grow: 1;
display: flex;
flex-direction: column;
&:not(:first-child) {
padding: 0 12px;
}
&__title {
flex: 1;
}
&__author {
flex: 1;
@include light-theme() {
color: $color-gray-45;
}
@include dark-theme() {
color: $color-gray-25;
}
}
&__blessed-icon {
@extend %blessed-sticker-pack-icon;
}
}
&__controls {
flex-shrink: 1;
display: flex;
justify-content: center;
align-items: center;
&__button {
background: none;
border: 0;
&--menu {
&::after {
content: '';
display: block;
min-width: 24px;
min-height: 24px;
@include light-theme {
@include color-svg(
'../images/icons/v2/more-horiz-24.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/more-horiz-24.svg',
$color-gray-25
);
}
}
}
}
}
}
.module-sticker-manager__install-button {
background: none;
border: 0;
color: $color-gray-90;
@include font-body-1-bold;
height: 24px;
background: $color-gray-05;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 12px;
@include dark-theme {
color: $color-gray-05;
background: $color-gray-75;
}
@include mouse-mode {
outline: none;
}
&--blue {
@include light-theme {
background: $color-ultramarine;
color: $color-white;
}
@include dark-theme {
background: $color-ultramarine-light;
color: $color-white;
}
}
}
.module-sticker-manager__preview-modal {
&__modal.module-Modal {
max-width: 440px;
width: 440px;
}
&__error {
color: $color-accent-red;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
width: 100%;
height: 100%;
padding: 0 80px 30px 80px;
@include font-body-1-bold;
}
&__sticker-grid {
width: 100%;
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
overflow-y: auto;
&__cell {
width: 96px;
height: 96px;
display: flex;
justify-content: center;
align-items: center;
&__image {
width: 100%;
height: 100%;
object-fit: contain;
}
&--placeholder {
border-radius: 4px;
@include light-theme() {
background: $color-gray-05;
}
@include dark-theme() {
background: $color-gray-60;
}
}
}
}
&__footer {
display: flex;
justify-content: space-between;
width: 100%;
&--info {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
justify-content: center;
overflow: hidden;
}
&--title {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
@include font-body-1-bold;
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-gray-05;
}
}
&--author {
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@include light-theme {
color: $color-gray-45;
}
@include dark-theme {
color: $color-gray-25;
}
}
&--blessed-icon {
@extend %blessed-sticker-pack-icon;
}
&--install {
flex-shrink: 0;
overflow: hidden;
}
}
}

View file

@ -111,14 +111,15 @@
@import './components/SignalConnectionsModal.scss'; @import './components/SignalConnectionsModal.scss';
@import './components/Slider.scss'; @import './components/Slider.scss';
@import './components/StagedLinkPreview.scss'; @import './components/StagedLinkPreview.scss';
@import './components/StickerManager.scss';
@import './components/Stories.scss'; @import './components/Stories.scss';
@import './components/StoriesSettingsModal.scss';
@import './components/StoryCreator.scss'; @import './components/StoryCreator.scss';
@import './components/StoryDetailsModal.scss'; @import './components/StoryDetailsModal.scss';
@import './components/StoryImage.scss'; @import './components/StoryImage.scss';
@import './components/StoryLinkPreview.scss'; @import './components/StoryLinkPreview.scss';
@import './components/StoryListItem.scss'; @import './components/StoryListItem.scss';
@import './components/StoryReplyQuote.scss'; @import './components/StoryReplyQuote.scss';
@import './components/StoriesSettingsModal.scss';
@import './components/StoryViewer.scss'; @import './components/StoryViewer.scss';
@import './components/StoryViewsNRepliesModal.scss'; @import './components/StoryViewsNRepliesModal.scss';
@import './components/SystemMessage.scss'; @import './components/SystemMessage.scss';

View file

@ -44,6 +44,7 @@ const knownPacks = [
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
blessedPacks: overrideProps.blessedPacks || [], blessedPacks: overrideProps.blessedPacks || [],
closeStickerPackPreview: action('closeStickerPackPreview'),
downloadStickerPack: action('downloadStickerPack'), downloadStickerPack: action('downloadStickerPack'),
i18n, i18n,
installStickerPack: action('installStickerPack'), installStickerPack: action('installStickerPack'),

View file

@ -1,35 +1,42 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames';
import { StickerManagerPackRow } from './StickerManagerPackRow'; import { StickerManagerPackRow } from './StickerManagerPackRow';
import { StickerPreviewModal } from './StickerPreviewModal'; import { StickerPreviewModal } from './StickerPreviewModal';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { StickerPackType } from '../../state/ducks/stickers'; import type { StickerPackType } from '../../state/ducks/stickers';
import { Tabs } from '../Tabs';
export type OwnProps = { export type OwnProps = {
readonly installedPacks: ReadonlyArray<StickerPackType>;
readonly receivedPacks: ReadonlyArray<StickerPackType>;
readonly blessedPacks: ReadonlyArray<StickerPackType>; readonly blessedPacks: ReadonlyArray<StickerPackType>;
readonly knownPacks?: ReadonlyArray<StickerPackType>; readonly closeStickerPackPreview: (packId: string) => unknown;
readonly downloadStickerPack: (packId: string, packKey: string) => unknown; readonly downloadStickerPack: (packId: string, packKey: string) => unknown;
readonly installStickerPack: (packId: string, packKey: string) => unknown;
readonly uninstallStickerPack: (packId: string, packKey: string) => unknown;
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly installStickerPack: (packId: string, packKey: string) => unknown;
readonly installedPacks: ReadonlyArray<StickerPackType>;
readonly knownPacks?: ReadonlyArray<StickerPackType>;
readonly receivedPacks: ReadonlyArray<StickerPackType>;
readonly uninstallStickerPack: (packId: string, packKey: string) => unknown;
}; };
export type Props = OwnProps; export type Props = OwnProps;
enum TabViews {
Available = 'Available',
Installed = 'Installed',
}
export const StickerManager = React.memo(function StickerManagerInner({ export const StickerManager = React.memo(function StickerManagerInner({
installedPacks,
receivedPacks,
knownPacks,
blessedPacks, blessedPacks,
closeStickerPackPreview,
downloadStickerPack, downloadStickerPack,
installStickerPack,
uninstallStickerPack,
i18n, i18n,
installStickerPack,
installedPacks,
knownPacks,
receivedPacks,
uninstallStickerPack,
}: Props) { }: Props) {
const focusRef = React.createRef<HTMLDivElement>(); const focusRef = React.createRef<HTMLDivElement>();
const [packToPreview, setPackToPreview] = const [packToPreview, setPackToPreview] =
@ -66,68 +73,94 @@ export const StickerManager = React.memo(function StickerManagerInner({
<> <>
{packToPreview ? ( {packToPreview ? (
<StickerPreviewModal <StickerPreviewModal
i18n={i18n} closeStickerPackPreview={closeStickerPackPreview}
pack={packToPreview}
closeStickerPackPreview={clearPackToPreview}
downloadStickerPack={downloadStickerPack} downloadStickerPack={downloadStickerPack}
i18n={i18n}
installStickerPack={installStickerPack} installStickerPack={installStickerPack}
onClose={clearPackToPreview}
pack={packToPreview}
uninstallStickerPack={uninstallStickerPack} uninstallStickerPack={uninstallStickerPack}
/> />
) : null} ) : null}
<div className="module-sticker-manager" tabIndex={-1} ref={focusRef}> <div className="module-sticker-manager" tabIndex={-1} ref={focusRef}>
{[ <Tabs
{ initialSelectedTab={TabViews.Available}
i18nKey: 'stickers--StickerManager--InstalledPacks', tabs={[
i18nEmptyKey: 'stickers--StickerManager--InstalledPacks--Empty', {
packs: installedPacks, id: TabViews.Available,
hideIfEmpty: false, label: i18n('stickers--StickerManager--Available'),
}, },
{ {
i18nKey: 'stickers--StickerManager--BlessedPacks', id: TabViews.Installed,
i18nEmptyKey: 'stickers--StickerManager--BlessedPacks--Empty', label: i18n('stickers--StickerManager--InstalledPacks'),
packs: blessedPacks, },
hideIfEmpty: true, ]}
}, >
{ {({ selectedTab }) => (
i18nKey: 'stickers--StickerManager--ReceivedPacks', <>
i18nEmptyKey: 'stickers--StickerManager--ReceivedPacks--Empty', {selectedTab === TabViews.Available && (
packs: receivedPacks, <>
hideIfEmpty: false, <h2 className="module-sticker-manager__text module-sticker-manager__text--heading">
}, {i18n('stickers--StickerManager--BlessedPacks')}
].map(section => { </h2>
if (section.hideIfEmpty && section.packs.length === 0) { {blessedPacks.length > 0 ? (
return null; blessedPacks.map(pack => (
} <StickerManagerPackRow
key={pack.id}
pack={pack}
i18n={i18n}
onClickPreview={previewPack}
installStickerPack={installStickerPack}
uninstallStickerPack={uninstallStickerPack}
/>
))
) : (
<div className="module-sticker-manager__empty">
{i18n('stickers--StickerManager--BlessedPacks--Empty')}
</div>
)}
return ( <h2 className="module-sticker-manager__text module-sticker-manager__text--heading">
<React.Fragment key={section.i18nKey}> {i18n('stickers--StickerManager--ReceivedPacks')}
<h2 </h2>
className={classNames( {receivedPacks.length > 0 ? (
'module-sticker-manager__text', receivedPacks.map(pack => (
'module-sticker-manager__text--heading' <StickerManagerPackRow
)} key={pack.id}
> pack={pack}
{i18n(section.i18nKey)} i18n={i18n}
</h2> onClickPreview={previewPack}
{section.packs.length > 0 ? ( installStickerPack={installStickerPack}
section.packs.map(pack => ( uninstallStickerPack={uninstallStickerPack}
<StickerManagerPackRow />
key={pack.id} ))
pack={pack} ) : (
i18n={i18n} <div className="module-sticker-manager__empty">
onClickPreview={previewPack} {i18n('stickers--StickerManager--ReceivedPacks--Empty')}
installStickerPack={installStickerPack} </div>
uninstallStickerPack={uninstallStickerPack} )}
/> </>
))
) : (
<div className="module-sticker-manager__empty">
{i18n(section.i18nEmptyKey)}
</div>
)} )}
</React.Fragment> {selectedTab === TabViews.Installed &&
); (installedPacks.length > 0 ? (
})} installedPacks.map(pack => (
<StickerManagerPackRow
key={pack.id}
pack={pack}
i18n={i18n}
onClickPreview={previewPack}
installStickerPack={installStickerPack}
uninstallStickerPack={uninstallStickerPack}
/>
))
) : (
<div className="module-sticker-manager__empty">
{i18n('stickers--StickerManager--InstalledPacks--Empty')}
</div>
))}
</>
)}
</Tabs>
</div> </div>
</> </>
); );

View file

@ -1,11 +1,11 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import { StickerPackInstallButton } from './StickerPackInstallButton';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { StickerPackType } from '../../state/ducks/stickers'; import type { StickerPackType } from '../../state/ducks/stickers';
import { Button, ButtonVariant } from '../Button';
export type OwnProps = { export type OwnProps = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
@ -136,17 +136,21 @@ export const StickerManagerPackRow = React.memo(
</div> </div>
<div className="module-sticker-manager__pack-row__controls"> <div className="module-sticker-manager__pack-row__controls">
{pack.status === 'installed' ? ( {pack.status === 'installed' ? (
<StickerPackInstallButton <Button
installed aria-label={i18n('stickers--StickerManager--Uninstall')}
i18n={i18n} variant={ButtonVariant.Secondary}
onClick={handleUninstall} onClick={handleUninstall}
/> >
{i18n('stickers--StickerManager--Uninstall')}
</Button>
) : ( ) : (
<StickerPackInstallButton <Button
installed={false} aria-label={i18n('stickers--StickerManager--Install')}
i18n={i18n} variant={ButtonVariant.Secondary}
onClick={handleInstall} onClick={handleInstall}
/> >
{i18n('stickers--StickerManager--Install')}
</Button>
)} )}
</div> </div>
</div> </div>

View file

@ -65,6 +65,7 @@ export function Full(): JSX.Element {
return ( return (
<StickerPreviewModal <StickerPreviewModal
closeStickerPackPreview={action('closeStickerPackPreview')} closeStickerPackPreview={action('closeStickerPackPreview')}
onClose={action('onClose')}
installStickerPack={action('installStickerPack')} installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')} uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')} downloadStickerPack={action('downloadStickerPack')}

View file

@ -1,19 +1,20 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import { createPortal } from 'react-dom';
import { isNumber, range } from 'lodash'; import { isNumber, range } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { StickerPackInstallButton } from './StickerPackInstallButton';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { StickerPackType } from '../../state/ducks/stickers'; import type { StickerPackType } from '../../state/ducks/stickers';
import { Spinner } from '../Spinner'; import { Spinner } from '../Spinner';
import { useRestoreFocus } from '../../hooks/useRestoreFocus'; import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { Modal } from '../Modal';
import { Button, ButtonVariant } from '../Button';
export type OwnProps = { export type OwnProps = {
readonly closeStickerPackPreview: () => unknown; readonly onClose?: () => unknown;
readonly closeStickerPackPreview: (packId: string) => unknown;
readonly downloadStickerPack: ( readonly downloadStickerPack: (
packId: string, packId: string,
packKey: string, packKey: string,
@ -34,7 +35,7 @@ function renderBody({ pack, i18n }: Props) {
if (pack && pack.status === 'error') { if (pack && pack.status === 'error') {
return ( return (
<div className="module-sticker-manager__preview-modal__container__error"> <div className="module-sticker-manager__preview-modal__error">
{i18n('stickers--StickerPreview--Error')} {i18n('stickers--StickerPreview--Error')}
</div> </div>
); );
@ -45,29 +46,28 @@ function renderBody({ pack, i18n }: Props) {
} }
return ( return (
<div className="module-sticker-manager__preview-modal__container__sticker-grid"> <div className="module-sticker-manager__preview-modal__sticker-grid">
{pack.stickers.map(({ id, url }) => ( {pack.stickers.map(({ id, url }) => (
<div <div
key={id} key={id}
className="module-sticker-manager__preview-modal__container__sticker-grid__cell" className="module-sticker-manager__preview-modal__sticker-grid__cell"
> >
<img <img
className="module-sticker-manager__preview-modal__container__sticker-grid__cell__image" className="module-sticker-manager__preview-modal__sticker-grid__cell__image"
src={url} src={url}
alt={pack.title} alt={pack.title}
/> />
</div> </div>
))} ))}
{pack.status === 'pending' && {range(pack.stickerCount - pack.stickers.length).map(i => (
range(pack.stickerCount - pack.stickers.length).map(i => ( <div
<div key={`placeholder-${i}`}
key={`placeholder-${i}`} className={classNames(
className={classNames( 'module-sticker-manager__preview-modal__sticker-grid__cell',
'module-sticker-manager__preview-modal__container__sticker-grid__cell', 'module-sticker-manager__preview-modal__sticker-grid__cell--placeholder'
'module-sticker-manager__preview-modal__container__sticker-grid__cell--placeholder' )}
)} />
/> ))}
))}
</div> </div>
); );
} }
@ -77,28 +77,18 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
) { ) {
const { const {
closeStickerPackPreview, closeStickerPackPreview,
pack,
i18n,
downloadStickerPack, downloadStickerPack,
i18n,
installStickerPack, installStickerPack,
onClose,
pack,
uninstallStickerPack, uninstallStickerPack,
} = props; } = props;
const [root, setRoot] = React.useState<HTMLElement | null>(null);
const [confirmingUninstall, setConfirmingUninstall] = React.useState(false); const [confirmingUninstall, setConfirmingUninstall] = React.useState(false);
// Restore focus on teardown // Restore focus on teardown
const [focusRef] = useRestoreFocus(); const [focusRef] = useRestoreFocus();
React.useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setRoot(div);
return () => {
document.body.removeChild(div);
};
}, []);
React.useEffect(() => { React.useEffect(() => {
if (pack && pack.status === 'known') { if (pack && pack.status === 'known') {
downloadStickerPack(pack.id, pack.key); downloadStickerPack(pack.id, pack.key);
@ -117,11 +107,12 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
React.useEffect(() => { const handleClose = React.useCallback(() => {
if (!pack) { if (pack?.id) {
closeStickerPackPreview(); closeStickerPackPreview(pack.id);
} }
}, [pack, closeStickerPackPreview]); onClose?.();
}, [closeStickerPackPreview, onClose, pack]);
const isInstalled = Boolean(pack && pack.status === 'installed'); const isInstalled = Boolean(pack && pack.status === 'installed');
const handleToggleInstall = React.useCallback(() => { const handleToggleInstall = React.useCallback(() => {
@ -132,16 +123,16 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
setConfirmingUninstall(true); setConfirmingUninstall(true);
} else if (pack.status === 'ephemeral') { } else if (pack.status === 'ephemeral') {
downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' }); downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' });
closeStickerPackPreview(); handleClose();
} else { } else {
installStickerPack(pack.id, pack.key); installStickerPack(pack.id, pack.key);
closeStickerPackPreview(); handleClose();
} }
}, [ }, [
downloadStickerPack, downloadStickerPack,
installStickerPack, installStickerPack,
isInstalled, isInstalled,
closeStickerPackPreview, handleClose,
pack, pack,
setConfirmingUninstall, setConfirmingUninstall,
]); ]);
@ -155,100 +146,70 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
// closeStickerPackPreview is called by <ConfirmationDialog />'s onClose // closeStickerPackPreview is called by <ConfirmationDialog />'s onClose
}, [uninstallStickerPack, setConfirmingUninstall, pack]); }, [uninstallStickerPack, setConfirmingUninstall, pack]);
React.useEffect(() => { const buttonLabel = isInstalled
const handler = ({ key }: KeyboardEvent) => { ? i18n('stickers--StickerManager--Uninstall')
if (key === 'Escape') { : i18n('stickers--StickerManager--Install');
closeStickerPackPreview();
}
};
document.addEventListener('keydown', handler); const modalFooter =
pack && pack.status !== 'error' ? (
return () => { <div className="module-sticker-manager__preview-modal__footer">
document.removeEventListener('keydown', handler); <div className="module-sticker-manager__preview-modal__footer--info">
}; <h3 className="module-sticker-manager__preview-modal__footer--title">
}, [closeStickerPackPreview]); {pack.title}
{pack.isBlessed ? (
const handleClickToClose = React.useCallback( <span className="module-sticker-manager__preview-modal__footer--blessed-icon" />
(e: React.MouseEvent) => { ) : null}
if (e.target === e.currentTarget) { </h3>
closeStickerPackPreview(); <h4 className="module-sticker-manager__preview-modal__footer--author">
} {pack.author}
}, </h4>
[closeStickerPackPreview] </div>
); <div className="module-sticker-manager__preview-modal__footer--install">
{pack.status === 'pending' ? (
return root <Spinner svgSize="small" size="14px" />
? createPortal(
// Not really a button. Just a background which can be clicked to close modal
// eslint-disable-next-line max-len
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
<div
role="button"
className="module-sticker-manager__preview-modal__overlay"
onClick={handleClickToClose}
>
{confirmingUninstall ? (
<ConfirmationDialog
dialogName="StickerPreviewModal.confirmUninstall"
i18n={i18n}
onClose={closeStickerPackPreview}
actions={[
{
style: 'negative',
text: i18n('stickers--StickerManager--Uninstall'),
action: handleUninstall,
},
]}
>
{i18n('stickers--StickerManager--UninstallWarning')}
</ConfirmationDialog>
) : ( ) : (
<div className="module-sticker-manager__preview-modal__container"> <Button
<header className="module-sticker-manager__preview-modal__container__header"> aria-label={buttonLabel}
<h2 className="module-sticker-manager__preview-modal__container__header__text"> ref={focusRef}
{i18n('stickers--StickerPreview--Title')} onClick={handleToggleInstall}
</h2> variant={ButtonVariant.Primary}
<button >
type="button" {buttonLabel}
onClick={closeStickerPackPreview} </Button>
className="module-sticker-manager__preview-modal__container__header__close-button"
aria-label={i18n('close')}
/>
</header>
{renderBody(props)}
{pack && pack.status !== 'error' ? (
<div className="module-sticker-manager__preview-modal__container__meta-overlay">
<div className="module-sticker-manager__preview-modal__container__meta-overlay__info">
<h3 className="module-sticker-manager__preview-modal__container__meta-overlay__info__title">
{pack.title}
{pack.isBlessed ? (
<span className="module-sticker-manager__preview-modal__container__meta-overlay__info__blessed-icon" />
) : null}
</h3>
<h4 className="module-sticker-manager__preview-modal__container__meta-overlay__info__author">
{pack.author}
</h4>
</div>
<div className="module-sticker-manager__preview-modal__container__meta-overlay__install">
{pack.status === 'pending' ? (
<Spinner svgSize="small" size="14px" />
) : (
<StickerPackInstallButton
ref={focusRef}
installed={isInstalled}
i18n={i18n}
onClick={handleToggleInstall}
blue
/>
)}
</div>
</div>
) : null}
</div>
)} )}
</div>, </div>
root </div>
) ) : undefined;
: null;
return (
<>
{confirmingUninstall && (
<ConfirmationDialog
dialogName="StickerPreviewModal.confirmUninstall"
actions={[
{
style: 'negative',
text: i18n('stickers--StickerManager--Uninstall'),
action: handleUninstall,
},
]}
i18n={i18n}
onClose={() => setConfirmingUninstall(false)}
>
{i18n('stickers--StickerManager--UninstallWarning')}
</ConfirmationDialog>
)}
<Modal
hasXButton
i18n={i18n}
modalFooter={modalFooter}
modalName="StickerPreviewModal"
moduleClassName="module-sticker-manager__preview-modal__modal"
onClose={handleClose}
title={i18n('stickers--StickerPreview--Title')}
>
{renderBody(props)}
</Modal>
</>
);
}); });

View file

@ -142,7 +142,7 @@ describe('storage service', function needsName() {
.click({ noWaitAfter: true }); .click({ noWaitAfter: true });
await window await window
.locator( .locator(
'.module-sticker-manager__preview-modal__container button >> "Install"' '.module-sticker-manager__preview-modal__footer--install button >> "Install"'
) )
.click(); .click();
@ -186,13 +186,15 @@ describe('storage service', function needsName() {
.click({ noWaitAfter: true }); .click({ noWaitAfter: true });
await window await window
.locator( .locator(
'.module-sticker-manager__preview-modal__container button ' + '.module-sticker-manager__preview-modal__footer--install button ' +
'>> "Uninstall"' '>> "Uninstall"'
) )
.click(); .click();
// Confirm // Confirm
await window.locator('.module-Modal button >> "Uninstall"').click(); await window
.locator('.module-Button--destructive >> "Uninstall"')
.click();
debug('waiting for sync message'); debug('waiting for sync message');
const { syncMessage } = await phone.waitForSyncMessage(entry => const { syncMessage } = await phone.waitForSyncMessage(entry =>