Stickers
Co-authored-by: scott@signal.org Co-authored-by: ken@signal.org
This commit is contained in:
parent
8c8856785b
commit
29de50c12a
100 changed files with 7572 additions and 693 deletions
16
ts/components/ConfirmationDialog.md
Normal file
16
ts/components/ConfirmationDialog.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
#### All Options
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<ConfirmationDialog
|
||||
i18n={util.i18n}
|
||||
onClose={() => console.log('onClose')}
|
||||
onAffirmative={() => console.log('onAffirmative')}
|
||||
affirmativeText="Affirm"
|
||||
onNegative={() => console.log('onNegative')}
|
||||
negativeText="Negate"
|
||||
>
|
||||
asdf child
|
||||
</ConfirmationDialog>
|
||||
</util.ConversationContext>
|
||||
```
|
117
ts/components/ConfirmationDialog.tsx
Normal file
117
ts/components/ConfirmationDialog.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly children: React.ReactNode;
|
||||
readonly affirmativeText?: string;
|
||||
readonly onAffirmative?: () => unknown;
|
||||
readonly onClose: () => unknown;
|
||||
readonly negativeText?: string;
|
||||
readonly onNegative?: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
function focusRef(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export const ConfirmationDialog = React.memo(
|
||||
({
|
||||
i18n,
|
||||
onClose,
|
||||
children,
|
||||
onAffirmative,
|
||||
onNegative,
|
||||
affirmativeText,
|
||||
negativeText,
|
||||
}: Props) => {
|
||||
React.useEffect(
|
||||
() => {
|
||||
const handler = ({ key }: KeyboardEvent) => {
|
||||
if (key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keyup', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handler);
|
||||
};
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
const handleCancel = React.useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
const handleNegative = React.useCallback(
|
||||
() => {
|
||||
onClose();
|
||||
if (onNegative) {
|
||||
onNegative();
|
||||
}
|
||||
},
|
||||
[onClose, onNegative]
|
||||
);
|
||||
|
||||
const handleAffirmative = React.useCallback(
|
||||
() => {
|
||||
onClose();
|
||||
if (onAffirmative) {
|
||||
onAffirmative();
|
||||
}
|
||||
},
|
||||
[onClose, onAffirmative]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="module-confirmation-dialog__container">
|
||||
<div className="module-confirmation-dialog__container__content">
|
||||
{children}
|
||||
</div>
|
||||
<div className="module-confirmation-dialog__container__buttons">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
ref={focusRef}
|
||||
className="module-confirmation-dialog__container__buttons__button"
|
||||
>
|
||||
{i18n('confirmation-dialog--Cancel')}
|
||||
</button>
|
||||
{onNegative && negativeText ? (
|
||||
<button
|
||||
onClick={handleNegative}
|
||||
className={classNames(
|
||||
'module-confirmation-dialog__container__buttons__button',
|
||||
'module-confirmation-dialog__container__buttons__button--negative'
|
||||
)}
|
||||
>
|
||||
{negativeText}
|
||||
</button>
|
||||
) : null}
|
||||
{onAffirmative && affirmativeText ? (
|
||||
<button
|
||||
onClick={handleAffirmative}
|
||||
className={classNames(
|
||||
'module-confirmation-dialog__container__buttons__button',
|
||||
'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||
)}
|
||||
>
|
||||
{affirmativeText}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
89
ts/components/ConfirmationModal.tsx
Normal file
89
ts/components/ConfirmationModal.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly children: React.ReactNode;
|
||||
readonly affirmativeText?: string;
|
||||
readonly onAffirmative?: () => unknown;
|
||||
readonly onClose: () => unknown;
|
||||
readonly negativeText?: string;
|
||||
readonly onNegative?: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
export const ConfirmationModal = React.memo(
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
({
|
||||
i18n,
|
||||
onClose,
|
||||
children,
|
||||
onAffirmative,
|
||||
onNegative,
|
||||
affirmativeText,
|
||||
negativeText,
|
||||
}: Props) => {
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
setRoot(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
setRoot(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const handler = ({ key }: KeyboardEvent) => {
|
||||
if (key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keyup', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handler);
|
||||
};
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
const handleCancel = React.useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
return root
|
||||
? createPortal(
|
||||
<div
|
||||
role="button"
|
||||
className="module-confirmation-dialog__overlay"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
onAffirmative={onAffirmative}
|
||||
onNegative={onNegative}
|
||||
affirmativeText={affirmativeText}
|
||||
negativeText={negativeText}
|
||||
>
|
||||
{children}
|
||||
</ConfirmationDialog>
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
: null;
|
||||
}
|
||||
);
|
|
@ -5,9 +5,10 @@ import { getIncrement, getTimerBucket } from '../../util/timer';
|
|||
|
||||
interface Props {
|
||||
withImageNoCaption: boolean;
|
||||
withSticker: boolean;
|
||||
expirationLength: number;
|
||||
expirationTimestamp: number;
|
||||
direction: 'incoming' | 'outgoing';
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
}
|
||||
|
||||
export class ExpireTimer extends React.Component<Props> {
|
||||
|
@ -44,6 +45,7 @@ export class ExpireTimer extends React.Component<Props> {
|
|||
expirationLength,
|
||||
expirationTimestamp,
|
||||
withImageNoCaption,
|
||||
withSticker,
|
||||
} = this.props;
|
||||
|
||||
const bucket = getTimerBucket(expirationTimestamp, expirationLength);
|
||||
|
@ -56,7 +58,8 @@ export class ExpireTimer extends React.Component<Props> {
|
|||
`module-expire-timer--${direction}`,
|
||||
withImageNoCaption
|
||||
? 'module-expire-timer--with-image-no-caption'
|
||||
: null
|
||||
: null,
|
||||
withSticker ? 'module-expire-timer--with-sticker' : null
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -418,3 +418,51 @@
|
|||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### No border, no background
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div style={{ padding: '10px', backgroundColor: 'lightgrey' }}>
|
||||
<div>
|
||||
<Image
|
||||
height="512"
|
||||
width="512"
|
||||
noBorder={true}
|
||||
noBackground={true}
|
||||
attachment={{}}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
url={util.squareStickerObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
height="256"
|
||||
width="256"
|
||||
noBorder={true}
|
||||
noBackground={true}
|
||||
attachment={{}}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
url={util.squareStickerObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
height="128"
|
||||
width="128"
|
||||
noBorder={true}
|
||||
noBackground={true}
|
||||
attachment={{}}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
url={util.squareStickerObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
|
|
@ -15,6 +15,8 @@ interface Props {
|
|||
|
||||
overlayText?: string;
|
||||
|
||||
noBorder?: boolean;
|
||||
noBackground?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
closeButton?: boolean;
|
||||
curveBottomLeft?: boolean;
|
||||
|
@ -49,6 +51,8 @@ export class Image extends React.Component<Props> {
|
|||
darkOverlay,
|
||||
height,
|
||||
i18n,
|
||||
noBackground,
|
||||
noBorder,
|
||||
onClick,
|
||||
onClickClose,
|
||||
onError,
|
||||
|
@ -74,6 +78,7 @@ export class Image extends React.Component<Props> {
|
|||
}}
|
||||
className={classNames(
|
||||
'module-image',
|
||||
!noBackground ? 'module-image--with-background' : null,
|
||||
canClick ? 'module-image__with-click-handler' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
|
@ -113,18 +118,20 @@ export class Image extends React.Component<Props> {
|
|||
alt={i18n('imageCaptionIconAlt')}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image__border-overlay',
|
||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
|
||||
softCorners ? 'module-image--soft-corners' : null,
|
||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||
)}
|
||||
/>
|
||||
{!noBorder ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image__border-overlay',
|
||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
|
||||
softCorners ? 'module-image--soft-corners' : null,
|
||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{closeButton ? (
|
||||
<div
|
||||
role="button"
|
||||
|
|
|
@ -384,3 +384,26 @@ const attachments = [
|
|||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Sticker
|
||||
|
||||
```
|
||||
const attachments = [
|
||||
{
|
||||
url: util.squareStickerObjectUrl,
|
||||
contentType: 'image/webp',
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid isSticker={true} stickerSize={128} attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid isSticker={true} stickerSize={128} withContentAbove withContentBelow attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
|
|
@ -20,6 +20,8 @@ interface Props {
|
|||
withContentAbove?: boolean;
|
||||
withContentBelow?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
isSticker?: boolean;
|
||||
stickerSize?: number;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
|
@ -34,6 +36,8 @@ export class ImageGrid extends React.Component<Props> {
|
|||
attachments,
|
||||
bottomOverlay,
|
||||
i18n,
|
||||
isSticker,
|
||||
stickerSize,
|
||||
onError,
|
||||
onClick,
|
||||
withContentAbove,
|
||||
|
@ -56,25 +60,31 @@ export class ImageGrid extends React.Component<Props> {
|
|||
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
||||
const { height, width } = getImageDimensions(attachments[0]);
|
||||
|
||||
const finalHeight = isSticker ? stickerSize : height;
|
||||
const finalWidth = isSticker ? stickerSize : width;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image-grid',
|
||||
'module-image-grid--one-image'
|
||||
'module-image-grid--one-image',
|
||||
isSticker ? 'module-image-grid--with-sticker' : null
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
noBackground={isSticker}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveTopRight={curveTopRight}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
curveBottomRight={curveBottomRight}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={height}
|
||||
width={width}
|
||||
height={finalHeight}
|
||||
width={finalWidth}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
|
@ -91,6 +101,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
i18n={i18n}
|
||||
attachment={attachments[0]}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
|
@ -104,6 +115,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveTopRight={curveTopRight}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
|
@ -125,6 +137,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
attachment={attachments[0]}
|
||||
|
@ -152,6 +165,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomRight={curveBottomRight}
|
||||
height={99}
|
||||
width={99}
|
||||
|
@ -201,6 +215,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
height={149}
|
||||
|
@ -214,6 +229,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
height={149}
|
||||
|
@ -268,6 +284,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
height={99}
|
||||
|
@ -281,6 +298,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
height={99}
|
||||
width={98}
|
||||
|
@ -293,6 +311,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[4], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[4])}
|
||||
height={99}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -39,11 +39,13 @@ interface Trigger {
|
|||
|
||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
const STICKER_SIZE = 128;
|
||||
|
||||
interface LinkPreviewType {
|
||||
title: string;
|
||||
domain: string;
|
||||
url: string;
|
||||
isStickerPack: boolean;
|
||||
image?: AttachmentType;
|
||||
}
|
||||
|
||||
|
@ -51,6 +53,7 @@ export type PropsData = {
|
|||
id: string;
|
||||
text?: string;
|
||||
textPending?: boolean;
|
||||
isSticker: boolean;
|
||||
direction: 'incoming' | 'outgoing';
|
||||
timestamp: number;
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||
|
@ -223,6 +226,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
expirationLength,
|
||||
expirationTimestamp,
|
||||
i18n,
|
||||
isSticker,
|
||||
status,
|
||||
text,
|
||||
textPending,
|
||||
|
@ -234,8 +238,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const isShowingImage = this.isShowingImage();
|
||||
const withImageNoCaption = Boolean(!text && isShowingImage);
|
||||
const withImageNoCaption = Boolean(!isSticker && !text && isShowingImage);
|
||||
const showError = status === 'error' && direction === 'outgoing';
|
||||
const metadataDirection = isSticker ? undefined : direction;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -250,7 +255,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<span
|
||||
className={classNames(
|
||||
'module-message__metadata__date',
|
||||
`module-message__metadata__date--${direction}`,
|
||||
isSticker ? 'module-message__metadata__date--with-sticker' : null,
|
||||
!isSticker
|
||||
? `module-message__metadata__date--${direction}`
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata__date--with-image-no-caption'
|
||||
: null
|
||||
|
@ -263,17 +271,19 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
extended={true}
|
||||
direction={direction}
|
||||
direction={metadataDirection}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
module="module-message__metadata__date"
|
||||
/>
|
||||
)}
|
||||
{expirationLength && expirationTimestamp ? (
|
||||
<ExpireTimer
|
||||
direction={direction}
|
||||
direction={metadataDirection}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
/>
|
||||
) : null}
|
||||
<span className="module-message__metadata__spacer" />
|
||||
|
@ -287,6 +297,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
className={classNames(
|
||||
'module-message__metadata__status-icon',
|
||||
`module-message__metadata__status-icon--${status}`,
|
||||
isSticker
|
||||
? 'module-message__metadata__status-icon--with-sticker'
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
||||
: null
|
||||
|
@ -302,24 +315,33 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
authorName,
|
||||
authorPhoneNumber,
|
||||
authorProfileName,
|
||||
collapseMetadata,
|
||||
conversationType,
|
||||
direction,
|
||||
i18n,
|
||||
isSticker,
|
||||
} = this.props;
|
||||
|
||||
if (collapseMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = authorName ? authorName : authorPhoneNumber;
|
||||
|
||||
if (direction !== 'incoming' || conversationType !== 'group' || !title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = isSticker ? '_with_sticker' : '';
|
||||
const moduleName = `module-message__author${suffix}`;
|
||||
|
||||
return (
|
||||
<div className="module-message__author">
|
||||
<div className={moduleName}>
|
||||
<ContactName
|
||||
phoneNumber={authorPhoneNumber}
|
||||
name={authorName}
|
||||
profileName={authorProfileName}
|
||||
module="module-message__author"
|
||||
module={moduleName}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
|
@ -329,15 +351,16 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
||||
public renderAttachment() {
|
||||
const {
|
||||
id,
|
||||
attachments,
|
||||
text,
|
||||
collapseMetadata,
|
||||
conversationType,
|
||||
direction,
|
||||
i18n,
|
||||
id,
|
||||
quote,
|
||||
showVisualAttachment,
|
||||
isSticker,
|
||||
text,
|
||||
} = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
|
||||
|
@ -359,23 +382,31 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
((isImage(attachments) && hasImage(attachments)) ||
|
||||
(isVideo(attachments) && hasVideoScreenshot(attachments)))
|
||||
) {
|
||||
const prefix = isSticker ? 'sticker' : 'attachment';
|
||||
const bottomOverlay = !isSticker && !collapseMetadata;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__attachment-container',
|
||||
`module-message__${prefix}-container`,
|
||||
withContentAbove
|
||||
? 'module-message__attachment-container--with-content-above'
|
||||
? `module-message__${prefix}-container--with-content-above`
|
||||
: null,
|
||||
withContentBelow
|
||||
? 'module-message__attachment-container--with-content-below'
|
||||
: null,
|
||||
isSticker && !collapseMetadata
|
||||
? 'module-message__sticker-container--with-content-below'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
<ImageGrid
|
||||
attachments={attachments}
|
||||
withContentAbove={withContentAbove}
|
||||
withContentBelow={withContentBelow}
|
||||
bottomOverlay={!collapseMetadata}
|
||||
withContentAbove={isSticker || withContentAbove}
|
||||
withContentBelow={isSticker || withContentBelow}
|
||||
isSticker={isSticker}
|
||||
stickerSize={STICKER_SIZE}
|
||||
bottomOverlay={bottomOverlay}
|
||||
i18n={i18n}
|
||||
onError={this.handleImageErrorBound}
|
||||
onClick={attachment => {
|
||||
|
@ -494,7 +525,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const previewHasImage = first.image && isImageAttachment(first.image);
|
||||
const width = first.image && first.image.width;
|
||||
const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH;
|
||||
const isFullSizeImage =
|
||||
!first.isStickerPack &&
|
||||
width &&
|
||||
width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -768,6 +802,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
disableMenu,
|
||||
downloadAttachment,
|
||||
id,
|
||||
isSticker,
|
||||
replyToMessage,
|
||||
timestamp,
|
||||
} = this.props;
|
||||
|
@ -783,7 +818,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const firstAttachment = attachments && attachments[0];
|
||||
|
||||
const downloadButton =
|
||||
!multipleAttachments && firstAttachment && !firstAttachment.pending ? (
|
||||
!isSticker &&
|
||||
!multipleAttachments &&
|
||||
firstAttachment &&
|
||||
!firstAttachment.pending ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
downloadAttachment({
|
||||
|
@ -850,6 +888,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
downloadAttachment,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
deleteMessage,
|
||||
showMessageDetail,
|
||||
replyToMessage,
|
||||
|
@ -866,7 +905,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const menu = (
|
||||
<ContextMenu id={triggerId}>
|
||||
{!multipleAttachments && attachments && attachments[0] ? (
|
||||
{!isSticker && !multipleAttachments && attachments && attachments[0] ? (
|
||||
<MenuItem
|
||||
attributes={{
|
||||
className: 'module-message__context__download',
|
||||
|
@ -931,9 +970,14 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public getWidth(): number | undefined {
|
||||
const { attachments, previews } = this.props;
|
||||
const { attachments, isSticker, previews } = this.props;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
if (isSticker) {
|
||||
// Padding is 8px, on both sides
|
||||
return STICKER_SIZE + 8 * 2;
|
||||
}
|
||||
|
||||
const dimensions = getGridDimensions(attachments);
|
||||
if (dimensions) {
|
||||
return dimensions.width;
|
||||
|
@ -949,6 +993,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const { width } = first.image;
|
||||
|
||||
if (
|
||||
!first.isStickerPack &&
|
||||
isImageAttachment(first.image) &&
|
||||
width &&
|
||||
width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH
|
||||
|
@ -999,11 +1044,13 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
authorPhoneNumber,
|
||||
authorColor,
|
||||
attachments,
|
||||
direction,
|
||||
id,
|
||||
isSticker,
|
||||
timestamp,
|
||||
} = this.props;
|
||||
const { expired, expiring } = this.state;
|
||||
const { expired, expiring, imageBroken } = this.state;
|
||||
|
||||
// This id is what connects our triple-dot click with our associated pop-up menu.
|
||||
// It needs to be unique.
|
||||
|
@ -1013,6 +1060,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (isSticker && (imageBroken || !attachments || !attachments.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = this.getWidth();
|
||||
const isShowingImage = this.isShowingImage();
|
||||
|
||||
|
@ -1029,8 +1080,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<div
|
||||
className={classNames(
|
||||
'module-message__container',
|
||||
`module-message__container--${direction}`,
|
||||
direction === 'incoming'
|
||||
isSticker ? 'module-message__container--with-sticker' : null,
|
||||
!isSticker ? `module-message__container--${direction}` : null,
|
||||
!isSticker && direction === 'incoming'
|
||||
? `module-message__container--incoming-${authorColor}`
|
||||
: null
|
||||
)}
|
||||
|
|
|
@ -11,6 +11,7 @@ interface Props {
|
|||
extended?: boolean;
|
||||
module?: string;
|
||||
withImageNoCaption?: boolean;
|
||||
withSticker?: boolean;
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
@ -48,6 +49,7 @@ export class Timestamp extends React.Component<Props> {
|
|||
module,
|
||||
timestamp,
|
||||
withImageNoCaption,
|
||||
withSticker,
|
||||
extended,
|
||||
} = this.props;
|
||||
const moduleName = module || 'module-timestamp';
|
||||
|
@ -61,7 +63,8 @@ export class Timestamp extends React.Component<Props> {
|
|||
className={classNames(
|
||||
moduleName,
|
||||
direction ? `${moduleName}--${direction}` : null,
|
||||
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null
|
||||
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
|
||||
withSticker ? `${moduleName}--with-sticker` : null
|
||||
)}
|
||||
title={moment(timestamp).format('llll')}
|
||||
>
|
||||
|
|
271
ts/components/stickers/StickerButton.md
Normal file
271
ts/components/stickers/StickerButton.md
Normal file
|
@ -0,0 +1,271 @@
|
|||
#### Default
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<StickerButton
|
||||
i18n={util.i18n}
|
||||
receivedPacks={[]}
|
||||
installedPacks={packs}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
recentStickers={[abeSticker, sticker1, sticker2, sticker3]}
|
||||
/>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### No Installed Packs
|
||||
|
||||
When there are no installed packs the button should call the `onClickAddPack`
|
||||
callback.
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<StickerButton
|
||||
i18n={util.i18n}
|
||||
receivedPacks={[]}
|
||||
installedPacks={packs}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
recentStickers={[abeSticker, sticker1, sticker2, sticker3]}
|
||||
/>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### No Advertised Packs and No Installed Packs
|
||||
|
||||
When there are no advertised packs and no installed packs the button should not render anything.
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerButton
|
||||
i18n={util.i18n}
|
||||
receivedPacks={[]}
|
||||
installedPacks={[]}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
recentStickers={[]}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
#### Installed Pack Tooltip
|
||||
|
||||
When a pack is installed there should be a tooltip saying as such.
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
title: 'Abe',
|
||||
cover: abeSticker,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...abeSticker, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker1,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker2,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'qux',
|
||||
cover: sticker3,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<StickerButton
|
||||
i18n={util.i18n}
|
||||
receivedPacks={[]}
|
||||
installedPacks={packs}
|
||||
installedPack={packs[0]}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
recentStickers={[]}
|
||||
/>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### New Installation Splash Tooltip
|
||||
|
||||
When the application is updated or freshly installed there should be a tooltip
|
||||
showing the user the sticker button.
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
title: 'Abe',
|
||||
cover: abeSticker,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...abeSticker, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker1,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker2,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'qux',
|
||||
cover: sticker3,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<StickerButton
|
||||
i18n={util.i18n}
|
||||
receivedPacks={[]}
|
||||
installedPacks={packs}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
recentStickers={[]}
|
||||
showIntroduction
|
||||
clearShowIntroduction={() => console.log('clearShowIntroduction')}
|
||||
/>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
255
ts/components/stickers/StickerButton.tsx
Normal file
255
ts/components/stickers/StickerButton.tsx
Normal file
|
@ -0,0 +1,255 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { StickerPicker } from './StickerPicker';
|
||||
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly receivedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly installedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly installedPack?: StickerPackType | null;
|
||||
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||
readonly clearInstalledStickerPack: () => unknown;
|
||||
readonly onClickAddPack: () => unknown;
|
||||
readonly onPickSticker: (packId: string, stickerId: number) => unknown;
|
||||
readonly showIntroduction?: boolean;
|
||||
readonly clearShowIntroduction: () => unknown;
|
||||
readonly showPickerHint: boolean;
|
||||
readonly clearShowPickerHint: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
export const StickerButton = React.memo(
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
({
|
||||
i18n,
|
||||
clearInstalledStickerPack,
|
||||
onClickAddPack,
|
||||
onPickSticker,
|
||||
recentStickers,
|
||||
receivedPacks,
|
||||
installedPack,
|
||||
installedPacks,
|
||||
showIntroduction,
|
||||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleClickButton = React.useCallback(
|
||||
() => {
|
||||
// Clear tooltip state
|
||||
clearInstalledStickerPack();
|
||||
|
||||
// Handle button click
|
||||
if (installedPacks.length === 0) {
|
||||
onClickAddPack();
|
||||
} else if (popperRoot) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
setOpen(true);
|
||||
}
|
||||
},
|
||||
[
|
||||
clearInstalledStickerPack,
|
||||
onClickAddPack,
|
||||
installedPacks,
|
||||
popperRoot,
|
||||
setOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const handlePickSticker = React.useCallback(
|
||||
(packId: string, stickerId: number) => {
|
||||
setOpen(false);
|
||||
onPickSticker(packId, stickerId);
|
||||
},
|
||||
[setOpen, onPickSticker]
|
||||
);
|
||||
|
||||
const handleClickAddPack = React.useCallback(
|
||||
() => {
|
||||
setOpen(false);
|
||||
if (showPickerHint) {
|
||||
clearShowPickerHint();
|
||||
}
|
||||
onClickAddPack();
|
||||
},
|
||||
[onClickAddPack, showPickerHint, clearShowPickerHint]
|
||||
);
|
||||
|
||||
const handleClearIntroduction = React.useCallback(
|
||||
() => {
|
||||
clearInstalledStickerPack();
|
||||
clearShowIntroduction();
|
||||
},
|
||||
[clearInstalledStickerPack, clearShowIntroduction]
|
||||
);
|
||||
|
||||
// Create popper root and handle outside clicks
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (open) {
|
||||
const root = document.createElement('div');
|
||||
setPopperRoot(root);
|
||||
document.body.appendChild(root);
|
||||
const handleOutsideClick = ({ target }: MouseEvent) => {
|
||||
if (!root.contains(target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(root);
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
setPopperRoot(null);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
},
|
||||
[open, setOpen, setPopperRoot]
|
||||
);
|
||||
|
||||
// Clear the installed pack after one minute
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (installedPack) {
|
||||
// tslint:disable-next-line:no-string-based-set-timeout
|
||||
const timerId = setTimeout(clearInstalledStickerPack, 60 * 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timerId);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
},
|
||||
[installedPack, clearInstalledStickerPack]
|
||||
);
|
||||
|
||||
if (installedPacks.length + receivedPacks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={handleClickButton}
|
||||
className={classNames({
|
||||
'module-sticker-button__button': true,
|
||||
'module-sticker-button__button--active': open,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Reference>
|
||||
{!open && !showIntroduction && installedPack ? (
|
||||
<Popper placement="top-end" key={installedPack.id}>
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
style={style}
|
||||
className="module-sticker-button__tooltip"
|
||||
role="button"
|
||||
onClick={clearInstalledStickerPack}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-button__tooltip__image"
|
||||
src={installedPack.cover.url}
|
||||
alt={installedPack.title}
|
||||
/>
|
||||
<span className="module-sticker-button__tooltip__text">
|
||||
<span className="module-sticker-button__tooltip__text__title">
|
||||
{installedPack.title}
|
||||
</span>{' '}
|
||||
installed
|
||||
</span>
|
||||
<div
|
||||
ref={arrowProps.ref}
|
||||
style={arrowProps.style}
|
||||
className={classNames(
|
||||
'module-sticker-button__tooltip__triangle',
|
||||
`module-sticker-button__tooltip__triangle--${placement}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
) : null}
|
||||
{!open && showIntroduction ? (
|
||||
<Popper placement="top-end">
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
style={style}
|
||||
className={classNames(
|
||||
'module-sticker-button__tooltip',
|
||||
'module-sticker-button__tooltip--introduction'
|
||||
)}
|
||||
role="button"
|
||||
onClick={handleClearIntroduction}
|
||||
>
|
||||
<div className="module-sticker-button__tooltip--introduction__image" />
|
||||
<div className="module-sticker-button__tooltip--introduction__meta">
|
||||
<div className="module-sticker-button__tooltip--introduction__meta__title">
|
||||
{i18n('stickers--StickerManager--Introduction--Title')}
|
||||
</div>
|
||||
<div className="module-sticker-button__tooltip--introduction__meta__subtitle">
|
||||
{i18n('stickers--StickerManager--Introduction--Body')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-sticker-button__tooltip--introduction__close">
|
||||
<button
|
||||
className="module-sticker-button__tooltip--introduction__close__button"
|
||||
onClick={handleClearIntroduction}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={arrowProps.ref}
|
||||
style={arrowProps.style}
|
||||
className={classNames(
|
||||
'module-sticker-button__tooltip__triangle',
|
||||
'module-sticker-button__tooltip__triangle--introduction',
|
||||
`module-sticker-button__tooltip__triangle--${placement}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
) : null}
|
||||
{open && popperRoot
|
||||
? createPortal(
|
||||
<Popper placement="top-end">
|
||||
{({ ref, style }) => (
|
||||
<StickerPicker
|
||||
ref={ref}
|
||||
i18n={i18n}
|
||||
style={style}
|
||||
packs={installedPacks}
|
||||
onClickAddPack={handleClickAddPack}
|
||||
onPickSticker={handlePickSticker}
|
||||
recentStickers={recentStickers}
|
||||
showPickerHint={showPickerHint}
|
||||
/>
|
||||
)}
|
||||
</Popper>,
|
||||
popperRoot
|
||||
)
|
||||
: null}
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
);
|
178
ts/components/stickers/StickerManager.md
Normal file
178
ts/components/stickers/StickerManager.md
Normal file
|
@ -0,0 +1,178 @@
|
|||
#### Default
|
||||
|
||||
```jsx
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
title: 'Foo',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
title: 'Baz',
|
||||
author: 'Foo McBarrington (Official)',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
title: 'Third',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
const receivedPacks = packs.map(p => ({ ...p, status: 'advertised' }));
|
||||
const installedPacks = packs.map(p => ({ ...p, status: 'installed' }));
|
||||
const blessedPacks = packs.map(p => ({
|
||||
...p,
|
||||
status: 'advertised',
|
||||
isBlessed: true,
|
||||
}));
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerManager
|
||||
i18n={util.i18n}
|
||||
installedPacks={installedPacks}
|
||||
receivedPacks={receivedPacks}
|
||||
blessedPacks={blessedPacks}
|
||||
installStickerPack={id => console.log('installStickerPack', id)}
|
||||
uninstallStickerPack={id => console.log('uninstallStickerPack', id)}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### No Advertised Packs
|
||||
|
||||
```jsx
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
title: 'Foo',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
title: 'Baz',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
title: 'Baz',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
const installedPacks = packs.map(p => ({ ...p, status: 'installed' }));
|
||||
const noPacks = [];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerManager
|
||||
i18n={util.i18n}
|
||||
installedPacks={installedPacks}
|
||||
receivedPacks={noPacks}
|
||||
blessedPacks={noPacks}
|
||||
installStickerPack={id => console.log('installStickerPack', id)}
|
||||
uninstallStickerPack={id => console.log('uninstallStickerPack', id)}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### No Installed Packs
|
||||
|
||||
```jsx
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
title: 'Foo',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
title: 'Baz',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
title: 'Baz',
|
||||
author: 'Foo McBarrington',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
const receivedPacks = packs.map(p => ({ ...p, status: 'installed' }));
|
||||
const noPacks = [];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerManager
|
||||
i18n={util.i18n}
|
||||
installedPacks={noPacks}
|
||||
receivedPacks={receivedPacks}
|
||||
blessedPacks={noPacks}
|
||||
installStickerPack={id => console.log('installStickerPack', id)}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### No Packs at All
|
||||
|
||||
```jsx
|
||||
const noPacks = [];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div style={{ height: '500px' }}>
|
||||
<StickerManager
|
||||
i18n={util.i18n}
|
||||
installedPacks={noPacks}
|
||||
receivedPacks={noPacks}
|
||||
blessedPacks={noPacks}
|
||||
installStickerPack={id => console.log('installStickerPack', id)}
|
||||
uninstallStickerPack={id => console.log('uninstallStickerPack', id)}
|
||||
/>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
107
ts/components/stickers/StickerManager.tsx
Normal file
107
ts/components/stickers/StickerManager.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { StickerManagerPackRow } from './StickerManagerPackRow';
|
||||
import { StickerPreviewModal } from './StickerPreviewModal';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { StickerPackType } from '../../state/ducks/stickers';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly installedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly receivedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly blessedPacks: ReadonlyArray<StickerPackType>;
|
||||
readonly installStickerPack: (packId: string, packKey: string) => unknown;
|
||||
readonly uninstallStickerPack: (packId: string, packKey: string) => unknown;
|
||||
readonly i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
export const StickerManager = React.memo(
|
||||
({
|
||||
installedPacks,
|
||||
receivedPacks,
|
||||
blessedPacks,
|
||||
installStickerPack,
|
||||
uninstallStickerPack,
|
||||
i18n,
|
||||
}: Props) => {
|
||||
const [
|
||||
packToPreview,
|
||||
setPackToPreview,
|
||||
] = React.useState<StickerPackType | null>(null);
|
||||
|
||||
const clearPackToPreview = React.useCallback(
|
||||
() => {
|
||||
setPackToPreview(null);
|
||||
},
|
||||
[setPackToPreview]
|
||||
);
|
||||
|
||||
const previewPack = React.useCallback(
|
||||
(pack: StickerPackType) => {
|
||||
setPackToPreview(pack);
|
||||
},
|
||||
[clearPackToPreview]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{packToPreview ? (
|
||||
<StickerPreviewModal
|
||||
i18n={i18n}
|
||||
pack={packToPreview}
|
||||
onClose={clearPackToPreview}
|
||||
installStickerPack={installStickerPack}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-sticker-manager">
|
||||
{[
|
||||
{
|
||||
i18nKey: 'stickers--StickerManager--InstalledPacks',
|
||||
i18nEmptyKey: 'stickers--StickerManager--InstalledPacks--Empty',
|
||||
packs: installedPacks,
|
||||
},
|
||||
{
|
||||
i18nKey: 'stickers--StickerManager--BlessedPacks',
|
||||
i18nEmptyKey: 'stickers--StickerManager--BlessedPacks--Empty',
|
||||
packs: blessedPacks,
|
||||
},
|
||||
{
|
||||
i18nKey: 'stickers--StickerManager--ReceivedPacks',
|
||||
i18nEmptyKey: 'stickers--StickerManager--ReceivedPacks--Empty',
|
||||
packs: receivedPacks,
|
||||
},
|
||||
].map(section => (
|
||||
<React.Fragment key={section.i18nKey}>
|
||||
<h2
|
||||
className={classNames(
|
||||
'module-sticker-manager__text',
|
||||
'module-sticker-manager__text--heading'
|
||||
)}
|
||||
>
|
||||
{i18n(section.i18nKey)}
|
||||
</h2>
|
||||
{section.packs.length > 0 ? (
|
||||
section.packs.map(pack => (
|
||||
<StickerManagerPackRow
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
i18n={i18n}
|
||||
onClickPreview={previewPack}
|
||||
installStickerPack={installStickerPack}
|
||||
uninstallStickerPack={uninstallStickerPack}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="module-sticker-manager__empty">
|
||||
{i18n(section.i18nEmptyKey)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
129
ts/components/stickers/StickerManagerPackRow.tsx
Normal file
129
ts/components/stickers/StickerManagerPackRow.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import * as React from 'react';
|
||||
import { StickerPackInstallButton } from './StickerPackInstallButton';
|
||||
import { ConfirmationModal } from '../ConfirmationModal';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { StickerPackType } from '../../state/ducks/stickers';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly pack: StickerPackType;
|
||||
readonly onClickPreview?: (sticker: StickerPackType) => unknown;
|
||||
readonly installStickerPack?: (packId: string, packKey: string) => unknown;
|
||||
readonly uninstallStickerPack?: (packId: string, packKey: string) => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
export const StickerManagerPackRow = React.memo(
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
({
|
||||
installStickerPack,
|
||||
uninstallStickerPack,
|
||||
onClickPreview,
|
||||
pack,
|
||||
i18n,
|
||||
}: Props) => {
|
||||
const { id, key, isBlessed } = pack;
|
||||
const [uninstalling, setUninstalling] = React.useState(false);
|
||||
|
||||
const clearUninstalling = React.useCallback(
|
||||
() => {
|
||||
setUninstalling(false);
|
||||
},
|
||||
[setUninstalling]
|
||||
);
|
||||
|
||||
const handleInstall = React.useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (installStickerPack) {
|
||||
installStickerPack(id, key);
|
||||
}
|
||||
},
|
||||
[installStickerPack, pack]
|
||||
);
|
||||
|
||||
const handleUninstall = React.useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isBlessed && uninstallStickerPack) {
|
||||
uninstallStickerPack(id, key);
|
||||
} else {
|
||||
setUninstalling(true);
|
||||
}
|
||||
},
|
||||
[setUninstalling, id, key, isBlessed]
|
||||
);
|
||||
|
||||
const handleConfirmUninstall = React.useCallback(
|
||||
() => {
|
||||
clearUninstalling();
|
||||
if (uninstallStickerPack) {
|
||||
uninstallStickerPack(id, key);
|
||||
}
|
||||
},
|
||||
[id, key, clearUninstalling]
|
||||
);
|
||||
|
||||
const handleClickPreview = React.useCallback(
|
||||
() => {
|
||||
if (onClickPreview) {
|
||||
onClickPreview(pack);
|
||||
}
|
||||
},
|
||||
[onClickPreview, pack]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{uninstalling ? (
|
||||
<ConfirmationModal
|
||||
i18n={i18n}
|
||||
onClose={clearUninstalling}
|
||||
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
||||
onNegative={handleConfirmUninstall}
|
||||
>
|
||||
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||
</ConfirmationModal>
|
||||
) : null}
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleClickPreview}
|
||||
className="module-sticker-manager__pack-row"
|
||||
>
|
||||
<img
|
||||
src={pack.cover.url}
|
||||
alt={pack.title}
|
||||
className="module-sticker-manager__pack-row__cover"
|
||||
/>
|
||||
<div className="module-sticker-manager__pack-row__meta">
|
||||
<div className="module-sticker-manager__pack-row__meta__title">
|
||||
{pack.title}
|
||||
{pack.isBlessed ? (
|
||||
<span className="module-sticker-manager__pack-row__meta__blessed-icon" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="module-sticker-manager__pack-row__meta__author">
|
||||
{pack.author}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-sticker-manager__pack-row__controls">
|
||||
{pack.status === 'advertised' ? (
|
||||
<StickerPackInstallButton
|
||||
installed={false}
|
||||
i18n={i18n}
|
||||
onClick={handleInstall}
|
||||
/>
|
||||
) : (
|
||||
<StickerPackInstallButton
|
||||
installed={true}
|
||||
i18n={i18n}
|
||||
onClick={handleUninstall}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
29
ts/components/stickers/StickerPackInstallButton.tsx
Normal file
29
ts/components/stickers/StickerPackInstallButton.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly installed: boolean;
|
||||
readonly i18n: LocalizerType;
|
||||
readonly blue?: boolean;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & React.HTMLProps<HTMLButtonElement>;
|
||||
|
||||
export const StickerPackInstallButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
Props
|
||||
>(({ i18n, installed, blue, ...props }: Props, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames({
|
||||
'module-sticker-manager__install-button': true,
|
||||
'module-sticker-manager__install-button--blue': blue,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{installed
|
||||
? i18n('stickers--StickerManager--Uninstall')
|
||||
: i18n('stickers--StickerManager--Install')}
|
||||
</button>
|
||||
));
|
321
ts/components/stickers/StickerPicker.md
Normal file
321
ts/components/stickers/StickerPicker.md
Normal file
|
@ -0,0 +1,321 @@
|
|||
#### Default
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
{
|
||||
id: 'qux',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'quux',
|
||||
cover: sticker3,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'corge',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'grault',
|
||||
cover: sticker1,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'garply',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'waldo',
|
||||
cover: sticker3,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
{
|
||||
id: 'fred',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'plugh',
|
||||
cover: sticker1,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'xyzzy',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'thud',
|
||||
cover: abeSticker,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...abeSticker, id })),
|
||||
},
|
||||
{
|
||||
id: 'banana',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'apple',
|
||||
cover: sticker1,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'strawberry',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'tombrady',
|
||||
cover: abeSticker,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...abeSticker, id })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPicker
|
||||
i18n={util.i18n}
|
||||
packs={packs}
|
||||
recentStickers={[
|
||||
abeSticker,
|
||||
sticker1,
|
||||
sticker2,
|
||||
sticker3,
|
||||
{ ...sticker2, id: 9999 },
|
||||
]}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### No Recently Used Stickers
|
||||
|
||||
The sticker picker defaults to the first pack when there are no recent stickers.
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' };
|
||||
const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' };
|
||||
const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' };
|
||||
|
||||
const packs = [
|
||||
{
|
||||
id: 'foo',
|
||||
cover: sticker1,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker1, id })),
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
cover: sticker2,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker2, id })),
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
cover: sticker3,
|
||||
stickerCount: 101,
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...sticker3, id })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPicker
|
||||
i18n={util.i18n}
|
||||
packs={packs}
|
||||
recentStickers={[]}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### Empty
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPicker
|
||||
i18n={util.i18n}
|
||||
packs={[]}
|
||||
recentStickers={[]}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
#### Pending Download
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const packs = [
|
||||
{
|
||||
id: 'tombrady',
|
||||
status: 'pending',
|
||||
cover: abeSticker,
|
||||
stickerCount: 30,
|
||||
stickers: [abeSticker],
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPicker
|
||||
i18n={util.i18n}
|
||||
packs={packs}
|
||||
recentStickers={[]}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### Picker Hint
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const packs = [
|
||||
{
|
||||
id: 'tombrady',
|
||||
cover: abeSticker,
|
||||
stickerCount: 100,
|
||||
stickers: Array(100)
|
||||
.fill(0)
|
||||
.map((_el, i) => ({ ...abeSticker, id: i })),
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPicker
|
||||
i18n={util.i18n}
|
||||
packs={packs}
|
||||
recentStickers={[]}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
showPickerHint={true}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### Pack With Error
|
||||
|
||||
```jsx
|
||||
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
const packs = [
|
||||
{
|
||||
id: 'tombrady',
|
||||
status: 'error',
|
||||
cover: abeSticker,
|
||||
stickerCount: 3,
|
||||
stickers: [],
|
||||
},
|
||||
{
|
||||
id: 'foo',
|
||||
status: 'error',
|
||||
cover: abeSticker,
|
||||
stickerCount: 3,
|
||||
stickers: [abeSticker],
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPicker
|
||||
i18n={util.i18n}
|
||||
packs={packs}
|
||||
recentStickers={[]}
|
||||
onClickAddPack={() => console.log('onClickAddPack')}
|
||||
onPickSticker={(packId, stickerId) =>
|
||||
console.log('onPickSticker', { packId, stickerId })
|
||||
}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
282
ts/components/stickers/StickerPicker.tsx
Normal file
282
ts/components/stickers/StickerPicker.tsx
Normal file
|
@ -0,0 +1,282 @@
|
|||
/* tslint:disable:max-func-body-length */
|
||||
/* tslint:disable:cyclomatic-complexity */
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onClickAddPack: () => unknown;
|
||||
readonly onPickSticker: (packId: string, stickerId: number) => unknown;
|
||||
readonly packs: ReadonlyArray<StickerPackType>;
|
||||
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||
readonly showPickerHint?: boolean;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
||||
function useTabs<T>(tabs: ReadonlyArray<T>, initialTab = tabs[0]) {
|
||||
const [tab, setTab] = React.useState(initialTab);
|
||||
const handlers = React.useMemo(
|
||||
() =>
|
||||
tabs.map(t => () => {
|
||||
setTab(t);
|
||||
}),
|
||||
tabs
|
||||
);
|
||||
|
||||
return [tab, handlers] as [T, ReadonlyArray<() => void>];
|
||||
}
|
||||
|
||||
const PACKS_PAGE_SIZE = 7;
|
||||
const PACK_ICON_WIDTH = 32;
|
||||
const PACK_PAGE_WIDTH = PACKS_PAGE_SIZE * PACK_ICON_WIDTH;
|
||||
|
||||
function getPacksPageOffset(page: number, packs: number): number {
|
||||
if (page === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isLastPacksPage(page, packs)) {
|
||||
return (
|
||||
PACK_PAGE_WIDTH * (Math.floor(packs / PACKS_PAGE_SIZE) - 1) +
|
||||
(packs % PACKS_PAGE_SIZE - 1) * PACK_ICON_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
return page * PACK_ICON_WIDTH * PACKS_PAGE_SIZE;
|
||||
}
|
||||
|
||||
function isLastPacksPage(page: number, packs: number): boolean {
|
||||
return page === Math.floor(packs / PACKS_PAGE_SIZE);
|
||||
}
|
||||
|
||||
export const StickerPicker = React.memo(
|
||||
React.forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
i18n,
|
||||
packs,
|
||||
recentStickers,
|
||||
onClickAddPack,
|
||||
onPickSticker,
|
||||
showPickerHint,
|
||||
style,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const tabIds = React.useMemo(
|
||||
() => ['recents', ...packs.map(({ id }) => id)],
|
||||
packs
|
||||
);
|
||||
const [currentTab, [recentsHandler, ...packsHandlers]] = useTabs(
|
||||
tabIds,
|
||||
// If there are no recent stickers, default to the first sticker pack, unless there are no sticker packs.
|
||||
tabIds[recentStickers.length > 0 ? 0 : Math.min(1, tabIds.length)]
|
||||
);
|
||||
const selectedPack = packs.find(({ id }) => id === currentTab);
|
||||
const {
|
||||
stickers = recentStickers,
|
||||
title: packTitle = 'Recent Stickers',
|
||||
} =
|
||||
selectedPack || {};
|
||||
|
||||
const [packsPage, setPacksPage] = React.useState(0);
|
||||
const onClickPrevPackPage = React.useCallback(
|
||||
() => {
|
||||
setPacksPage(i => i - 1);
|
||||
},
|
||||
[setPacksPage]
|
||||
);
|
||||
const onClickNextPackPage = React.useCallback(
|
||||
() => {
|
||||
setPacksPage(i => i + 1);
|
||||
},
|
||||
[setPacksPage]
|
||||
);
|
||||
|
||||
const isEmpty = stickers.length === 0;
|
||||
const downloadError =
|
||||
selectedPack &&
|
||||
selectedPack.status === 'error' &&
|
||||
selectedPack.stickerCount !== selectedPack.stickers.length;
|
||||
const pendingCount =
|
||||
selectedPack && selectedPack.status === 'pending'
|
||||
? selectedPack.stickerCount - stickers.length
|
||||
: 0;
|
||||
|
||||
const hasPacks = packs.length > 0;
|
||||
const isRecents = hasPacks && currentTab === 'recents';
|
||||
const showPendingText = pendingCount > 0;
|
||||
const showDownlaodErrorText = downloadError;
|
||||
const showEmptyText = !downloadError && isEmpty;
|
||||
const showText =
|
||||
showPendingText || showDownlaodErrorText || showEmptyText;
|
||||
const showLongText = showPickerHint;
|
||||
|
||||
return (
|
||||
<div className="module-sticker-picker" ref={ref} style={style}>
|
||||
<div className="module-sticker-picker__header">
|
||||
<div className="module-sticker-picker__header__packs">
|
||||
<div
|
||||
className="module-sticker-picker__header__packs__slider"
|
||||
style={{
|
||||
transform: `translateX(-${getPacksPageOffset(
|
||||
packsPage,
|
||||
packs.length
|
||||
)}px)`,
|
||||
}}
|
||||
>
|
||||
{hasPacks ? (
|
||||
<button
|
||||
onClick={recentsHandler}
|
||||
className={classNames({
|
||||
'module-sticker-picker__header__button': true,
|
||||
'module-sticker-picker__header__button--recents': true,
|
||||
'module-sticker-picker__header__button--selected':
|
||||
currentTab === 'recents',
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
{packs.map((pack, i) => (
|
||||
<button
|
||||
key={pack.id}
|
||||
onClick={packsHandlers[i]}
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
{
|
||||
'module-sticker-picker__header__button--selected':
|
||||
currentTab === pack.id,
|
||||
'module-sticker-picker__header__button--error':
|
||||
pack.status === 'error',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-picker__header__button__image"
|
||||
src={pack.cover.url}
|
||||
alt={pack.title}
|
||||
title={pack.title}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{packsPage > 0 ? (
|
||||
<button
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
'module-sticker-picker__header__button--prev-page'
|
||||
)}
|
||||
onClick={onClickPrevPackPage}
|
||||
/>
|
||||
) : null}
|
||||
{!isLastPacksPage(packsPage, packs.length) ? (
|
||||
<button
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
'module-sticker-picker__header__button--next-page'
|
||||
)}
|
||||
onClick={onClickNextPackPage}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
className={classNames(
|
||||
'module-sticker-picker__header__button',
|
||||
'module-sticker-picker__header__button--add-pack',
|
||||
{
|
||||
'module-sticker-picker__header__button--hint': showPickerHint,
|
||||
}
|
||||
)}
|
||||
onClick={onClickAddPack}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames('module-sticker-picker__body', {
|
||||
'module-sticker-picker__body--empty': isEmpty,
|
||||
})}
|
||||
>
|
||||
{showPickerHint ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-sticker-picker__body__text',
|
||||
'module-sticker-picker__body__text--hint',
|
||||
{
|
||||
'module-sticker-picker__body__text--pin': showEmptyText,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{i18n('stickers--StickerPicker--Hint')}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasPacks ? (
|
||||
<div className="module-sticker-picker__body__text">
|
||||
{i18n('stickers--StickerPicker--NoPacks')}
|
||||
</div>
|
||||
) : null}
|
||||
{pendingCount > 0 ? (
|
||||
<div className="module-sticker-picker__body__text">
|
||||
{i18n('stickers--StickerPicker--DownloadPending')}
|
||||
</div>
|
||||
) : null}
|
||||
{downloadError ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-sticker-picker__body__text',
|
||||
'module-sticker-picker__body__text--error'
|
||||
)}
|
||||
>
|
||||
{stickers.length > 0
|
||||
? i18n('stickers--StickerPicker--DownloadError')
|
||||
: i18n('stickers--StickerPicker--Empty')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasPacks && showEmptyText ? (
|
||||
<div
|
||||
className={classNames('module-sticker-picker__body__text', {
|
||||
'module-sticker-picker__body__text--error': !isRecents,
|
||||
})}
|
||||
>
|
||||
{isRecents
|
||||
? i18n('stickers--StickerPicker--NoRecents')
|
||||
: i18n('stickers--StickerPicker--Empty')}
|
||||
</div>
|
||||
) : null}
|
||||
{!isEmpty ? (
|
||||
<div
|
||||
className={classNames('module-sticker-picker__body__content', {
|
||||
'module-sticker-picker__body__content--under-text': showText,
|
||||
'module-sticker-picker__body__content--under-long-text': showLongText,
|
||||
})}
|
||||
>
|
||||
{stickers.map(({ packId, id, url }) => (
|
||||
<button
|
||||
key={`${packId}-${id}`}
|
||||
className="module-sticker-picker__body__cell"
|
||||
onClick={() => onPickSticker(packId, id)}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-picker__body__cell__image"
|
||||
src={url}
|
||||
alt={packTitle}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{Array(pendingCount)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="module-sticker-picker__body__cell__placeholder"
|
||||
role="presentation"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
29
ts/components/stickers/StickerPreviewModal.md
Normal file
29
ts/components/stickers/StickerPreviewModal.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
#### Not yet installed
|
||||
|
||||
```jsx
|
||||
const abeSticker = { url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||
|
||||
const pack = {
|
||||
id: 'foo',
|
||||
cover: abeSticker,
|
||||
title: 'Foo',
|
||||
isBlessed: true,
|
||||
author: 'Foo McBarrington',
|
||||
status: 'advertised',
|
||||
stickers: Array(101)
|
||||
.fill(0)
|
||||
.map((n, id) => ({ ...abeSticker, id })),
|
||||
};
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StickerPreviewModal
|
||||
onClose={() => console.log('onClose')}
|
||||
installStickerPack={(...args) => console.log('installStickerPack', ...args)}
|
||||
uninstallStickerPack={(...args) =>
|
||||
console.log('uninstallStickerPack', ...args)
|
||||
}
|
||||
i18n={util.i18n}
|
||||
pack={pack}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
165
ts/components/stickers/StickerPreviewModal.tsx
Normal file
165
ts/components/stickers/StickerPreviewModal.tsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { StickerPackInstallButton } from './StickerPackInstallButton';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { StickerPackType } from '../../state/ducks/stickers';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly onClose: () => unknown;
|
||||
readonly installStickerPack: (packId: string, packKey: string) => unknown;
|
||||
readonly uninstallStickerPack: (packId: string, packKey: string) => unknown;
|
||||
readonly pack: StickerPackType;
|
||||
readonly i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
||||
function focusRef(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export const StickerPreviewModal = React.memo(
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
({
|
||||
onClose,
|
||||
pack,
|
||||
i18n,
|
||||
installStickerPack,
|
||||
uninstallStickerPack,
|
||||
}: Props) => {
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
const [confirmingUninstall, setConfirmingUninstall] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
setRoot(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
setRoot(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isInstalled = pack.status === 'installed';
|
||||
const handleToggleInstall = React.useCallback(
|
||||
() => {
|
||||
if (isInstalled) {
|
||||
setConfirmingUninstall(true);
|
||||
} else {
|
||||
installStickerPack(pack.id, pack.key);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isInstalled, pack, setConfirmingUninstall, installStickerPack, onClose]
|
||||
);
|
||||
|
||||
const handleUninstall = React.useCallback(
|
||||
() => {
|
||||
uninstallStickerPack(pack.id, pack.key);
|
||||
setConfirmingUninstall(false);
|
||||
// onClose is called by the confirmation modal
|
||||
},
|
||||
[uninstallStickerPack, setConfirmingUninstall, pack]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const handler = ({ key }: KeyboardEvent) => {
|
||||
if (key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keyup', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handler);
|
||||
};
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
const handleClickToClose = React.useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
return root
|
||||
? createPortal(
|
||||
<div
|
||||
role="button"
|
||||
className="module-sticker-manager__preview-modal__overlay"
|
||||
onClick={handleClickToClose}
|
||||
>
|
||||
{confirmingUninstall ? (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
||||
onNegative={handleUninstall}
|
||||
>
|
||||
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||
</ConfirmationDialog>
|
||||
) : (
|
||||
<div className="module-sticker-manager__preview-modal__container">
|
||||
<header className="module-sticker-manager__preview-modal__container__header">
|
||||
<h2 className="module-sticker-manager__preview-modal__container__header__text">
|
||||
{i18n('stickers--StickerPreview--Title')}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="module-sticker-manager__preview-modal__container__header__close-button"
|
||||
/>
|
||||
</header>
|
||||
<div className="module-sticker-manager__preview-modal__container__sticker-grid">
|
||||
{pack.stickers.map(({ id, url }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="module-sticker-manager__preview-modal__container__sticker-grid__cell"
|
||||
>
|
||||
<img
|
||||
className="module-sticker-manager__preview-modal__container__sticker-grid__cell__image"
|
||||
src={url}
|
||||
alt={pack.title}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<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">
|
||||
<StickerPackInstallButton
|
||||
ref={focusRef}
|
||||
installed={isInstalled}
|
||||
i18n={i18n}
|
||||
onClick={handleToggleInstall}
|
||||
blue={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
: null;
|
||||
}
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue