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