Symbol Menu, Reaction Picker, Status Picker: Fixes and redesign (#2945)
This commit is contained in:
parent
649fb46777
commit
9567e6ca38
@ -1,5 +1,14 @@
|
||||
.root {
|
||||
--emoji-size: 2.5rem;
|
||||
--emoji-size: 2.25rem;
|
||||
|
||||
height: 100%;
|
||||
max-height: calc(100 * var(--vh));
|
||||
|
||||
:global(.symbol-set-container) {
|
||||
--symbol-set-gap-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.activated {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { RefObject } from 'react';
|
||||
import React, {
|
||||
useState, useEffect, memo, useRef, useMemo, useCallback,
|
||||
useEffect, memo, useRef, useMemo, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getGlobal, withGlobal } from '../../global';
|
||||
|
||||
@ -21,9 +20,9 @@ import {
|
||||
STICKER_SIZE_PICKER_HEADER,
|
||||
TOP_SYMBOL_SET_ID,
|
||||
} from '../../config';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import animateScroll from '../../util/animateScroll';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
import { pickTruthy, unique } from '../../util/iteratees';
|
||||
@ -39,6 +38,7 @@ import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import { useStickerPickerObservers } from './hooks/useStickerPickerObservers';
|
||||
import useScrolledState from '../../hooks/useScrolledState';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
import Button from '../ui/Button';
|
||||
@ -46,12 +46,10 @@ import StickerButton from './StickerButton';
|
||||
import StickerSet from './StickerSet';
|
||||
import StickerSetCover from '../middle/composer/StickerSetCover';
|
||||
|
||||
import '../middle/composer/StickerPicker.scss';
|
||||
import pickerStyles from '../middle/composer/StickerPicker.module.scss';
|
||||
import styles from './CustomEmojiPicker.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
scrollContainerRef?: RefObject<HTMLDivElement>;
|
||||
scrollHeaderRef?: RefObject<HTMLDivElement>;
|
||||
chatId?: string;
|
||||
className?: string;
|
||||
isHidden?: boolean;
|
||||
@ -63,6 +61,7 @@ type OwnProps = {
|
||||
selectedReactionIds?: string[];
|
||||
isStatusPicker?: boolean;
|
||||
isReactionPicker?: boolean;
|
||||
isTranslucent?: boolean;
|
||||
onContextMenuOpen?: NoneToVoidFunction;
|
||||
onContextMenuClose?: NoneToVoidFunction;
|
||||
onContextMenuClick?: NoneToVoidFunction;
|
||||
@ -85,8 +84,8 @@ type StateProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const SMOOTH_SCROLL_DISTANCE = 100;
|
||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||
const HEADER_BUTTON_WIDTH = 2.5 * REM; // px (including margin)
|
||||
|
||||
const DEFAULT_ID_PREFIX = 'custom-emoji-set';
|
||||
const TOP_REACTIONS_COUNT = 16;
|
||||
const RECENT_REACTIONS_COUNT = 32;
|
||||
@ -100,8 +99,6 @@ const STICKER_SET_IDS_WITH_COVER = new Set([
|
||||
]);
|
||||
|
||||
const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
scrollContainerRef,
|
||||
scrollHeaderRef,
|
||||
className,
|
||||
isHidden,
|
||||
loadAndPlay,
|
||||
@ -119,6 +116,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
canAnimate,
|
||||
isReactionPicker,
|
||||
isStatusPicker,
|
||||
isTranslucent,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium,
|
||||
withDefaultTopicIcons,
|
||||
@ -131,22 +129,19 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
onContextMenuClick,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let headerRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||
if (scrollContainerRef) {
|
||||
containerRef = scrollContainerRef;
|
||||
}
|
||||
if (scrollHeaderRef) {
|
||||
headerRef = scrollHeaderRef;
|
||||
}
|
||||
|
||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||
const { isMobile } = useAppLayout();
|
||||
const {
|
||||
handleScroll: handleContentScroll,
|
||||
isAtBeginning: shouldHideTopBorder,
|
||||
} = useScrolledState();
|
||||
|
||||
const recentCustomEmojis = useMemo(() => {
|
||||
return isStatusPicker
|
||||
@ -155,11 +150,13 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]);
|
||||
|
||||
const {
|
||||
activeSetIndex,
|
||||
observeIntersectionForSet,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
observeIntersectionForCovers,
|
||||
} = useStickerPickerObservers(containerRef, headerRef, idPrefix, setActiveSetIndex, isHidden);
|
||||
selectStickerSet,
|
||||
} = useStickerPickerObservers(containerRef, headerRef, idPrefix, isHidden);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -260,7 +257,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
const canRenderContent = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||
const shouldRenderContent = areAddedLoaded && canRenderContent && !noPopulatedSets;
|
||||
|
||||
useHorizontalScroll(headerRef, !(isMobile && shouldRenderContent));
|
||||
useHorizontalScroll(headerRef, isMobile || !shouldRenderContent);
|
||||
|
||||
// Scroll container and header when active set changes
|
||||
useEffect(() => {
|
||||
@ -278,12 +275,6 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
animateHorizontalScroll(header, newLeft);
|
||||
}, [areAddedLoaded, activeSetIndex]);
|
||||
|
||||
const selectStickerSet = useCallback((index: number) => {
|
||||
setActiveSetIndex(index);
|
||||
const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!;
|
||||
animateScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
||||
}, [idPrefix]);
|
||||
|
||||
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||
onCustomEmojiSelect(emoji);
|
||||
}, [onCustomEmojiSelect]);
|
||||
@ -295,8 +286,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
||||
const firstSticker = stickerSet.stickers?.[0];
|
||||
const buttonClassName = buildClassName(
|
||||
'symbol-set-button sticker-set-button',
|
||||
index === activeSetIndex && 'activated',
|
||||
pickerStyles.stickerCover,
|
||||
index === activeSetIndex && styles.activated,
|
||||
);
|
||||
|
||||
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
||||
@ -307,6 +298,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (STICKER_SET_IDS_WITH_COVER.has(stickerSet.id) || stickerSet.hasThumbnail || !firstSticker) {
|
||||
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === POPULAR_SYMBOL_SET_ID;
|
||||
const isFaded = FADED_BUTTON_SET_IDS.has(stickerSet.id);
|
||||
return (
|
||||
<Button
|
||||
@ -317,9 +309,9 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
faded={isFaded}
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => selectStickerSet(index)}
|
||||
onClick={() => selectStickerSet(isRecent ? 0 : index)}
|
||||
>
|
||||
{(stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === POPULAR_SYMBOL_SET_ID) ? (
|
||||
{isRecent ? (
|
||||
<i className="icon icon-recent" />
|
||||
) : (
|
||||
<StickerSetCover
|
||||
@ -345,6 +337,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
noContextMenu
|
||||
isCurrentUserPremium
|
||||
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
||||
withTranslucentThumb={isTranslucent}
|
||||
onClick={selectStickerSet}
|
||||
clickArg={index}
|
||||
/>
|
||||
@ -357,7 +350,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<div className={fullClassName}>
|
||||
{noPopulatedSets ? (
|
||||
<div className="picker-disabled">{lang('NoStickers')}</div>
|
||||
<div className={pickerStyles.pickerDisabled}>{lang('NoStickers')}</div>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
@ -365,11 +358,23 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const headerClassName = buildClassName(
|
||||
pickerStyles.header,
|
||||
'no-selection no-scrollbar',
|
||||
!shouldHideTopBorder && pickerStyles.headerWithBorder,
|
||||
);
|
||||
const listClassName = buildClassName(
|
||||
pickerStyles.main,
|
||||
pickerStyles.main_customEmoji,
|
||||
'no-selection',
|
||||
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={fullClassName}>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className="StickerPicker-header no-selection no-scrollbar"
|
||||
className={headerClassName}
|
||||
>
|
||||
<div className="shared-canvas-container">
|
||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||
@ -379,7 +384,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
onScroll={handleContentScroll}
|
||||
className={listClassName}
|
||||
>
|
||||
{allSets.map((stickerSet, i) => {
|
||||
const shouldHideHeader = stickerSet.id === TOP_SYMBOL_SET_ID
|
||||
@ -405,6 +411,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
selectedReactionIds={selectedReactionIds}
|
||||
availableReactions={availableReactions}
|
||||
isTranslucent={isTranslucent}
|
||||
onReactionSelect={handleReactionSelect}
|
||||
onStickerSelect={handleEmojiSelect}
|
||||
onContextMenuOpen={onContextMenuOpen}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.root {
|
||||
--custom-emoji-size: 2.5rem;
|
||||
--custom-emoji-size: 2.25rem;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
display: inline-block;
|
||||
|
||||
@ -49,7 +49,7 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
), [availableReactions, reaction]);
|
||||
const thumbDataUri = availableReaction?.staticIcon?.thumbnail?.dataUri;
|
||||
const animationId = availableReaction?.selectAnimation?.id;
|
||||
const coords = useCoordsInSharedCanvas(ref, sharedCanvasHqRef);
|
||||
const coords = useCoordsInSharedCanvas(ref, sharedCanvasRef);
|
||||
const mediaData = useMedia(
|
||||
availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation) : undefined,
|
||||
!animationId,
|
||||
@ -82,15 +82,18 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||
withTranslucentThumb
|
||||
/>
|
||||
) : (
|
||||
<AnimatedIconWithPreview
|
||||
tgsUrl={mediaData}
|
||||
thumbDataUri={thumbDataUri}
|
||||
play={loadAndPlay}
|
||||
noLoop={false}
|
||||
size={EMOJI_SIZE_PICKER}
|
||||
isLowPriority
|
||||
className={transitionClassNames}
|
||||
sharedCanvas={sharedCanvasHqRef!.current || undefined}
|
||||
sharedCanvas={sharedCanvasRef!.current || undefined}
|
||||
sharedCanvasCoords={coords}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
.StickerButton {
|
||||
--custom-emoji-size: 2.5rem;
|
||||
--custom-emoji-size: 2.25rem;
|
||||
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
||||
|
||||
display: inline-block;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0.5rem;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: var(--border-radius-messages-small);
|
||||
background: transparent no-repeat center;
|
||||
background-size: contain;
|
||||
transition: background-color 0.15s ease, opacity 0.3s ease !important;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.custom-emoji {
|
||||
color: var(--color-primary);
|
||||
@ -30,6 +30,8 @@
|
||||
&.set-expand {
|
||||
padding: 0;
|
||||
vertical-align: bottom;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.sticker-locked {
|
||||
@ -100,8 +102,7 @@
|
||||
}
|
||||
|
||||
.AnimatedSticker,
|
||||
img,
|
||||
video {
|
||||
.sticker-media {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -109,8 +110,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
.sticker-media {
|
||||
object-fit: contain;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
@ -118,8 +118,8 @@
|
||||
|
||||
.sticker-remove-button {
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: -0.5rem;
|
||||
top: -0.125rem;
|
||||
right: -0.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0.125rem;
|
||||
@ -140,4 +140,8 @@
|
||||
.bubble {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.SymbolMenu & .bubble {
|
||||
--offset-y: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ type OwnProps<T> = {
|
||||
isSelected?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||
withTranslucentThumb?: boolean;
|
||||
observeIntersection: ObserveFn;
|
||||
observeIntersectionForShowing?: ObserveFn;
|
||||
noShowPremium?: boolean;
|
||||
@ -75,6 +76,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
isCurrentUserPremium,
|
||||
noShowPremium,
|
||||
sharedCanvasRef,
|
||||
withTranslucentThumb,
|
||||
onClick,
|
||||
clickArg,
|
||||
onFaveClick,
|
||||
@ -125,7 +127,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
[isStatusPicker],
|
||||
);
|
||||
|
||||
const getLayout = () => ({ withPortal: isStatusPicker });
|
||||
const getLayout = () => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true });
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
@ -303,6 +305,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
noPlay={!shouldPlay}
|
||||
withSharedAnimation
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
withTranslucentThumb={withTranslucentThumb}
|
||||
customColor={customColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -51,6 +51,7 @@ type OwnProps = {
|
||||
selectedReactionIds?: string[];
|
||||
withDefaultTopicIcon?: boolean;
|
||||
withDefaultStatusIcon?: boolean;
|
||||
isTranslucent?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForPlayingItems: ObserveFn;
|
||||
observeIntersectionForShowingItems: ObserveFn;
|
||||
@ -87,6 +88,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
withDefaultTopicIcon,
|
||||
selectedReactionIds,
|
||||
withDefaultStatusIcon,
|
||||
isTranslucent,
|
||||
observeIntersection,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
@ -129,8 +131,9 @@ const StickerSet: FC<OwnProps> = ({
|
||||
// `isNearActive` is set in advance during animation, but it is not reliable for short sets
|
||||
const shouldRender = isNearActive || isIntersecting;
|
||||
|
||||
const stickerMarginPx = isMobile ? 8 : 16;
|
||||
const stickerMarginPx = isMobile ? 8 : 4;
|
||||
const emojiMarginPx = isMobile ? 8 : 10;
|
||||
const emojiVerticalMarginPx = isMobile ? 8 : 4;
|
||||
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
||||
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
|
||||
const isPopular = stickerSet.id === POPULAR_SYMBOL_SET_ID;
|
||||
@ -186,13 +189,14 @@ const StickerSet: FC<OwnProps> = ({
|
||||
|
||||
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
|
||||
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
||||
const verticalMargin = isEmoji ? emojiVerticalMarginPx : stickerMarginPx;
|
||||
|
||||
const calculateItemsPerRow = useCallback((width: number) => {
|
||||
if (!width) {
|
||||
return getItemsPerRowFallback(windowWidth);
|
||||
}
|
||||
|
||||
return Math.floor(width / (itemSize + margin));
|
||||
return Math.floor((width + margin) / (itemSize + margin));
|
||||
}, [itemSize, margin, windowWidth]);
|
||||
|
||||
const handleResize = useCallback((entry: ResizeObserverEntry) => {
|
||||
@ -225,12 +229,15 @@ const StickerSet: FC<OwnProps> = ({
|
||||
const itemsBeforeCutout = itemsPerRow * 3 - 1;
|
||||
const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count;
|
||||
|
||||
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, totalItemsCount) / itemsPerRow) * (itemSize + margin);
|
||||
const height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * (itemSize + margin);
|
||||
const itemHeight = itemSize + verticalMargin;
|
||||
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, totalItemsCount) / itemsPerRow)
|
||||
* itemHeight - verticalMargin;
|
||||
const height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * itemHeight - verticalMargin;
|
||||
|
||||
const favoriteStickerIdsSet = useMemo(() => (
|
||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||
), [favoriteStickers]);
|
||||
const withAddSetButton = !shouldHideHeader && !isRecent && isEmoji && !isInstalled && !isPopular;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -243,14 +250,17 @@ const StickerSet: FC<OwnProps> = ({
|
||||
>
|
||||
{!shouldHideHeader && (
|
||||
<div className="symbol-set-header">
|
||||
<p className="symbol-set-name">
|
||||
<p className={buildClassName('symbol-set-name', withAddSetButton && 'symbol-set-name-external')}>
|
||||
{isLocked && <i className="symbol-set-locked-icon icon icon-lock-badge" />}
|
||||
{stickerSet.title}
|
||||
{withAddSetButton && Boolean(stickerSet.stickers) && (
|
||||
<span className="symbol-set-amount">{lang('Stickers', stickerSet.stickers.length, 'i')}</span>
|
||||
)}
|
||||
</p>
|
||||
{isRecent && (
|
||||
<i className="symbol-set-remove icon icon-close" onClick={openConfirmModal} />
|
||||
)}
|
||||
{!isRecent && isEmoji && !isInstalled && !isPopular && (
|
||||
{withAddSetButton && (
|
||||
<Button
|
||||
className="symbol-set-add-button"
|
||||
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
|
||||
@ -335,6 +345,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
canViewSet
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
sharedCanvasRef={canvasRef}
|
||||
withTranslucentThumb={isTranslucent}
|
||||
onClick={onStickerSelect}
|
||||
clickArg={sticker}
|
||||
isSelected={isSelected}
|
||||
|
||||
@ -14,12 +14,17 @@
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.modal-dialog {
|
||||
width: 18.875rem;
|
||||
width: 20.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&.with-top-border {
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
box-shadow: inset 0 -0.5px 0 0 var(--color-borders-alternate);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@ -37,6 +42,20 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stickers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4.5rem));
|
||||
justify-content: space-between;
|
||||
row-gap: 0.25rem;
|
||||
column-gap: var(--symbol-set-gap-size, 0.625rem);
|
||||
text-align: initial;
|
||||
padding-bottom: 0.25rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
padding: 0.5rem 0;
|
||||
border-top: 1px solid var(--color-borders);
|
||||
@ -47,6 +66,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.StickerButton {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Loading {
|
||||
width: 100%;
|
||||
height: 22.8125rem;
|
||||
|
||||
@ -25,6 +25,7 @@ import useLang from '../../hooks/useLang';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useSchedule from '../../hooks/useSchedule';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useScrolledState from '../../hooks/useScrolledState';
|
||||
|
||||
import Modal from '../ui/Modal';
|
||||
import Button from '../ui/Button';
|
||||
@ -88,6 +89,10 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
||||
const isEmoji = renderingStickerSet?.isEmoji;
|
||||
|
||||
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
|
||||
const {
|
||||
handleScroll: handleContentScroll,
|
||||
isAtBeginning: shouldHideTopBorder,
|
||||
} = useScrolledState();
|
||||
|
||||
const {
|
||||
observe: observeIntersection,
|
||||
@ -167,8 +172,10 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
||||
}, [isMobile]);
|
||||
|
||||
function renderHeader() {
|
||||
const fullClassName = buildClassName('modal-header', !shouldHideTopBorder && 'with-top-border');
|
||||
|
||||
return (
|
||||
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={onClose}>
|
||||
<i className="icon icon-close" />
|
||||
</Button>
|
||||
@ -195,8 +202,8 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{renderingStickerSet?.stickers ? (
|
||||
<>
|
||||
<div ref={containerRef} className="stickers custom-scroll">
|
||||
<div className="shared-canvas-container">
|
||||
<div ref={containerRef} className="stickers custom-scroll" onScroll={handleContentScroll}>
|
||||
<div className="shared-canvas-container stickers-grid">
|
||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||
{renderingStickerSet.stickers.map((sticker) => (
|
||||
<StickerButton
|
||||
|
||||
@ -97,7 +97,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
const thumbDataUri = useThumbnail(sticker);
|
||||
// Use preview instead of thumb but only if it's already loaded
|
||||
const [preloadedPreviewData] = useState(mediaLoader.getFromMemory(previewMediaHash));
|
||||
const thumbData = preloadedPreviewData || thumbDataUri;
|
||||
const thumbData = customColor ? thumbDataUri : (preloadedPreviewData || thumbDataUri);
|
||||
|
||||
const shouldForcePreview = isUnsupportedVideo || (isStatic && isSmall);
|
||||
fullMediaHash ||= shouldForcePreview ? previewMediaHash : `sticker${id}`;
|
||||
@ -138,6 +138,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
isThumbOpaque && styles.thumbOpaque,
|
||||
thumbClassName,
|
||||
thumbClassNames,
|
||||
'sticker-media',
|
||||
)}
|
||||
alt=""
|
||||
draggable={false}
|
||||
@ -168,7 +169,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
) : isVideo ? (
|
||||
<OptimizedVideo
|
||||
canPlay={shouldPlay && shouldLoop}
|
||||
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
||||
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames, 'sticker-media')}
|
||||
src={fullMediaData}
|
||||
playsInline
|
||||
muted
|
||||
@ -179,7 +180,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
||||
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames, 'sticker-media')}
|
||||
src={fullMediaData}
|
||||
alt={emoji}
|
||||
draggable={false}
|
||||
|
||||
@ -2,22 +2,28 @@ import type { RefObject } from 'react';
|
||||
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
import { useRef } from '../../../lib/teact/teact';
|
||||
import { useCallback, useRef, useState } from '../../../lib/teact/teact';
|
||||
import { ANIMATION_END_DELAY } from '../../../config';
|
||||
import animateScroll from '../../../util/animateScroll';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
|
||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||
const STICKER_INTERSECTION_MARGIN = 100;
|
||||
const SLIDE_TRANSITION_DURATION = 350 + ANIMATION_END_DELAY;
|
||||
const SCROLL_MAX_DISTANCE_WHEN_CLOSE = 200;
|
||||
const SCROLL_MAX_DISTANCE_WHEN_FAR = 80;
|
||||
const FOCUS_MARGIN = 0.5 * REM;
|
||||
|
||||
export function useStickerPickerObservers(
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
headerRef: RefObject<HTMLDivElement>,
|
||||
idPrefix: string,
|
||||
setActiveSetIndex: (index: number) => void,
|
||||
isHidden?: boolean,
|
||||
) {
|
||||
const stickerSetIntersectionsRef = useRef<boolean[]>([]);
|
||||
|
||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForSet,
|
||||
freeze: freezeForSet,
|
||||
@ -79,10 +85,29 @@ export function useStickerPickerObservers(
|
||||
}
|
||||
}, [freezeForSet, freezeForShowingItems, isHidden, unfreezeForSet, unfreezeForShowingItems]);
|
||||
|
||||
const selectStickerSet = useCallback((index: number) => {
|
||||
setActiveSetIndex((currentIndex) => {
|
||||
const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!;
|
||||
const isClose = Math.abs(currentIndex - index) === 1;
|
||||
|
||||
animateScroll(
|
||||
containerRef.current!,
|
||||
stickerSetEl,
|
||||
'start',
|
||||
FOCUS_MARGIN,
|
||||
isClose ? SCROLL_MAX_DISTANCE_WHEN_CLOSE : SCROLL_MAX_DISTANCE_WHEN_FAR,
|
||||
);
|
||||
|
||||
return index;
|
||||
});
|
||||
}, [containerRef, idPrefix]);
|
||||
|
||||
return {
|
||||
activeSetIndex,
|
||||
observeIntersectionForSet,
|
||||
observeIntersectionForShowingItems,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForCovers,
|
||||
selectStickerSet,
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,12 +2,19 @@
|
||||
--offset-y: 3.25rem !important;
|
||||
--offset-x: auto !important;
|
||||
--color-text: var(--color-primary);
|
||||
--color-background: var(--color-background-compact-menu);
|
||||
--border-radius-default: 1.25rem;
|
||||
|
||||
left: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 26rem;
|
||||
height: 26rem;
|
||||
max-width: calc(var(--symbol-menu-width) + 0.25rem); // Reserve width for scrollbar
|
||||
height: var(--symbol-menu-height);
|
||||
padding: 0 !important;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
@supports (overflow: overlay) {
|
||||
width: var(--symbol-menu-width);
|
||||
}
|
||||
|
||||
@media (max-width: 26rem) {
|
||||
left: 0.5rem !important;
|
||||
|
||||
@ -35,11 +35,6 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const { loadFeaturedEmojiStickers } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollHeaderRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const transformOriginX = useRef<number>();
|
||||
const [isContextMenuShown, markContextMenuShown, unmarkContextMenuShown] = useFlag();
|
||||
useEffect(() => {
|
||||
@ -52,15 +47,6 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [areFeaturedStickersLoaded, isOpen, loadFeaturedEmojiStickers]);
|
||||
|
||||
const handleResetScrollPosition = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
if (scrollHeaderRef.current) {
|
||||
scrollHeaderRef.current.scrollLeft = 0;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEmojiSelect = useCallback((sticker: ApiSticker) => {
|
||||
onEmojiStatusSelect(sticker);
|
||||
onClose();
|
||||
@ -76,14 +62,13 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||
onClose={onClose}
|
||||
transformOriginX={transformOriginX.current}
|
||||
noCloseOnBackdrop={isContextMenuShown}
|
||||
onCloseAnimationEnd={handleResetScrollPosition}
|
||||
>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="status-emoji-set-"
|
||||
loadAndPlay={isOpen}
|
||||
isHidden={!isOpen}
|
||||
isStatusPicker
|
||||
scrollHeaderRef={scrollHeaderRef}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
isTranslucent
|
||||
onContextMenuOpen={markContextMenuShown}
|
||||
onContextMenuClose={unmarkContextMenuShown}
|
||||
onCustomEmojiSelect={handleEmojiSelect}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
z-index: var(--z-reaction-interaction-effect);
|
||||
|
||||
@keyframes show-interaction {
|
||||
from {
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body.is-symbol-menu-open & {
|
||||
bottom: calc(var(--base-bottom-pos) + var(--symbol-menu-height) + var(--symbol-menu-footer-height));
|
||||
bottom: calc(var(--base-bottom-pos) + var(--symbol-menu-height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -459,7 +459,7 @@
|
||||
}
|
||||
|
||||
body.is-symbol-menu-open & {
|
||||
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height) + var(--symbol-menu-footer-height))), 0);
|
||||
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height))), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -178,7 +178,7 @@
|
||||
padding: 0 0.5rem;
|
||||
|
||||
body.is-symbol-menu-open & {
|
||||
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height) + var(--symbol-menu-footer-height))), 0);
|
||||
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height))), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -270,8 +270,8 @@
|
||||
}
|
||||
|
||||
.topic-header-icon {
|
||||
--custom-emoji-size: 2.5rem;
|
||||
font-size: 2.5rem;
|
||||
--custom-emoji-size: 2.25rem;
|
||||
font-size: 2.25rem;
|
||||
|
||||
.emoji-small {
|
||||
width: 1.25rem;
|
||||
|
||||
@ -79,10 +79,10 @@
|
||||
&.mobile.symbolMenuOpen :global(.modal-dialog) {
|
||||
transition: var(--layer-transition);
|
||||
|
||||
transform: translate3d(0, calc((var(--symbol-menu-footer-height) + var(--symbol-menu-height) - env(safe-area-inset-bottom)) * -1), 0);
|
||||
transform: translate3d(0, calc((var(--symbol-menu-height) - env(safe-area-inset-bottom)) * -1), 0);
|
||||
|
||||
@supports not (bottom: env(safe-area-inset-bottom)) {
|
||||
transform: translate3d(0, calc((var(--symbol-menu-footer-height) + var(--symbol-menu-height)) * -1), 0);
|
||||
transform: translate3d(0, calc((var(--symbol-menu-height)) * -1), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
margin: 0.3125rem;
|
||||
border-radius: var(--border-radius-messages-small);
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.5rem;
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
|
||||
background-color: transparent;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
@ -3,7 +3,8 @@ import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { RECENT_SYMBOL_SET_ID } from '../../../config';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { EMOJI_SIZE_PICKER, RECENT_SYMBOL_SET_ID } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
|
||||
@ -15,9 +16,10 @@ import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import EmojiButton from './EmojiButton';
|
||||
|
||||
const EMOJIS_PER_ROW_ON_DESKTOP = 8;
|
||||
const EMOJI_MARGIN = 10;
|
||||
const MOBILE_CONTAINER_PADDING = 8;
|
||||
const EMOJI_SIZE = 40;
|
||||
const EMOJI_MARGIN = 0.625 * REM;
|
||||
const EMOJI_VERTICAL_MARGIN = 0.25 * REM;
|
||||
const EMOJI_VERTICAL_MARGIN_MOBILE = 0.5 * REM;
|
||||
const MOBILE_CONTAINER_PADDING = 0.5 * REM;
|
||||
|
||||
type OwnProps = {
|
||||
category: EmojiCategory;
|
||||
@ -42,9 +44,12 @@ const EmojiCategory: FC<OwnProps> = ({
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const emojisPerRow = isMobile
|
||||
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (EMOJI_SIZE + EMOJI_MARGIN))
|
||||
? Math.floor(
|
||||
(windowSize.get().width - MOBILE_CONTAINER_PADDING + EMOJI_MARGIN) / (EMOJI_SIZE_PICKER + EMOJI_MARGIN),
|
||||
)
|
||||
: EMOJIS_PER_ROW_ON_DESKTOP;
|
||||
const height = Math.ceil(category.emojis.length / emojisPerRow) * (EMOJI_SIZE + EMOJI_MARGIN);
|
||||
const height = Math.ceil(category.emojis.length / emojisPerRow)
|
||||
* (EMOJI_SIZE_PICKER + (isMobile ? EMOJI_VERTICAL_MARGIN_MOBILE : EMOJI_VERTICAL_MARGIN));
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -1,30 +1,38 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.EmojiPicker {
|
||||
--emoji-size: 2.5rem;
|
||||
--emoji-size: 2.25rem;
|
||||
--color-primary: var(--color-text);
|
||||
|
||||
height: 100%;
|
||||
|
||||
&-main {
|
||||
height: calc(100% - 3rem);
|
||||
overflow-y: auto;
|
||||
padding: 0.4375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
@include overflow-y-overlay();
|
||||
}
|
||||
|
||||
&-header {
|
||||
height: 3rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
box-shadow: 0 0 2px var(--color-default-shadow);
|
||||
|
||||
&.with-top-border {
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
box-shadow: inset 0 -0.5px 0 0 var(--color-borders-alternate);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
display: block;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
padding: 0.4375rem 0;
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import type {
|
||||
} from '../../../util/emoji';
|
||||
|
||||
import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { uncompressEmoji } from '../../../util/emoji';
|
||||
@ -24,6 +25,7 @@ import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useScrolledState from '../../../hooks/useScrolledState';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Loading from '../../ui/Loading';
|
||||
@ -54,8 +56,8 @@ const ICONS_BY_CATEGORY: Record<string, string> = {
|
||||
|
||||
const OPEN_ANIMATION_DELAY = 200;
|
||||
const SMOOTH_SCROLL_DISTANCE = 100;
|
||||
const FOCUS_MARGIN = 50;
|
||||
const HEADER_BUTTON_WIDTH = 42; // px. Includes margins
|
||||
const FOCUS_MARGIN = 3.25 * REM;
|
||||
const HEADER_BUTTON_WIDTH = 2.625 * REM; // Includes margins
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
|
||||
const categoryIntersections: boolean[] = [];
|
||||
@ -78,6 +80,10 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
const [emojis, setEmojis] = useState<AllEmojis>();
|
||||
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0);
|
||||
const { isMobile } = useAppLayout();
|
||||
const {
|
||||
handleScroll: handleContentScroll,
|
||||
isAtBeginning: shouldHideTopBorder,
|
||||
} = useScrolledState();
|
||||
|
||||
const { observe: observeIntersection } = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
@ -200,13 +206,23 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const headerClassName = buildClassName(
|
||||
'EmojiPicker-header',
|
||||
!shouldHideTopBorder && 'with-top-border',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
<div ref={headerRef} className="EmojiPicker-header" dir={lang.isRtl ? 'rtl' : ''}>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={headerClassName}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
{allCategories.map(renderCategoryButton)}
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
onScroll={handleContentScroll}
|
||||
className={buildClassName('EmojiPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
>
|
||||
{allCategories.map((category, i) => (
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
overflow-y: hidden;
|
||||
|
||||
.EmojiButton {
|
||||
flex: 0 0 2.5rem;
|
||||
flex: 0 0 2.25rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,31 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.GifPicker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-auto-rows: 6.25rem;
|
||||
grid-gap: 0.25rem;
|
||||
grid-gap: 0.125rem;
|
||||
grid-auto-flow: dense;
|
||||
height: 100%;
|
||||
height: calc(100% - 0.1875rem);
|
||||
overflow-y: auto;
|
||||
padding: 0.25rem;
|
||||
margin: 0 0.1875rem;
|
||||
padding-bottom: 0.1875rem;
|
||||
border-radius: 1.125rem 1.125rem 0 0;
|
||||
position: relative;
|
||||
top: 0.1875rem;
|
||||
|
||||
@supports (overflow: overlay) {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
@include overflow-y-overlay();
|
||||
|
||||
.Loading, .picker-disabled {
|
||||
grid-column: 1 / -1;
|
||||
height: var(--menu-height);
|
||||
}
|
||||
|
||||
.SymbolMenu.mobile-menu & {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
border-radius: var(--border-radius-default) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,29 +63,31 @@ const GifPicker: FC<OwnProps & StateProps> = ({
|
||||
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName('GifPicker', className, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
>
|
||||
{!canSendGifs ? (
|
||||
<div className="picker-disabled">Sending GIFs is not allowed in this chat.</div>
|
||||
) : canRenderContents && savedGifs && savedGifs.length ? (
|
||||
savedGifs.map((gif) => (
|
||||
<GifButton
|
||||
key={gif.id}
|
||||
gif={gif}
|
||||
observeIntersection={observeIntersection}
|
||||
isDisabled={!loadAndPlay}
|
||||
onClick={canSendGifs ? onGifSelect : undefined}
|
||||
onUnsaveClick={handleUnsaveClick}
|
||||
isSavedMessages={isSavedMessages}
|
||||
/>
|
||||
))
|
||||
) : canRenderContents && savedGifs ? (
|
||||
<div className="picker-disabled">No saved GIFs.</div>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
<div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName('GifPicker', className, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
>
|
||||
{!canSendGifs ? (
|
||||
<div className="picker-disabled">Sending GIFs is not allowed in this chat.</div>
|
||||
) : canRenderContents && savedGifs && savedGifs.length ? (
|
||||
savedGifs.map((gif) => (
|
||||
<GifButton
|
||||
key={gif.id}
|
||||
gif={gif}
|
||||
observeIntersection={observeIntersection}
|
||||
isDisabled={!loadAndPlay}
|
||||
onClick={canSendGifs ? onGifSelect : undefined}
|
||||
onUnsaveClick={handleUnsaveClick}
|
||||
isSavedMessages={isSavedMessages}
|
||||
/>
|
||||
))
|
||||
) : canRenderContents && savedGifs ? (
|
||||
<div className="picker-disabled">No saved GIFs.</div>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
112
src/components/middle/composer/StickerPicker.module.scss
Normal file
112
src/components/middle/composer/StickerPicker.module.scss
Normal file
@ -0,0 +1,112 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
--color-primary: var(--color-text);
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
--symbol-set-gap-size: 0.25rem;
|
||||
|
||||
position: relative;
|
||||
height: calc(100% - 3rem);
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0.25rem;
|
||||
|
||||
@include overflow-y-overlay();
|
||||
|
||||
&_customEmoji {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
:global(.bubble) {
|
||||
border-radius: var(--border-radius-default) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 3rem;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 0.375rem;
|
||||
|
||||
scrollbar-width: none;
|
||||
scrollbar-color: rgba(0, 0, 0, 0);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Spacer to counter last button margin not being accounted in scroll width
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 1px;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
&.headerWithBorder {
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
box-shadow: inset 0 -0.5px 0 0 var(--color-borders-alternate);
|
||||
}
|
||||
|
||||
// Allows `shared-canvas` to fill the whole available width
|
||||
& > :global(.shared-canvas-container) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:global(.shared-canvas) {
|
||||
max-width: 1280px; // STICKER_PICKER_MAX_SHARED_COVERS * (STICKER_SIZE_PICKER_HEADER + 10 * 2)
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.stickerCover {
|
||||
display: inline-grid;
|
||||
vertical-align: middle;
|
||||
grid-template-areas: "cover";
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 2.25rem !important;
|
||||
height: 2.25rem;
|
||||
margin: 0.375rem 0.125rem !important;
|
||||
padding: 0;
|
||||
border-radius: var(--border-radius-messages-small) !important;
|
||||
|
||||
&:global(.StickerButton) {
|
||||
background-size: 1.875rem;
|
||||
|
||||
:global(.sticker-media),
|
||||
:global(.AnimatedSticker) {
|
||||
position: static;
|
||||
grid-area: cover;
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.activated {
|
||||
color: var(--color-text-lighter);
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
|
||||
:global(.theme-dark) & {
|
||||
--color-text-lighter: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pickerDisabled {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
.StickerPicker {
|
||||
height: 100%;
|
||||
|
||||
&-main {
|
||||
position: relative;
|
||||
height: calc(100% - 3rem);
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
&-header {
|
||||
height: 3rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
padding: 0.125rem 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 0 2px var(--color-default-shadow);
|
||||
|
||||
scrollbar-width: none;
|
||||
scrollbar-color: rgba(0, 0, 0, 0);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Spacer to counter last button margin not being accounted in scroll width
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 1px;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.sticker-set-button {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.StickerButton {
|
||||
background-size: 2rem;
|
||||
|
||||
video, img, .AnimatedSticker {
|
||||
top: 0.375rem;
|
||||
left: 0.375rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-set-cover {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
video, img, .AnimatedSticker {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.activated {
|
||||
background-color: var(--color-background-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.shared-canvas-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.shared-canvas {
|
||||
max-width: 1280px; // STICKER_PICKER_MAX_SHARED_COVERS * (STICKER_SIZE_PICKER_HEADER + 10 * 2)
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.symbol-set-container {
|
||||
width: 100%;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.sticker-set-button {
|
||||
width: 2.75rem !important;
|
||||
height: 2.75rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: var(--border-radius-messages-small);
|
||||
}
|
||||
|
||||
.picker-disabled {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useState, useEffect, memo, useRef, useMemo, useCallback,
|
||||
useEffect, memo, useRef, useMemo, useCallback,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -16,10 +16,10 @@ import {
|
||||
STICKER_PICKER_MAX_SHARED_COVERS,
|
||||
STICKER_SIZE_PICKER_HEADER,
|
||||
} from '../../../config';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { isUserId } from '../../../global/helpers';
|
||||
import animateScroll from '../../../util/animateScroll';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
|
||||
import { pickTruthy, uniqueByField } from '../../../util/iteratees';
|
||||
@ -32,6 +32,7 @@ import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||
import { useStickerPickerObservers } from '../../common/hooks/useStickerPickerObservers';
|
||||
import useScrolledState from '../../../hooks/useScrolledState';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Loading from '../../ui/Loading';
|
||||
@ -41,13 +42,14 @@ import StickerSet from '../../common/StickerSet';
|
||||
import StickerSetCover from './StickerSetCover';
|
||||
import PremiumIcon from '../../common/PremiumIcon';
|
||||
|
||||
import './StickerPicker.scss';
|
||||
import styles from './StickerPicker.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
threadId?: number;
|
||||
className: string;
|
||||
isHidden?: boolean;
|
||||
isTranslucent?: boolean;
|
||||
loadAndPlay: boolean;
|
||||
canSendStickers?: boolean;
|
||||
onStickerSelect: (
|
||||
@ -68,14 +70,14 @@ type StateProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const SMOOTH_SCROLL_DISTANCE = 100;
|
||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||
const HEADER_BUTTON_WIDTH = 2.5 * REM; // px (including margin)
|
||||
|
||||
const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
threadId,
|
||||
className,
|
||||
isHidden,
|
||||
isTranslucent,
|
||||
loadAndPlay,
|
||||
canSendStickers,
|
||||
recentStickers,
|
||||
@ -104,16 +106,21 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||
const {
|
||||
handleScroll: handleContentScroll,
|
||||
isAtBeginning: shouldHideTopBorder,
|
||||
} = useScrolledState();
|
||||
|
||||
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
|
||||
|
||||
const {
|
||||
activeSetIndex,
|
||||
observeIntersectionForSet,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
observeIntersectionForCovers,
|
||||
} = useStickerPickerObservers(containerRef, headerRef, 'sticker-set', setActiveSetIndex, isHidden);
|
||||
selectStickerSet,
|
||||
} = useStickerPickerObservers(containerRef, headerRef, 'sticker-set', isHidden);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -204,7 +211,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||
const shouldRenderContents = areAddedLoaded && canRenderContents && !noPopulatedSets && canSendStickers;
|
||||
|
||||
useHorizontalScroll(headerRef, !shouldRenderContents);
|
||||
useHorizontalScroll(headerRef, !shouldRenderContents || !headerRef.current);
|
||||
|
||||
// Scroll container and header when active set changes
|
||||
useEffect(() => {
|
||||
@ -222,12 +229,6 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
animateHorizontalScroll(header, newLeft);
|
||||
}, [areAddedLoaded, activeSetIndex]);
|
||||
|
||||
const selectStickerSet = useCallback((index: number) => {
|
||||
setActiveSetIndex(index);
|
||||
const stickerSetEl = document.getElementById(`sticker-set-${index}`)!;
|
||||
animateScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
||||
}, []);
|
||||
|
||||
const handleStickerSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => {
|
||||
onStickerSelect(sticker, isSilent, shouldSchedule, true);
|
||||
addRecentSticker({ sticker });
|
||||
@ -252,11 +253,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
||||
const firstSticker = stickerSet.stickers?.[0];
|
||||
const buttonClassName = buildClassName(
|
||||
'symbol-set-button sticker-set-button',
|
||||
index === activeSetIndex && 'activated',
|
||||
);
|
||||
|
||||
const buttonClassName = buildClassName(styles.stickerCover, index === activeSetIndex && styles.activated);
|
||||
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
||||
|
||||
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
||||
@ -308,6 +305,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
noContextMenu
|
||||
isCurrentUserPremium
|
||||
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
||||
withTranslucentThumb={isTranslucent}
|
||||
onClick={selectStickerSet}
|
||||
clickArg={index}
|
||||
/>
|
||||
@ -315,15 +313,15 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const fullClassName = buildClassName('StickerPicker', className);
|
||||
const fullClassName = buildClassName(styles.root, className);
|
||||
|
||||
if (!shouldRenderContents) {
|
||||
return (
|
||||
<div className={fullClassName}>
|
||||
{!canSendStickers ? (
|
||||
<div className="picker-disabled">{lang('ErrorSendRestrictedStickersAll')}</div>
|
||||
<div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
|
||||
) : noPopulatedSets ? (
|
||||
<div className="picker-disabled">{lang('NoStickers')}</div>
|
||||
<div className={styles.pickerDisabled}>{lang('NoStickers')}</div>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
@ -331,12 +329,15 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const headerClassName = buildClassName(
|
||||
styles.header,
|
||||
'no-selection no-scrollbar',
|
||||
!shouldHideTopBorder && styles.headerWithBorder,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={fullClassName}>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className="StickerPicker-header no-selection no-scrollbar"
|
||||
>
|
||||
<div ref={headerRef} className={headerClassName}>
|
||||
<div className="shared-canvas-container">
|
||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||
{allSets.map(renderCover)}
|
||||
@ -345,7 +346,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
onScroll={handleContentScroll}
|
||||
className={buildClassName(styles.main, 'no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
>
|
||||
{allSets.map((stickerSet, i) => (
|
||||
<StickerSet
|
||||
@ -360,6 +362,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
favoriteStickers={favoriteStickers}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
isTranslucent={isTranslucent}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
onStickerUnfave={handleStickerUnfave}
|
||||
onStickerFave={handleStickerFave}
|
||||
|
||||
@ -1,3 +1,19 @@
|
||||
.video {
|
||||
width: 100%;
|
||||
.root {
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--border-radius-messages-small) !important;
|
||||
|
||||
:global(.AnimatedSticker) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video,
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
}, [isIntersecting, loadStickers, stickerSet]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="sticker-set-cover">
|
||||
<div ref={containerRef} className={buildClassName(styles.root, 'sticker-set-cover')}>
|
||||
{isReady ? (
|
||||
isLottie ? (
|
||||
<AnimatedSticker
|
||||
@ -91,7 +91,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
) : (
|
||||
<img
|
||||
src={mediaData || staticMediaData}
|
||||
className={transitionClassNames}
|
||||
className={buildClassName(styles.image, transitionClassNames)}
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
|
||||
@ -3,13 +3,7 @@
|
||||
.SymbolMenu {
|
||||
&.attachment-modal-symbol-menu {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
&:not(.mobile-menu) {
|
||||
@media (max-height: 800px) {
|
||||
--symbol-menu-height: 40vh;
|
||||
}
|
||||
z-index: var(--z-symbol-menu-modal);
|
||||
}
|
||||
|
||||
&.mobile-menu {
|
||||
@ -24,19 +18,19 @@
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height) + env(safe-area-inset-bottom)), 0);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + env(safe-area-inset-bottom)), 0);
|
||||
|
||||
&.open:not(.in-attachment-modal) {
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
body.is-media-viewer-open & {
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)), 0);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height)), 0);
|
||||
}
|
||||
}
|
||||
|
||||
&.open.in-attachment-modal {
|
||||
z-index: calc(var(--z-modal) + 1);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)), 0);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height)), 0);
|
||||
}
|
||||
|
||||
// Target: Old Firefox (Waterfox Classic)
|
||||
@ -44,7 +38,7 @@
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)), 0);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height)), 0);
|
||||
}
|
||||
|
||||
body.animation-level-0 & {
|
||||
@ -57,7 +51,7 @@
|
||||
}
|
||||
|
||||
&-main {
|
||||
height: var(--symbol-menu-height);
|
||||
height: calc(var(--symbol-menu-height) - var(--symbol-menu-footer-height));
|
||||
max-height: calc(100vh - var(--symbol-menu-footer-height) - env(safe-area-inset-bottom));
|
||||
|
||||
// Target: Old Firefox (Waterfox Classic)
|
||||
@ -68,11 +62,11 @@
|
||||
|
||||
&-footer {
|
||||
height: var(--symbol-menu-footer-height);
|
||||
border-top: 1px solid var(--color-borders);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 2px var(--color-default-shadow);
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
box-shadow: 0 -0.5px var(--color-borders-alternate);
|
||||
position: relative;
|
||||
|
||||
.Button {
|
||||
@ -84,6 +78,7 @@
|
||||
|
||||
&.activated {
|
||||
pointer-events: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.symbol-tab-button {
|
||||
@ -145,6 +140,9 @@
|
||||
.bubble {
|
||||
--offset-y: 4rem;
|
||||
|
||||
background: var(--color-background-compact-menu);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 1.25rem;
|
||||
width: calc(var(--symbol-menu-width) + 0.25rem); // Reserve width for scrollbar
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
@ -160,6 +158,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.StickerButton.custom-emoji {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.picker-disabled {
|
||||
height: var(--symbol-menu-height);
|
||||
display: flex;
|
||||
@ -189,20 +191,11 @@
|
||||
}
|
||||
|
||||
.symbol-set {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.symbol-set-locked::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: -0.25rem;
|
||||
top: 0.75rem;
|
||||
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><rect width='100%' height='100%' style='stroke: rgba(112, 117, 121, 0.7); width: calc(100% - 4px); height: calc(100% - 4px);' fill='none' stroke-dashoffset='5' stroke-width='2' stroke-dasharray='8' stroke-linecap='round' rx='8' ry='8' x='2' y='2' /></svg>");
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -215,14 +208,33 @@
|
||||
line-height: 1.6875rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
padding: 0 0.5rem;
|
||||
padding: 0 0.25rem 0.125rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
unicode-bidi: plaintext;
|
||||
z-index: 1;
|
||||
background-color: var(--color-background);
|
||||
|
||||
&-external {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&-amount {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
line-height: 1.125rem;
|
||||
margin-top: -0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
color: rgba(var(--color-text-secondary-rgb), 0.75);
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
.symbol-set-locked & {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-locked-icon {
|
||||
@ -266,9 +278,9 @@
|
||||
.symbol-set-container {
|
||||
display: grid !important;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4rem));
|
||||
grid-gap: 0.625rem;
|
||||
padding: 0.3125rem;
|
||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4.5rem));
|
||||
row-gap: 0.25rem;
|
||||
column-gap: var(--symbol-set-gap-size, 0.625rem);
|
||||
text-align: initial;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
@ -218,8 +218,9 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
className="picker-tab"
|
||||
isHidden={!isOpen || !isActive}
|
||||
loadAndPlay={isOpen && (isActive || isFrom)}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
chatId={chatId}
|
||||
isTranslucent={!isMobile}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
/>
|
||||
);
|
||||
case SymbolMenuTabs.Stickers:
|
||||
@ -229,9 +230,10 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
isHidden={!isOpen || !isActive}
|
||||
loadAndPlay={canSendStickers ? isOpen && (isActive || isFrom) : false}
|
||||
canSendStickers={canSendStickers}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isTranslucent={!isMobile}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
/>
|
||||
);
|
||||
case SymbolMenuTabs.GIFs:
|
||||
@ -256,7 +258,11 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
<>
|
||||
<div className="SymbolMenu-main" onClick={stopPropagation}>
|
||||
{isActivated && (
|
||||
<Transition name="slide" activeKey={activeTab} renderCount={Object.values(SYMBOL_MENU_TAB_TITLES).length}>
|
||||
<Transition
|
||||
name="slide"
|
||||
activeKey={activeTab}
|
||||
renderCount={Object.values(SYMBOL_MENU_TAB_TITLES).length}
|
||||
>
|
||||
{renderContent}
|
||||
</Transition>
|
||||
)}
|
||||
|
||||
@ -157,7 +157,7 @@ export default function useInputCustomEmojis(
|
||||
}, []);
|
||||
|
||||
const unfreezeAnimation = useCallback(() => {
|
||||
playersById.current.forEach((player) => {
|
||||
playersById.current?.forEach((player) => {
|
||||
player.play();
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -108,8 +108,6 @@ type StateProps = {
|
||||
threadId?: number;
|
||||
};
|
||||
|
||||
const REACTION_PICKER_APPEARANCE_DURATION_MS = 250;
|
||||
|
||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
availableReactions,
|
||||
topReactions,
|
||||
@ -193,7 +191,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [noAnimationOnClose, setNoAnimationOnClose] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||
@ -262,8 +259,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
setIsReportModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback((noCloseAnimation = false) => {
|
||||
setNoAnimationOnClose(noCloseAnimation);
|
||||
const closeMenu = useCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
@ -424,10 +420,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleReactionPickerOpen = useCallback((position: IAnchorPosition) => {
|
||||
openReactionPicker({ chatId: message.chatId, messageId: message.id, position });
|
||||
setTimeout(() => {
|
||||
closeMenu(true);
|
||||
}, REACTION_PICKER_APPEARANCE_DURATION_MS);
|
||||
}, [closeMenu, message.chatId, message.id]);
|
||||
}, [message.chatId, message.id]);
|
||||
|
||||
const handleTranslate = useCallback(() => {
|
||||
requestMessageTranslation({
|
||||
@ -504,7 +497,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canSelectLanguage={canSelectLanguage}
|
||||
hasCustomEmoji={hasCustomEmoji}
|
||||
customEmojiSets={customEmojiSets}
|
||||
noTransition={noAnimationOnClose}
|
||||
isDownloading={isDownloading}
|
||||
seenByRecentUsers={seenByRecentUsers}
|
||||
noReplies={noReplies}
|
||||
|
||||
@ -2,14 +2,19 @@
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
|
||||
.scrollable-content {
|
||||
&_items {
|
||||
overflow: auto;
|
||||
overflow: overlay;
|
||||
padding: 0.5rem 0;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
&-hidden {
|
||||
opacity: 0;
|
||||
transition: 300ms opacity;
|
||||
}
|
||||
}
|
||||
|
||||
&.compact .scrollable-content {
|
||||
&.compact &_items {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
@ -25,7 +30,7 @@
|
||||
padding: 3.5rem 0 0 !important;
|
||||
}
|
||||
|
||||
&.with-reactions .scrollable-content {
|
||||
&.with-reactions &_items {
|
||||
background: var(--color-background-compact-menu);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
|
||||
|
||||
@ -203,8 +203,9 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
const isSponsoredMessage = !('id' in message);
|
||||
const messageId = !isSponsoredMessage ? message.id : '';
|
||||
|
||||
const [areItemsHidden, hideItems] = useFlag();
|
||||
const [isReady, markIsReady, unmarkIsReady] = useFlag();
|
||||
const { isMobile } = useAppLayout();
|
||||
const { isMobile, isDesktop } = useAppLayout();
|
||||
|
||||
const handleAfterCopy = useCallback(() => {
|
||||
showNotification({
|
||||
@ -256,7 +257,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
|
||||
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessageWrapper');
|
||||
const extraHeightPinned = (((isMobile && !extraHeightAudioPlayer)
|
||||
|| (!isMobile && pinnedElement?.classList.contains('full-width')))
|
||||
|| (!isMobile && pinnedElement?.classList.contains('full-width')))
|
||||
&& pinnedElement?.offsetHeight) || 0;
|
||||
|
||||
return {
|
||||
@ -264,10 +265,10 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
|
||||
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
|
||||
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
|
||||
shouldAvoidNegativePosition: true,
|
||||
shouldAvoidNegativePosition: !isDesktop,
|
||||
menuElMinWidth: withReactions && isMobile ? REACTION_SELECTOR_WIDTH_REM * REM : undefined,
|
||||
};
|
||||
}, [isMobile, withReactions]);
|
||||
}, [isDesktop, isMobile, withReactions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@ -285,11 +286,16 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
} = useMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||
|
||||
useEffect(() => {
|
||||
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
|
||||
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionPicker');
|
||||
|
||||
return enableScrolling;
|
||||
}, [withScroll]);
|
||||
|
||||
const handleOpenReactionPicker = useCallback((position: IAnchorPosition) => {
|
||||
onReactionPickerOpen!(position);
|
||||
hideItems();
|
||||
}, [onReactionPickerOpen]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
@ -319,12 +325,15 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
isReady={isReady}
|
||||
canBuyPremium={canBuyPremium}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
onShowMore={onReactionPickerOpen!}
|
||||
onShowMore={handleOpenReactionPicker}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="scrollable-content custom-scroll"
|
||||
className={buildClassName(
|
||||
'MessageContextMenu_items scrollable-content custom-scroll',
|
||||
areItemsHidden && 'MessageContextMenu_items-hidden',
|
||||
)}
|
||||
style={menuStyle}
|
||||
ref={scrollableRef}
|
||||
>
|
||||
|
||||
@ -24,8 +24,8 @@
|
||||
pointer-events: none;
|
||||
|
||||
&.effect {
|
||||
top: -2.5rem;
|
||||
left: -2.5rem;
|
||||
top: -2.25rem;
|
||||
left: -2.25rem;
|
||||
}
|
||||
|
||||
&:not(:global(.open)) {
|
||||
|
||||
@ -10,14 +10,24 @@
|
||||
}
|
||||
|
||||
.menuContent {
|
||||
width: 26.25rem;
|
||||
height: 26.25rem;
|
||||
--color-background: var(--color-background-compact-menu);
|
||||
--border-radius-default: 1.25rem;
|
||||
|
||||
width: calc(var(--symbol-menu-width) + 0.25rem); // Reserve width for scrollbar
|
||||
height: var(--symbol-menu-height);
|
||||
padding: 0 !important;
|
||||
transform-origin: 50% 3.5rem !important;
|
||||
transform-origin: 9rem 4.625rem !important;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
@supports (overflow: overlay) {
|
||||
width: var(--symbol-menu-width);
|
||||
}
|
||||
|
||||
&:global(.bubble) {
|
||||
transform: scale(0.7) !important;
|
||||
transition: opacity 140ms cubic-bezier(0.2, 0, 0.2, 1), transform 140ms cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||
transform: scale(0.8) !important;
|
||||
transition: opacity 150ms cubic-bezier(0.2, 0, 0.2, 1), transform 150ms cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||
--offset-x: -0.75rem;
|
||||
--offset-y: calc(100% + 0.625rem);
|
||||
}
|
||||
|
||||
&:global(.bubble.open) {
|
||||
@ -42,6 +52,11 @@
|
||||
|
||||
.onlyReactions {
|
||||
height: auto;
|
||||
transform-origin: 9rem 1.75rem !important;
|
||||
|
||||
&:global(.bubble) {
|
||||
--offset-y: calc(100% + 0.1875rem);
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
||||
@ -33,8 +33,8 @@ interface StateProps {
|
||||
position?: IAnchorPosition;
|
||||
}
|
||||
|
||||
const FULL_PICKER_SHIFT_DELTA = { x: -30, y: -66 };
|
||||
const LIMITED_PICKER_SHIFT_DELTA = { x: -25, y: -10 };
|
||||
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
|
||||
const LIMITED_PICKER_SHIFT_DELTA = { x: -21, y: -10 };
|
||||
|
||||
const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
isOpen,
|
||||
@ -44,13 +44,6 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const { toggleReaction, closeReactionPicker } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollHeaderRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const limitedScrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const renderedMessageId = useCurrentOrPrev(message?.id, true);
|
||||
const renderedChatId = useCurrentOrPrev(message?.chatId, true);
|
||||
const storedPosition = useCurrentOrPrev(position, true);
|
||||
@ -73,18 +66,6 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
positionX, positionY, transformOriginX, transformOriginY, style,
|
||||
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||
|
||||
const handleResetScrollPosition = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
if (scrollHeaderRef.current) {
|
||||
scrollHeaderRef.current.scrollLeft = 0;
|
||||
}
|
||||
if (limitedScrollContainerRef.current) {
|
||||
limitedScrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleCustomReaction = useCallback((sticker: ApiSticker) => {
|
||||
if (!renderedChatId || !renderedMessageId) {
|
||||
return;
|
||||
@ -120,17 +101,12 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
}, []);
|
||||
}, [message?.reactions?.results]);
|
||||
|
||||
const bubbleFullClassName = buildClassName(
|
||||
styles.menuContent,
|
||||
!withCustomReactions && styles.onlyReactions,
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isOpen}
|
||||
ref={menuRef}
|
||||
className={styles.menu}
|
||||
bubbleClassName={bubbleFullClassName}
|
||||
className={buildClassName(styles.menu, 'ReactionPicker')}
|
||||
bubbleClassName={buildClassName(styles.menuContent, !withCustomReactions && styles.onlyReactions)}
|
||||
withPortal
|
||||
noCompact
|
||||
positionX={positionX}
|
||||
@ -138,25 +114,24 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={style}
|
||||
backdropExcludedSelector=".Modal.confirm"
|
||||
onClose={closeReactionPicker}
|
||||
onCloseAnimationEnd={handleResetScrollPosition}
|
||||
>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="message-emoji-set-"
|
||||
loadAndPlay={isOpen}
|
||||
isHidden={!isOpen || !withCustomReactions}
|
||||
loadAndPlay={Boolean(isOpen && withCustomReactions)}
|
||||
isReactionPicker
|
||||
className={!withCustomReactions ? styles.hidden : undefined}
|
||||
scrollHeaderRef={scrollHeaderRef}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
selectedReactionIds={selectedReactionIds}
|
||||
isTranslucent
|
||||
onCustomEmojiSelect={handleToggleCustomReaction}
|
||||
onReactionSelect={handleToggleReaction}
|
||||
selectedReactionIds={selectedReactionIds}
|
||||
/>
|
||||
{!withCustomReactions && Boolean(renderedChatId) && (
|
||||
<ReactionPickerLimited
|
||||
chatId={renderedChatId}
|
||||
loadAndPlay={isOpen}
|
||||
scrollContainerRef={limitedScrollContainerRef}
|
||||
onReactionSelect={handleToggleReaction}
|
||||
selectedReactionIds={selectedReactionIds}
|
||||
/>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
.root {
|
||||
--emoji-size: 2.5rem;
|
||||
--emoji-size: 2.25rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
height: auto;
|
||||
max-height: 18rem;
|
||||
max-height: min(18rem, 100%);
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { RefObject } from 'react';
|
||||
import React, { memo, useRef, useMemo } from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../global';
|
||||
|
||||
@ -18,7 +17,6 @@ import ReactionEmoji from '../../common/ReactionEmoji';
|
||||
import styles from './ReactionPickerLimited.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
scrollContainerRef?: RefObject<HTMLDivElement>;
|
||||
chatId: string;
|
||||
loadAndPlay: boolean;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
@ -34,7 +32,7 @@ type StateProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const REACTION_SIZE = 40;
|
||||
const REACTION_SIZE = 36;
|
||||
const GRID_GAP_THRESHOLD = 600;
|
||||
const MODAL_PADDING_SIZE_REM = 0.5;
|
||||
const MODAL_MAX_HEIGHT_REM = 18;
|
||||
@ -43,7 +41,6 @@ const GRID_GAP_DESKTOP_REM = 0.625;
|
||||
const GRID_GAP_MOBILE_REM = 0.5;
|
||||
|
||||
const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
scrollContainerRef,
|
||||
loadAndPlay,
|
||||
enabledReactions,
|
||||
availableReactions,
|
||||
@ -51,17 +48,12 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
selectedReactionIds,
|
||||
onReactionSelect,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let containerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
if (scrollContainerRef) {
|
||||
containerRef = scrollContainerRef;
|
||||
}
|
||||
|
||||
const allAvailableReactions = useMemo(() => {
|
||||
if (!enabledReactions) {
|
||||
@ -83,17 +75,14 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
const itemsInRow = Math.floor((availableWidth + gapWidth) / (REACTION_SIZE + gapWidth));
|
||||
const rowsCount = Math.ceil(allAvailableReactions.length / itemsInRow);
|
||||
|
||||
const pickerMaxHeight = rowsCount * REACTION_SIZE + (rowsCount + 1) * gapWidth + MODAL_PADDING_SIZE_REM * REM;
|
||||
const pickerMaxHeight = rowsCount * REACTION_SIZE + (rowsCount - 1) * gapWidth + MODAL_PADDING_SIZE_REM * REM * 2;
|
||||
|
||||
return Math.min(MODAL_MAX_HEIGHT_REM * REM, pickerMaxHeight);
|
||||
}, [allAvailableReactions.length, windowWidth]);
|
||||
|
||||
return (
|
||||
<div className={styles.root} style={`height: ${pickerHeight}px`}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName(styles.wrapper, 'no-selection', isTouchScreen ? 'no-scrollbar' : 'custom-scroll')}
|
||||
>
|
||||
<div className={buildClassName(styles.wrapper, 'no-selection', isTouchScreen ? 'no-scrollbar' : 'custom-scroll')}>
|
||||
<div className="symbol-set-container shared-canvas-container">
|
||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
|
||||
|
||||
@ -20,21 +20,12 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--withBlur {
|
||||
background: none;
|
||||
filter: none;
|
||||
&__bubble-big,
|
||||
&__bubble-small,
|
||||
&__items-wrapper {
|
||||
background: var(--color-background-compact-menu);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.ReactionSelector__bubble-big,
|
||||
.ReactionSelector__bubble-small,
|
||||
.ReactionSelector__items-wrapper {
|
||||
background: var(--color-background-compact-menu);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.ReactionSelector__bubble-big,
|
||||
.ReactionSelector__bubble-small,
|
||||
.ReactionSelector__items-wrapper {
|
||||
filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow));
|
||||
|
||||
body.is-safari & {
|
||||
@ -44,7 +35,6 @@
|
||||
}
|
||||
|
||||
&__bubble-big {
|
||||
background: var(--color-background);
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
@ -77,7 +67,6 @@
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-background);
|
||||
|
||||
&--isRtl {
|
||||
right: auto;
|
||||
@ -93,7 +82,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 3rem;
|
||||
background: var(--color-background);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
width: fit-content;
|
||||
|
||||
@ -12,7 +12,6 @@ import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import {
|
||||
isSameReaction, canSendReaction, getReactionUniqueKey, sortReactions,
|
||||
} from '../../../global/helpers';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
@ -50,7 +49,6 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
const lang = useLang();
|
||||
|
||||
const availableReactions = useMemo(() => {
|
||||
@ -94,7 +92,7 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
if (!reactionsToRender.length) return undefined;
|
||||
|
||||
return (
|
||||
<div className={cn('&', !isTouchScreen && 'withBlur', lang.isRtl && 'isRtl')} ref={ref}>
|
||||
<div className={cn('&', lang.isRtl && 'isRtl')} ref={ref}>
|
||||
<div className={cn('bubble-small', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items-wrapper')}>
|
||||
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
||||
|
||||
@ -135,6 +135,7 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
|
||||
<div className={buildClassName(styles.section, styles.bottom)}>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="create-topic-icons-set-"
|
||||
isHidden={!isActive}
|
||||
loadAndPlay={isActive}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
className={styles.iconPicker}
|
||||
|
||||
@ -146,6 +146,7 @@ const EditTopic: FC<OwnProps & StateProps> = ({
|
||||
<div className={buildClassName(styles.section, styles.bottom)}>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="edit-topic-icons-set-"
|
||||
isHidden={!isActive}
|
||||
loadAndPlay={isActive}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
className={styles.iconPicker}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
.StickerSearch {
|
||||
container: stickers / inline-size;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@supports (overflow-y: overlay) {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
padding: 1rem;
|
||||
@ -71,5 +77,17 @@
|
||||
|
||||
.StickerButton {
|
||||
margin: 0.125rem;
|
||||
|
||||
@media (max-width: 380px) {
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 376px) {
|
||||
.StickerSearch .StickerButton:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ type OwnProps = {
|
||||
shouldSkipTransition?: boolean;
|
||||
footer?: string;
|
||||
noCloseOnBackdrop?: boolean;
|
||||
backdropExcludedSelector?: string;
|
||||
noCompact?: boolean;
|
||||
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
@ -68,6 +69,7 @@ const Menu: FC<OwnProps> = ({
|
||||
autoClose = false,
|
||||
footer,
|
||||
noCloseOnBackdrop = false,
|
||||
backdropExcludedSelector,
|
||||
noCompact,
|
||||
onCloseAnimationEnd,
|
||||
onClose,
|
||||
@ -118,6 +120,8 @@ const Menu: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
backdropContainerRef,
|
||||
noCloseOnBackdrop ? undefined : onClose,
|
||||
undefined,
|
||||
backdropExcludedSelector,
|
||||
);
|
||||
|
||||
const bubbleFullClassName = buildClassName(
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
z-index: var(--z-modal);
|
||||
|
||||
&.confirm {
|
||||
z-index: var(--z-lock-screen);
|
||||
z-index: var(--z-modal-confirm);
|
||||
}
|
||||
|
||||
&.delete,
|
||||
|
||||
@ -161,15 +161,15 @@ export const STICKER_SIZE_INLINE_DESKTOP_FACTOR = 13;
|
||||
export const STICKER_SIZE_INLINE_MOBILE_FACTOR = 11;
|
||||
export const STICKER_SIZE_AUTH = 160;
|
||||
export const STICKER_SIZE_AUTH_MOBILE = 120;
|
||||
export const STICKER_SIZE_PICKER = 64;
|
||||
export const EMOJI_SIZE_PICKER = 40;
|
||||
export const STICKER_SIZE_PICKER = 72;
|
||||
export const EMOJI_SIZE_PICKER = 36;
|
||||
export const COMPOSER_EMOJI_SIZE_PICKER = 32;
|
||||
export const STICKER_SIZE_GENERAL_SETTINGS = 48;
|
||||
export const STICKER_SIZE_PICKER_HEADER = 32;
|
||||
export const STICKER_PICKER_MAX_SHARED_COVERS = 20;
|
||||
export const STICKER_SIZE_SEARCH = 64;
|
||||
export const STICKER_SIZE_MODAL = 64;
|
||||
export const EMOJI_SIZE_MODAL = 40;
|
||||
export const STICKER_SIZE_SEARCH = 72;
|
||||
export const STICKER_SIZE_MODAL = 72;
|
||||
export const EMOJI_SIZE_MODAL = 36;
|
||||
export const STICKER_SIZE_TWO_FA = 160;
|
||||
export const STICKER_SIZE_PASSCODE = 160;
|
||||
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
||||
|
||||
@ -20,7 +20,9 @@ import { updateTabState } from '../../reducers/tabs';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { isSameReaction, getUserReactions, isMessageLocal } from '../../helpers';
|
||||
import {
|
||||
isSameReaction, getUserReactions, isMessageLocal, getDocumentMediaHash,
|
||||
} from '../../helpers';
|
||||
|
||||
const INTERACTION_RANDOM_OFFSET = 40;
|
||||
|
||||
@ -43,6 +45,9 @@ addActionHandler('loadAvailableReactions', async (global): Promise<void> => {
|
||||
if (availableReaction.appearAnimation) {
|
||||
mediaLoader.fetch(`sticker${availableReaction.appearAnimation.id}`, ApiMediaFormat.BlobUrl);
|
||||
}
|
||||
if (availableReaction.selectAnimation) {
|
||||
mediaLoader.fetch(getDocumentMediaHash(availableReaction.selectAnimation), ApiMediaFormat.BlobUrl);
|
||||
}
|
||||
});
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
@ -10,8 +10,13 @@ export default function useVirtualBackdrop(
|
||||
menuRef: RefObject<HTMLElement>,
|
||||
onClose?: () => void | undefined,
|
||||
ignoreRightClick?: boolean,
|
||||
excludedClosestSelector?: string,
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!isOpen || !onClose) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleEvent = (e: MouseEvent) => {
|
||||
const menu = menuRef.current;
|
||||
const target = e.target as HTMLElement | null;
|
||||
@ -19,22 +24,22 @@ export default function useVirtualBackdrop(
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
if ((
|
||||
!menu.contains(e.target as Node | null)
|
||||
|| target.classList.contains(BACKDROP_CLASSNAME)
|
||||
) {
|
||||
) && !(excludedClosestSelector && (
|
||||
target.matches(excludedClosestSelector) || target.closest(excludedClosestSelector)
|
||||
))) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen && onClose) {
|
||||
document.addEventListener('mousedown', handleEvent);
|
||||
}
|
||||
document.addEventListener('mousedown', handleEvent);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleEvent);
|
||||
};
|
||||
}, [ignoreRightClick, isOpen, menuRef, onClose]);
|
||||
}, [excludedClosestSelector, ignoreRightClick, isOpen, menuRef, onClose]);
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ type Frame =
|
||||
| typeof WAITING
|
||||
| ImageBitmap;
|
||||
|
||||
const MAX_WORKERS = 4;
|
||||
const MAX_WORKERS = Math.min(navigator.hardwareConcurrency || 4, 4);
|
||||
const HIGH_PRIORITY_QUALITY = (IS_ANDROID || IS_IOS) ? 0.75 : 1;
|
||||
const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
|
||||
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
|
||||
|
||||
@ -88,6 +88,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--color-text-green-rgb: #{toRGB($color-text-green)};
|
||||
--color-borders: #{$color-borders};
|
||||
--color-borders-input: #{$color-borders};
|
||||
--color-borders-alternate: rgba(0, 0, 0, 0.1);
|
||||
--color-dividers: #{$color-dividers};
|
||||
--color-dividers-android: #{$color-dividers-android};
|
||||
--color-webpage-initial-background: #{$color-dark-gray};
|
||||
@ -197,8 +198,8 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--custom-emoji-size: 1.25rem;
|
||||
--custom-emoji-border-radius: 0;
|
||||
|
||||
--symbol-menu-width: 26.25rem;
|
||||
--symbol-menu-height: 23.25rem;
|
||||
--symbol-menu-width: 24rem;
|
||||
--symbol-menu-height: 22.375rem;
|
||||
--symbol-menu-footer-height: 3rem;
|
||||
|
||||
@media (min-width: 1276px) and (max-width: 1920px) {
|
||||
@ -212,14 +213,17 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
@media (max-width: 600px) {
|
||||
--right-column-width: 100vw;
|
||||
--symbol-menu-width: 100vw;
|
||||
--symbol-menu-height: 14.6875rem;
|
||||
--symbol-menu-height: 17.6875rem;
|
||||
}
|
||||
|
||||
--z-modal-confirm: 10000;
|
||||
--z-symbol-menu-modal: 5000;
|
||||
--z-lock-screen: 3000;
|
||||
--z-ui-loader-mask: 2000;
|
||||
--z-notification: 1700;
|
||||
--z-confetti: 1600;
|
||||
--z-reaction-picker: 1200;
|
||||
--z-reaction-interaction-effect: 1000;
|
||||
--z-right-column: 900;
|
||||
--z-header-menu: 990;
|
||||
--z-header-menu-backdrop: 980;
|
||||
|
||||
@ -278,7 +278,8 @@ body:not(.is-ios) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shared-canvas, .absolute-video-container {
|
||||
.shared-canvas,
|
||||
.absolute-video-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user