Symbol Menu, Reaction Picker, Status Picker: Fixes and redesign (#2945)

This commit is contained in:
Alexander Zinchuk 2023-04-23 18:33:26 +04:00
parent 649fb46777
commit 9567e6ca38
55 changed files with 605 additions and 427 deletions

View File

@ -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);
}

View File

@ -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}

View File

@ -1,5 +1,5 @@
.root {
--custom-emoji-size: 2.5rem;
--custom-emoji-size: 2.25rem;
cursor: var(--custom-cursor, pointer);
display: inline-block;

View File

@ -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}
/>
)}

View File

@ -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;
}
}

View File

@ -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}
/>
)}

View File

@ -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}

View File

@ -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;

View File

@ -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

View File

@ -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}

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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}

View File

@ -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 {

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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) => (

View File

@ -9,7 +9,7 @@
overflow-y: hidden;
.EmojiButton {
flex: 0 0 2.5rem;
flex: 0 0 2.25rem;
margin-right: 0;
}
}

View File

@ -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;
}
}

View File

@ -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>
);
};

View 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;
}

View File

@ -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;
}
}

View File

@ -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}

View File

@ -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%;
}

View File

@ -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=""
/>
)

View File

@ -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) {

View File

@ -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>
)}

View File

@ -157,7 +157,7 @@ export default function useInputCustomEmojis(
}, []);
const unfreezeAnimation = useCallback(() => {
playersById.current.forEach((player) => {
playersById.current?.forEach((player) => {
player.play();
});
}, []);

View File

@ -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}

View File

@ -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);

View File

@ -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}
>

View File

@ -24,8 +24,8 @@
pointer-events: none;
&.effect {
top: -2.5rem;
left: -2.5rem;
top: -2.25rem;
left: -2.25rem;
}
&:not(:global(.open)) {

View File

@ -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 {

View File

@ -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}
/>

View File

@ -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;
}

View File

@ -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" />

View File

@ -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;

View File

@ -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')} />

View File

@ -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}

View File

@ -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}

View File

@ -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;
}
}

View File

@ -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(

View File

@ -3,7 +3,7 @@
z-index: var(--z-modal);
&.confirm {
z-index: var(--z-lock-screen);
z-index: var(--z-modal-confirm);
}
&.delete,

View File

@ -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;

View File

@ -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();

View File

@ -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]);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;