Stickers: Add 'Copy Link' option (#2502)

This commit is contained in:
Alexander Zinchuk 2023-02-08 00:47:49 +01:00
parent 0f1a523c2f
commit 3f7392699a
5 changed files with 83 additions and 40 deletions

View File

@ -1,25 +1,27 @@
import React, { import React, {
memo, useCallback, useEffect, useRef, memo, useCallback, useEffect, useMemo, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import type { ApiSticker, ApiStickerSet } from '../../api/types'; import type { ApiSticker, ApiStickerSet } from '../../api/types';
import { EMOJI_SIZE_MODAL, STICKER_SIZE_MODAL } from '../../config'; import { EMOJI_SIZE_MODAL, STICKER_SIZE_MODAL, TME_LINK_PREFIX } from '../../config';
import { import {
selectCanScheduleUntilOnline, selectCanScheduleUntilOnline,
selectChat, selectChat,
selectCurrentMessageList, selectCurrentMessageList,
selectIsChatWithSelf, selectIsCurrentUserPremium, selectIsChatWithSelf, selectIsCurrentUserPremium,
selectIsSetPremium,
selectShouldSchedule, selectShouldSchedule,
selectStickerSet, selectStickerSet,
} from '../../global/selectors'; } from '../../global/selectors';
import renderText from './helpers/renderText';
import { copyTextToClipboard } from '../../util/clipboard';
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import renderText from './helpers/renderText'; import useAppLayout from '../../hooks/useAppLayout';
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
import useSchedule from '../../hooks/useSchedule'; import useSchedule from '../../hooks/useSchedule';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
@ -27,6 +29,8 @@ import Modal from '../ui/Modal';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import StickerButton from './StickerButton'; import StickerButton from './StickerButton';
import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem';
import './StickerSetModal.scss'; import './StickerSetModal.scss';
@ -43,7 +47,6 @@ type StateProps = {
canScheduleUntilOnline?: boolean; canScheduleUntilOnline?: boolean;
shouldSchedule?: boolean; shouldSchedule?: boolean;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isSetPremium?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
}; };
@ -58,7 +61,6 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
canScheduleUntilOnline, canScheduleUntilOnline,
shouldSchedule, shouldSchedule,
isSavedMessages, isSavedMessages,
isSetPremium,
isCurrentUserPremium, isCurrentUserPremium,
onClose, onClose,
}) => { }) => {
@ -66,7 +68,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
loadStickers, loadStickers,
toggleStickerSet, toggleStickerSet,
sendMessage, sendMessage,
openPremiumModal, showNotification,
} = getActions(); } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -76,12 +78,13 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
const lang = useLang(); const lang = useLang();
const { isMobile } = useAppLayout();
const prevStickerSet = usePrevious(stickerSet); const prevStickerSet = usePrevious(stickerSet);
const renderingStickerSet = stickerSet || prevStickerSet; const renderingStickerSet = stickerSet || prevStickerSet;
const isAdded = Boolean(!renderingStickerSet?.isArchived && renderingStickerSet?.installedDate); const isAdded = Boolean(!renderingStickerSet?.isArchived && renderingStickerSet?.installedDate);
const isEmoji = renderingStickerSet?.isEmoji; const isEmoji = renderingStickerSet?.isEmoji;
const isButtonLocked = !isAdded && isSetPremium && !isCurrentUserPremium;
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline); const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
@ -118,20 +121,24 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
const handleButtonClick = useCallback(() => { const handleButtonClick = useCallback(() => {
if (renderingStickerSet) { if (renderingStickerSet) {
if (isButtonLocked) {
openPremiumModal({ initialSection: 'animated_emoji' });
return;
}
toggleStickerSet({ stickerSetId: renderingStickerSet.id }); toggleStickerSet({ stickerSetId: renderingStickerSet.id });
onClose(); onClose();
} }
}, [isButtonLocked, onClose, openPremiumModal, renderingStickerSet, toggleStickerSet]); }, [onClose, renderingStickerSet, toggleStickerSet]);
const handleCopyLink = useCallback(() => {
if (!renderingStickerSet) return;
const { shortName } = renderingStickerSet;
const suffix = isEmoji ? 'addemoji' : 'addstickers';
const url = `${TME_LINK_PREFIX}${suffix}/${shortName}`;
copyTextToClipboard(url);
showNotification({
message: lang('LinkCopied'),
});
}, [isEmoji, lang, renderingStickerSet, showNotification]);
const renderButtonText = () => { const renderButtonText = () => {
if (!renderingStickerSet) return lang('Loading'); if (!renderingStickerSet) return lang('Loading');
if (isButtonLocked) {
return lang('EmojiInput.UnlockPack', renderingStickerSet.title);
}
const suffix = isEmoji ? 'Emoji' : 'Sticker'; const suffix = isEmoji ? 'Emoji' : 'Sticker';
@ -142,14 +149,48 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
); );
}; };
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
return ({ onTrigger, isOpen: isMenuOpen }) => (
<Button
round
ripple={!isMobile}
size="smaller"
color="translucent"
className={isMenuOpen ? 'active' : ''}
onClick={onTrigger}
ariaLabel="More actions"
>
<i className="icon-more" />
</Button>
);
}, [isMobile]);
function renderHeader() {
return (
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
<Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={onClose}>
<i className="icon-close" />
</Button>
<div className="modal-title">
{renderingStickerSet ? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}
</div>
<DropdownMenu
className="stickers-more-menu"
trigger={MoreMenuButton}
positionX="right"
>
<MenuItem icon="copy" onClick={handleCopyLink}>{lang('StickersCopy')}</MenuItem>
</DropdownMenu>
</div>
);
}
return ( return (
<Modal <Modal
className="StickerSetModal" className="StickerSetModal"
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
hasCloseButton header={renderHeader()}
title={renderingStickerSet
? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}
> >
{renderingStickerSet?.stickers ? ( {renderingStickerSet?.stickers ? (
<> <>
@ -175,8 +216,6 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
size="smaller" size="smaller"
fluid fluid
color={isAdded ? 'danger' : 'primary'} color={isAdded ? 'danger' : 'primary'}
isShiny={isButtonLocked}
withPremiumGradient={isButtonLocked}
onClick={handleButtonClick} onClick={handleButtonClick}
> >
{renderButtonText()} {renderButtonText()}
@ -206,7 +245,6 @@ export default memo(withGlobal<OwnProps>(
: stickerSetShortName ? { shortName: stickerSetShortName } : undefined; : stickerSetShortName ? { shortName: stickerSetShortName } : undefined;
const stickerSet = stickerSetInfo ? selectStickerSet(global, stickerSetInfo) : undefined; const stickerSet = stickerSetInfo ? selectStickerSet(global, stickerSetInfo) : undefined;
const isSetPremium = stickerSet && selectIsSetPremium(stickerSet);
return { return {
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId), canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
@ -215,7 +253,6 @@ export default memo(withGlobal<OwnProps>(
shouldSchedule: selectShouldSchedule(global), shouldSchedule: selectShouldSchedule(global),
stickerSet, stickerSet,
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
isSetPremium,
}; };
}, },
)(StickerSetModal)); )(StickerSetModal));

View File

@ -3,6 +3,8 @@
.modal-header { .modal-header {
border-bottom: 1px solid var(--color-dividers); border-bottom: 1px solid var(--color-dividers);
padding: 0.5rem; padding: 0.5rem;
transition: 0.25s ease-in-out background-color;
} }
.modal-dialog { .modal-dialog {
@ -21,6 +23,8 @@
padding: 0; padding: 0;
border-bottom-right-radius: var(--border-radius-default); border-bottom-right-radius: var(--border-radius-default);
border-bottom-left-radius: var(--border-radius-default); border-bottom-left-radius: var(--border-radius-default);
transition: 0.25s ease-in-out background-color;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@ -84,7 +88,7 @@
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
transition: opacity 0.2s ease-in-out; transition: opacity 0.25s ease-in-out;
} }
.hide { .hide {
@ -106,8 +110,12 @@
position: absolute; position: absolute;
bottom: 0; bottom: 0;
border-radius: 0; border-radius: 0;
z-index: 1;
transform: translateY(100%); transform: translateY(100%);
transition: 0.25s ease-in-out transform; transition-property: background-color, color, transform;
transition-duration: 0.25s;
transition-timing-function: ease-in-out;
&.visible { &.visible {
transform: translateY(0); transform: translateY(0);

View File

@ -341,7 +341,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
isBackButtonVisible && styles.stateBack, isBackButtonVisible && styles.stateBack,
); );
const header = useMemo(() => { function renderHeader() {
return ( return (
<div className="modal-header" style={`background-color: ${headerColor}`}> <div className="modal-header" style={`background-color: ${headerColor}`}>
<Button <Button
@ -380,10 +380,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
</DropdownMenu> </DropdownMenu>
</div> </div>
); );
}, [ }
lang, handleBackClick, bot, MoreMenuButton, chat, openBotChat, handleRefreshClick, attachBot,
handleToggleClick, handleSettingsButtonClick, isBackButtonVisible, headerColor, backButtonClassName,
]);
const prevMainButtonColor = usePrevious(mainButton?.color, true); const prevMainButtonColor = usePrevious(mainButton?.color, true);
const prevMainButtonTextColor = usePrevious(mainButton?.textColor, true); const prevMainButtonTextColor = usePrevious(mainButton?.textColor, true);
@ -430,8 +427,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
className={styles.root} className={styles.root}
isOpen={isOpen} isOpen={isOpen}
onClose={handleClose} onClose={handleClose}
header={header} header={renderHeader()}
hasCloseButton
style={`background-color: ${backgroundColor}`} style={`background-color: ${backgroundColor}`}
> >
<Spinner className={buildClassName(styles.loadingSpinner, isLoaded && styles.hide)} /> <Spinner className={buildClassName(styles.loadingSpinner, isLoaded && styles.hide)} />

View File

@ -67,7 +67,7 @@
} }
} }
body.has-open-dialog &:not(.CustomSendMenu):not(.web-app-more-menu):not(.attachment-modal-more-menu) .bubble { body.has-open-dialog &:not(.CustomSendMenu):not(.web-app-more-menu):not(.attachment-modal-more-menu):not(.stickers-more-menu) .bubble {
transition: none !important; transition: none !important;
} }

View File

@ -337,16 +337,16 @@ function scheduleUpdate(componentInstance: ComponentInstance) {
export function renderComponent(componentInstance: ComponentInstance) { export function renderComponent(componentInstance: ComponentInstance) {
idsToExcludeFromUpdate.add(componentInstance.id); idsToExcludeFromUpdate.add(componentInstance.id);
renderingInstance = componentInstance;
componentInstance.hooks.state.cursor = 0;
componentInstance.hooks.effects.cursor = 0;
componentInstance.hooks.memos.cursor = 0;
componentInstance.hooks.refs.cursor = 0;
const { Component, props } = componentInstance; const { Component, props } = componentInstance;
let newRenderedValue; let newRenderedValue;
try { try {
renderingInstance = componentInstance;
componentInstance.hooks.state.cursor = 0;
componentInstance.hooks.effects.cursor = 0;
componentInstance.hooks.memos.cursor = 0;
componentInstance.hooks.refs.cursor = 0;
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_startAt: number | undefined; let DEBUG_startAt: number | undefined;
if (DEBUG) { if (DEBUG) {
@ -388,6 +388,8 @@ export function renderComponent(componentInstance: ComponentInstance) {
} }
} }
} catch (err: any) { } catch (err: any) {
// eslint-disable-next-line no-console
console.error(`[Teact] Error while rendering component ${componentInstance.name}`);
handleError(err); handleError(err);
newRenderedValue = componentInstance.renderedValue; newRenderedValue = componentInstance.renderedValue;