Message: Support custom reactions (#2217)
This commit is contained in:
parent
aa9958db3a
commit
d127f11f13
@ -25,6 +25,8 @@ interface GramJsAppConfig extends LimitsConfig {
|
||||
reactions_uniq_max: number;
|
||||
chat_read_mark_size_threshold: number;
|
||||
chat_read_mark_expire_period: number;
|
||||
reactions_user_max_default: number;
|
||||
reactions_user_max_premium: number;
|
||||
autologin_domains: string[];
|
||||
autologin_token: string;
|
||||
url_auth_domains: string[];
|
||||
@ -77,6 +79,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue): ApiAppConfig {
|
||||
premiumPromoOrder: appConfig.premium_promo_order,
|
||||
isPremiumPurchaseBlocked: appConfig.premium_purchase_blocked,
|
||||
defaultEmojiStatusesStickerSetId: appConfig.default_emoji_statuses_stickerset_id,
|
||||
maxUserReactionsDefault: appConfig.reactions_user_max_default,
|
||||
maxUserReactionsPremium: appConfig.reactions_user_max_premium,
|
||||
limits: {
|
||||
uploadMaxFileparts: getLimit(appConfig, 'upload_max_fileparts', 'uploadMaxFileparts'),
|
||||
stickersFaved: getLimit(appConfig, 'stickers_faved_limit', 'stickersFaved'),
|
||||
|
||||
@ -12,6 +12,7 @@ import type {
|
||||
ApiChatInviteImporter,
|
||||
ApiChatSettings,
|
||||
ApiSendAsPeerId,
|
||||
ApiChatReactions,
|
||||
} from '../../types';
|
||||
import { pick, pickTruthy } from '../../../util/iteratees';
|
||||
import {
|
||||
@ -462,14 +463,18 @@ export function buildApiChatSettings({
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiChatReactions(availableReactions?: GramJs.TypeChatReactions): string[] | undefined {
|
||||
if (availableReactions instanceof GramJs.ChatReactionsAll) {
|
||||
// TODO Hack before custom reactions are implemented
|
||||
// eslint-disable-next-line max-len
|
||||
return ['👍', '👎', '❤', '🔥', '🥰', '👏', '😁', '🤔', '🤯', '😱', '🤬', '😢', '🎉', '🤩', '🤮', '💩', '🙏', '👌', '🕊', '🤡', '🥱', '🥴', '😍', '🐳', '❤🔥', '🌚', '🌭', '💯', '🤣', '⚡', '🍌', '🏆', '💔', '🤨', '😐', '🍓', '🍾', '💋', '🖕', '😈', '😴', '😭', '🤓', '👻', '👨💻', '👀', '🎃', '🙈', '😇', '😨', '🤝', '✍️', '🤗', '🫡'];
|
||||
export function buildApiChatReactions(chatReactions?: GramJs.TypeChatReactions): ApiChatReactions | undefined {
|
||||
if (chatReactions instanceof GramJs.ChatReactionsAll) {
|
||||
return {
|
||||
type: 'all',
|
||||
areCustomAllowed: chatReactions.allowCustom,
|
||||
};
|
||||
}
|
||||
if (availableReactions instanceof GramJs.ChatReactionsSome) {
|
||||
return availableReactions.reactions.map(buildApiReaction).filter(Boolean);
|
||||
if (chatReactions instanceof GramJs.ChatReactionsSome) {
|
||||
return {
|
||||
type: 'some',
|
||||
allowed: chatReactions.reactions.map(buildApiReaction).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -34,6 +34,8 @@ import type {
|
||||
ApiWebDocument,
|
||||
ApiMessageEntityDefault,
|
||||
ApiMessageExtendedMediaPreview,
|
||||
ApiReaction,
|
||||
ApiReactionEmoji,
|
||||
} from '../../types';
|
||||
import {
|
||||
ApiMessageEntityTypes,
|
||||
@ -223,20 +225,30 @@ export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiRe
|
||||
|
||||
return {
|
||||
canSeeList,
|
||||
results: results.map(buildReactionCount).filter(Boolean),
|
||||
results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator),
|
||||
recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) {
|
||||
const diff = b.count - a.count;
|
||||
if (diff) return diff;
|
||||
if (a.chosenOrder !== undefined && b.chosenOrder !== undefined) {
|
||||
return a.chosenOrder - b.chosenOrder;
|
||||
}
|
||||
if (a.chosenOrder !== undefined) return 1;
|
||||
if (b.chosenOrder !== undefined) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined {
|
||||
const { chosenOrder, count, reaction } = reactionCount;
|
||||
|
||||
// TODO: Add custom reactions support
|
||||
const apiReaction = buildApiReaction(reaction);
|
||||
if (!apiReaction) return undefined;
|
||||
|
||||
return {
|
||||
isChosen: chosenOrder !== undefined, // TODO: Add custom reactions support
|
||||
chosenOrder,
|
||||
count,
|
||||
reaction: apiReaction,
|
||||
};
|
||||
@ -247,7 +259,6 @@ export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReactio
|
||||
peerId, reaction, big, unread,
|
||||
} = userReaction;
|
||||
|
||||
// TODO: Add custom reactions support
|
||||
const apiReaction = buildApiReaction(reaction);
|
||||
if (!apiReaction) return undefined;
|
||||
|
||||
@ -259,11 +270,19 @@ export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReactio
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiReaction(reaction: GramJs.TypeReaction): string | undefined {
|
||||
export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | undefined {
|
||||
if (reaction instanceof GramJs.ReactionEmoji) {
|
||||
return reaction.emoticon;
|
||||
return {
|
||||
emoticon: reaction.emoticon,
|
||||
};
|
||||
}
|
||||
// TODO: Add custom reactions support
|
||||
|
||||
if (reaction instanceof GramJs.ReactionCustomEmoji) {
|
||||
return {
|
||||
documentId: reaction.documentId.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -281,7 +300,7 @@ export function buildApiAvailableReaction(availableReaction: GramJs.AvailableRea
|
||||
staticIcon: buildApiDocument(staticIcon),
|
||||
aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined,
|
||||
centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined,
|
||||
reaction,
|
||||
reaction: { emoticon: reaction } as ApiReactionEmoji,
|
||||
title,
|
||||
isInactive: inactive,
|
||||
isPremium: premium,
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
} from '../../types';
|
||||
import type { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types';
|
||||
|
||||
import { buildApiDocument } from './messages';
|
||||
import { buildApiDocument, buildApiReaction } from './messages';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
@ -224,8 +224,7 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA
|
||||
}
|
||||
|
||||
export function buildApiConfig(config: GramJs.Config): ApiConfig {
|
||||
const defaultReaction = config.reactionsDefault
|
||||
&& 'emoticon' in config.reactionsDefault ? config.reactionsDefault.emoticon : undefined;
|
||||
const defaultReaction = config.reactionsDefault && buildApiReaction(config.reactionsDefault);
|
||||
return {
|
||||
expiresAt: config.expires,
|
||||
gifSearchUsername: config.gifSearchUsername,
|
||||
|
||||
@ -171,7 +171,7 @@ export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetC
|
||||
|
||||
export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmojiInteraction {
|
||||
return {
|
||||
timestamps: json.a.map((l) => l.t),
|
||||
timestamps: json.a.map(({ t }) => t),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ import type {
|
||||
ApiThemeParameters,
|
||||
ApiPoll,
|
||||
ApiRequestInputInvoice,
|
||||
ApiChatReactions,
|
||||
ApiReaction,
|
||||
} from '../../types';
|
||||
import {
|
||||
ApiMessageEntityTypes,
|
||||
@ -547,15 +549,34 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildInputReaction(reaction?: string) {
|
||||
if (!reaction) return new GramJs.ReactionEmpty();
|
||||
return new GramJs.ReactionEmoji({
|
||||
emoticon: reaction,
|
||||
});
|
||||
export function buildInputReaction(reaction?: ApiReaction) {
|
||||
if (reaction && 'emoticon' in reaction) {
|
||||
return new GramJs.ReactionEmoji({
|
||||
emoticon: reaction.emoticon,
|
||||
});
|
||||
}
|
||||
|
||||
if (reaction && 'documentId' in reaction) {
|
||||
return new GramJs.ReactionCustomEmoji({
|
||||
documentId: BigInt(reaction.documentId),
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.ReactionEmpty();
|
||||
}
|
||||
|
||||
export function buildInputChatReactions(chatReactions: string[]) {
|
||||
return new GramJs.ChatReactionsSome({
|
||||
reactions: chatReactions.map(buildInputReaction),
|
||||
});
|
||||
export function buildInputChatReactions(chatReactions?: ApiChatReactions) {
|
||||
if (chatReactions?.type === 'all') {
|
||||
return new GramJs.ChatReactionsAll({
|
||||
allowCustom: chatReactions.areCustomAllowed,
|
||||
});
|
||||
}
|
||||
|
||||
if (chatReactions?.type === 'some') {
|
||||
return new GramJs.ChatReactionsSome({
|
||||
reactions: chatReactions.allowed.map(buildInputReaction),
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.ChatReactionsNone();
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import type {
|
||||
ApiChatBannedRights,
|
||||
ApiChatAdminRights,
|
||||
ApiGroupCall,
|
||||
ApiUserStatus, ApiPhoto,
|
||||
ApiUserStatus, ApiPhoto, ApiChatReactions,
|
||||
} from '../../types';
|
||||
|
||||
import {
|
||||
@ -1266,7 +1266,7 @@ export async function importChatInvite({ hash }: { hash: string }) {
|
||||
export function setChatEnabledReactions({
|
||||
chat, enabledReactions,
|
||||
}: {
|
||||
chat: ApiChat; enabledReactions: string[];
|
||||
chat: ApiChat; enabledReactions?: ApiChatReactions;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.SetChatAvailableReactions({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
|
||||
@ -42,7 +42,7 @@ export {
|
||||
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
||||
fetchFeaturedEmojiStickers,
|
||||
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects,
|
||||
} from './symbols';
|
||||
|
||||
export {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiChat } from '../../types';
|
||||
import type { ApiChat, ApiReaction } from '../../types';
|
||||
import { invokeRequest } from './client';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
||||
@ -74,12 +74,12 @@ export async function getAvailableReactions() {
|
||||
}
|
||||
|
||||
export function sendReaction({
|
||||
chat, messageId, reaction,
|
||||
chat, messageId, reactions,
|
||||
}: {
|
||||
chat: ApiChat; messageId: number; reaction?: string;
|
||||
chat: ApiChat; messageId: number; reactions?: ApiReaction[];
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.SendReaction({
|
||||
...(reaction && { reaction: [buildInputReaction(reaction)] }),
|
||||
reaction: reactions?.map((r) => buildInputReaction(r)),
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
msgId: messageId,
|
||||
}), true);
|
||||
@ -99,7 +99,7 @@ export function fetchMessageReactions({
|
||||
export async function fetchMessageReactionsList({
|
||||
chat, messageId, reaction, offset,
|
||||
}: {
|
||||
chat: ApiChat; messageId: number; reaction?: string; offset?: string;
|
||||
chat: ApiChat; messageId: number; reaction?: ApiReaction; offset?: string;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.GetMessageReactionsList({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
@ -128,7 +128,7 @@ export async function fetchMessageReactionsList({
|
||||
export function setDefaultReaction({
|
||||
reaction,
|
||||
}: {
|
||||
reaction: string;
|
||||
reaction: ApiReaction;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.SetDefaultReaction({
|
||||
reaction: buildInputReaction(reaction),
|
||||
|
||||
@ -218,6 +218,21 @@ export async function fetchAnimatedEmojiEffects() {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGenericEmojiEffects() {
|
||||
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||
stickerset: new GramJs.InputStickerSetEmojiGenericAnimations(),
|
||||
}));
|
||||
|
||||
if (!(result instanceof GramJs.messages.StickerSet)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
set: buildStickerSet(result.set),
|
||||
stickers: processStickerResult(result.documents),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPremiumGifts() {
|
||||
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||
stickerset: new GramJs.InputStickerSetPremiumGifts(),
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { ApiMessage, ApiPhoto, ApiStickerSet } from './messages';
|
||||
import type {
|
||||
ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet,
|
||||
} from './messages';
|
||||
import type { ApiBotCommand } from './bots';
|
||||
import type { ApiChatInviteImporter } from './misc';
|
||||
import type { ApiFakeType, ApiUsername } from './users';
|
||||
@ -101,7 +103,7 @@ export interface ApiChatFullInfo {
|
||||
};
|
||||
linkedChatId?: string;
|
||||
botCommands?: ApiBotCommand[];
|
||||
enabledReactions?: string[];
|
||||
enabledReactions?: ApiChatReactions;
|
||||
sendAsId?: string;
|
||||
canViewStatistics?: boolean;
|
||||
recentRequesterIds?: string[];
|
||||
|
||||
@ -436,15 +436,15 @@ export interface ApiReactions {
|
||||
|
||||
export interface ApiUserReaction {
|
||||
userId: string;
|
||||
reaction: string;
|
||||
reaction: ApiReaction;
|
||||
isBig?: boolean;
|
||||
isUnread?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiReactionCount {
|
||||
isChosen?: boolean;
|
||||
chosenOrder?: number;
|
||||
count: number;
|
||||
reaction: string;
|
||||
reaction: ApiReaction;
|
||||
}
|
||||
|
||||
export interface ApiAvailableReaction {
|
||||
@ -454,12 +454,34 @@ export interface ApiAvailableReaction {
|
||||
staticIcon?: ApiDocument;
|
||||
centerIcon?: ApiDocument;
|
||||
aroundAnimation?: ApiDocument;
|
||||
reaction: string;
|
||||
reaction: ApiReactionEmoji;
|
||||
title: string;
|
||||
isInactive?: boolean;
|
||||
isPremium?: boolean;
|
||||
}
|
||||
|
||||
type ApiChatReactionsAll = {
|
||||
type: 'all';
|
||||
areCustomAllowed?: true;
|
||||
};
|
||||
|
||||
type ApiChatReactionsSome = {
|
||||
type: 'some';
|
||||
allowed: ApiReaction[];
|
||||
};
|
||||
|
||||
export type ApiChatReactions = ApiChatReactionsAll | ApiChatReactionsSome;
|
||||
|
||||
export type ApiReactionEmoji = {
|
||||
emoticon: string;
|
||||
};
|
||||
|
||||
export type ApiReactionCustomEmoji = {
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
|
||||
|
||||
export interface ApiThreadInfo {
|
||||
threadId: number;
|
||||
chatId: string;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiDocument, ApiPhoto } from './messages';
|
||||
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
|
||||
import type { ApiUser } from './users';
|
||||
import type { ApiLimitType } from '../../global/types';
|
||||
|
||||
@ -174,12 +174,14 @@ export interface ApiAppConfig {
|
||||
premiumPromoOrder: string[];
|
||||
defaultEmojiStatusesStickerSetId: string;
|
||||
maxUniqueReactions: number;
|
||||
maxUserReactionsDefault: number;
|
||||
maxUserReactionsPremium: number;
|
||||
limits: Record<ApiLimitType, readonly [number, number]>;
|
||||
}
|
||||
|
||||
export interface ApiConfig {
|
||||
expiresAt: number;
|
||||
defaultReaction?: string;
|
||||
defaultReaction?: ApiReaction;
|
||||
gifSearchUsername?: string;
|
||||
maxGroupSize: number;
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
.root, .media, .thumb {
|
||||
border-radius: 0 !important;
|
||||
border-radius: var(--custom-emoji-border-radius) !important;
|
||||
}
|
||||
|
||||
.highlightCatch {
|
||||
|
||||
@ -1,35 +1,62 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { getGlobal } from '../../global';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { isSameReaction } from '../../global/helpers';
|
||||
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
|
||||
import blankUrl from '../../assets/blank.png';
|
||||
import './ReactionStaticEmoji.scss';
|
||||
|
||||
type OwnProps = {
|
||||
reaction: string;
|
||||
ref?: RefObject<HTMLImageElement>;
|
||||
reaction: ApiReaction;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
className?: string;
|
||||
size?: number;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
const ReactionStaticEmoji: FC<OwnProps> = ({
|
||||
reaction,
|
||||
ref,
|
||||
availableReactions,
|
||||
className,
|
||||
size,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const staticIconId = getGlobal().availableReactions?.find((l) => l.reaction === reaction)?.staticIcon?.id;
|
||||
const isCustom = 'documentId' in reaction;
|
||||
const availableReaction = useMemo(() => (
|
||||
availableReactions?.find((available) => isSameReaction(available.reaction, reaction))
|
||||
), [availableReactions, reaction]);
|
||||
const staticIconId = availableReaction?.staticIcon?.id;
|
||||
const mediaData = useMedia(`document${staticIconId}`, !staticIconId, ApiMediaFormat.BlobUrl);
|
||||
|
||||
const transitionClassNames = useMediaTransition(mediaData);
|
||||
|
||||
if (isCustom) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
className={buildClassName('ReactionStaticEmoji', className)}
|
||||
size={size}
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={buildClassName('ReactionStaticEmoji', className)}
|
||||
ref={ref}
|
||||
src={mediaData}
|
||||
alt=""
|
||||
className={buildClassName('ReactionStaticEmoji', transitionClassNames, className)}
|
||||
style={size ? `width: ${size}px; height: ${size}px` : undefined}
|
||||
src={mediaData || blankUrl}
|
||||
alt={availableReaction?.title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,19 +7,22 @@ import { addCustomEmojiCallback, removeCustomEmojiCallback } from '../../../util
|
||||
|
||||
import useEnsureCustomEmoji from '../../../hooks/useEnsureCustomEmoji';
|
||||
|
||||
export default function useCustomEmoji(documentId: string) {
|
||||
const [customEmoji, setCustomEmoji] = useState<ApiSticker | undefined>(getGlobal().customEmojis.byId[documentId]);
|
||||
export default function useCustomEmoji(documentId?: string) {
|
||||
const [customEmoji, setCustomEmoji] = useState<ApiSticker | undefined>(
|
||||
documentId ? getGlobal().customEmojis.byId[documentId] : undefined,
|
||||
);
|
||||
|
||||
useEnsureCustomEmoji(documentId);
|
||||
|
||||
const handleGlobalChange = useCallback(() => {
|
||||
if (!documentId) return;
|
||||
setCustomEmoji(getGlobal().customEmojis.byId[documentId]);
|
||||
}, [documentId]);
|
||||
|
||||
useEffect(handleGlobalChange, [documentId, handleGlobalChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customEmoji) return undefined;
|
||||
if (customEmoji || !documentId) return undefined;
|
||||
|
||||
addCustomEmojiCallback(handleGlobalChange, documentId);
|
||||
|
||||
|
||||
@ -357,9 +357,7 @@
|
||||
}
|
||||
|
||||
.SettingsDefaultReaction {
|
||||
.ReactionStaticEmoji {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
.current-default-reaction {
|
||||
margin-inline-end: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiAvailableReaction } from '../../../api/types';
|
||||
|
||||
import { selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
@ -18,14 +16,12 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
isPremium?: boolean;
|
||||
selectedReaction?: string;
|
||||
};
|
||||
|
||||
const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
availableReactions,
|
||||
isPremium,
|
||||
selectedReaction,
|
||||
onReset,
|
||||
}) => {
|
||||
@ -36,17 +32,23 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const options = availableReactions?.filter((l) => (
|
||||
!(l.isInactive || (!isPremium && l.isPremium))
|
||||
)).map((l) => {
|
||||
return {
|
||||
label: <><ReactionStaticEmoji reaction={l.reaction} />{l.title}</>,
|
||||
value: l.reaction,
|
||||
};
|
||||
}) || [];
|
||||
const options = useMemo(() => (
|
||||
(availableReactions || []).filter((availableReaction) => !availableReaction.isInactive)
|
||||
.map((availableReaction) => ({
|
||||
label: (
|
||||
<>
|
||||
<ReactionStaticEmoji reaction={availableReaction.reaction} availableReactions={availableReactions} />
|
||||
{availableReaction.title}
|
||||
</>
|
||||
),
|
||||
value: availableReaction.reaction.emoticon,
|
||||
}))
|
||||
), [availableReactions]);
|
||||
|
||||
const handleChange = useCallback((reaction: string) => {
|
||||
setDefaultReaction({ reaction });
|
||||
setDefaultReaction({
|
||||
reaction: { emoticon: reaction },
|
||||
});
|
||||
}, [setDefaultReaction]);
|
||||
|
||||
return (
|
||||
@ -64,12 +66,10 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global) => {
|
||||
const { availableReactions, config } = global;
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
return {
|
||||
availableReactions,
|
||||
selectedReaction: config?.defaultReaction,
|
||||
isPremium,
|
||||
};
|
||||
},
|
||||
)(SettingsQuickReaction));
|
||||
|
||||
@ -6,10 +6,16 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
import type { ISettings } from '../../../types';
|
||||
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction,
|
||||
ApiReaction,
|
||||
ApiSticker,
|
||||
ApiStickerSet,
|
||||
} from '../../../api/types';
|
||||
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
@ -20,6 +26,8 @@ import Checkbox from '../../ui/Checkbox';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import StickerSetCard from '../../common/StickerSetCard';
|
||||
|
||||
const DEFAULT_REACTION_SIZE = 1.5 * REM;
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
@ -34,7 +42,8 @@ type StateProps =
|
||||
addedSetIds?: string[];
|
||||
customEmojiSetIds?: string[];
|
||||
stickerSetsById: Record<string, ApiStickerSet>;
|
||||
defaultReaction?: string;
|
||||
defaultReaction?: ApiReaction;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
};
|
||||
|
||||
const SettingsStickers: FC<OwnProps & StateProps> = ({
|
||||
@ -45,6 +54,7 @@ const SettingsStickers: FC<OwnProps & StateProps> = ({
|
||||
defaultReaction,
|
||||
shouldSuggestStickers,
|
||||
shouldLoopStickers,
|
||||
availableReactions,
|
||||
onReset,
|
||||
onScreenSelect,
|
||||
}) => {
|
||||
@ -109,7 +119,12 @@ const SettingsStickers: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.QuickReaction)}
|
||||
>
|
||||
<ReactionStaticEmoji reaction={defaultReaction} />
|
||||
<ReactionStaticEmoji
|
||||
reaction={defaultReaction}
|
||||
className="current-default-reaction"
|
||||
size={DEFAULT_REACTION_SIZE}
|
||||
availableReactions={availableReactions}
|
||||
/>
|
||||
<div className="title">{lang('DoubleTapSetting')}</div>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
@ -191,6 +191,7 @@ const Main: FC<StateProps> = ({
|
||||
loadAttachBots,
|
||||
loadContactList,
|
||||
loadCustomEmojis,
|
||||
loadGenericEmojiEffects,
|
||||
closePaymentModal,
|
||||
clearReceipt,
|
||||
checkAppVersion,
|
||||
@ -213,6 +214,7 @@ const Main: FC<StateProps> = ({
|
||||
loadAppConfig();
|
||||
loadAvailableReactions();
|
||||
loadAnimatedEmojis();
|
||||
loadGenericEmojiEffects();
|
||||
loadNotificationSettings();
|
||||
loadNotificationExceptions();
|
||||
loadTopInlineBots();
|
||||
@ -225,7 +227,7 @@ const Main: FC<StateProps> = ({
|
||||
}, [
|
||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
|
||||
loadPremiumGifts, checkAppVersion, loadConfig,
|
||||
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects,
|
||||
]);
|
||||
|
||||
// Language-based API calls
|
||||
|
||||
@ -17,7 +17,6 @@ import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import Button from '../../ui/Button';
|
||||
import PremiumLimitPreview from './common/PremiumLimitPreview';
|
||||
import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo';
|
||||
import PremiumFeaturePreviewReactions from './previews/PremiumFeaturePreviewReactions';
|
||||
import SliderDots from '../../common/SliderDots';
|
||||
import PremiumFeaturePreviewStickers from './previews/PremiumFeaturePreviewStickers';
|
||||
|
||||
@ -25,7 +24,7 @@ import styles from './PremiumFeatureModal.module.scss';
|
||||
|
||||
export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
||||
double_limits: 'PremiumPreviewLimits',
|
||||
unique_reactions: 'PremiumPreviewReactions',
|
||||
infinite_reactions: 'PremiumPreviewReactions2',
|
||||
premium_stickers: 'PremiumPreviewStickers',
|
||||
animated_emoji: 'PremiumPreviewEmoji',
|
||||
no_ads: 'PremiumPreviewNoAds',
|
||||
@ -39,7 +38,7 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
||||
|
||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
double_limits: 'PremiumPreviewLimitsDescription',
|
||||
unique_reactions: 'PremiumPreviewReactionsDescription',
|
||||
infinite_reactions: 'PremiumPreviewReactions2Description',
|
||||
premium_stickers: 'PremiumPreviewStickersDescription',
|
||||
no_ads: 'PremiumPreviewNoAdsDescription',
|
||||
animated_emoji: 'PremiumPreviewEmojiDescription',
|
||||
@ -57,7 +56,7 @@ export const PREMIUM_FEATURE_SECTIONS = [
|
||||
'faster_download',
|
||||
'voice_to_text',
|
||||
'no_ads',
|
||||
'unique_reactions',
|
||||
'infinite_reactions',
|
||||
'premium_stickers',
|
||||
'animated_emoji',
|
||||
'advanced_chat_management',
|
||||
@ -242,21 +241,6 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (section === 'unique_reactions') {
|
||||
return (
|
||||
<div className={styles.slide}>
|
||||
<div className={styles.frame}>
|
||||
<PremiumFeaturePreviewReactions isActive={currentSlideIndex === index} />
|
||||
</div>
|
||||
<h1 className={styles.title}>
|
||||
{lang(PREMIUM_FEATURE_TITLES.unique_reactions)}
|
||||
</h1>
|
||||
<div className={styles.description}>
|
||||
{renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS.unique_reactions), ['br'])}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (section === 'premium_stickers') {
|
||||
return (
|
||||
|
||||
@ -48,7 +48,7 @@ const LIMIT_ACCOUNTS = 4;
|
||||
|
||||
const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
||||
double_limits: PremiumLimits,
|
||||
unique_reactions: PremiumReactions,
|
||||
infinite_reactions: PremiumReactions,
|
||||
premium_stickers: PremiumStickers,
|
||||
animated_emoji: PremiumEmoji,
|
||||
no_ads: PremiumAds,
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sticker {
|
||||
--x: 0px;
|
||||
--y: 0px;
|
||||
--scale: 0;
|
||||
transition: 0.25s ease-in-out transform, 0.25s ease-in-out opacity;
|
||||
position: absolute;
|
||||
transform:
|
||||
translate(var(--x), var(--y))
|
||||
scale(var(--scale));
|
||||
opacity: var(--scale);
|
||||
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.effect-sticker {
|
||||
composes: sticker;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
@ -1,161 +0,0 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { GlobalState } from '../../../../global/types';
|
||||
import type { ApiAvailableReaction } from '../../../../api/types';
|
||||
|
||||
import cycleRestrict from '../../../../util/cycleRestrict';
|
||||
import useMedia from '../../../../hooks/useMedia';
|
||||
import useInterval from '../../../../hooks/useInterval';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
|
||||
import AnimatedSticker from '../../../common/AnimatedSticker';
|
||||
|
||||
import styles from './PremiumFeaturePreviewReactions.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
availableReactions: GlobalState['availableReactions'];
|
||||
};
|
||||
|
||||
const EMOJI_SIZE_MULTIPLIER = 0.2;
|
||||
const EFFECT_SIZE_MULTIPLIER = 0.6;
|
||||
const ROTATE_INTERVAL = 3000;
|
||||
const CLICK_DELAY = 4000;
|
||||
const MAX_EMOJIS = 15;
|
||||
|
||||
const AnimatedCircleReaction: FC<{
|
||||
size: number;
|
||||
realIndex: number;
|
||||
reaction: ApiAvailableReaction;
|
||||
index: number;
|
||||
maxLength: number;
|
||||
handleClick: (index: number) => void;
|
||||
isActivated: boolean;
|
||||
canPlay: boolean;
|
||||
}> = ({
|
||||
size, realIndex, isActivated, canPlay,
|
||||
reaction, index, maxLength, handleClick,
|
||||
}) => {
|
||||
const mediaData = useMedia(`document${reaction.activateAnimation?.id}`);
|
||||
const mediaDataAround = useMedia(`document${reaction.aroundAnimation?.id}`);
|
||||
const [isAnimated, animate, inanimate] = useFlag(isActivated);
|
||||
const [isEffectEnded, markEffectEnded, unmarkEffectEnded] = useFlag(false);
|
||||
|
||||
const circleSize = (size - size * EMOJI_SIZE_MULTIPLIER) / 2;
|
||||
|
||||
const t = index / maxLength;
|
||||
const angle = t * (Math.PI * 2);
|
||||
const totalAngle = angle - (Math.PI / 6) * Math.cos(angle);
|
||||
const scaleNotFull = 0.2 + (0.7 * (Math.sin(totalAngle) + 1)) / 2;
|
||||
const scale = scaleNotFull > 0.85 ? 1 : scaleNotFull;
|
||||
|
||||
const x = Math.cos(totalAngle) * circleSize;
|
||||
const y = Math.sin(totalAngle) * circleSize * 0.6;
|
||||
|
||||
const handleClickEmoji = useCallback(() => {
|
||||
handleClick(realIndex);
|
||||
}, [handleClick, realIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActivated) {
|
||||
animate();
|
||||
unmarkEffectEnded();
|
||||
}
|
||||
}, [isActivated, animate, unmarkEffectEnded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isActivated && !isEffectEnded && (
|
||||
<AnimatedSticker
|
||||
className={styles.effectSticker}
|
||||
tgsUrl={mediaDataAround}
|
||||
play={canPlay}
|
||||
isLowPriority
|
||||
noLoop
|
||||
size={EFFECT_SIZE_MULTIPLIER * size}
|
||||
style={`--x: ${x}px; --y: ${y}px; --scale: ${scale};`}
|
||||
onEnded={markEffectEnded}
|
||||
/>
|
||||
)}
|
||||
<AnimatedSticker
|
||||
className={styles.sticker}
|
||||
tgsUrl={mediaData}
|
||||
onClick={handleClickEmoji}
|
||||
play={isAnimated && canPlay}
|
||||
noLoop
|
||||
size={EMOJI_SIZE_MULTIPLIER * size}
|
||||
style={`--x: ${x}px; --y: ${y}px; --scale: ${scale};`}
|
||||
onEnded={inanimate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const PremiumFeaturePreviewReactions: FC<OwnProps & StateProps> = ({
|
||||
availableReactions, isActive,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isIntervalPaused, pauseInterval, unpauseInterval] = useFlag();
|
||||
const lastUnpauseTimeout = useRef<NodeJS.Timeout>();
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [size, setSize] = useState(0);
|
||||
|
||||
const renderedReactions = availableReactions?.filter((l) => l.isPremium)?.slice(0, MAX_EMOJIS) || [];
|
||||
|
||||
useInterval(() => {
|
||||
setOffset((current) => cycleRestrict(renderedReactions.length, current + 1));
|
||||
}, isIntervalPaused || !isActive ? undefined : ROTATE_INTERVAL);
|
||||
|
||||
const handleClickEmoji = useCallback((i: number) => {
|
||||
setOffset(i);
|
||||
pauseInterval();
|
||||
if (lastUnpauseTimeout.current) clearTimeout(lastUnpauseTimeout.current);
|
||||
lastUnpauseTimeout.current = setTimeout(() => {
|
||||
unpauseInterval();
|
||||
}, CLICK_DELAY);
|
||||
}, [pauseInterval, unpauseInterval]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
setSize(container.closest('.modal-dialog')!.clientWidth);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
ref={containerRef}
|
||||
>
|
||||
{renderedReactions.map((l, i) => {
|
||||
return (
|
||||
<AnimatedCircleReaction
|
||||
size={size}
|
||||
reaction={l}
|
||||
realIndex={i}
|
||||
index={(i - offset + renderedReactions.length / 4) % renderedReactions.length}
|
||||
maxLength={renderedReactions.length}
|
||||
handleClick={handleClickEmoji}
|
||||
isActivated={offset === i}
|
||||
canPlay={isActive}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
return {
|
||||
availableReactions: global.availableReactions,
|
||||
};
|
||||
},
|
||||
)(PremiumFeaturePreviewReactions));
|
||||
@ -242,7 +242,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
if (!messageIds || !messagesById) {
|
||||
return;
|
||||
}
|
||||
const ids = messageIds.filter((l) => messagesById[l]?.reactions);
|
||||
const ids = messageIds.filter((id) => messagesById[id]?.reactions);
|
||||
|
||||
if (!ids.length) return;
|
||||
|
||||
@ -592,7 +592,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
isViewportNewest={Boolean(isViewportNewest)}
|
||||
isUnread={Boolean(firstUnreadId)}
|
||||
withUsers={withUsers}
|
||||
areReactionsInMeta={isPrivate}
|
||||
noAvatars={noAvatars}
|
||||
containerRef={containerRef}
|
||||
anchorIdRef={anchorIdRef}
|
||||
|
||||
@ -39,7 +39,6 @@ interface OwnProps {
|
||||
threadId: number;
|
||||
type: MessageListType;
|
||||
isReady: boolean;
|
||||
areReactionsInMeta: boolean;
|
||||
isScrollingRef: { current: boolean | undefined };
|
||||
isScrollPatchNeededRef: { current: boolean | undefined };
|
||||
threadTopMessageId: number | undefined;
|
||||
@ -60,7 +59,6 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
isViewportNewest,
|
||||
isUnread,
|
||||
withUsers,
|
||||
areReactionsInMeta,
|
||||
noAvatars,
|
||||
containerRef,
|
||||
anchorIdRef,
|
||||
@ -202,7 +200,6 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
noAvatars={noAvatars}
|
||||
withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)}
|
||||
withSenderName={position.isFirstInGroup && withUsers && !isOwn}
|
||||
areReactionsInMeta={areReactionsInMeta}
|
||||
threadId={threadId}
|
||||
messageListType={type}
|
||||
noComments={hasLinkedChat === false}
|
||||
|
||||
@ -11,6 +11,12 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-heart {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.reaction-filter-emoji {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@ -4,17 +4,19 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ApiAvailableReaction, ApiMessage, ApiReaction } from '../../api/types';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
import { LoadMoreDirection } from '../../types';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { selectChatMessage } from '../../global/selectors';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatIntegerCompact } from '../../util/textFormat';
|
||||
import { unique } from '../../util/iteratees';
|
||||
import { isSameReaction, getReactionUniqueKey } from '../../global/helpers';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import Modal from '../ui/Modal';
|
||||
@ -37,6 +39,7 @@ export type StateProps = Pick<ApiMessage, 'reactors' | 'reactions' | 'seenByUser
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
animationLevel: AnimationLevel;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
};
|
||||
|
||||
const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
@ -47,6 +50,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
messageId,
|
||||
seenByUserIds,
|
||||
animationLevel,
|
||||
availableReactions,
|
||||
}) => {
|
||||
const {
|
||||
loadReactors,
|
||||
@ -59,7 +63,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
const [isClosing, startClosing, stopClosing] = useFlag(false);
|
||||
const [chosenTab, setChosenTab] = useState<string | undefined>(undefined);
|
||||
const [chosenTab, setChosenTab] = useState<ApiReaction | undefined>(undefined);
|
||||
const canShowFilters = reactors && reactions && reactors.count >= MIN_REACTIONS_COUNT_FOR_FILTERS
|
||||
&& reactions.results.length > 1;
|
||||
const chatIdRef = useRef<string>();
|
||||
@ -99,14 +103,22 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
}, [chatId, loadReactors, messageId]);
|
||||
|
||||
const allReactions = useMemo(() => {
|
||||
return reactors?.reactions ? unique(reactors.reactions.map((l) => l.reaction)) : [];
|
||||
const uniqueReactions: ApiReaction[] = [];
|
||||
reactors?.reactions?.forEach(({ reaction }) => {
|
||||
if (!uniqueReactions.some((r) => isSameReaction(r, reaction))) {
|
||||
uniqueReactions.push(reaction);
|
||||
}
|
||||
});
|
||||
return uniqueReactions;
|
||||
}, [reactors]);
|
||||
|
||||
const userIds = useMemo(() => {
|
||||
if (chosenTab) {
|
||||
return reactors?.reactions.filter((l) => l.reaction === chosenTab).map((l) => l.userId);
|
||||
return reactors?.reactions
|
||||
.filter(({ reaction }) => isSameReaction(reaction, chosenTab))
|
||||
.map(({ userId }) => userId);
|
||||
}
|
||||
return unique(reactors?.reactions.map((l) => l.userId).concat(seenByUserIds || []) || []);
|
||||
return unique(reactors?.reactions.map(({ userId }) => userId).concat(seenByUserIds || []) || []);
|
||||
}, [chosenTab, reactors, seenByUserIds]);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(
|
||||
@ -138,17 +150,22 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
{reactors?.count && formatIntegerCompact(reactors.count)}
|
||||
</Button>
|
||||
{allReactions.map((reaction) => {
|
||||
const count = reactions?.results.find((l) => l.reaction === reaction)?.count;
|
||||
const count = reactions?.results
|
||||
.find((reactionsCount) => isSameReaction(reactionsCount.reaction, reaction))?.count;
|
||||
return (
|
||||
<Button
|
||||
key={reaction}
|
||||
className={buildClassName(chosenTab === reaction && 'chosen')}
|
||||
key={getReactionUniqueKey(reaction)}
|
||||
className={buildClassName(isSameReaction(chosenTab, reaction) && 'chosen')}
|
||||
size="tiny"
|
||||
ripple
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setChosenTab(reaction)}
|
||||
>
|
||||
<ReactionStaticEmoji reaction={reaction} className="reaction-filter-emoji" />
|
||||
<ReactionStaticEmoji
|
||||
reaction={reaction}
|
||||
className="reaction-filter-emoji"
|
||||
availableReactions={availableReactions}
|
||||
/>
|
||||
{count && formatIntegerCompact(count)}
|
||||
</Button>
|
||||
);
|
||||
@ -166,19 +183,26 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
{viewportIds?.flatMap(
|
||||
(userId) => {
|
||||
const user = usersById[userId];
|
||||
const userReactions = reactors?.reactions.filter((l) => l.userId === userId);
|
||||
const userReactions = reactors?.reactions.filter((reactor) => reactor.userId === userId);
|
||||
const items: React.ReactNode[] = [];
|
||||
userReactions?.forEach((r) => {
|
||||
if (chosenTab && !isSameReaction(r.reaction, chosenTab)) return;
|
||||
items.push(
|
||||
<ListItem
|
||||
key={`${userId}-${r.reaction}`}
|
||||
key={`${userId}-${getReactionUniqueKey(r.reaction)}`}
|
||||
className="chat-item-clickable reactors-list-item"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleClick(userId)}
|
||||
>
|
||||
<Avatar user={user} size="small" animationLevel={animationLevel} withVideo />
|
||||
<FullNameTitle peer={user} withEmojiStatus />
|
||||
{r.reaction && <ReactionStaticEmoji className="reactors-list-emoji" reaction={r.reaction} />}
|
||||
{r.reaction && (
|
||||
<ReactionStaticEmoji
|
||||
className="reactors-list-emoji"
|
||||
reaction={r.reaction}
|
||||
availableReactions={availableReactions}
|
||||
/>
|
||||
)}
|
||||
</ListItem>,
|
||||
);
|
||||
});
|
||||
|
||||
@ -160,7 +160,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isCurrentUserPremium) {
|
||||
const addedPremiumStickers = existingAddedSetIds
|
||||
.map((l) => l.stickers?.filter((sticker) => sticker.hasEffect))
|
||||
.map(({ stickers }) => stickers?.filter((sticker) => sticker.hasEffect))
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { MessageListType } from '../../../global/types';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet,
|
||||
ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet, ApiChatReactions, ApiReaction,
|
||||
} from '../../../api/types';
|
||||
import type { IAlbum, IAnchorPosition } from '../../../types';
|
||||
|
||||
@ -28,7 +28,6 @@ import {
|
||||
} from '../../../global/helpers';
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID, TME_LINK_PREFIX } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
@ -42,8 +41,6 @@ import PinMessageModal from '../../common/PinMessageModal';
|
||||
import MessageContextMenu from './MessageContextMenu';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
const START_SIZE = 2 * REM;
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
chatUsername?: string;
|
||||
@ -67,7 +64,6 @@ type StateProps = {
|
||||
canShowReactionsCount?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
canShowReactionList?: boolean;
|
||||
canRemoveReaction?: boolean;
|
||||
canUnpin?: boolean;
|
||||
canDelete?: boolean;
|
||||
canReport?: boolean;
|
||||
@ -87,7 +83,7 @@ type StateProps = {
|
||||
canClosePoll?: boolean;
|
||||
activeDownloads: number[];
|
||||
canShowSeenBy?: boolean;
|
||||
enabledReactions?: string[];
|
||||
enabledReactions?: ApiChatReactions;
|
||||
canScheduleUntilOnline?: boolean;
|
||||
maxUniqueReactions?: number;
|
||||
};
|
||||
@ -115,7 +111,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canReport,
|
||||
canShowReactionsCount,
|
||||
canShowReactionList,
|
||||
canRemoveReaction,
|
||||
canEdit,
|
||||
enabledReactions,
|
||||
maxUniqueReactions,
|
||||
@ -150,7 +145,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
cancelMessageMediaDownload,
|
||||
loadSeenBy,
|
||||
openSeenByModal,
|
||||
sendReaction,
|
||||
openReactorListModal,
|
||||
loadFullChat,
|
||||
loadReactors,
|
||||
@ -159,6 +153,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
loadStickers,
|
||||
cancelPollVote,
|
||||
closePoll,
|
||||
toggleReaction,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
@ -376,12 +371,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
}, [closeMenu, message, saveGif]);
|
||||
|
||||
const handleSendReaction = useCallback((reaction: string | undefined, x: number, y: number) => {
|
||||
sendReaction({
|
||||
chatId: message.chatId, messageId: message.id, reaction, x, y, startSize: START_SIZE,
|
||||
const handleToggleReaction = useCallback((reaction: ApiReaction) => {
|
||||
toggleReaction({
|
||||
chatId: message.chatId, messageId: message.id, reaction,
|
||||
});
|
||||
closeMenu();
|
||||
}, [closeMenu, message.chatId, message.id, sendReaction]);
|
||||
}, [closeMenu, message, toggleReaction]);
|
||||
|
||||
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
||||
|
||||
@ -408,7 +403,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
anchor={anchor}
|
||||
canShowReactionsCount={canShowReactionsCount}
|
||||
canShowReactionList={canShowReactionList}
|
||||
canRemoveReaction={canRemoveReaction}
|
||||
canSendNow={canSendNow}
|
||||
canReschedule={canReschedule}
|
||||
canReply={canReply}
|
||||
@ -453,7 +447,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
onCancelVote={handleCancelVote}
|
||||
onClosePoll={openClosePollDialog}
|
||||
onShowSeenBy={handleOpenSeenByModal}
|
||||
onSendReaction={handleSendReaction}
|
||||
onToggleReaction={handleToggleReaction}
|
||||
onShowReactors={handleOpenReactorListModal}
|
||||
/>
|
||||
<DeleteMessageModal
|
||||
@ -529,7 +523,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isAction = isActionMessage(message);
|
||||
const canShowReactionsCount = !isLocal && !isChannel && !isScheduled && !isAction && !isPrivate && message.reactions
|
||||
&& !areReactionsEmpty(message.reactions) && message.reactions.canSeeList;
|
||||
const canRemoveReaction = isPrivate && message.reactions?.results?.some((l) => l.isChosen);
|
||||
const isProtected = selectIsMessageProtected(global, message);
|
||||
const canCopyNumber = Boolean(message.content.contact);
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
@ -569,7 +562,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
hasFullInfo: Boolean(chat?.fullInfo),
|
||||
canShowReactionsCount,
|
||||
canShowReactionList: !isLocal && !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID,
|
||||
canRemoveReaction,
|
||||
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
|
||||
customEmojiSetsInfo,
|
||||
customEmojiSets,
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
.root {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
offset-path: var(--offset-path);
|
||||
offset-rotate: 0deg;
|
||||
animation: 1.5s particle ease-out;
|
||||
}
|
||||
|
||||
@keyframes particle {
|
||||
0% {
|
||||
offset-distance: 0%;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
offset-distance: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
56
src/components/middle/message/CustomReactionAnimation.tsx
Normal file
56
src/components/middle/message/CustomReactionAnimation.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiReactionCustomEmoji } from '../../../api/types';
|
||||
|
||||
import { getStickerPreviewHash } from '../../../global/helpers';
|
||||
import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/environment';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
import styles from './CustomReactionAnimation.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
reaction: ApiReactionCustomEmoji;
|
||||
};
|
||||
|
||||
const EFFECT_AMOUNT = 7;
|
||||
|
||||
const CustomReactionAnimation: FC<OwnProps> = ({
|
||||
reaction,
|
||||
}) => {
|
||||
const stickerHash = getStickerPreviewHash(reaction.documentId);
|
||||
|
||||
const previewMediaData = useMedia(stickerHash);
|
||||
|
||||
const paths: string[] = useMemo(() => {
|
||||
if (!IS_OFFSET_PATH_SUPPORTED) return [];
|
||||
return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath());
|
||||
}, []);
|
||||
|
||||
if (!previewMediaData) return undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{paths.map((path) => {
|
||||
const style = `--offset-path: path('${path}');`;
|
||||
return (
|
||||
<img
|
||||
src={previewMediaData}
|
||||
alt=""
|
||||
className={styles.particle}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomReactionAnimation);
|
||||
|
||||
function generateRandomDropPath() {
|
||||
const x = (10 + Math.random() * 60) * (Math.random() > 0.5 ? 1 : -1);
|
||||
const y = 20 + Math.random() * 80;
|
||||
|
||||
return `M 0 0 C 0 0 ${x} ${-y - 20} ${x} ${y}`;
|
||||
}
|
||||
@ -72,6 +72,8 @@
|
||||
}
|
||||
|
||||
.quick-reaction {
|
||||
--custom-emoji-size: 2rem;
|
||||
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -0.5rem;
|
||||
@ -79,7 +81,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: scale(1);
|
||||
transform: scale(0.75);
|
||||
opacity: 0;
|
||||
transition: transform 0.2s ease-out, opacity 0.2s ease-out;
|
||||
transition-delay: 0.2s;
|
||||
@ -90,16 +92,12 @@
|
||||
|
||||
&:hover {
|
||||
transition-delay: unset;
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
.ReactionStaticEmoji {
|
||||
width: 1.125rem;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.last-in-list .quick-reaction:hover {
|
||||
transform: translateY(-0.1875rem) scale(1.4);
|
||||
transform: translateY(-0.1875rem) scale(1);
|
||||
}
|
||||
|
||||
&.own .quick-reaction {
|
||||
|
||||
@ -19,6 +19,8 @@ import type {
|
||||
ApiAvailableReaction,
|
||||
ApiChatMember,
|
||||
ApiUsername,
|
||||
ApiReaction,
|
||||
ApiStickerSet,
|
||||
} from '../../../api/types';
|
||||
import type {
|
||||
AnimationLevel, FocusDirection, IAlbum, ISettings,
|
||||
@ -83,7 +85,11 @@ import {
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import { calculateDimensionsForMessageMedia, ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
|
||||
import {
|
||||
calculateDimensionsForMessageMedia,
|
||||
REM,
|
||||
ROUND_VIDEO_DIMENSIONS_PX,
|
||||
} from '../../common/helpers/mediaDimensions';
|
||||
import { buildContentClassName } from './helpers/buildContentClassName';
|
||||
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
|
||||
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
@ -153,7 +159,6 @@ type OwnProps =
|
||||
noAvatars?: boolean;
|
||||
withAvatar?: boolean;
|
||||
withSenderName?: boolean;
|
||||
areReactionsInMeta?: boolean;
|
||||
threadId: number;
|
||||
messageListType: MessageListType;
|
||||
noComments: boolean;
|
||||
@ -194,6 +199,7 @@ type StateProps = {
|
||||
highlight?: string;
|
||||
animatedEmoji?: string;
|
||||
animatedCustomEmoji?: string;
|
||||
genericEffects?: ApiStickerSet;
|
||||
isInSelectMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
isGroupSelected?: boolean;
|
||||
@ -207,8 +213,8 @@ type StateProps = {
|
||||
threadInfo?: ApiThreadInfo;
|
||||
reactionMessage?: ApiMessage;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
defaultReaction?: string;
|
||||
activeReaction?: ActiveReaction;
|
||||
defaultReaction?: ApiReaction;
|
||||
activeReactions?: ActiveReaction[];
|
||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||
hasUnreadReaction?: boolean;
|
||||
isTranscribing?: boolean;
|
||||
@ -226,7 +232,6 @@ type MetaPosition =
|
||||
type ReactionsPosition =
|
||||
'inside'
|
||||
| 'outside'
|
||||
| 'in-meta'
|
||||
| 'none';
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
@ -236,6 +241,7 @@ const APPENDIX_OWN = { __html: '<svg width="9" height="20" xmlns="http://www.w3.
|
||||
const APPENDIX_NOT_OWN = { __html: '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>' };
|
||||
const APPEARANCE_DELAY = 10;
|
||||
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
||||
const QUICK_REACTION_SIZE = 2 * REM;
|
||||
|
||||
const Message: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
@ -247,7 +253,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
noAvatars,
|
||||
withAvatar,
|
||||
withSenderName,
|
||||
areReactionsInMeta,
|
||||
noComments,
|
||||
appearanceOrder,
|
||||
isFirstInGroup,
|
||||
@ -288,6 +293,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
highlight,
|
||||
animatedEmoji,
|
||||
animatedCustomEmoji,
|
||||
genericEffects,
|
||||
isInSelectMode,
|
||||
isSelected,
|
||||
isGroupSelected,
|
||||
@ -295,7 +301,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
reactionMessage,
|
||||
availableReactions,
|
||||
defaultReaction,
|
||||
activeReaction,
|
||||
activeReactions,
|
||||
activeEmojiInteractions,
|
||||
messageListType,
|
||||
isPinnedList,
|
||||
@ -502,7 +508,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
Boolean(message.inlineButtons) && 'has-inline-buttons',
|
||||
isSwiped && 'is-swiped',
|
||||
transitionClassNames,
|
||||
(Boolean(activeReaction) || hasActiveStickerEffect) && 'has-active-reaction',
|
||||
(Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction',
|
||||
);
|
||||
|
||||
const {
|
||||
@ -545,9 +551,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
let reactionsPosition!: ReactionsPosition;
|
||||
if (areReactionsInMeta) {
|
||||
reactionsPosition = 'in-meta';
|
||||
} else if (hasReactions) {
|
||||
if (hasReactions) {
|
||||
if (isCustomShape || ((photo || video) && !hasText)) {
|
||||
reactionsPosition = 'outside';
|
||||
} else if (asForwarded) {
|
||||
@ -655,13 +659,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const meta = (
|
||||
<MessageMeta
|
||||
message={message}
|
||||
reactionMessage={reactionMessage}
|
||||
outgoingStatus={outgoingStatus}
|
||||
signature={signature}
|
||||
withReactions={reactionsPosition === 'in-meta'}
|
||||
withReactionOffset={reactionsPosition === 'inside'}
|
||||
availableReactions={availableReactions}
|
||||
activeReaction={activeReaction}
|
||||
onClick={handleMetaClick}
|
||||
/>
|
||||
);
|
||||
@ -672,10 +673,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<Reactions
|
||||
activeReaction={activeReaction}
|
||||
activeReactions={activeReactions}
|
||||
message={reactionMessage!}
|
||||
metaChildren={meta}
|
||||
availableReactions={availableReactions}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1100,10 +1103,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
{withQuickReactionButton && (
|
||||
<div
|
||||
className={buildClassName('quick-reaction', isQuickReactionVisible && !activeReaction && 'visible')}
|
||||
className={buildClassName('quick-reaction', isQuickReactionVisible && !activeReactions && 'visible')}
|
||||
onClick={handleSendQuickReaction}
|
||||
>
|
||||
<ReactionStaticEmoji reaction={defaultReaction!} />
|
||||
<ReactionStaticEmoji
|
||||
reaction={defaultReaction}
|
||||
size={QUICK_REACTION_SIZE}
|
||||
availableReactions={availableReactions}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -1114,8 +1122,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
<Reactions
|
||||
message={reactionMessage!}
|
||||
isOutside
|
||||
activeReaction={activeReaction}
|
||||
activeReactions={activeReactions}
|
||||
availableReactions={availableReactions}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -1260,7 +1270,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
threadInfo: actualThreadInfo,
|
||||
availableReactions: global.availableReactions,
|
||||
defaultReaction: isMessageLocal(message) ? undefined : selectDefaultReaction(global, chatId),
|
||||
activeReaction: reactionMessage && global.activeReactions[reactionMessage.id],
|
||||
activeReactions: reactionMessage && global.activeReactions[reactionMessage.id],
|
||||
activeEmojiInteractions: global.activeEmojiInteractions,
|
||||
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
||||
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
||||
@ -1271,6 +1281,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isPremium: selectIsCurrentUserPremium(global),
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
senderAdminMember,
|
||||
genericEffects: global.genericEmojiEffects,
|
||||
};
|
||||
},
|
||||
)(Message));
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React, {
|
||||
memo, useMemo, useCallback, useEffect, useRef,
|
||||
memo, useCallback, useEffect, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiMessage, ApiSponsoredMessage, ApiStickerSet, ApiUser,
|
||||
ApiAvailableReaction, ApiChatReactions, ApiMessage, ApiReaction, ApiSponsoredMessage, ApiStickerSet, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
@ -35,7 +35,7 @@ type OwnProps = {
|
||||
anchor: IAnchorPosition;
|
||||
message: ApiMessage | ApiSponsoredMessage;
|
||||
canSendNow?: boolean;
|
||||
enabledReactions?: string[];
|
||||
enabledReactions?: ApiChatReactions;
|
||||
maxUniqueReactions?: number;
|
||||
canReschedule?: boolean;
|
||||
canReply?: boolean;
|
||||
@ -45,7 +45,6 @@ type OwnProps = {
|
||||
canReport?: boolean;
|
||||
canShowReactionsCount?: boolean;
|
||||
canShowReactionList?: boolean;
|
||||
canRemoveReaction?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
canEdit?: boolean;
|
||||
canForward?: boolean;
|
||||
@ -90,7 +89,7 @@ type OwnProps = {
|
||||
onShowReactors?: () => void;
|
||||
onAboutAds?: () => void;
|
||||
onSponsoredHide?: () => void;
|
||||
onSendReaction?: (reaction: string | undefined, x: number, y: number) => void;
|
||||
onToggleReaction?: (reaction: ApiReaction) => void;
|
||||
};
|
||||
|
||||
const SCROLLBAR_WIDTH = 10;
|
||||
@ -128,7 +127,6 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
isDownloading,
|
||||
canShowSeenBy,
|
||||
canShowReactionsCount,
|
||||
canRemoveReaction,
|
||||
canShowReactionList,
|
||||
seenByRecentUsers,
|
||||
hasCustomEmoji,
|
||||
@ -155,7 +153,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
onClosePoll,
|
||||
onShowSeenBy,
|
||||
onShowReactors,
|
||||
onSendReaction,
|
||||
onToggleReaction,
|
||||
onCopyMessages,
|
||||
onAboutAds,
|
||||
onSponsoredHide,
|
||||
@ -166,18 +164,13 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const noReactions = !isPrivate && !enabledReactions?.length;
|
||||
const noReactions = !isPrivate && !enabledReactions;
|
||||
const withReactions = canShowReactionList && !noReactions;
|
||||
const isSponsoredMessage = !('id' in message);
|
||||
const messageId = !isSponsoredMessage ? message.id : '';
|
||||
|
||||
const [isReady, markIsReady, unmarkIsReady] = useFlag();
|
||||
|
||||
const currentReactions = useMemo(() => {
|
||||
if (isSponsoredMessage) return undefined;
|
||||
return message.reactions?.results.map((reaction) => reaction.reaction);
|
||||
}, [isSponsoredMessage, message]);
|
||||
|
||||
const handleAfterCopy = useCallback(() => {
|
||||
showNotification({
|
||||
message: lang('Share.Link.Copied'),
|
||||
@ -239,10 +232,6 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
};
|
||||
}, [withReactions]);
|
||||
|
||||
const handleRemoveReaction = useCallback(() => {
|
||||
onSendReaction!(undefined, 0, 0);
|
||||
}, [onSendReaction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
unmarkIsReady();
|
||||
@ -280,12 +269,12 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
>
|
||||
{canShowReactionList && (
|
||||
{withReactions && (
|
||||
<ReactionSelector
|
||||
enabledReactions={enabledReactions}
|
||||
currentReactions={currentReactions}
|
||||
currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined}
|
||||
maxUniqueReactions={maxUniqueReactions}
|
||||
onSendReaction={onSendReaction!}
|
||||
onToggleReaction={onToggleReaction!}
|
||||
isPrivate={isPrivate}
|
||||
availableReactions={availableReactions}
|
||||
isReady={isReady}
|
||||
@ -299,7 +288,6 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
style={menuStyle}
|
||||
ref={scrollableRef}
|
||||
>
|
||||
{canRemoveReaction && <MenuItem icon="heart-outline" onClick={handleRemoveReaction}>Remove Reaction</MenuItem>}
|
||||
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
||||
{canReschedule && (
|
||||
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
||||
|
||||
@ -13,12 +13,6 @@
|
||||
max-width: 100%;
|
||||
user-select: none;
|
||||
|
||||
.ReactionAnimatedEmoji {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.message-time,
|
||||
.message-imported,
|
||||
.message-signature,
|
||||
|
||||
@ -3,7 +3,6 @@ import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
|
||||
import { formatDateTimeToString, formatTime } from '../../../util/dateFormat';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
@ -14,32 +13,29 @@ import useFlag from '../../../hooks/useFlag';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import MessageOutgoingStatus from '../../common/MessageOutgoingStatus';
|
||||
import ReactionAnimatedEmoji from './ReactionAnimatedEmoji';
|
||||
|
||||
import './MessageMeta.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
reactionMessage?: ApiMessage;
|
||||
withReactions?: boolean;
|
||||
withReactionOffset?: boolean;
|
||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||
signature?: string;
|
||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
activeReaction?: ActiveReaction;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
};
|
||||
|
||||
const MessageMeta: FC<OwnProps> = ({
|
||||
message, outgoingStatus, signature, onClick, withReactions,
|
||||
activeReaction, withReactionOffset, availableReactions,
|
||||
reactionMessage,
|
||||
message,
|
||||
outgoingStatus,
|
||||
signature,
|
||||
withReactionOffset,
|
||||
onClick,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
const lang = useLang();
|
||||
const [isActivated, markActivated] = useFlag();
|
||||
|
||||
const reactions = withReactions && reactionMessage?.reactions?.results.filter((l) => l.count > 0);
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@ -80,14 +76,6 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
onClick={onClick}
|
||||
data-ignore-on-paste
|
||||
>
|
||||
{reactions && reactions.map((l) => (
|
||||
<ReactionAnimatedEmoji
|
||||
activeReaction={activeReaction}
|
||||
reaction={l.reaction}
|
||||
isInMeta
|
||||
availableReactions={availableReactions}
|
||||
/>
|
||||
))}
|
||||
{Boolean(message.views) && (
|
||||
<>
|
||||
<span className="message-views">
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
.root {
|
||||
--custom-emoji-border-radius: 0.25rem;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
&.is-custom-emoji {
|
||||
margin-right: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.animated-icon, .effect {
|
||||
position: fixed;
|
||||
top: -0.375rem;
|
||||
left: -0.375rem;
|
||||
pointer-events: none;
|
||||
|
||||
&.effect {
|
||||
top: -2.5rem;
|
||||
left: -2.5rem;
|
||||
}
|
||||
|
||||
&:not(:global(.open)) {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:global(.closing) {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.animating {
|
||||
// Fix for redundant scroll on iOS
|
||||
transform: translateZ(0);
|
||||
// Fix for redundant scroll in Firefox
|
||||
contain: layout;
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
.ReactionAnimatedEmoji {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-animating {
|
||||
// Fix for redundant scroll on iOS
|
||||
transform: translateZ(0);
|
||||
// Fix for redundant scroll in Firefox
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.AnimatedSticker {
|
||||
position: fixed;
|
||||
top: -0.375rem;
|
||||
left: -0.375rem;
|
||||
pointer-events: none;
|
||||
|
||||
&.effect {
|
||||
top: -2.5rem;
|
||||
left: -2.5rem;
|
||||
}
|
||||
|
||||
&:not(.open) {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&.closing {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.in-meta {
|
||||
.AnimatedSticker {
|
||||
top: -0.4375rem;
|
||||
left: -0.4375rem;
|
||||
|
||||
&.effect {
|
||||
top: -2.5625rem;
|
||||
left: -2.5625rem;
|
||||
}
|
||||
|
||||
// Fix for weird positioning in Chrome
|
||||
canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,41 +1,87 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
import type { ApiAvailableReaction } from '../../../api/types';
|
||||
import type { ApiAvailableReaction, ApiReaction, ApiStickerSet } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { isSameReaction } from '../../../global/helpers';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useCustomEmoji from '../../common/hooks/useCustomEmoji';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||
import CustomReactionAnimation from './CustomReactionAnimation';
|
||||
|
||||
import './ReactionAnimatedEmoji.scss';
|
||||
import styles from './ReactionAnimatedEmoji.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
reaction: string;
|
||||
activeReaction?: ActiveReaction;
|
||||
isInMeta?: boolean;
|
||||
reaction: ApiReaction;
|
||||
activeReactions?: ActiveReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
genericEffects?: ApiStickerSet;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
const CENTER_ICON_SIZE = 30;
|
||||
const EFFECT_SIZE = 100;
|
||||
const CENTER_ICON_SIZE = 1.875 * REM;
|
||||
const EFFECT_SIZE = 6.25 * REM;
|
||||
|
||||
const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
reaction,
|
||||
activeReaction,
|
||||
isInMeta,
|
||||
genericEffects,
|
||||
activeReactions,
|
||||
availableReactions,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const { stopActiveReaction } = getActions();
|
||||
|
||||
const availableReaction = availableReactions?.find((r) => r.reaction === reaction);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isCustom = 'documentId' in reaction;
|
||||
|
||||
const availableReaction = useMemo(() => (
|
||||
availableReactions?.find((r) => isSameReaction(r.reaction, reaction))
|
||||
), [availableReactions, reaction]);
|
||||
const centerIconId = availableReaction?.centerIcon?.id;
|
||||
const effectId = availableReaction?.aroundAnimation?.id;
|
||||
|
||||
const customEmoji = useCustomEmoji(isCustom ? reaction.documentId : undefined);
|
||||
|
||||
const assignedEffectId = useMemo(() => {
|
||||
if (!isCustom) return availableReaction?.aroundAnimation?.id;
|
||||
|
||||
if (!customEmoji) return undefined;
|
||||
const assignedId = availableReactions?.find((available) => available.reaction.emoticon === customEmoji.emoji)
|
||||
?.aroundAnimation?.id;
|
||||
return assignedId;
|
||||
}, [availableReaction, availableReactions, customEmoji, isCustom]);
|
||||
|
||||
const effectId = useMemo(() => {
|
||||
if (assignedEffectId) {
|
||||
return assignedEffectId;
|
||||
}
|
||||
|
||||
if (!genericEffects?.stickers) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { stickers } = genericEffects;
|
||||
const randomIndex = Math.floor(Math.random() * stickers.length);
|
||||
|
||||
return stickers[randomIndex].id;
|
||||
}, [assignedEffectId, genericEffects]);
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
const mediaHashCenterIcon = centerIconId && `sticker${centerIconId}`;
|
||||
const mediaHashEffect = effectId && `sticker${effectId}`;
|
||||
@ -43,51 +89,67 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
const mediaDataCenterIcon = useMedia(mediaHashCenterIcon, !centerIconId);
|
||||
const mediaDataEffect = useMedia(mediaHashEffect, !effectId);
|
||||
|
||||
const shouldPlay = Boolean(activeReaction?.reaction === reaction && mediaDataCenterIcon && mediaDataEffect);
|
||||
const activeReaction = useMemo(() => (
|
||||
activeReactions?.find((active) => isSameReaction(active.reaction, reaction))
|
||||
), [activeReactions, reaction]);
|
||||
|
||||
const shouldPlay = Boolean(activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect);
|
||||
const {
|
||||
shouldRender: shouldRenderAnimation,
|
||||
transitionClassNames: animationClassNames,
|
||||
} = useShowTransition(shouldPlay, undefined, true, 'slow');
|
||||
|
||||
const handleEnded = useCallback(() => {
|
||||
stopActiveReaction({ messageId: activeReaction?.messageId, reaction });
|
||||
if (!activeReaction?.messageId) return;
|
||||
stopActiveReaction({ messageId: activeReaction.messageId, reaction });
|
||||
}, [activeReaction?.messageId, reaction, stopActiveReaction]);
|
||||
|
||||
const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag();
|
||||
const shouldRenderStatic = !shouldPlay || !isAnimationLoaded;
|
||||
const shouldRenderStatic = !isCustom && (!shouldPlay || !isAnimationLoaded);
|
||||
|
||||
const className = buildClassName(
|
||||
'ReactionAnimatedEmoji',
|
||||
isInMeta && 'in-meta',
|
||||
shouldRenderAnimation && 'is-animating',
|
||||
styles.root,
|
||||
shouldRenderAnimation && styles.animating,
|
||||
isCustom && styles.isCustomEmoji,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{shouldRenderStatic && <ReactionStaticEmoji reaction={reaction} />}
|
||||
<div className={className} ref={ref}>
|
||||
{shouldRenderStatic && <ReactionStaticEmoji reaction={reaction} availableReactions={availableReactions} />}
|
||||
{isCustom && (
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
className={styles.customEmoji}
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
/>
|
||||
)}
|
||||
{shouldRenderAnimation && (
|
||||
<>
|
||||
<AnimatedSticker
|
||||
key={centerIconId}
|
||||
className={animationClassNames}
|
||||
size={CENTER_ICON_SIZE}
|
||||
tgsUrl={mediaDataCenterIcon}
|
||||
play
|
||||
noLoop
|
||||
forceOnHeavyAnimation
|
||||
onLoad={markAnimationLoaded}
|
||||
onEnded={unmarkAnimationLoaded}
|
||||
/>
|
||||
<AnimatedSticker
|
||||
key={effectId}
|
||||
className={buildClassName('effect', animationClassNames)}
|
||||
className={buildClassName(styles.effect, animationClassNames)}
|
||||
size={EFFECT_SIZE}
|
||||
tgsUrl={mediaDataEffect}
|
||||
play
|
||||
play={isIntersecting}
|
||||
noLoop
|
||||
forceOnHeavyAnimation
|
||||
onEnded={handleEnded}
|
||||
/>
|
||||
{isCustom ? (
|
||||
!assignedEffectId && isIntersecting && <CustomReactionAnimation reaction={reaction} />
|
||||
) : (
|
||||
<AnimatedSticker
|
||||
key={centerIconId}
|
||||
className={buildClassName(styles.animatedIcon, animationClassNames)}
|
||||
size={CENTER_ICON_SIZE}
|
||||
tgsUrl={mediaDataCenterIcon}
|
||||
play={isIntersecting}
|
||||
noLoop
|
||||
forceOnHeavyAnimation
|
||||
onLoad={markAnimationLoaded}
|
||||
onEnded={unmarkAnimationLoaded}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiMessage, ApiReactionCount, ApiUser,
|
||||
ApiAvailableReaction, ApiMessage, ApiReactionCount, ApiStickerSet, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { isSameReaction, isReactionChosen } from '../../../global/helpers';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -17,25 +19,28 @@ import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
|
||||
import './Reactions.scss';
|
||||
|
||||
const MAX_REACTORS_AVATARS = 3;
|
||||
|
||||
const ReactionButton: FC<{
|
||||
reaction: ApiReactionCount;
|
||||
message: ApiMessage;
|
||||
activeReaction?: ActiveReaction;
|
||||
activeReactions?: ActiveReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
withRecentReactors?: boolean;
|
||||
genericEffects?: ApiStickerSet;
|
||||
observeIntersection?: ObserveFn;
|
||||
}> = ({
|
||||
reaction,
|
||||
message,
|
||||
activeReaction,
|
||||
activeReactions,
|
||||
availableReactions,
|
||||
withRecentReactors,
|
||||
genericEffects,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const { sendReaction } = getActions();
|
||||
|
||||
const { toggleReaction } = getActions();
|
||||
const { recentReactions } = message.reactions!;
|
||||
|
||||
const recentReactors = useMemo(() => {
|
||||
if (!recentReactions || reaction.count > MAX_REACTORS_AVATARS) {
|
||||
if (!withRecentReactors || !recentReactions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -43,29 +48,31 @@ const ReactionButton: FC<{
|
||||
const usersById = getGlobal().users.byId;
|
||||
|
||||
return recentReactions
|
||||
.filter((recentReaction) => recentReaction.reaction === reaction.reaction)
|
||||
.filter((recentReaction) => isSameReaction(recentReaction.reaction, reaction.reaction))
|
||||
.map((recentReaction) => usersById[recentReaction.userId])
|
||||
.filter(Boolean) as ApiUser[];
|
||||
}, [reaction, recentReactions]);
|
||||
}, [reaction.reaction, recentReactions, withRecentReactors]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
sendReaction({
|
||||
reaction: reaction.isChosen ? undefined : reaction.reaction,
|
||||
toggleReaction({
|
||||
reaction: reaction.reaction,
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
}, [message, reaction, sendReaction]);
|
||||
}, [message, reaction, toggleReaction]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buildClassName(reaction.isChosen && 'chosen')}
|
||||
className={buildClassName(isReactionChosen(reaction) && 'chosen')}
|
||||
size="tiny"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ReactionAnimatedEmoji
|
||||
activeReaction={activeReaction}
|
||||
activeReactions={activeReactions}
|
||||
reaction={reaction.reaction}
|
||||
availableReactions={availableReactions}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
{recentReactors?.length ? (
|
||||
<div className="avatars">
|
||||
|
||||
@ -3,24 +3,28 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiAvailableReaction } from '../../../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
||||
} from '../../../api/types';
|
||||
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { getTouchY } from '../../../util/scrollLock';
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import { IS_COMPACT_MENU } from '../../../util/environment';
|
||||
import { isSameReaction, canSendReaction, getReactionUniqueKey } from '../../../global/helpers';
|
||||
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
|
||||
import './ReactionSelector.scss';
|
||||
|
||||
type OwnProps = {
|
||||
enabledReactions?: string[];
|
||||
onSendReaction: (reaction: string, x: number, y: number) => void;
|
||||
enabledReactions?: ApiChatReactions;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
isPrivate?: boolean;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
currentReactions?: string[];
|
||||
currentReactions?: ApiReactionCount[];
|
||||
maxUniqueReactions?: number;
|
||||
isReady?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
@ -36,7 +40,7 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isReady,
|
||||
onSendReaction,
|
||||
onToggleReaction,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const itemsScrollRef = useRef<HTMLDivElement>(null);
|
||||
@ -57,15 +61,26 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
};
|
||||
|
||||
const reactionsToRender = useMemo(() => {
|
||||
return availableReactions?.map((reaction) => {
|
||||
if (reaction.isInactive) return undefined;
|
||||
if (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction))) return undefined;
|
||||
return availableReactions?.map((availableReaction) => {
|
||||
if (availableReaction.isInactive) return undefined;
|
||||
if (!isPrivate && (!enabledReactions || !canSendReaction(availableReaction.reaction, enabledReactions))) {
|
||||
return undefined;
|
||||
}
|
||||
if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions
|
||||
&& !currentReactions.includes(reaction.reaction)) return undefined;
|
||||
return reaction;
|
||||
&& !currentReactions.some(({ reaction }) => isSameReaction(reaction, availableReaction.reaction))) {
|
||||
return undefined;
|
||||
}
|
||||
return availableReaction;
|
||||
}) || [];
|
||||
}, [availableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions]);
|
||||
|
||||
const userReactionIndexes = useMemo(() => {
|
||||
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
||||
return new Set(chosenReactions.map(({ reaction }) => (
|
||||
reactionsToRender.findIndex((r) => r && isSameReaction(r.reaction, reaction))
|
||||
)));
|
||||
}, [currentReactions, reactionsToRender]);
|
||||
|
||||
if (!reactionsToRender.length) return undefined;
|
||||
|
||||
return (
|
||||
@ -78,11 +93,12 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
if (!reaction) return undefined;
|
||||
return (
|
||||
<ReactionSelectorReaction
|
||||
key={reaction.reaction}
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
previewIndex={i}
|
||||
isReady={isReady}
|
||||
onSendReaction={onSendReaction}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -29,4 +29,16 @@
|
||||
min-width: 1.5rem;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
&--chosen::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-background-compact-menu-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAvailableReaction } from '../../../api/types';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../../api/types';
|
||||
|
||||
import { IS_COMPACT_MENU } from '../../../util/environment';
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
@ -18,17 +18,19 @@ type OwnProps = {
|
||||
reaction: ApiAvailableReaction;
|
||||
previewIndex: number;
|
||||
isReady?: boolean;
|
||||
onSendReaction: (reaction: string, x: number, y: number) => void;
|
||||
chosen?: boolean;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
};
|
||||
|
||||
const cn = createClassNameBuilder('ReactionSelectorReaction');
|
||||
|
||||
const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
reaction, previewIndex, onSendReaction, isReady,
|
||||
reaction,
|
||||
previewIndex,
|
||||
isReady,
|
||||
chosen,
|
||||
onToggleReaction,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady);
|
||||
|
||||
const [isActivated, activate, deactivate] = useFlag();
|
||||
@ -38,17 +40,13 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
const shouldRenderAnimated = Boolean(isReady && mediaData);
|
||||
|
||||
function handleClick() {
|
||||
if (!containerRef.current) return;
|
||||
const { x, y } = containerRef.current.getBoundingClientRect();
|
||||
|
||||
onSendReaction(reaction.reaction, x, y);
|
||||
onToggleReaction(reaction.reaction);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('&', IS_COMPACT_MENU && 'compact')}
|
||||
className={cn('&', IS_COMPACT_MENU && 'compact', chosen && 'chosen')}
|
||||
onClick={handleClick}
|
||||
ref={containerRef}
|
||||
onMouseEnter={isReady ? activate : undefined}
|
||||
>
|
||||
{shouldRenderStatic && (
|
||||
|
||||
@ -23,12 +23,7 @@
|
||||
text-transform: none;
|
||||
color: var(--accent-color);
|
||||
overflow: visible;
|
||||
|
||||
.ReactionAnimatedEmoji, .icon-heart {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
line-height: 1.25rem;
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAvailableReaction, ApiMessage } from '../../../api/types';
|
||||
import type { ApiAvailableReaction, ApiMessage, ApiStickerSet } from '../../../api/types';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getReactionUniqueKey } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import ReactionButton from './ReactionButton';
|
||||
@ -13,27 +15,40 @@ import './Reactions.scss';
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
isOutside?: boolean;
|
||||
activeReaction?: ActiveReaction;
|
||||
activeReactions?: ActiveReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
metaChildren?: React.ReactNode;
|
||||
genericEffects?: ApiStickerSet;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
const MAX_RECENT_AVATARS = 3;
|
||||
|
||||
const Reactions: FC<OwnProps> = ({
|
||||
message,
|
||||
isOutside,
|
||||
activeReaction,
|
||||
activeReactions,
|
||||
availableReactions,
|
||||
metaChildren,
|
||||
genericEffects,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const totalCount = useMemo(() => (
|
||||
message.reactions!.results.reduce((acc, reaction) => acc + reaction.count, 0)
|
||||
), [message]);
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Reactions', isOutside && 'is-outside')}>
|
||||
{message.reactions!.results.map((reaction) => (
|
||||
<ReactionButton
|
||||
key={reaction.reaction}
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
reaction={reaction}
|
||||
message={message}
|
||||
activeReaction={activeReaction}
|
||||
activeReactions={activeReactions}
|
||||
availableReactions={availableReactions}
|
||||
withRecentReactors={totalCount <= MAX_RECENT_AVATARS}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
))}
|
||||
{metaChildren}
|
||||
|
||||
@ -712,7 +712,7 @@
|
||||
|
||||
--custom-emoji-size: var(--emoji-only-size);
|
||||
|
||||
.emoji {
|
||||
.AnimatedEmoji {
|
||||
width: var(--emoji-only-size);
|
||||
height: var(--emoji-only-size);
|
||||
}
|
||||
|
||||
@ -63,13 +63,10 @@ export default function useOuterHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
function handleSendQuickReaction(e: React.MouseEvent) {
|
||||
const { x, y } = e.currentTarget.getBoundingClientRect();
|
||||
function handleSendQuickReaction() {
|
||||
sendDefaultReaction({
|
||||
chatId,
|
||||
messageId,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
}
|
||||
|
||||
@ -90,14 +87,10 @@ export default function useOuterHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
function handleDoubleTap(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
const { pageX: x, pageY: y } = e;
|
||||
|
||||
function handleDoubleTap() {
|
||||
sendDefaultReaction({
|
||||
chatId,
|
||||
messageId,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
}
|
||||
|
||||
@ -112,7 +105,7 @@ export default function useOuterHandlers(
|
||||
if (doubleTapTimeoutRef.current) {
|
||||
clearInterval(doubleTapTimeoutRef.current);
|
||||
doubleTapTimeoutRef.current = undefined;
|
||||
handleDoubleTap(e);
|
||||
handleDoubleTap();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ManagementScreens, ManagementProgress } from '../../../types';
|
||||
import type { ApiChat, ApiExportedInvite } from '../../../api/types';
|
||||
import type { ApiAvailableReaction, ApiChat, ApiExportedInvite } from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { getChatAvatarHash, getHasAdminRight, isChatPublic } from '../../../global/helpers';
|
||||
@ -43,7 +43,7 @@ type StateProps = {
|
||||
canInvite?: boolean;
|
||||
exportedInvites?: ApiExportedInvite[];
|
||||
lastSyncTime?: number;
|
||||
availableReactionsCount?: number;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
};
|
||||
|
||||
const CHANNEL_TITLE_EMPTY = 'Channel title can\'t be empty';
|
||||
@ -58,10 +58,10 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
canInvite,
|
||||
exportedInvites,
|
||||
lastSyncTime,
|
||||
availableReactionsCount,
|
||||
isActive,
|
||||
availableReactions,
|
||||
onScreenSelect,
|
||||
onClose,
|
||||
isActive,
|
||||
}) => {
|
||||
const {
|
||||
updateChat,
|
||||
@ -191,7 +191,21 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
openChat({ id: undefined });
|
||||
}, [chat.isCreator, chat.id, closeDeleteDialog, closeManagement, leaveChannel, deleteChannel, openChat]);
|
||||
|
||||
const enabledReactionsCount = chat.fullInfo?.enabledReactions?.length || 0;
|
||||
const chatReactionsDescription = useMemo(() => {
|
||||
if (!chat.fullInfo?.enabledReactions) {
|
||||
return lang('ReactionsOff');
|
||||
}
|
||||
|
||||
if (chat.fullInfo.enabledReactions.type === 'all') {
|
||||
return lang('ReactionsAll');
|
||||
}
|
||||
|
||||
const enabledLength = chat.fullInfo.enabledReactions.allowed.length;
|
||||
const totalLength = availableReactions?.filter((reaction) => !reaction.isInactive).length || 0;
|
||||
|
||||
const text = totalLength ? `${enabledLength} / ${totalLength}` : `${enabledLength}`;
|
||||
return text;
|
||||
}, [availableReactions, chat, lang]);
|
||||
const isChannelPublic = useMemo(() => isChatPublic(chat), [chat]);
|
||||
|
||||
if (chat.isRestricted || chat.isForbidden) {
|
||||
@ -275,7 +289,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<span className="title">{lang('Reactions')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{enabledReactionsCount}/{availableReactionsCount}
|
||||
{chatReactionsDescription}
|
||||
</span>
|
||||
</ListItem>
|
||||
<div className="ListItem no-selection narrow">
|
||||
@ -358,7 +372,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canInvite: getHasAdminRight(chat, 'inviteUsers'),
|
||||
lastSyncTime: global.lastSyncTime,
|
||||
exportedInvites: invites,
|
||||
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
||||
availableReactions: global.availableReactions,
|
||||
};
|
||||
},
|
||||
)(ManageChannel));
|
||||
|
||||
@ -6,7 +6,9 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ManagementScreens, ManagementProgress } from '../../../types';
|
||||
import type { ApiChat, ApiChatBannedRights, ApiExportedInvite } from '../../../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChat, ApiChatBannedRights, ApiExportedInvite,
|
||||
} from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import {
|
||||
@ -51,8 +53,8 @@ type StateProps = {
|
||||
canInvite?: boolean;
|
||||
exportedInvites?: ApiExportedInvite[];
|
||||
lastSyncTime?: number;
|
||||
availableReactionsCount?: number;
|
||||
isChannelsPremiumLimitReached: boolean;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
};
|
||||
|
||||
const GROUP_TITLE_EMPTY = 'Group title can\'t be empty';
|
||||
@ -71,13 +73,13 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
canChangeInfo,
|
||||
canBanUsers,
|
||||
canInvite,
|
||||
onScreenSelect,
|
||||
onClose,
|
||||
isActive,
|
||||
exportedInvites,
|
||||
lastSyncTime,
|
||||
availableReactionsCount,
|
||||
isChannelsPremiumLimitReached,
|
||||
availableReactions,
|
||||
onScreenSelect,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
togglePreHistoryHidden,
|
||||
@ -212,7 +214,21 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
checkbox.checked = !chat.fullInfo?.isPreHistoryHidden;
|
||||
}, [isChannelsPremiumLimitReached, chat.fullInfo?.isPreHistoryHidden]);
|
||||
|
||||
const enabledReactionsCount = chat.fullInfo?.enabledReactions?.length || 0;
|
||||
const chatReactionsDescription = useMemo(() => {
|
||||
if (!chat.fullInfo?.enabledReactions) {
|
||||
return lang('ReactionsOff');
|
||||
}
|
||||
|
||||
if (chat.fullInfo.enabledReactions.type === 'all') {
|
||||
return lang('ReactionsAll');
|
||||
}
|
||||
|
||||
const enabledLength = chat.fullInfo.enabledReactions.allowed.length;
|
||||
const totalLength = availableReactions?.filter((reaction) => !reaction.isInactive).length || 0;
|
||||
|
||||
const text = totalLength ? `${enabledLength} / ${totalLength}` : `${enabledLength}`;
|
||||
return text;
|
||||
}, [availableReactions, chat, lang]);
|
||||
|
||||
const enabledPermissionsCount = useMemo(() => {
|
||||
if (!chat.defaultBannedRights) {
|
||||
@ -330,7 +346,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<span className="title">{lang('Reactions')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{enabledReactionsCount}/{availableReactionsCount}
|
||||
{chatReactionsDescription}
|
||||
</span>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
@ -437,8 +453,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
canInvite: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'inviteUsers'),
|
||||
exportedInvites: invites,
|
||||
lastSyncTime: global.lastSyncTime,
|
||||
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
||||
isChannelsPremiumLimitReached: global.limitReachedModal?.limit === 'channels',
|
||||
availableReactions: global.availableReactions,
|
||||
};
|
||||
},
|
||||
)(ManageGroup));
|
||||
|
||||
@ -4,8 +4,11 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiChat } from '../../../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChat, ApiChatReactions, ApiReaction,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { isSameReaction } from '../../../global/helpers';
|
||||
import { selectChat } from '../../../global/selectors';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
@ -14,6 +17,7 @@ import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
@ -24,7 +28,7 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
enabledReactions?: string[];
|
||||
enabledReactions?: ApiChatReactions;
|
||||
};
|
||||
|
||||
const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||
@ -39,13 +43,24 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localEnabledReactions, setLocalEnabledReactions] = useState(enabledReactions || []);
|
||||
const [localEnabledReactions, setLocalEnabledReactions] = useState<ApiChatReactions | undefined>(enabledReactions);
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const reactionsOptions = useMemo(() => [{
|
||||
value: 'all',
|
||||
label: lang('AllReactions'),
|
||||
}, {
|
||||
value: 'some',
|
||||
label: lang('SomeReactions'),
|
||||
}, {
|
||||
value: 'none',
|
||||
label: lang('NoReactions'),
|
||||
}], [lang]);
|
||||
|
||||
const handleSaveReactions = useCallback(() => {
|
||||
if (!chat) return;
|
||||
setIsLoading(true);
|
||||
@ -59,24 +74,46 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
setIsLoading(false);
|
||||
setIsTouched(false);
|
||||
setLocalEnabledReactions(enabledReactions || []);
|
||||
setLocalEnabledReactions(enabledReactions);
|
||||
}, [enabledReactions]);
|
||||
|
||||
const availableActiveReactions = useMemo<ApiAvailableReaction[] | undefined>(
|
||||
() => availableReactions?.filter((l) => !l.isInactive),
|
||||
() => availableReactions?.filter(({ isInactive }) => !isInactive),
|
||||
[availableReactions],
|
||||
);
|
||||
|
||||
const handleReactionsOptionChange = useCallback((value: string) => {
|
||||
if (value === 'all') {
|
||||
setLocalEnabledReactions({ type: 'all' });
|
||||
} else if (value === 'some') {
|
||||
setLocalEnabledReactions({
|
||||
type: 'some',
|
||||
allowed: enabledReactions?.type === 'some' ? enabledReactions.allowed : [],
|
||||
});
|
||||
} else {
|
||||
setLocalEnabledReactions(undefined);
|
||||
}
|
||||
setIsTouched(true);
|
||||
}, [enabledReactions]);
|
||||
|
||||
const handleReactionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!chat || !availableActiveReactions) return;
|
||||
|
||||
const { name, checked } = e.currentTarget;
|
||||
const newEnabledReactions = name === 'all' ? (checked ? availableActiveReactions.map((l) => l.reaction) : [])
|
||||
: (!checked
|
||||
? localEnabledReactions.filter((l) => l !== name)
|
||||
: [...localEnabledReactions, name]);
|
||||
|
||||
setLocalEnabledReactions(newEnabledReactions);
|
||||
if (localEnabledReactions?.type === 'some') {
|
||||
const reaction = { emoticon: name } as ApiReaction;
|
||||
if (checked) {
|
||||
setLocalEnabledReactions({
|
||||
type: 'some',
|
||||
allowed: [...localEnabledReactions.allowed, reaction],
|
||||
});
|
||||
} else {
|
||||
setLocalEnabledReactions({
|
||||
type: 'some',
|
||||
allowed: localEnabledReactions.allowed.filter((local) => !isSameReaction(local, reaction)),
|
||||
});
|
||||
}
|
||||
}
|
||||
setIsTouched(true);
|
||||
}, [availableActiveReactions, chat, localEnabledReactions]);
|
||||
|
||||
@ -84,31 +121,43 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||
<div className="Management">
|
||||
<div className="custom-scroll">
|
||||
<div className="section">
|
||||
<div className="ListItem no-selection">
|
||||
<Checkbox
|
||||
name="all"
|
||||
checked={!localEnabledReactions || localEnabledReactions.length > 0}
|
||||
label={lang('EnableReactions')}
|
||||
onChange={handleReactionChange}
|
||||
/>
|
||||
</div>
|
||||
{availableActiveReactions?.map(({ reaction, title }) => (
|
||||
<div className="ListItem no-selection">
|
||||
<Checkbox
|
||||
name={reaction}
|
||||
checked={!localEnabledReactions || localEnabledReactions?.includes(reaction)}
|
||||
disabled={localEnabledReactions?.length === 0}
|
||||
label={(
|
||||
<div className="Reaction">
|
||||
<ReactionStaticEmoji reaction={reaction} />
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
onChange={handleReactionChange}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<h3 className="section-heading">
|
||||
{lang('AvailableReactions')}
|
||||
</h3>
|
||||
<RadioGroup
|
||||
selected={localEnabledReactions?.type || 'none'}
|
||||
name="reactions"
|
||||
options={reactionsOptions}
|
||||
onChange={handleReactionsOptionChange}
|
||||
/>
|
||||
<p className="section-info mt-4">
|
||||
{localEnabledReactions?.type === 'all' && lang('EnableAllReactionsInfo')}
|
||||
{localEnabledReactions?.type === 'some' && lang('EnableSomeReactionsInfo')}
|
||||
{!localEnabledReactions && lang('DisableReactionsInfo')}
|
||||
</p>
|
||||
</div>
|
||||
{localEnabledReactions?.type === 'some' && (
|
||||
<div className="section">
|
||||
<h3 className="section-heading">
|
||||
{lang('AvailableReactions')}
|
||||
</h3>
|
||||
{availableActiveReactions?.map(({ reaction, title }) => (
|
||||
<div className="ListItem no-selection">
|
||||
<Checkbox
|
||||
name={reaction.emoticon}
|
||||
checked={localEnabledReactions?.allowed.some((r) => isSameReaction(reaction, r))}
|
||||
label={(
|
||||
<div className="Reaction">
|
||||
<ReactionStaticEmoji reaction={reaction} availableReactions={availableReactions} />
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
onChange={handleReactionChange}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FloatingActionButton
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
selectChatMessage, selectCurrentChat,
|
||||
selectDefaultReaction,
|
||||
selectLocalAnimatedEmojiEffectByName,
|
||||
selectMaxUserReactions,
|
||||
selectMessageIdsByGroupId,
|
||||
} from '../../selectors';
|
||||
import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions';
|
||||
@ -15,7 +16,7 @@ import {
|
||||
} from '../../reducers';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||
import { isMessageLocal } from '../../helpers';
|
||||
import { isSameReaction, getUserReactions, isMessageLocal } from '../../helpers';
|
||||
|
||||
const INTERACTION_RANDOM_OFFSET = 40;
|
||||
|
||||
@ -85,30 +86,24 @@ addActionHandler('sendEmojiInteraction', (global, actions, payload) => {
|
||||
|
||||
addActionHandler('sendDefaultReaction', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, messageId, x, y,
|
||||
chatId, messageId,
|
||||
} = payload;
|
||||
const reaction = selectDefaultReaction(global, chatId);
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
|
||||
if (!reaction || !message || isMessageLocal(message)) return;
|
||||
|
||||
actions.sendReaction({
|
||||
actions.toggleReaction({
|
||||
chatId,
|
||||
messageId,
|
||||
reaction,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('sendReaction', (global, actions, payload) => {
|
||||
const {
|
||||
chatId,
|
||||
}: { chatId: string } = payload;
|
||||
addActionHandler('toggleReaction', (global, actions, payload) => {
|
||||
const { chatId, reaction } = payload;
|
||||
let { messageId } = payload;
|
||||
|
||||
let { reaction } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
let message = selectChatMessage(global, chatId, messageId);
|
||||
|
||||
@ -125,30 +120,38 @@ addActionHandler('sendReaction', (global, actions, payload) => {
|
||||
: message;
|
||||
messageId = message?.id || messageId;
|
||||
|
||||
if (message.reactions?.results?.some((l) => l.reaction === reaction && l.isChosen)) {
|
||||
reaction = undefined;
|
||||
}
|
||||
const userReactions = getUserReactions(message);
|
||||
const hasReaction = userReactions.some((userReaction) => isSameReaction(userReaction, reaction));
|
||||
|
||||
void callApi('sendReaction', { chat, messageId, reaction });
|
||||
const newUserReactions = hasReaction
|
||||
? userReactions.filter((userReaction) => !isSameReaction(userReaction, reaction)) : [...userReactions, reaction];
|
||||
|
||||
const limit = selectMaxUserReactions(global);
|
||||
|
||||
const reactions = newUserReactions.slice(-limit);
|
||||
|
||||
void callApi('sendReaction', { chat, messageId, reactions });
|
||||
|
||||
const { animationLevel } = global.settings.byKey;
|
||||
|
||||
if (animationLevel === ANIMATION_LEVEL_MAX) {
|
||||
const newActiveReactions = hasReaction ? omit(global.activeReactions, [messageId]) : {
|
||||
...global.activeReactions,
|
||||
[messageId]: [
|
||||
...(global.activeReactions[messageId] || []),
|
||||
{
|
||||
messageId,
|
||||
reaction,
|
||||
},
|
||||
],
|
||||
};
|
||||
global = {
|
||||
...global,
|
||||
activeReactions: {
|
||||
...(reaction ? global.activeReactions : omit(global.activeReactions, [messageId])),
|
||||
...(reaction && {
|
||||
[messageId]: {
|
||||
reaction,
|
||||
messageId,
|
||||
},
|
||||
}),
|
||||
},
|
||||
activeReactions: newActiveReactions,
|
||||
};
|
||||
}
|
||||
|
||||
return addMessageReaction(global, chatId, messageId, reaction);
|
||||
return addMessageReaction(global, message, reactions);
|
||||
});
|
||||
|
||||
addActionHandler('openChat', (global) => {
|
||||
@ -161,13 +164,21 @@ addActionHandler('openChat', (global) => {
|
||||
addActionHandler('stopActiveReaction', (global, actions, payload) => {
|
||||
const { messageId, reaction } = payload;
|
||||
|
||||
if (global.activeReactions[messageId]?.reaction !== reaction) {
|
||||
if (!global.activeReactions[messageId]?.some((active) => isSameReaction(active.reaction, reaction))) {
|
||||
return global;
|
||||
}
|
||||
|
||||
const newMessageActiveReactions = global.activeReactions[messageId]
|
||||
.filter((active) => !isSameReaction(active.reaction, reaction));
|
||||
|
||||
const newActiveReactions = newMessageActiveReactions.length ? {
|
||||
...global.activeReactions,
|
||||
[messageId]: newMessageActiveReactions,
|
||||
} : omit(global.activeReactions, [messageId]);
|
||||
|
||||
return {
|
||||
...global,
|
||||
activeReactions: omit(global.activeReactions, [messageId]),
|
||||
activeReactions: newActiveReactions,
|
||||
};
|
||||
});
|
||||
|
||||
@ -200,7 +211,7 @@ addActionHandler('stopActiveEmojiInteraction', (global, actions, payload) => {
|
||||
|
||||
return {
|
||||
...global,
|
||||
activeEmojiInteractions: global.activeEmojiInteractions?.filter((l) => l.id !== id),
|
||||
activeEmojiInteractions: global.activeEmojiInteractions?.filter((active) => active.id !== id),
|
||||
};
|
||||
});
|
||||
|
||||
@ -349,16 +360,16 @@ addActionHandler('animateUnreadReaction', (global, actions, payload) => {
|
||||
|
||||
if (!message) return undefined;
|
||||
|
||||
const unread = message.reactions?.recentReactions?.find((l) => l.isUnread);
|
||||
const unread = message.reactions?.recentReactions?.filter(({ isUnread }) => isUnread);
|
||||
|
||||
if (!unread) return undefined;
|
||||
|
||||
const reaction = unread?.reaction;
|
||||
const reactions = unread.map((recent) => recent.reaction);
|
||||
|
||||
return [messageId, {
|
||||
return [messageId, reactions.map((r) => ({
|
||||
messageId,
|
||||
reaction,
|
||||
}];
|
||||
reaction: r,
|
||||
}))];
|
||||
}).filter(Boolean)),
|
||||
},
|
||||
};
|
||||
|
||||
@ -167,9 +167,21 @@ addActionHandler('loadGreetingStickers', async (global) => {
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('loadFeaturedStickers', (global) => {
|
||||
addActionHandler('loadFeaturedStickers', async (global) => {
|
||||
const { hash } = global.stickers.featured || {};
|
||||
void loadFeaturedStickers(hash);
|
||||
const featuredStickers = await callApi('fetchFeaturedStickers', { hash });
|
||||
if (!featuredStickers) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
setGlobal(updateStickerSets(
|
||||
global,
|
||||
'featured',
|
||||
featuredStickers.hash,
|
||||
featuredStickers.sets,
|
||||
));
|
||||
});
|
||||
|
||||
addActionHandler('loadPremiumGifts', async () => {
|
||||
@ -193,9 +205,39 @@ addActionHandler('loadStickers', (global, actions, payload) => {
|
||||
void loadStickers(stickerSetInfo);
|
||||
});
|
||||
|
||||
addActionHandler('loadAnimatedEmojis', () => {
|
||||
void loadAnimatedEmojis();
|
||||
void loadAnimatedEmojiEffects();
|
||||
addActionHandler('loadAnimatedEmojis', async (global) => {
|
||||
const [emojis, effects] = await Promise.all([
|
||||
callApi('fetchAnimatedEmojis'),
|
||||
callApi('fetchAnimatedEmojiEffects'),
|
||||
]);
|
||||
if (!emojis || !effects) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
global = replaceAnimatedEmojis(global, { ...emojis.set, stickers: emojis.stickers });
|
||||
global = {
|
||||
...global,
|
||||
animatedEmojiEffects: { ...effects.set, stickers: effects.stickers },
|
||||
};
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadGenericEmojiEffects', async (global) => {
|
||||
const stickerSet = await callApi('fetchGenericEmojiEffects');
|
||||
if (!stickerSet) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
|
||||
const { set, stickers } = stickerSet;
|
||||
|
||||
setGlobal({
|
||||
...global,
|
||||
genericEmojiEffects: { ...set, stickers },
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('loadSavedGifs', (global) => {
|
||||
@ -405,20 +447,6 @@ async function loadFavoriteStickers(hash?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadFeaturedStickers(hash?: string) {
|
||||
const featuredStickers = await callApi('fetchFeaturedStickers', { hash });
|
||||
if (!featuredStickers) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobal(updateStickerSets(
|
||||
getGlobal(),
|
||||
'featured',
|
||||
featuredStickers.hash,
|
||||
featuredStickers.sets,
|
||||
));
|
||||
}
|
||||
|
||||
async function loadStickers(stickerSetInfo: ApiStickerSetInfo) {
|
||||
const stickerSet = await callApi(
|
||||
'fetchStickers',
|
||||
@ -453,31 +481,6 @@ async function loadStickers(stickerSetInfo: ApiStickerSetInfo) {
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
async function loadAnimatedEmojis() {
|
||||
const stickerSet = await callApi('fetchAnimatedEmojis');
|
||||
if (!stickerSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { set, stickers } = stickerSet;
|
||||
|
||||
setGlobal(replaceAnimatedEmojis(getGlobal(), { ...set, stickers }));
|
||||
}
|
||||
|
||||
async function loadAnimatedEmojiEffects() {
|
||||
const stickerSet = await callApi('fetchAnimatedEmojiEffects');
|
||||
if (!stickerSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { set, stickers } = stickerSet;
|
||||
|
||||
setGlobal({
|
||||
...getGlobal(),
|
||||
animatedEmojiEffects: { ...set, stickers },
|
||||
});
|
||||
}
|
||||
|
||||
function unfaveSticker(sticker: ApiSticker) {
|
||||
const global = getGlobal();
|
||||
|
||||
|
||||
@ -351,6 +351,28 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
return acc;
|
||||
}, {} as Record<string, ApiChat>);
|
||||
}
|
||||
|
||||
// TODO Remove in Apr 2023 (this was re-designed but can be hardcoded in cache)
|
||||
if (cached.messages.byChatId) {
|
||||
const wasUpdated = Object.values(cached.messages.byChatId)
|
||||
.some((messages) => Object.values(messages.byId).some(({ reactions }) => {
|
||||
return reactions?.results[0]?.reaction && typeof reactions.results[0].reaction !== 'string';
|
||||
}));
|
||||
if (!wasUpdated) {
|
||||
for (const messages of Object.values(cached.messages.byChatId)) {
|
||||
for (const message of Object.values(messages.byId)) {
|
||||
delete message.reactions;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof cached.config?.defaultReaction === 'string') {
|
||||
cached.config.defaultReaction = { emoticon: cached.config.defaultReaction };
|
||||
}
|
||||
if (typeof cached.availableReactions?.[0].reaction === 'string') {
|
||||
cached.availableReactions = cached.availableReactions
|
||||
.map((r) => ({ ...r, reaction: { emoticon: r.reaction as unknown as string } }));
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiReactions, ApiUser,
|
||||
ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiUser,
|
||||
} from '../../api/types';
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useLang';
|
||||
@ -237,10 +237,6 @@ export function getMessageContentFilename(message: ApiMessage) {
|
||||
return baseFilename;
|
||||
}
|
||||
|
||||
export function areReactionsEmpty(reactions: ApiReactions) {
|
||||
return !reactions.results.some((l) => l.count > 0);
|
||||
}
|
||||
|
||||
export function isGeoLiveExpired(message: ApiMessage, timestamp = Date.now() / 1000) {
|
||||
const { location } = message.content;
|
||||
if (location?.type !== 'geoLive') return false;
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import type {
|
||||
ApiMessage, ApiReactions,
|
||||
ApiChatReactions,
|
||||
ApiMessage,
|
||||
ApiReaction,
|
||||
ApiReactions,
|
||||
ApiReactionCount,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
@ -12,3 +16,53 @@ export function checkIfHasUnreadReactions(global: GlobalState, reactions: ApiRea
|
||||
({ isUnread, userId }) => isUnread && userId !== currentUserId,
|
||||
);
|
||||
}
|
||||
|
||||
export function areReactionsEmpty(reactions: ApiReactions) {
|
||||
return !reactions.results.some((l) => l.count > 0);
|
||||
}
|
||||
|
||||
export function isSameReaction(first?: ApiReaction, second?: ApiReaction) {
|
||||
if (!first || !second) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('emoticon' in first && 'emoticon' in second) {
|
||||
return first.emoticon === second.emoticon;
|
||||
}
|
||||
|
||||
if ('documentId' in first && 'documentId' in second) {
|
||||
return first.documentId === second.documentId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canSendReaction(reaction: ApiReaction, chatReactions: ApiChatReactions) {
|
||||
if (chatReactions.type === 'all') {
|
||||
return 'emoticon' in reaction || chatReactions.areCustomAllowed;
|
||||
}
|
||||
|
||||
if (chatReactions.type === 'some') {
|
||||
return chatReactions.allowed.some((r) => isSameReaction(r, reaction));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getUserReactions(message: ApiMessage): ApiReaction[] {
|
||||
return message.reactions?.results?.filter((r): r is Required<ApiReactionCount> => isReactionChosen(r))
|
||||
.sort((a, b) => a.chosenOrder - b.chosenOrder)
|
||||
.map((r) => r.reaction) || [];
|
||||
}
|
||||
|
||||
export function getReactionUniqueKey(reaction: ApiReaction) {
|
||||
if ('emoticon' in reaction) {
|
||||
return reaction.emoticon;
|
||||
}
|
||||
|
||||
return reaction.documentId;
|
||||
}
|
||||
|
||||
export function isReactionChosen(reaction: ApiReactionCount) {
|
||||
return reaction.chosenOrder !== undefined;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { updateChatMessage } from './messages';
|
||||
import type { GlobalState } from '../types';
|
||||
import { selectChatMessage } from '../selectors';
|
||||
import type { ApiChat, ApiMessage, ApiReaction } from '../../api/types';
|
||||
|
||||
import { MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
|
||||
import {
|
||||
MIN_LEFT_COLUMN_WIDTH,
|
||||
@ -9,7 +9,8 @@ import {
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import { updateChat } from './chats';
|
||||
import type { ApiChat } from '../../api/types';
|
||||
import { isSameReaction, isReactionChosen } from '../helpers';
|
||||
import { updateChatMessage } from './messages';
|
||||
|
||||
function getLeftColumnWidth(windowWidth: number) {
|
||||
if (windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN) {
|
||||
@ -35,44 +36,54 @@ export function subtractXForEmojiInteraction(global: GlobalState, x: number) {
|
||||
: 0);
|
||||
}
|
||||
|
||||
export function addMessageReaction(global: GlobalState, chatId: string, messageId: number, reaction: string) {
|
||||
const reactions = selectChatMessage(global, chatId, messageId)?.reactions || { results: [] };
|
||||
export function addMessageReaction(
|
||||
global: GlobalState, message: ApiMessage, userReactions: ApiReaction[],
|
||||
) {
|
||||
const currentReactions = message.reactions || { results: [] };
|
||||
|
||||
// Update UI without waiting for server response
|
||||
let results = reactions.results.map((l) => (l.reaction === reaction
|
||||
? {
|
||||
...l,
|
||||
count: l.isChosen ? l.count : l.count + 1,
|
||||
isChosen: true,
|
||||
} : (l.isChosen ? {
|
||||
...l,
|
||||
isChosen: false,
|
||||
count: l.count - 1,
|
||||
} : l)))
|
||||
.filter((l) => l.count > 0);
|
||||
const results = currentReactions.results.map((current) => (
|
||||
isReactionChosen(current) ? {
|
||||
...current,
|
||||
chosenOrder: undefined,
|
||||
count: current.count - 1,
|
||||
} : current
|
||||
)).filter(({ count }) => count > 0);
|
||||
|
||||
let { recentReactions } = reactions;
|
||||
|
||||
if (reaction && !results.some((l) => l.reaction === reaction)) {
|
||||
const { currentUserId } = global;
|
||||
|
||||
results = [...results, {
|
||||
reaction,
|
||||
isChosen: true,
|
||||
count: 1,
|
||||
}];
|
||||
|
||||
if (reactions.canSeeList) {
|
||||
recentReactions = [...(recentReactions || []), {
|
||||
userId: currentUserId!,
|
||||
userReactions.forEach((reaction, i) => {
|
||||
const existingIndex = results.findIndex((r) => isSameReaction(r.reaction, reaction));
|
||||
if (existingIndex > -1) {
|
||||
results[existingIndex] = {
|
||||
...results[existingIndex],
|
||||
chosenOrder: i,
|
||||
count: results[existingIndex].count + 1,
|
||||
};
|
||||
} else {
|
||||
results.push({
|
||||
reaction,
|
||||
}];
|
||||
chosenOrder: i,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let { recentReactions = [] } = currentReactions;
|
||||
|
||||
if (recentReactions.length) {
|
||||
recentReactions = recentReactions.filter(({ userId }) => userId !== global.currentUserId);
|
||||
}
|
||||
|
||||
return updateChatMessage(global, chatId, messageId, {
|
||||
userReactions.forEach((reaction) => {
|
||||
const { currentUserId } = global;
|
||||
recentReactions.unshift({
|
||||
userId: currentUserId!,
|
||||
reaction,
|
||||
});
|
||||
});
|
||||
|
||||
return updateChatMessage(global, message.chatId, message.id, {
|
||||
reactions: {
|
||||
...reactions,
|
||||
...currentReactions,
|
||||
results,
|
||||
recentReactions,
|
||||
},
|
||||
|
||||
@ -16,7 +16,9 @@ import { LOCAL_MESSAGE_MIN_ID, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID }
|
||||
import {
|
||||
selectChat, selectChatBot, selectIsChatWithBot, selectIsChatWithSelf,
|
||||
} from './chats';
|
||||
import { selectIsUserOrChatContact, selectUser, selectUserStatus } from './users';
|
||||
import {
|
||||
selectIsCurrentUserPremium, selectIsUserOrChatContact, selectUser, selectUserStatus,
|
||||
} from './users';
|
||||
import {
|
||||
getSendingState,
|
||||
isChatChannel,
|
||||
@ -41,6 +43,7 @@ import {
|
||||
getMessageDocument,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageOriginalId,
|
||||
canSendReaction,
|
||||
} from '../helpers';
|
||||
import { findLast } from '../../util/iteratees';
|
||||
import { selectIsStickerFavorite } from './symbols';
|
||||
@ -953,10 +956,7 @@ export function selectDefaultReaction(global: GlobalState, chatId: string) {
|
||||
|
||||
const isPrivate = isUserId(chatId);
|
||||
const defaultReaction = global.config?.defaultReaction;
|
||||
const { availableReactions } = global;
|
||||
if (!defaultReaction || !availableReactions?.some(
|
||||
(l) => l.reaction === defaultReaction && !l.isInactive,
|
||||
)) {
|
||||
if (!defaultReaction) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -964,14 +964,20 @@ export function selectDefaultReaction(global: GlobalState, chatId: string) {
|
||||
return defaultReaction;
|
||||
}
|
||||
|
||||
const enabledReactions = selectChat(global, chatId)?.fullInfo?.enabledReactions;
|
||||
if (!enabledReactions?.includes(defaultReaction)) {
|
||||
const chatReactions = selectChat(global, chatId)?.fullInfo?.enabledReactions;
|
||||
if (!chatReactions || !canSendReaction(defaultReaction, chatReactions)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return defaultReaction;
|
||||
}
|
||||
|
||||
export function selectMaxUserReactions(global: GlobalState): number {
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
const { maxUserReactionsPremium = 3, maxUserReactionsDefault = 1 } = global.appConfig || {};
|
||||
return isPremium ? maxUserReactionsPremium : maxUserReactionsDefault;
|
||||
}
|
||||
|
||||
// Slow, not to be used in `withGlobal`
|
||||
export function selectVisibleUsers(global: GlobalState) {
|
||||
const { chatId, threadId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
@ -47,6 +47,8 @@ import type {
|
||||
ApiReceipt,
|
||||
ApiPaymentCredentials,
|
||||
ApiConfig,
|
||||
ApiReaction,
|
||||
ApiChatReactions,
|
||||
} from '../api/types';
|
||||
import type {
|
||||
FocusDirection,
|
||||
@ -99,7 +101,7 @@ export interface ActiveEmojiInteraction {
|
||||
|
||||
export interface ActiveReaction {
|
||||
messageId?: number;
|
||||
reaction?: string;
|
||||
reaction?: ApiReaction;
|
||||
}
|
||||
|
||||
export interface Thread {
|
||||
@ -341,6 +343,7 @@ export type GlobalState = {
|
||||
|
||||
animatedEmojis?: ApiStickerSet;
|
||||
animatedEmojiEffects?: ApiStickerSet;
|
||||
genericEmojiEffects?: ApiStickerSet;
|
||||
premiumGifts?: ApiStickerSet;
|
||||
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
||||
|
||||
@ -395,7 +398,7 @@ export type GlobalState = {
|
||||
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||
activeReactions: Record<number, ActiveReaction>;
|
||||
activeReactions: Record<number, ActiveReaction[]>;
|
||||
|
||||
localTextSearch: {
|
||||
byChatThreadKey: Record<string, {
|
||||
@ -812,6 +815,38 @@ export interface ActionPayloads {
|
||||
ids: number[];
|
||||
};
|
||||
|
||||
// Reactions
|
||||
loadAvailableReactions: never;
|
||||
|
||||
loadMessageReactions: {
|
||||
chatId: string;
|
||||
ids: number[];
|
||||
};
|
||||
|
||||
toggleReaction: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
reaction: ApiReaction;
|
||||
};
|
||||
|
||||
setDefaultReaction: {
|
||||
reaction: ApiReaction;
|
||||
};
|
||||
sendDefaultReaction: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
setChatEnabledReactions: {
|
||||
chatId: string;
|
||||
enabledReactions?: ApiChatReactions;
|
||||
};
|
||||
|
||||
stopActiveReaction: {
|
||||
messageId: number;
|
||||
reaction: ApiReaction;
|
||||
};
|
||||
|
||||
// Media Viewer & Audio Player
|
||||
openMediaViewer: {
|
||||
chatId?: string;
|
||||
@ -931,6 +966,7 @@ export interface ActionPayloads {
|
||||
};
|
||||
loadAnimatedEmojis: never;
|
||||
loadGreetingStickers: never;
|
||||
loadGenericEmojiEffects: never;
|
||||
|
||||
addRecentSticker: {
|
||||
sticker: ApiSticker;
|
||||
@ -1270,7 +1306,7 @@ export type NonTypedActionNames = (
|
||||
'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' |
|
||||
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
||||
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
||||
'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' | 'setChatEnabledReactions' |
|
||||
'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
||||
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
|
||||
// messages
|
||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||
@ -1278,12 +1314,11 @@ export type NonTypedActionNames = (
|
||||
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||
'setReplyingToId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' | 'sendReaction' |
|
||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
||||
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' | 'loadAvailableReactions' |
|
||||
'stopActiveEmojiInteraction' | 'interactWithAnimatedEmoji' | 'loadReactors' | 'setDefaultReaction' |
|
||||
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
|
||||
'stopActiveReaction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
||||
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' |
|
||||
'stopActiveEmojiInteraction' | 'interactWithAnimatedEmoji' | 'loadReactors' |
|
||||
'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'copySelectedMessages' | 'copyMessagesByIds' |
|
||||
'setEditingId' |
|
||||
// scheduled messages
|
||||
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
||||
|
||||
@ -32,8 +32,9 @@ function notifyCustomEmojiRender(emojiId: string) {
|
||||
|
||||
addCustomEmojiInputRenderCallback(notifyCustomEmojiRender);
|
||||
|
||||
export default function useEnsureCustomEmoji(id: string) {
|
||||
export default function useEnsureCustomEmoji(id?: string) {
|
||||
const lastSyncTime = useLastSyncTime();
|
||||
if (!id) return;
|
||||
notifyCustomEmojiRender(id);
|
||||
|
||||
if (getGlobal().customEmojis.byId[id]) {
|
||||
|
||||
@ -185,6 +185,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--right-column-width: 26.5rem;
|
||||
--header-height: 3.5rem;
|
||||
--custom-emoji-size: 1.25rem;
|
||||
--custom-emoji-border-radius: 0;
|
||||
|
||||
--symbol-menu-width: 26.25rem;
|
||||
--symbol-menu-height: 23.25rem;
|
||||
|
||||
@ -110,9 +110,9 @@ if (IS_OPFS_SUPPORTED) {
|
||||
})();
|
||||
}
|
||||
|
||||
export const IS_BACKDROP_BLUR_SUPPORTED = !IS_TEST && (
|
||||
CSS.supports('backdrop-filter: blur()') || CSS.supports('-webkit-backdrop-filter: blur()')
|
||||
);
|
||||
export const IS_OFFSET_PATH_SUPPORTED = CSS.supports('offset-rotate: 0deg');
|
||||
export const IS_BACKDROP_BLUR_SUPPORTED = CSS.supports('backdrop-filter: blur()')
|
||||
|| CSS.supports('-webkit-backdrop-filter: blur()');
|
||||
export const IS_COMPACT_MENU = !IS_TOUCH_ENV;
|
||||
export const IS_SCROLL_PATCH_NEEDED = !IS_MAC_OS && !IS_IOS && !IS_ANDROID;
|
||||
export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user