Introduce Reactions and Animated Emoji Interactions (#1583)
This commit is contained in:
parent
e6a2f4b2b6
commit
d3d9d440a6
5
src/@types/global.d.ts
vendored
5
src/@types/global.d.ts
vendored
@ -18,6 +18,11 @@ declare namespace React {
|
|||||||
interface VideoHTMLAttributes {
|
interface VideoHTMLAttributes {
|
||||||
srcObject?: MediaStream;
|
srcObject?: MediaStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MouseEvent {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnyLiteral = Record<string, any>;
|
type AnyLiteral = Record<string, any>;
|
||||||
|
|||||||
46
src/api/gramjs/apiBuilders/appConfig.ts
Normal file
46
src/api/gramjs/apiBuilders/appConfig.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import BigInt from 'big-integer';
|
||||||
|
import localDb from '../localDb';
|
||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
import { ApiAppConfig } from '../../types';
|
||||||
|
import { buildJson } from './misc';
|
||||||
|
|
||||||
|
type GramJsAppConfig = {
|
||||||
|
emojies_sounds: Record<string, {
|
||||||
|
id: string;
|
||||||
|
access_hash: string;
|
||||||
|
file_reference_base64: string;
|
||||||
|
}>;
|
||||||
|
emojies_send_dice: string[];
|
||||||
|
groupcall_video_participants_max: number;
|
||||||
|
reactions_default: string;
|
||||||
|
reactions_uniq_max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||||
|
const { emojies_sounds } = appConfig;
|
||||||
|
return Object.keys(emojies_sounds).reduce((acc: Record<string, string>, key) => {
|
||||||
|
const l = emojies_sounds[key];
|
||||||
|
localDb.documents[l.id] = new GramJs.Document({
|
||||||
|
id: BigInt(l.id),
|
||||||
|
accessHash: BigInt(l.access_hash),
|
||||||
|
dcId: 1,
|
||||||
|
mimeType: 'audio/ogg',
|
||||||
|
fileReference: Buffer.from(atob(l.file_reference_base64
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/'))),
|
||||||
|
} as GramJs.Document);
|
||||||
|
|
||||||
|
acc[key] = l.id;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiConfig(json: GramJs.TypeJSONValue): ApiAppConfig {
|
||||||
|
const appConfig = buildJson(json) as GramJsAppConfig;
|
||||||
|
|
||||||
|
return {
|
||||||
|
emojiSounds: buildEmojiSounds(appConfig),
|
||||||
|
defaultReaction: appConfig.reactions_default,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -303,6 +303,7 @@ export function buildChatTypingStatus(
|
|||||||
serverTimeOffset: number,
|
serverTimeOffset: number,
|
||||||
) {
|
) {
|
||||||
let action: string = '';
|
let action: string = '';
|
||||||
|
let emoticon: string | undefined;
|
||||||
if (update.action instanceof GramJs.SendMessageCancelAction) {
|
if (update.action instanceof GramJs.SendMessageCancelAction) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else if (update.action instanceof GramJs.SendMessageTypingAction) {
|
} else if (update.action instanceof GramJs.SendMessageTypingAction) {
|
||||||
@ -333,10 +334,16 @@ export function buildChatTypingStatus(
|
|||||||
action = 'lng_send_action_choose_sticker';
|
action = 'lng_send_action_choose_sticker';
|
||||||
} else if (update.action instanceof GramJs.SpeakingInGroupCallAction) {
|
} else if (update.action instanceof GramJs.SpeakingInGroupCallAction) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
} else if (update.action instanceof GramJs.SendMessageEmojiInteractionSeen) {
|
||||||
|
action = 'lng_user_action_watching_animations';
|
||||||
|
emoticon = update.action.emoticon;
|
||||||
|
} else if (update.action instanceof GramJs.SendMessageEmojiInteraction) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action,
|
action,
|
||||||
|
...(emoticon && { emoji: emoticon }),
|
||||||
...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }),
|
...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }),
|
||||||
timestamp: Date.now() + serverTimeOffset * 1000,
|
timestamp: Date.now() + serverTimeOffset * 1000,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,8 +22,12 @@ import {
|
|||||||
ApiThreadInfo,
|
ApiThreadInfo,
|
||||||
ApiInvoice,
|
ApiInvoice,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
ApiUser,
|
ApiReactions,
|
||||||
|
ApiReactionCount,
|
||||||
|
ApiUserReaction,
|
||||||
|
ApiAvailableReaction,
|
||||||
ApiSponsoredMessage,
|
ApiSponsoredMessage,
|
||||||
|
ApiUser,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -141,7 +145,7 @@ type UniversalMessage = (
|
|||||||
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
|
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
|
||||||
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
|
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
|
||||||
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
|
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
|
||||||
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards'
|
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions'
|
||||||
)>
|
)>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -192,6 +196,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
|||||||
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
|
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
|
||||||
views: mtpMessage.views,
|
views: mtpMessage.views,
|
||||||
isFromScheduled: mtpMessage.fromScheduled,
|
isFromScheduled: mtpMessage.fromScheduled,
|
||||||
|
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
|
||||||
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
||||||
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
||||||
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
||||||
@ -214,6 +219,54 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions {
|
||||||
|
const {
|
||||||
|
recentReactons, results, canSeeList,
|
||||||
|
} = reactions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canSeeList,
|
||||||
|
results: results.map(buildReactionCount),
|
||||||
|
recentReactions: recentReactons?.map(buildMessageUserReaction),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount {
|
||||||
|
const { chosen, count, reaction } = reactionCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isChosen: chosen,
|
||||||
|
count,
|
||||||
|
reaction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessageUserReaction(userReaction: GramJs.MessageUserReaction): ApiUserReaction {
|
||||||
|
const { userId, reaction } = userReaction;
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: buildApiPeerId(userId, 'user'),
|
||||||
|
reaction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction {
|
||||||
|
const {
|
||||||
|
selectAnimation, staticIcon, reaction, title,
|
||||||
|
inactive, aroundAnimation, centerIcon,
|
||||||
|
} = availableReaction;
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectAnimation: buildApiDocument(selectAnimation),
|
||||||
|
staticIcon: buildApiDocument(staticIcon),
|
||||||
|
aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined,
|
||||||
|
centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined,
|
||||||
|
reaction,
|
||||||
|
title,
|
||||||
|
isInactive: inactive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildMessageTextContent(
|
export function buildMessageTextContent(
|
||||||
message: string,
|
message: string,
|
||||||
entities?: GramJs.TypeMessageEntity[],
|
entities?: GramJs.TypeMessageEntity[],
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import { ApiCountry, ApiSession, ApiWallpaper } from '../../types';
|
import {
|
||||||
|
ApiCountry, ApiSession, ApiWallpaper,
|
||||||
|
} from '../../types';
|
||||||
import { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types';
|
import { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types';
|
||||||
|
|
||||||
import { buildApiDocument } from './messages';
|
import { buildApiDocument } from './messages';
|
||||||
@ -155,3 +157,16 @@ export function buildApiCountryList(countries: GramJs.help.Country[]) {
|
|||||||
general: generalList,
|
general: generalList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildJson(json: GramJs.TypeJSONValue): any {
|
||||||
|
if (json instanceof GramJs.JsonNull) return undefined;
|
||||||
|
if (json instanceof GramJs.JsonString
|
||||||
|
|| json instanceof GramJs.JsonBool
|
||||||
|
|| json instanceof GramJs.JsonNumber) return json.value;
|
||||||
|
if (json instanceof GramJs.JsonArray) return json.value.map(buildJson);
|
||||||
|
|
||||||
|
return json.value.reduce((acc: Record<string, any>, el) => {
|
||||||
|
acc[el.key] = buildJson(el.value);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import { ApiSticker, ApiStickerSet } from '../../types';
|
import {
|
||||||
|
ApiEmojiInteraction, ApiSticker, ApiStickerSet, GramJsEmojiInteraction,
|
||||||
|
} from '../../types';
|
||||||
import { MEMOJI_STICKER_ID } from '../../../config';
|
import { MEMOJI_STICKER_ID } from '../../../config';
|
||||||
|
|
||||||
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
||||||
@ -108,3 +110,9 @@ export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetC
|
|||||||
|
|
||||||
return stickerSet;
|
return stickerSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmojiInteraction {
|
||||||
|
return {
|
||||||
|
timestamps: json.a.map((l) => l.t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -341,6 +341,7 @@ async function getFullChatInfo(chatId: string): Promise<{
|
|||||||
exportedInvite,
|
exportedInvite,
|
||||||
botInfo,
|
botInfo,
|
||||||
call,
|
call,
|
||||||
|
availableReactions,
|
||||||
} = result.fullChat;
|
} = result.fullChat;
|
||||||
|
|
||||||
const members = buildChatMembers(participants);
|
const members = buildChatMembers(participants);
|
||||||
@ -358,6 +359,7 @@ async function getFullChatInfo(chatId: string): Promise<{
|
|||||||
inviteLink: exportedInvite.link,
|
inviteLink: exportedInvite.link,
|
||||||
}),
|
}),
|
||||||
groupCallId: call?.id.toString(),
|
groupCallId: call?.id.toString(),
|
||||||
|
enabledReactions: availableReactions,
|
||||||
},
|
},
|
||||||
users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any),
|
users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any),
|
||||||
groupCall: call ? {
|
groupCall: call ? {
|
||||||
@ -403,6 +405,7 @@ async function getFullChannelInfo(
|
|||||||
hiddenPrehistory,
|
hiddenPrehistory,
|
||||||
call,
|
call,
|
||||||
botInfo,
|
botInfo,
|
||||||
|
availableReactions,
|
||||||
defaultSendAs,
|
defaultSendAs,
|
||||||
} = result.fullChat;
|
} = result.fullChat;
|
||||||
|
|
||||||
@ -454,6 +457,7 @@ async function getFullChannelInfo(
|
|||||||
groupCallId: call ? String(call.id) : undefined,
|
groupCallId: call ? String(call.id) : undefined,
|
||||||
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined,
|
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined,
|
||||||
botCommands,
|
botCommands,
|
||||||
|
enabledReactions: availableReactions,
|
||||||
sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined,
|
sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined,
|
||||||
},
|
},
|
||||||
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
|
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
|
||||||
@ -1143,6 +1147,17 @@ export async function importChatInvite({ hash }: { hash: string }) {
|
|||||||
return buildApiChatFromPreview(updates.chats[0]);
|
return buildApiChatFromPreview(updates.chats[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setChatEnabledReactions({
|
||||||
|
chat, enabledReactions,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat; enabledReactions: string[];
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.SetChatAvailableReactions({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
availableReactions: enabledReactions,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
export function toggleIsProtected({
|
export function toggleIsProtected({
|
||||||
chat, isProtected,
|
chat, isProtected,
|
||||||
}: { chat: ApiChat; isProtected: boolean }) {
|
}: { chat: ApiChat; isProtected: boolean }) {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export {
|
|||||||
fetchChats, fetchFullChat, searchChats, requestChatUpdate,
|
fetchChats, fetchFullChat, searchChats, requestChatUpdate,
|
||||||
saveDraft, clearDraft, fetchChat, updateChatMutedState,
|
saveDraft, clearDraft, fetchChat, updateChatMutedState,
|
||||||
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
||||||
toggleChatPinned, toggleChatArchived, toggleDialogUnread,
|
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
|
||||||
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
|
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
|
||||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||||
@ -34,7 +34,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
||||||
faveSticker, fetchStickers, fetchSavedGifs, searchStickers, installStickerSet, uninstallStickerSet,
|
faveSticker, fetchStickers, fetchSavedGifs, searchStickers, installStickerSet, uninstallStickerSet,
|
||||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords,
|
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||||
} from './symbols';
|
} from './symbols';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -47,7 +47,7 @@ export {
|
|||||||
fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations,
|
fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations,
|
||||||
fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings,
|
fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings,
|
||||||
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
|
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
|
||||||
updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList,
|
updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig,
|
||||||
} from './settings';
|
} from './settings';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -67,3 +67,8 @@ export {
|
|||||||
editGroupCallTitle, editGroupCallParticipant, exportGroupCallInvite, fetchGroupCallParticipants,
|
editGroupCallTitle, editGroupCallParticipant, exportGroupCallInvite, fetchGroupCallParticipants,
|
||||||
joinGroupCallPresentation, leaveGroupCall, leaveGroupCallPresentation, toggleGroupCallStartSubscription,
|
joinGroupCallPresentation, leaveGroupCall, leaveGroupCallPresentation, toggleGroupCallStartSubscription,
|
||||||
} from './calls';
|
} from './calls';
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList,
|
||||||
|
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
|
||||||
|
} from './reactions';
|
||||||
|
|||||||
@ -17,9 +17,10 @@ import { getEntityTypeById } from '../gramjsBuilders';
|
|||||||
import * as cacheApi from '../../../util/cacheApi';
|
import * as cacheApi from '../../../util/cacheApi';
|
||||||
|
|
||||||
type EntityType = (
|
type EntityType = (
|
||||||
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument'
|
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
||||||
|
'document'
|
||||||
);
|
);
|
||||||
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument']);
|
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']);
|
||||||
|
|
||||||
export default async function downloadMedia(
|
export default async function downloadMedia(
|
||||||
{
|
{
|
||||||
@ -75,7 +76,9 @@ async function download(
|
|||||||
) {
|
) {
|
||||||
const mediaMatch = url.startsWith('webDocument')
|
const mediaMatch = url.startsWith('webDocument')
|
||||||
? url.match(/(webDocument):(.+)/)
|
? url.match(/(webDocument):(.+)/)
|
||||||
: url.match(/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/);
|
: url.match(
|
||||||
|
/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/,
|
||||||
|
);
|
||||||
if (!mediaMatch) {
|
if (!mediaMatch) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -102,7 +105,8 @@ async function download(
|
|||||||
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
||||||
entityType = getEntityTypeById(entityId);
|
entityType = getEntityTypeById(entityId);
|
||||||
} else {
|
} else {
|
||||||
entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument';
|
entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument' |
|
||||||
|
'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
@ -130,6 +134,9 @@ async function download(
|
|||||||
case 'webDocument':
|
case 'webDocument':
|
||||||
entity = localDb.webDocuments[entityId];
|
entity = localDb.webDocuments[entityId];
|
||||||
break;
|
break;
|
||||||
|
case 'document':
|
||||||
|
entity = localDb.documents[entityId];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import {
|
import {
|
||||||
ApiChat,
|
|
||||||
ApiAttachment,
|
ApiAttachment,
|
||||||
|
ApiChat,
|
||||||
|
ApiGlobalMessageSearchType,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
OnApiUpdate,
|
|
||||||
ApiMessageSearchType,
|
|
||||||
ApiUser,
|
|
||||||
ApiSticker,
|
|
||||||
ApiVideo,
|
|
||||||
ApiNewPoll,
|
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
|
ApiMessageSearchType,
|
||||||
|
ApiNewPoll,
|
||||||
ApiOnProgress,
|
ApiOnProgress,
|
||||||
|
ApiReportReason,
|
||||||
|
ApiSticker,
|
||||||
ApiThreadInfo,
|
ApiThreadInfo,
|
||||||
|
ApiUser,
|
||||||
|
ApiVideo,
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
MESSAGE_DELETED,
|
MESSAGE_DELETED,
|
||||||
ApiGlobalMessageSearchType,
|
OnApiUpdate,
|
||||||
ApiReportReason,
|
|
||||||
ApiSponsoredMessage,
|
ApiSponsoredMessage,
|
||||||
ApiSendMessageAction,
|
ApiSendMessageAction,
|
||||||
ApiContact,
|
ApiContact,
|
||||||
@ -31,23 +31,23 @@ import {
|
|||||||
import { invokeRequest, uploadFile } from './client';
|
import { invokeRequest, uploadFile } from './client';
|
||||||
import {
|
import {
|
||||||
buildApiMessage,
|
buildApiMessage,
|
||||||
|
buildLocalForwardedMessage,
|
||||||
buildLocalMessage,
|
buildLocalMessage,
|
||||||
buildWebPage,
|
buildWebPage,
|
||||||
buildLocalForwardedMessage,
|
|
||||||
buildApiSponsoredMessage,
|
buildApiSponsoredMessage,
|
||||||
} from '../apiBuilders/messages';
|
} from '../apiBuilders/messages';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import {
|
import {
|
||||||
buildInputEntity,
|
buildInputEntity,
|
||||||
|
buildInputMediaDocument,
|
||||||
buildInputPeer,
|
buildInputPeer,
|
||||||
|
buildInputPoll,
|
||||||
|
buildInputReportReason,
|
||||||
|
buildMtpMessageEntity,
|
||||||
generateRandomBigInt,
|
generateRandomBigInt,
|
||||||
getEntityTypeById,
|
getEntityTypeById,
|
||||||
buildInputMediaDocument,
|
|
||||||
buildInputPoll,
|
|
||||||
buildMtpMessageEntity,
|
|
||||||
isMessageWithMedia,
|
isMessageWithMedia,
|
||||||
isServiceMessageWithMedia,
|
isServiceMessageWithMedia,
|
||||||
buildInputReportReason,
|
|
||||||
buildSendMessageAction,
|
buildSendMessageAction,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
|
|||||||
136
src/api/gramjs/methods/reactions.ts
Normal file
136
src/api/gramjs/methods/reactions.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { ApiChat, ApiUser } from '../../types';
|
||||||
|
import { invokeRequest } from './client';
|
||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
import { buildInputPeer } from '../gramjsBuilders';
|
||||||
|
import localDb from '../localDb';
|
||||||
|
import { buildApiAvailableReaction, buildMessageUserReaction } from '../apiBuilders/messages';
|
||||||
|
import { REACTION_LIST_LIMIT } from '../../../config';
|
||||||
|
import { addEntitiesWithPhotosToLocalDb } from '../helpers';
|
||||||
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
|
|
||||||
|
export function sendWatchingEmojiInteraction({
|
||||||
|
chat,
|
||||||
|
emoticon,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat; emoticon: string;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.SetTyping({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
action: new GramJs.SendMessageEmojiInteractionSeen({
|
||||||
|
emoticon,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendEmojiInteraction({
|
||||||
|
chat,
|
||||||
|
emoticon,
|
||||||
|
messageId,
|
||||||
|
timestamps,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat; messageId: number; emoticon: string; timestamps: number[];
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.SetTyping({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
action: new GramJs.SendMessageEmojiInteraction({
|
||||||
|
emoticon,
|
||||||
|
msgId: messageId,
|
||||||
|
interaction: new GramJs.DataJSON({
|
||||||
|
data: JSON.stringify({
|
||||||
|
v: 1,
|
||||||
|
a: timestamps.map((t: number) => ({
|
||||||
|
t,
|
||||||
|
i: 1,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAvailableReactions() {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetAvailableReactions({}));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.messages.AvailableReactionsNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.reactions.forEach((reaction) => {
|
||||||
|
if (reaction.staticIcon instanceof GramJs.Document) {
|
||||||
|
localDb.documents[String(reaction.staticIcon.id)] = reaction.staticIcon;
|
||||||
|
}
|
||||||
|
if (reaction.selectAnimation instanceof GramJs.Document) {
|
||||||
|
localDb.documents[String(reaction.selectAnimation.id)] = reaction.selectAnimation;
|
||||||
|
}
|
||||||
|
if (reaction.aroundAnimation instanceof GramJs.Document) {
|
||||||
|
localDb.documents[String(reaction.aroundAnimation.id)] = reaction.aroundAnimation;
|
||||||
|
}
|
||||||
|
if (reaction.centerIcon instanceof GramJs.Document) {
|
||||||
|
localDb.documents[String(reaction.centerIcon.id)] = reaction.centerIcon;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.reactions.map(buildApiAvailableReaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendReaction({
|
||||||
|
chat, messageId, reaction,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat; messageId: number; reaction?: string;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.SendReaction({
|
||||||
|
...(reaction && { reaction }),
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
msgId: messageId,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchMessageReactions({
|
||||||
|
ids, chat,
|
||||||
|
}: {
|
||||||
|
ids: number[]; chat: ApiChat;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.GetMessagesReactions({
|
||||||
|
id: ids,
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMessageReactionsList({
|
||||||
|
chat, messageId, reaction, offset,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat; messageId: number; reaction?: string; offset?: string;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetMessageReactionsList({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
id: messageId,
|
||||||
|
...(reaction && { reaction }),
|
||||||
|
limit: REACTION_LIST_LIMIT,
|
||||||
|
...(offset && { offset }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesWithPhotosToLocalDb(result.users);
|
||||||
|
|
||||||
|
const { nextOffset, reactions, count } = result;
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any),
|
||||||
|
nextOffset,
|
||||||
|
reactions: reactions.map(buildMessageUserReaction),
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDefaultReaction({
|
||||||
|
reaction,
|
||||||
|
}: {
|
||||||
|
reaction: string;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.messages.SetDefaultReaction({
|
||||||
|
reaction,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -2,24 +2,35 @@ import BigInt from 'big-integer';
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiChat, ApiLangString, ApiLanguage, ApiNotifyException, ApiUser, ApiWallpaper,
|
ApiAppConfig,
|
||||||
|
ApiChat,
|
||||||
|
ApiLangString,
|
||||||
|
ApiLanguage,
|
||||||
|
ApiNotifyException,
|
||||||
|
ApiUser,
|
||||||
|
ApiWallpaper,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { ApiPrivacyKey, InputPrivacyRules, LangCode } from '../../../types';
|
import { ApiPrivacyKey, InputPrivacyRules, LangCode } from '../../../types';
|
||||||
|
|
||||||
import { BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK, LANG_PACKS } from '../../../config';
|
import { BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK, LANG_PACKS } from '../../../config';
|
||||||
import {
|
import {
|
||||||
buildApiWallpaper, buildApiSession, buildPrivacyRules, buildApiNotifyException, buildApiCountryList,
|
buildApiCountryList,
|
||||||
|
buildApiNotifyException,
|
||||||
|
buildApiSession,
|
||||||
|
buildApiWallpaper,
|
||||||
|
buildPrivacyRules,
|
||||||
} from '../apiBuilders/misc';
|
} from '../apiBuilders/misc';
|
||||||
|
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import { buildInputPrivacyKey, buildInputPeer, buildInputEntity } from '../gramjsBuilders';
|
import { buildInputEntity, buildInputPeer, buildInputPrivacyKey } from '../gramjsBuilders';
|
||||||
import { invokeRequest, uploadFile, getClient } from './client';
|
import { getClient, invokeRequest, uploadFile } from './client';
|
||||||
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
|
import { buildApiConfig } from '../apiBuilders/appConfig';
|
||||||
|
|
||||||
const MAX_INT_32 = 2 ** 31 - 1;
|
const MAX_INT_32 = 2 ** 31 - 1;
|
||||||
const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz'];
|
const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz'];
|
||||||
@ -429,6 +440,13 @@ export function updateContentSettings(isEnabled: boolean) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAppConfig(): Promise<ApiAppConfig | undefined> {
|
||||||
|
const result = await invokeRequest(new GramJs.help.GetAppConfig());
|
||||||
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
return buildApiConfig(result);
|
||||||
|
}
|
||||||
|
|
||||||
function updateLocalDb(
|
function updateLocalDb(
|
||||||
result: (
|
result: (
|
||||||
GramJs.account.PrivacyRules | GramJs.contacts.Blocked | GramJs.contacts.BlockedSlice |
|
GramJs.account.PrivacyRules | GramJs.contacts.Blocked | GramJs.contacts.BlockedSlice |
|
||||||
|
|||||||
@ -130,6 +130,21 @@ export async function fetchAnimatedEmojis() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAnimatedEmojiEffects() {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||||
|
stickerset: new GramJs.InputStickerSetAnimatedEmojiAnimations(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!(result instanceof GramJs.messages.StickerSet)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set: buildStickerSet(result.set),
|
||||||
|
stickers: processStickerResult(result.documents),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
|
export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
|
||||||
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
|
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
|
||||||
q: query,
|
q: query,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
buildPollResults,
|
buildPollResults,
|
||||||
buildApiMessageFromNotification,
|
buildApiMessageFromNotification,
|
||||||
buildMessageDraft,
|
buildMessageDraft,
|
||||||
|
buildMessageReactions,
|
||||||
} from './apiBuilders/messages';
|
} from './apiBuilders/messages';
|
||||||
import {
|
import {
|
||||||
buildChatMember,
|
buildChatMember,
|
||||||
@ -46,6 +47,7 @@ import {
|
|||||||
getGroupCallId,
|
getGroupCallId,
|
||||||
} from './apiBuilders/calls';
|
} from './apiBuilders/calls';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
||||||
|
import { buildApiEmojiInteraction } from './apiBuilders/symbols';
|
||||||
|
|
||||||
type Update = (
|
type Update = (
|
||||||
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
|
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
|
||||||
@ -292,6 +294,13 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
chatId: message.chatId,
|
chatId: message.chatId,
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdateMessageReactions) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateMessageReactions',
|
||||||
|
id: update.msgId,
|
||||||
|
chatId: getApiChatIdFromMtpPeer(update.peer),
|
||||||
|
reactions: buildMessageReactions(update.reactions),
|
||||||
|
});
|
||||||
} else if (update instanceof GramJs.UpdateDeleteMessages) {
|
} else if (update instanceof GramJs.UpdateDeleteMessages) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'deleteMessages',
|
'@type': 'deleteMessages',
|
||||||
@ -614,11 +623,21 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
? buildApiPeerId(update.userId, 'user')
|
? buildApiPeerId(update.userId, 'user')
|
||||||
: buildApiPeerId(update.chatId, 'chat');
|
: buildApiPeerId(update.chatId, 'chat');
|
||||||
|
|
||||||
onUpdate({
|
if (update.action instanceof GramJs.SendMessageEmojiInteraction) {
|
||||||
'@type': 'updateChatTypingStatus',
|
onUpdate({
|
||||||
id,
|
'@type': 'updateStartEmojiInteraction',
|
||||||
typingStatus: buildChatTypingStatus(update, serverTimeOffset),
|
id,
|
||||||
});
|
emoji: update.action.emoticon,
|
||||||
|
messageId: update.action.msgId,
|
||||||
|
interaction: buildApiEmojiInteraction(JSON.parse(update.action.interaction.data)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateChatTypingStatus',
|
||||||
|
id,
|
||||||
|
typingStatus: buildChatTypingStatus(update, serverTimeOffset),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (update instanceof GramJs.UpdateChannelUserTyping) {
|
} else if (update instanceof GramJs.UpdateChannelUserTyping) {
|
||||||
const id = buildApiPeerId(update.channelId, 'channel');
|
const id = buildApiPeerId(update.channelId, 'channel');
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export interface ApiTypingStatus {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
action: string;
|
action: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
emoji?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiChatFullInfo {
|
export interface ApiChatFullInfo {
|
||||||
@ -86,6 +87,7 @@ export interface ApiChatFullInfo {
|
|||||||
};
|
};
|
||||||
linkedChatId?: string;
|
linkedChatId?: string;
|
||||||
botCommands?: ApiBotCommand[];
|
botCommands?: ApiBotCommand[];
|
||||||
|
enabledReactions?: string[];
|
||||||
sendAsId?: string;
|
sendAsId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -268,6 +268,39 @@ export interface ApiMessage {
|
|||||||
isFromScheduled?: boolean;
|
isFromScheduled?: boolean;
|
||||||
seenByUserIds?: string[];
|
seenByUserIds?: string[];
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
|
reactors?: {
|
||||||
|
nextOffset?: string;
|
||||||
|
count: number;
|
||||||
|
reactions: ApiUserReaction[];
|
||||||
|
};
|
||||||
|
reactions?: ApiReactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiReactions {
|
||||||
|
canSeeList?: boolean;
|
||||||
|
results: ApiReactionCount[];
|
||||||
|
recentReactions?: ApiUserReaction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiUserReaction {
|
||||||
|
userId: string;
|
||||||
|
reaction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiReactionCount {
|
||||||
|
isChosen?: boolean;
|
||||||
|
count: number;
|
||||||
|
reaction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiAvailableReaction {
|
||||||
|
selectAnimation?: ApiDocument;
|
||||||
|
staticIcon?: ApiDocument;
|
||||||
|
centerIcon?: ApiDocument;
|
||||||
|
aroundAnimation?: ApiDocument;
|
||||||
|
reaction: string;
|
||||||
|
title: string;
|
||||||
|
isInactive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiThreadInfo {
|
export interface ApiThreadInfo {
|
||||||
|
|||||||
@ -109,3 +109,20 @@ export interface ApiCountryCode extends ApiCountry {
|
|||||||
prefixes?: string[];
|
prefixes?: string[];
|
||||||
patterns?: string[];
|
patterns?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiAppConfig {
|
||||||
|
emojiSounds: Record<string, string>;
|
||||||
|
defaultReaction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GramJsEmojiInteraction {
|
||||||
|
v: number;
|
||||||
|
a: {
|
||||||
|
i: number;
|
||||||
|
t: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiEmojiInteraction {
|
||||||
|
timestamps: number[];
|
||||||
|
}
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import {
|
|||||||
ApiChatFolder,
|
ApiChatFolder,
|
||||||
} from './chats';
|
} from './chats';
|
||||||
import {
|
import {
|
||||||
ApiFormattedText, ApiMessage, ApiPhoto, ApiPoll, ApiStickerSet, ApiThreadInfo,
|
ApiFormattedText, ApiMessage, ApiPhoto, ApiPoll, ApiReactions, ApiStickerSet, ApiThreadInfo,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
import { ApiUser, ApiUserFullInfo, ApiUserStatus } from './users';
|
import { ApiUser, ApiUserFullInfo, ApiUserStatus } from './users';
|
||||||
import {
|
import {
|
||||||
|
ApiEmojiInteraction,
|
||||||
ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
|
ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
import {
|
import {
|
||||||
@ -102,6 +103,14 @@ export type ApiUpdateChatTypingStatus = {
|
|||||||
typingStatus: ApiTypingStatus | undefined;
|
typingStatus: ApiTypingStatus | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateStartEmojiInteraction = {
|
||||||
|
'@type': 'updateStartEmojiInteraction';
|
||||||
|
id: string;
|
||||||
|
emoji: string;
|
||||||
|
messageId: number;
|
||||||
|
interaction: ApiEmojiInteraction;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdateChatFullInfo = {
|
export type ApiUpdateChatFullInfo = {
|
||||||
'@type': 'updateChatFullInfo';
|
'@type': 'updateChatFullInfo';
|
||||||
id: string;
|
id: string;
|
||||||
@ -284,6 +293,13 @@ export type ApiUpdateDraftMessage = {
|
|||||||
replyingToId?: number;
|
replyingToId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateMessageReactions = {
|
||||||
|
'@type': 'updateMessageReactions';
|
||||||
|
id: number;
|
||||||
|
chatId: string;
|
||||||
|
reactions: ApiReactions;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiDeleteContact = {
|
export type ApiDeleteContact = {
|
||||||
'@type': 'deleteContact';
|
'@type': 'deleteContact';
|
||||||
id: string;
|
id: string;
|
||||||
@ -435,13 +451,13 @@ export type ApiUpdate = (
|
|||||||
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
|
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
|
||||||
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos |
|
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos |
|
||||||
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
|
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
|
||||||
ApiUpdateError | ApiUpdateResetContacts |
|
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
|
||||||
ApiUpdateFavoriteStickers | ApiUpdateStickerSet |
|
ApiUpdateFavoriteStickers | ApiUpdateStickerSet |
|
||||||
ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
|
ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
|
||||||
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages |
|
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages |
|
||||||
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode |
|
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode |
|
||||||
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
||||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite |
|
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
||||||
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
||||||
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId
|
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId
|
||||||
);
|
);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
src/assets/tgs/animatedEmojis/Cumshot.tgs
Normal file
BIN
src/assets/tgs/animatedEmojis/Cumshot.tgs
Normal file
Binary file not shown.
BIN
src/assets/tgs/animatedEmojis/Eggplant.tgs
Normal file
BIN
src/assets/tgs/animatedEmojis/Eggplant.tgs
Normal file
Binary file not shown.
BIN
src/assets/tgs/animatedEmojis/Peach.tgs
Normal file
BIN
src/assets/tgs/animatedEmojis/Peach.tgs
Normal file
Binary file not shown.
@ -12,6 +12,8 @@ export { default as PinMessageModal } from '../components/common/PinMessageModal
|
|||||||
export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllMessagesModal';
|
export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllMessagesModal';
|
||||||
export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar';
|
export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar';
|
||||||
export { default as SeenByModal } from '../components/common/SeenByModal';
|
export { default as SeenByModal } from '../components/common/SeenByModal';
|
||||||
|
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
||||||
|
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
||||||
|
|
||||||
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
||||||
export { default as Settings } from '../components/left/settings/Settings';
|
export { default as Settings } from '../components/left/settings/Settings';
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useCallback, useRef, useState,
|
FC, memo,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMediaFormat, ApiSticker } from '../../api/types';
|
import { ApiMediaFormat, ApiSticker } from '../../api/types';
|
||||||
|
import { ActiveEmojiInteraction } from '../../global/types';
|
||||||
|
|
||||||
import { LIKE_STICKER_ID } from './helpers/mediaDimensions';
|
import { LIKE_STICKER_ID } from './helpers/mediaDimensions';
|
||||||
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useAnimatedEmoji from './hooks/useAnimatedEmoji';
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
|
|
||||||
@ -16,30 +17,43 @@ import './AnimatedEmoji.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
sticker: ApiSticker;
|
sticker: ApiSticker;
|
||||||
|
effect?: ApiSticker;
|
||||||
|
isOwn?: boolean;
|
||||||
|
soundId?: string;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
size?: 'large' | 'medium' | 'small';
|
size?: 'large' | 'medium' | 'small';
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
forceLoadPreview?: boolean;
|
forceLoadPreview?: boolean;
|
||||||
|
messageId?: number;
|
||||||
|
chatId?: string;
|
||||||
|
activeEmojiInteraction?: ActiveEmojiInteraction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const QUALITY = 1;
|
const QUALITY = 1;
|
||||||
const WIDTH = {
|
|
||||||
large: 160,
|
|
||||||
medium: 128,
|
|
||||||
small: 104,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AnimatedEmoji: FC<OwnProps> = ({
|
const AnimatedEmoji: FC<OwnProps> = ({
|
||||||
sticker,
|
sticker,
|
||||||
|
effect,
|
||||||
|
isOwn,
|
||||||
|
soundId,
|
||||||
size = 'medium',
|
size = 'medium',
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
forceLoadPreview,
|
forceLoadPreview,
|
||||||
|
messageId,
|
||||||
|
chatId,
|
||||||
|
activeEmojiInteraction,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
const {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
markAnimationLoaded,
|
||||||
|
isAnimationLoaded,
|
||||||
|
ref,
|
||||||
|
width,
|
||||||
|
style,
|
||||||
|
handleClick,
|
||||||
|
playKey,
|
||||||
|
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteraction, isOwn, undefined, effect?.emoji);
|
||||||
|
|
||||||
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
|
||||||
const localMediaHash = `sticker${sticker.id}`;
|
const localMediaHash = `sticker${sticker.id}`;
|
||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
@ -56,19 +70,11 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
|||||||
const mediaData = useMedia(localMediaHash, !isIntersecting, ApiMediaFormat.Lottie, lastSyncTime);
|
const mediaData = useMedia(localMediaHash, !isIntersecting, ApiMediaFormat.Lottie, lastSyncTime);
|
||||||
const isMediaLoaded = Boolean(mediaData);
|
const isMediaLoaded = Boolean(mediaData);
|
||||||
|
|
||||||
const [playKey, setPlayKey] = useState(String(Math.random()));
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
setPlayKey(String(Math.random()));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const width = WIDTH[size];
|
|
||||||
const style = `width: ${width}px; height: ${width}px;`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="AnimatedEmoji media-inner"
|
className="AnimatedEmoji media-inner"
|
||||||
// @ts-ignore
|
// @ts-ignore teact feature
|
||||||
style={style}
|
style={style}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
@ -78,7 +84,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
|||||||
{!isAnimationLoaded && previewBlobUrl && (
|
{!isAnimationLoaded && previewBlobUrl && (
|
||||||
<img src={previewBlobUrl} className={transitionClassNames} alt="" />
|
<img src={previewBlobUrl} className={transitionClassNames} alt="" />
|
||||||
)}
|
)}
|
||||||
{isMediaLoaded && (
|
{isMediaLoaded && localMediaHash && (
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
key={localMediaHash}
|
key={localMediaHash}
|
||||||
id={localMediaHash}
|
id={localMediaHash}
|
||||||
@ -94,4 +100,4 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AnimatedEmoji;
|
export default memo(AnimatedEmoji);
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type OwnProps = {
|
|||||||
isLowPriority?: boolean;
|
isLowPriority?: boolean;
|
||||||
onLoad?: NoneToVoidFunction;
|
onLoad?: NoneToVoidFunction;
|
||||||
color?: [number, number, number];
|
color?: [number, number, number];
|
||||||
|
onEnded?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RLottieClass = typeof import('../../lib/rlottie/RLottie').default;
|
type RLottieClass = typeof import('../../lib/rlottie/RLottie').default;
|
||||||
@ -54,6 +55,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
isLowPriority,
|
isLowPriority,
|
||||||
onLoad,
|
onLoad,
|
||||||
color,
|
color,
|
||||||
|
onEnded,
|
||||||
}) => {
|
}) => {
|
||||||
const [animation, setAnimation] = useState<RLottieInstance>();
|
const [animation, setAnimation] = useState<RLottieInstance>();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -89,6 +91,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
},
|
},
|
||||||
onLoad,
|
onLoad,
|
||||||
color,
|
color,
|
||||||
|
onEnded,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (speed) {
|
if (speed) {
|
||||||
@ -109,7 +112,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [color, animation, animationData, id, isLowPriority, noLoop, onLoad, quality, size, speed]);
|
}, [color, animation, animationData, id, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!animation) return;
|
if (!animation) return;
|
||||||
|
|||||||
85
src/components/common/LocalAnimatedEmoji.tsx
Normal file
85
src/components/common/LocalAnimatedEmoji.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useEffect, useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ActiveEmojiInteraction } from '../../global/types';
|
||||||
|
|
||||||
|
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
|
import getAnimationData, { ANIMATED_STICKERS_PATHS } from './helpers/animatedAssets';
|
||||||
|
import useAnimatedEmoji from './hooks/useAnimatedEmoji';
|
||||||
|
|
||||||
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
|
|
||||||
|
const QUALITY = 1;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
localSticker?: string;
|
||||||
|
localEffect?: string;
|
||||||
|
isOwn?: boolean;
|
||||||
|
soundId?: string;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
size?: 'large' | 'medium' | 'small';
|
||||||
|
lastSyncTime?: number;
|
||||||
|
forceLoadPreview?: boolean;
|
||||||
|
messageId?: number;
|
||||||
|
chatId?: string;
|
||||||
|
activeEmojiInteraction?: ActiveEmojiInteraction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocalAnimatedEmoji: FC<OwnProps> = ({
|
||||||
|
localSticker,
|
||||||
|
localEffect,
|
||||||
|
isOwn,
|
||||||
|
soundId,
|
||||||
|
size = 'medium',
|
||||||
|
observeIntersection,
|
||||||
|
messageId,
|
||||||
|
chatId,
|
||||||
|
activeEmojiInteraction,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
playKey,
|
||||||
|
ref,
|
||||||
|
style,
|
||||||
|
width,
|
||||||
|
handleClick,
|
||||||
|
markAnimationLoaded,
|
||||||
|
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteraction, isOwn, localEffect);
|
||||||
|
const id = `local_emoji_${localSticker}`;
|
||||||
|
|
||||||
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
|
const [localStickerAnimationData, setLocalStickerAnimationData] = useState<AnyLiteral>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (localSticker) {
|
||||||
|
getAnimationData(localSticker as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => {
|
||||||
|
setLocalStickerAnimationData(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [localSticker]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="AnimatedEmoji media-inner"
|
||||||
|
// @ts-ignore teact feature
|
||||||
|
style={style}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{localStickerAnimationData && (
|
||||||
|
<AnimatedSticker
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
animationData={localStickerAnimationData}
|
||||||
|
size={width}
|
||||||
|
quality={QUALITY}
|
||||||
|
play={isIntersecting && playKey}
|
||||||
|
noLoop
|
||||||
|
onLoad={markAnimationLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(LocalAnimatedEmoji);
|
||||||
4
src/components/common/ReactionStaticEmoji.scss
Normal file
4
src/components/common/ReactionStaticEmoji.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.ReactionStaticEmoji {
|
||||||
|
width: 1rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
36
src/components/common/ReactionStaticEmoji.tsx
Normal file
36
src/components/common/ReactionStaticEmoji.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { RefObject } from 'react';
|
||||||
|
import React, { FC, memo } from '../../lib/teact/teact';
|
||||||
|
import { getGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
|
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import './ReactionStaticEmoji.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
reaction: string;
|
||||||
|
ref?: RefObject<HTMLImageElement>;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReactionStaticEmoji: FC<OwnProps> = ({
|
||||||
|
reaction,
|
||||||
|
ref,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const staticIconId = getGlobal().availableReactions?.find((l) => l.reaction === reaction)?.staticIcon?.id;
|
||||||
|
const mediaData = useMedia(`document${staticIconId}`, !staticIconId, ApiMediaFormat.BlobUrl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={buildClassName('ReactionStaticEmoji', className)}
|
||||||
|
ref={ref}
|
||||||
|
src={mediaData}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactionStaticEmoji);
|
||||||
@ -28,7 +28,7 @@ const TypingStatus: FC<OwnProps & StateProps> = ({ typingStatus, typingUser }) =
|
|||||||
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span>
|
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span>
|
||||||
)}
|
)}
|
||||||
{/* fix for translation "username _is_ typing" */}
|
{/* fix for translation "username _is_ typing" */}
|
||||||
{lang(typingStatus.action).replace('{user}', '').trim()}
|
{lang(typingStatus.action).replace('{user}', '').replace('{emoji}', typingStatus.emoji).trim()}
|
||||||
<span className="ellipsis" />
|
<span className="ellipsis" />
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,35 +3,41 @@ import { ApiMediaFormat } from '../../../api/types';
|
|||||||
import * as mediaLoader from '../../../util/mediaLoader';
|
import * as mediaLoader from '../../../util/mediaLoader';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import MonkeyIdle from '../../../assets/TwoFactorSetupMonkeyIdle.tgs';
|
import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import MonkeyTracking from '../../../assets/TwoFactorSetupMonkeyTracking.tgs';
|
import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import MonkeyClose from '../../../assets/TwoFactorSetupMonkeyClose.tgs';
|
import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import MonkeyPeek from '../../../assets/TwoFactorSetupMonkeyPeek.tgs';
|
import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import FoldersAll from '../../../assets/FoldersAll.tgs';
|
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import FoldersNew from '../../../assets/FoldersNew.tgs';
|
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import DiscussionGroups from '../../../assets/DiscussionGroupsDucks.tgs';
|
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import CameraFlip from '../../../assets/animatedIcons/CameraFlip.tgs';
|
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import HandFilled from '../../../assets/animatedIcons/HandFilled.tgs';
|
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import HandOutline from '../../../assets/animatedIcons/HandOutline.tgs';
|
import HandOutline from '../../../assets/tgs/calls/HandOutline.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Speaker from '../../../assets/animatedIcons/Speaker.tgs';
|
import Speaker from '../../../assets/tgs/calls/Speaker.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import VoiceAllowTalk from '../../../assets/animatedIcons/VoiceAllowTalk.tgs';
|
import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import VoiceMini from '../../../assets/animatedIcons/VoiceMini.tgs';
|
import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import VoiceMuted from '../../../assets/animatedIcons/VoiceMuted.tgs';
|
import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import VoiceOutlined from '../../../assets/animatedIcons/VoiceOutlined.tgs';
|
import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs';
|
||||||
|
// @ts-ignore
|
||||||
|
import Peach from '../../../assets/tgs/animatedEmojis/Peach.tgs';
|
||||||
|
// @ts-ignore
|
||||||
|
import Eggplant from '../../../assets/tgs/animatedEmojis/Eggplant.tgs';
|
||||||
|
// @ts-ignore
|
||||||
|
import Cumshot from '../../../assets/tgs/animatedEmojis/Cumshot.tgs';
|
||||||
|
|
||||||
export const ANIMATED_STICKERS_PATHS = {
|
export const ANIMATED_STICKERS_PATHS = {
|
||||||
MonkeyIdle,
|
MonkeyIdle,
|
||||||
@ -49,6 +55,9 @@ export const ANIMATED_STICKERS_PATHS = {
|
|||||||
VoiceMini,
|
VoiceMini,
|
||||||
VoiceMuted,
|
VoiceMuted,
|
||||||
VoiceOutlined,
|
VoiceOutlined,
|
||||||
|
Peach,
|
||||||
|
Eggplant,
|
||||||
|
Cumshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) {
|
export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) {
|
||||||
|
|||||||
155
src/components/common/hooks/useAnimatedEmoji.ts
Normal file
155
src/components/common/hooks/useAnimatedEmoji.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
useCallback, useEffect, useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import safePlay from '../../../util/safePlay';
|
||||||
|
import { getDispatch } from '../../../lib/teact/teactn';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import { ActiveEmojiInteraction } from '../../../global/types';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import { selectLocalAnimatedEmojiEffectByName } from '../../../modules/selectors';
|
||||||
|
|
||||||
|
const WIDTH = {
|
||||||
|
large: 160,
|
||||||
|
medium: 128,
|
||||||
|
small: 104,
|
||||||
|
};
|
||||||
|
const INTERACTION_BUNCH_TIME = 1000;
|
||||||
|
const MS_DIVIDER = 1000;
|
||||||
|
const TIME_DEFAULT = 0;
|
||||||
|
|
||||||
|
export default function useAnimatedEmoji(
|
||||||
|
size: 'large' | 'medium' | 'small',
|
||||||
|
chatId?: string,
|
||||||
|
messageId?: number,
|
||||||
|
soundId?: string,
|
||||||
|
activeEmojiInteraction?: ActiveEmojiInteraction,
|
||||||
|
isOwn?: boolean,
|
||||||
|
localEffect?: string,
|
||||||
|
emoji?: string,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
interactWithAnimatedEmoji, sendEmojiInteraction, sendWatchingEmojiInteraction,
|
||||||
|
} = getDispatch();
|
||||||
|
|
||||||
|
const hasEffect = localEffect || emoji;
|
||||||
|
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const audioRef = useRef<HTMLAudioElement | undefined>(null);
|
||||||
|
|
||||||
|
const soundMediaData = useMedia(soundId ? `document${soundId}` : undefined, !soundId);
|
||||||
|
|
||||||
|
const width = WIDTH[size];
|
||||||
|
const style = `width: ${width}px; height: ${width}px;`;
|
||||||
|
|
||||||
|
const [playKey, setPlayKey] = useState(String(Math.random()));
|
||||||
|
const interactions = useRef<number[] | undefined>(undefined);
|
||||||
|
const startedInteractions = useRef<number | undefined>(undefined);
|
||||||
|
const sendInteractionBunch = useCallback(() => {
|
||||||
|
const container = ref.current;
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
sendEmojiInteraction({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
localEffect,
|
||||||
|
emoji,
|
||||||
|
interactions: interactions.current,
|
||||||
|
});
|
||||||
|
startedInteractions.current = undefined;
|
||||||
|
interactions.current = undefined;
|
||||||
|
}, [sendEmojiInteraction, chatId, messageId, localEffect, emoji]);
|
||||||
|
|
||||||
|
const play = useCallback(() => {
|
||||||
|
setPlayKey(String(Math.random()));
|
||||||
|
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (soundMediaData) {
|
||||||
|
if (audio) {
|
||||||
|
audio.pause();
|
||||||
|
audio.remove();
|
||||||
|
}
|
||||||
|
audioRef.current = new Audio();
|
||||||
|
audioRef.current.src = soundMediaData;
|
||||||
|
safePlay(audioRef.current);
|
||||||
|
audioRef.current.addEventListener('ended', () => {
|
||||||
|
audioRef.current = undefined;
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
}, [soundMediaData]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
play();
|
||||||
|
|
||||||
|
const container = ref.current;
|
||||||
|
|
||||||
|
if (!hasEffect || !container || !messageId || !chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
interactWithAnimatedEmoji({
|
||||||
|
localEffect,
|
||||||
|
emoji,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
startSize: width,
|
||||||
|
isReversed: !isOwn,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!interactions.current) {
|
||||||
|
interactions.current = [];
|
||||||
|
startedInteractions.current = performance.now();
|
||||||
|
setTimeout(sendInteractionBunch, INTERACTION_BUNCH_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
interactions.current.push(startedInteractions.current
|
||||||
|
? (performance.now() - startedInteractions.current) / MS_DIVIDER
|
||||||
|
: TIME_DEFAULT);
|
||||||
|
}, [
|
||||||
|
chatId, emoji, hasEffect, interactWithAnimatedEmoji, isOwn,
|
||||||
|
localEffect, messageId, play, sendInteractionBunch, width,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set an end anchor for remote activated interaction
|
||||||
|
useEffect(() => {
|
||||||
|
const container = ref.current;
|
||||||
|
|
||||||
|
if (!container || !activeEmojiInteraction) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
messageId: selectedMessageId, endX, endY,
|
||||||
|
} = activeEmojiInteraction;
|
||||||
|
|
||||||
|
if (!endX && !endY && selectedMessageId === messageId) {
|
||||||
|
const { x, y } = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
sendWatchingEmojiInteraction({
|
||||||
|
chatId,
|
||||||
|
emoticon: localEffect ? selectLocalAnimatedEmojiEffectByName(localEffect) : emoji,
|
||||||
|
startSize: width,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
isReversed: !isOwn,
|
||||||
|
});
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
activeEmojiInteraction, chatId, emoji, isOwn, localEffect, messageId, play, sendWatchingEmojiInteraction, width,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
playKey,
|
||||||
|
ref,
|
||||||
|
style,
|
||||||
|
width,
|
||||||
|
handleClick,
|
||||||
|
markAnimationLoaded,
|
||||||
|
isAnimationLoaded,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -122,6 +122,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
case SettingsScreens.GeneralChatBackground:
|
case SettingsScreens.GeneralChatBackground:
|
||||||
|
case SettingsScreens.QuickReaction:
|
||||||
setSettingsScreen(SettingsScreens.General);
|
setSettingsScreen(SettingsScreens.General);
|
||||||
return;
|
return;
|
||||||
case SettingsScreens.GeneralChatBackgroundColor:
|
case SettingsScreens.GeneralChatBackgroundColor:
|
||||||
|
|||||||
@ -301,3 +301,19 @@
|
|||||||
.username-link {
|
.username-link {
|
||||||
color: var(--color-links);
|
color: var(--color-links);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-quick-reaction {
|
||||||
|
.ReactionStaticEmoji {
|
||||||
|
margin-inline-end: 1rem;
|
||||||
|
width: 1.5rem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.SettingsDefaultReaction {
|
||||||
|
.ReactionStaticEmoji {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-inline-end: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import SettingsPrivacyActiveSessions from './SettingsPrivacyActiveSessions';
|
|||||||
import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers';
|
import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers';
|
||||||
import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
||||||
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
||||||
|
import SettingsQuickReaction from './SettingsQuickReaction';
|
||||||
|
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
|
|
||||||
@ -180,10 +181,15 @@ const Settings: FC<OwnProps> = ({
|
|||||||
isActive={isScreenActive
|
isActive={isScreenActive
|
||||||
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
||||||
|| screen === SettingsScreens.GeneralChatBackground
|
|| screen === SettingsScreens.GeneralChatBackground
|
||||||
|
|| screen === SettingsScreens.QuickReaction
|
||||||
|| isPrivacyScreen || isFoldersScreen}
|
|| isPrivacyScreen || isFoldersScreen}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case SettingsScreens.QuickReaction:
|
||||||
|
return (
|
||||||
|
<SettingsQuickReaction onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
||||||
|
);
|
||||||
case SettingsScreens.Notifications:
|
case SettingsScreens.Notifications:
|
||||||
return (
|
return (
|
||||||
<SettingsNotifications onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
<SettingsNotifications onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import Checkbox from '../../ui/Checkbox';
|
|||||||
import RadioGroup, { IRadioOption } from '../../ui/RadioGroup';
|
import RadioGroup, { IRadioOption } from '../../ui/RadioGroup';
|
||||||
import SettingsStickerSet from './SettingsStickerSet';
|
import SettingsStickerSet from './SettingsStickerSet';
|
||||||
import StickerSetModal from '../../common/StickerSetModal.async';
|
import StickerSetModal from '../../common/StickerSetModal.async';
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@ -38,6 +39,7 @@ type StateProps =
|
|||||||
)> & {
|
)> & {
|
||||||
stickerSetIds?: string[];
|
stickerSetIds?: string[];
|
||||||
stickerSetsById?: Record<string, ApiStickerSet>;
|
stickerSetsById?: Record<string, ApiStickerSet>;
|
||||||
|
defaultReaction?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANIMATION_LEVEL_OPTIONS = [
|
const ANIMATION_LEVEL_OPTIONS = [
|
||||||
@ -60,6 +62,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
onReset,
|
onReset,
|
||||||
stickerSetIds,
|
stickerSetIds,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
|
defaultReaction,
|
||||||
messageTextSize,
|
messageTextSize,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
messageSendKeyCombo,
|
messageSendKeyCombo,
|
||||||
@ -189,6 +192,16 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('AccDescrStickers')}</h4>
|
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('AccDescrStickers')}</h4>
|
||||||
|
|
||||||
|
{defaultReaction && (
|
||||||
|
<ListItem
|
||||||
|
className="SettingsDefaultReaction"
|
||||||
|
onClick={() => onScreenSelect(SettingsScreens.QuickReaction)}
|
||||||
|
>
|
||||||
|
<ReactionStaticEmoji reaction={defaultReaction} />
|
||||||
|
<div className="title">{lang('DoubleTapSetting')}</div>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={lang('SuggestStickers')}
|
label={lang('SuggestStickers')}
|
||||||
checked={shouldSuggestStickers}
|
checked={shouldSuggestStickers}
|
||||||
@ -237,6 +250,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
]),
|
]),
|
||||||
stickerSetIds: global.stickers.added.setIds,
|
stickerSetIds: global.stickers.added.setIds,
|
||||||
stickerSetsById: global.stickers.setsById,
|
stickerSetsById: global.stickers.setsById,
|
||||||
|
defaultReaction: global.appConfig?.defaultReaction,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsGeneral));
|
)(SettingsGeneral));
|
||||||
|
|||||||
@ -87,6 +87,8 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
return <h3>{lang('lng_settings_information')}</h3>;
|
return <h3>{lang('lng_settings_information')}</h3>;
|
||||||
case SettingsScreens.General:
|
case SettingsScreens.General:
|
||||||
return <h3>{lang('General')}</h3>;
|
return <h3>{lang('General')}</h3>;
|
||||||
|
case SettingsScreens.QuickReaction:
|
||||||
|
return <h3>{lang('DoubleTapSetting')}</h3>;
|
||||||
case SettingsScreens.Notifications:
|
case SettingsScreens.Notifications:
|
||||||
return <h3>{lang('Notifications')}</h3>;
|
return <h3>{lang('Notifications')}</h3>;
|
||||||
case SettingsScreens.DataStorage:
|
case SettingsScreens.DataStorage:
|
||||||
|
|||||||
65
src/components/left/settings/SettingsQuickReaction.tsx
Normal file
65
src/components/left/settings/SettingsQuickReaction.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { SettingsScreens } from '../../../types';
|
||||||
|
import { ApiAvailableReaction } from '../../../api/types';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
import RadioGroup from '../../ui/RadioGroup';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
selectedReaction?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||||
|
isActive,
|
||||||
|
onReset,
|
||||||
|
onScreenSelect,
|
||||||
|
availableReactions,
|
||||||
|
selectedReaction,
|
||||||
|
}) => {
|
||||||
|
const { setDefaultReaction } = getDispatch();
|
||||||
|
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General);
|
||||||
|
|
||||||
|
const options = availableReactions?.filter((l) => !l.isInactive).map((l) => {
|
||||||
|
return {
|
||||||
|
label: <><ReactionStaticEmoji reaction={l.reaction} />{l.title}</>,
|
||||||
|
value: l.reaction,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const handleChange = useCallback((reaction: string) => {
|
||||||
|
setDefaultReaction({ reaction });
|
||||||
|
}, [setDefaultReaction]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content settings-item custom-scroll settings-quick-reaction">
|
||||||
|
<RadioGroup
|
||||||
|
name="quick-reaction-settings"
|
||||||
|
options={options}
|
||||||
|
selected={selectedReaction}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global) => {
|
||||||
|
const { availableReactions, appConfig } = global;
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableReactions,
|
||||||
|
selectedReaction: appConfig?.defaultReaction,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(SettingsQuickReaction));
|
||||||
@ -108,12 +108,14 @@ const Main: FC<StateProps> = ({
|
|||||||
loadTopInlineBots,
|
loadTopInlineBots,
|
||||||
loadEmojiKeywords,
|
loadEmojiKeywords,
|
||||||
loadCountryList,
|
loadCountryList,
|
||||||
|
loadAvailableReactions,
|
||||||
loadStickerSets,
|
loadStickerSets,
|
||||||
loadAddedStickers,
|
loadAddedStickers,
|
||||||
loadFavoriteStickers,
|
loadFavoriteStickers,
|
||||||
ensureTimeFormat,
|
ensureTimeFormat,
|
||||||
openStickerSetShortName,
|
openStickerSetShortName,
|
||||||
checkVersionNotification,
|
checkVersionNotification,
|
||||||
|
loadAppConfig,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
const isSynced = Boolean(lastSyncTime);
|
const isSynced = Boolean(lastSyncTime);
|
||||||
|
|
||||||
@ -127,6 +129,8 @@ const Main: FC<StateProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastSyncTime) {
|
if (lastSyncTime) {
|
||||||
updateIsOnline(true);
|
updateIsOnline(true);
|
||||||
|
loadAppConfig();
|
||||||
|
loadAvailableReactions();
|
||||||
loadAnimatedEmojis();
|
loadAnimatedEmojis();
|
||||||
loadNotificationSettings();
|
loadNotificationSettings();
|
||||||
loadNotificationExceptions();
|
loadNotificationExceptions();
|
||||||
@ -135,7 +139,7 @@ const Main: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||||
loadTopInlineBots, updateIsOnline,
|
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Language-based API calls
|
// Language-based API calls
|
||||||
|
|||||||
15
src/components/middle/EmojiInteractionAnimation.async.tsx
Normal file
15
src/components/middle/EmojiInteractionAnimation.async.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { FC, memo } from '../../lib/teact/teact';
|
||||||
|
import { OwnProps } from './EmojiInteractionAnimation';
|
||||||
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const EmojiInteractionAnimationAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { emojiInteraction } = props;
|
||||||
|
const EmojiInteractionAnimation = useModuleLoader(Bundles.Extra, 'EmojiInteractionAnimation', !emojiInteraction);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return EmojiInteractionAnimation ? <EmojiInteractionAnimation {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(EmojiInteractionAnimationAsync);
|
||||||
89
src/components/middle/EmojiInteractionAnimation.scss
Normal file
89
src/components/middle/EmojiInteractionAnimation.scss
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
.EmojiInteractionAnimation {
|
||||||
|
--start-x: 0;
|
||||||
|
--start-y: 0;
|
||||||
|
--scale: 0;
|
||||||
|
--end-scale: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
@keyframes hide-reaction-reversed {
|
||||||
|
from {
|
||||||
|
transform: translate(100%, -50%) scaleX(-1) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
left: var(--end-x, var(--start-x));
|
||||||
|
top: var(--end-y, var(--start-y));
|
||||||
|
transform: translate(50%, 0) scale(var(--end-scale, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes show-reaction-reversed {
|
||||||
|
from {
|
||||||
|
transform: translate(50%, 0) scaleX(-1) scale(var(--scale, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate(100%, -50%) scaleX(-1) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes hide-reaction {
|
||||||
|
from {
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
left: var(--end-x, var(--start-x));
|
||||||
|
top: var(--end-y, var(--start-y));
|
||||||
|
transform: translate(0, 0) scale(var(--end-scale, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes show-reaction {
|
||||||
|
from {
|
||||||
|
transform: translate(0, 0) scale(var(--scale, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.AnimatedSticker {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--start-y);
|
||||||
|
left: var(--start-x);
|
||||||
|
transform: scale(var(--scale), 0);
|
||||||
|
transform-origin: left top;
|
||||||
|
|
||||||
|
transition: 0.25s cubic-bezier(.29,.81,.27,.99) opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reversed .AnimatedSticker {
|
||||||
|
transform: scale(var(--scale), 0) scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.playing .AnimatedSticker {
|
||||||
|
animation: show-reaction forwards 0.25s cubic-bezier(.29,.81,.27,.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reversed.playing .AnimatedSticker {
|
||||||
|
animation: show-reaction-reversed forwards 0.25s cubic-bezier(.29,.81,.27,.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hiding .AnimatedSticker {
|
||||||
|
opacity: 0;
|
||||||
|
animation: hide-reaction forwards 0.25s cubic-bezier(.29,.81,.27,.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reversed.hiding .AnimatedSticker {
|
||||||
|
animation: hide-reaction-reversed forwards 0.25s cubic-bezier(.29,.81,.27,.99);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/components/middle/EmojiInteractionAnimation.tsx
Normal file
122
src/components/middle/EmojiInteractionAnimation.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect, useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getDispatch, withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ActiveEmojiInteraction } from '../../global/types';
|
||||||
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
|
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import {
|
||||||
|
selectAnimatedEmojiEffect,
|
||||||
|
} from '../../modules/selectors';
|
||||||
|
import { REM } from '../common/helpers/mediaDimensions';
|
||||||
|
import getAnimationData, { ANIMATED_STICKERS_PATHS } from '../common/helpers/animatedAssets';
|
||||||
|
|
||||||
|
import AnimatedSticker from '../common/AnimatedSticker';
|
||||||
|
|
||||||
|
import './EmojiInteractionAnimation.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
emojiInteraction: ActiveEmojiInteraction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
effectAnimationId?: string;
|
||||||
|
localEffectAnimation?: string;
|
||||||
|
isReversed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HIDE_ANIMATION_DURATION = 250;
|
||||||
|
const PLAYING_DURATION = 3000;
|
||||||
|
const END_SIZE = 1.125 * REM;
|
||||||
|
const EFFECT_SIZE = 240;
|
||||||
|
|
||||||
|
const EmojiInteractionAnimation: FC<OwnProps & StateProps> = ({
|
||||||
|
emojiInteraction,
|
||||||
|
effectAnimationId,
|
||||||
|
localEffectAnimation,
|
||||||
|
isReversed,
|
||||||
|
}) => {
|
||||||
|
const { stopActiveEmojiInteraction } = getDispatch();
|
||||||
|
|
||||||
|
const [isHiding, startHiding] = useFlag(false);
|
||||||
|
const [isPlaying, startPlaying] = useFlag(false);
|
||||||
|
|
||||||
|
const stop = useCallback(() => {
|
||||||
|
startHiding();
|
||||||
|
setTimeout(() => {
|
||||||
|
stopActiveEmojiInteraction();
|
||||||
|
}, HIDE_ANIMATION_DURATION);
|
||||||
|
}, [startHiding, stopActiveEmojiInteraction]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('touchstart', stop);
|
||||||
|
document.addEventListener('touchmove', stop);
|
||||||
|
document.addEventListener('mousedown', stop);
|
||||||
|
document.addEventListener('wheel', stop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('touchstart', stop);
|
||||||
|
document.removeEventListener('touchmove', stop);
|
||||||
|
document.removeEventListener('mousedown', stop);
|
||||||
|
document.removeEventListener('wheel', stop);
|
||||||
|
};
|
||||||
|
}, [stop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(stop, PLAYING_DURATION);
|
||||||
|
}, [stop]);
|
||||||
|
|
||||||
|
const mediaDataEffect = useMedia(`sticker${effectAnimationId}`, !effectAnimationId, ApiMediaFormat.Lottie);
|
||||||
|
|
||||||
|
const [localEffectAnimationData, setLocalEffectAnimationData] = useState<AnyLiteral>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (localEffectAnimation) {
|
||||||
|
getAnimationData(localEffectAnimation as keyof typeof ANIMATED_STICKERS_PATHS).then((data) => {
|
||||||
|
setLocalEffectAnimationData(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [localEffectAnimation]);
|
||||||
|
|
||||||
|
const scale = (emojiInteraction.startSize || 0) / EFFECT_SIZE;
|
||||||
|
const endScale = END_SIZE / EFFECT_SIZE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
'EmojiInteractionAnimation', isHiding && 'hiding', isPlaying && 'playing', isReversed && 'reversed',
|
||||||
|
)}
|
||||||
|
// @ts-ignore teact feature
|
||||||
|
style={`--end-scale: ${endScale}; --scale: ${scale}; --start-x: ${emojiInteraction.x}px;`
|
||||||
|
+ `--start-y: ${emojiInteraction.y}px;${
|
||||||
|
emojiInteraction.endX
|
||||||
|
&& emojiInteraction.endY ? `--end-x: ${emojiInteraction.endX}px; --end-y: ${emojiInteraction.endY}px;` : ''}`}
|
||||||
|
>
|
||||||
|
<AnimatedSticker
|
||||||
|
id={`effect_${effectAnimationId}`}
|
||||||
|
size={EFFECT_SIZE}
|
||||||
|
animationData={(localEffectAnimationData || mediaDataEffect) as AnyLiteral}
|
||||||
|
play={isPlaying}
|
||||||
|
noLoop
|
||||||
|
onLoad={startPlaying}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { emojiInteraction }): StateProps => {
|
||||||
|
const animatedEffect = emojiInteraction.animatedEffect !== undefined
|
||||||
|
&& selectAnimatedEmojiEffect(global, emojiInteraction.animatedEffect);
|
||||||
|
return {
|
||||||
|
effectAnimationId: animatedEffect ? animatedEffect.id : undefined,
|
||||||
|
localEffectAnimation: !animatedEffect && emojiInteraction.animatedEffect
|
||||||
|
&& Object.keys(ANIMATED_STICKERS_PATHS).includes(emojiInteraction.animatedEffect)
|
||||||
|
? emojiInteraction.animatedEffect : undefined,
|
||||||
|
isReversed: emojiInteraction.isReversed,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(EmojiInteractionAnimation));
|
||||||
@ -47,6 +47,7 @@ import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll
|
|||||||
import renderText from '../common/helpers/renderText';
|
import renderText from '../common/helpers/renderText';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useWindowSize from '../../hooks/useWindowSize';
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
|
import useInterval from '../../hooks/useInterval';
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import MessageListContent from './MessageListContent';
|
import MessageListContent from './MessageListContent';
|
||||||
@ -92,6 +93,7 @@ type StateProps = {
|
|||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MESSAGE_REACTIONS_POLLING_INTERVAL = 15 * 1000;
|
||||||
const BOTTOM_THRESHOLD = 20;
|
const BOTTOM_THRESHOLD = 20;
|
||||||
const UNREAD_DIVIDER_TOP = 10;
|
const UNREAD_DIVIDER_TOP = 10;
|
||||||
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
|
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
|
||||||
@ -135,7 +137,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
withBottomShift,
|
withBottomShift,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadViewportMessages, setScrollOffset, loadSponsoredMessages } = getDispatch();
|
const {
|
||||||
|
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions,
|
||||||
|
} = getDispatch();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -204,6 +208,17 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current);
|
return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current);
|
||||||
}, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]);
|
}, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]);
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
if (!messageIds || !messagesById) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ids = messageIds.filter((l) => messagesById[l]?.reactions);
|
||||||
|
|
||||||
|
if (!ids.length) return;
|
||||||
|
|
||||||
|
loadMessageReactions({ chatId, ids });
|
||||||
|
}, MESSAGE_REACTIONS_POLLING_INTERVAL);
|
||||||
|
|
||||||
const loadMoreAround = useMemo(() => {
|
const loadMoreAround = useMemo(() => {
|
||||||
if (type !== 'thread') {
|
if (type !== 'thread') {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -520,6 +535,7 @@ 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}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ 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;
|
||||||
@ -53,6 +54,7 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
isViewportNewest,
|
isViewportNewest,
|
||||||
isUnread,
|
isUnread,
|
||||||
withUsers,
|
withUsers,
|
||||||
|
areReactionsInMeta,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
containerRef,
|
containerRef,
|
||||||
anchorIdRef,
|
anchorIdRef,
|
||||||
@ -188,6 +190,7 @@ 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}
|
||||||
|
|||||||
@ -4,7 +4,11 @@ import React, {
|
|||||||
import { getDispatch, withGlobal } from '../../lib/teact/teactn';
|
import { getDispatch, withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiChatBannedRights, MAIN_THREAD_ID } from '../../api/types';
|
import { ApiChatBannedRights, MAIN_THREAD_ID } from '../../api/types';
|
||||||
import { MessageListType, MessageList as GlobalMessageList } from '../../global/types';
|
import {
|
||||||
|
MessageListType,
|
||||||
|
MessageList as GlobalMessageList,
|
||||||
|
ActiveEmojiInteraction,
|
||||||
|
} from '../../global/types';
|
||||||
import { ThemeKey } from '../../types';
|
import { ThemeKey } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -67,6 +71,8 @@ import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
|
|||||||
import PaymentModal from '../payment/PaymentModal.async';
|
import PaymentModal from '../payment/PaymentModal.async';
|
||||||
import ReceiptModal from '../payment/ReceiptModal.async';
|
import ReceiptModal from '../payment/ReceiptModal.async';
|
||||||
import SeenByModal from '../common/SeenByModal.async';
|
import SeenByModal from '../common/SeenByModal.async';
|
||||||
|
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
||||||
|
import ReactorListModal from './ReactorListModal.async';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
|
|
||||||
@ -94,6 +100,7 @@ type StateProps = {
|
|||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
isReceiptModalOpen?: boolean;
|
isReceiptModalOpen?: boolean;
|
||||||
isSeenByModalOpen: boolean;
|
isSeenByModalOpen: boolean;
|
||||||
|
isReactorListModalOpen: boolean;
|
||||||
animationLevel?: number;
|
animationLevel?: number;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
currentTransitionKey: number;
|
currentTransitionKey: number;
|
||||||
@ -102,6 +109,7 @@ type StateProps = {
|
|||||||
canSubscribe?: boolean;
|
canSubscribe?: boolean;
|
||||||
canStartBot?: boolean;
|
canStartBot?: boolean;
|
||||||
canRestartBot?: boolean;
|
canRestartBot?: boolean;
|
||||||
|
activeEmojiInteraction?: ActiveEmojiInteraction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
|
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
|
||||||
@ -134,6 +142,7 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
isSeenByModalOpen,
|
isSeenByModalOpen,
|
||||||
|
isReactorListModalOpen,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
currentTransitionKey,
|
currentTransitionKey,
|
||||||
@ -141,6 +150,7 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
canSubscribe,
|
canSubscribe,
|
||||||
canStartBot,
|
canStartBot,
|
||||||
canRestartBot,
|
canRestartBot,
|
||||||
|
activeEmojiInteraction,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
@ -484,6 +494,7 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
onClose={clearReceipt}
|
onClose={clearReceipt}
|
||||||
/>
|
/>
|
||||||
<SeenByModal isOpen={isSeenByModalOpen} />
|
<SeenByModal isOpen={isSeenByModalOpen} />
|
||||||
|
<ReactorListModal isOpen={isReactorListModalOpen} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -507,6 +518,9 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
onUnpin={handleUnpinAllMessages}
|
onUnpin={handleUnpinAllMessages}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeEmojiInteraction && (
|
||||||
|
<EmojiInteractionAnimation emojiInteraction={activeEmojiInteraction} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -520,7 +534,7 @@ export default memo(withGlobal(
|
|||||||
|
|
||||||
const { messageLists } = global.messages;
|
const { messageLists } = global.messages;
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
const { isLeftColumnShown, chats: { listIds } } = global;
|
const { isLeftColumnShown, chats: { listIds }, activeEmojiInteraction } = global;
|
||||||
|
|
||||||
const state: StateProps = {
|
const state: StateProps = {
|
||||||
theme,
|
theme,
|
||||||
@ -535,8 +549,10 @@ export default memo(withGlobal(
|
|||||||
isPaymentModalOpen: global.payment.isPaymentModalOpen,
|
isPaymentModalOpen: global.payment.isPaymentModalOpen,
|
||||||
isReceiptModalOpen: Boolean(global.payment.receipt),
|
isReceiptModalOpen: Boolean(global.payment.receipt),
|
||||||
isSeenByModalOpen: Boolean(global.seenByModal),
|
isSeenByModalOpen: Boolean(global.seenByModal),
|
||||||
|
isReactorListModalOpen: Boolean(global.reactorModal),
|
||||||
animationLevel: global.settings.byKey.animationLevel,
|
animationLevel: global.settings.byKey.animationLevel,
|
||||||
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
||||||
|
activeEmojiInteraction,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!currentMessageList || !listIds.active) {
|
if (!currentMessageList || !listIds.active) {
|
||||||
|
|||||||
15
src/components/middle/ReactorListModal.async.tsx
Normal file
15
src/components/middle/ReactorListModal.async.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { FC, memo } from '../../lib/teact/teact';
|
||||||
|
import { OwnProps } from './ReactorListModal';
|
||||||
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const ReactorListModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const ReactorListModal = useModuleLoader(Bundles.Extra, 'ReactorListModal', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return ReactorListModal ? <ReactorListModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactorListModalAsync);
|
||||||
38
src/components/middle/ReactorListModal.scss
Normal file
38
src/components/middle/ReactorListModal.scss
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.ReactorListModal {
|
||||||
|
--color-reaction: var(--color-message-reaction);
|
||||||
|
--hover-color-reaction: var(--color-message-reaction-hover);
|
||||||
|
--accent-color: var(--color-primary);
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Reactions {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-filter-emoji {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactor-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactors-list-item {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactors-list-item .ListItem-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactors-list-emoji {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/components/middle/ReactorListModal.tsx
Normal file
203
src/components/middle/ReactorListModal.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import React, {
|
||||||
|
FC, useCallback, memo, useMemo, useEffect, useState, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getDispatch, getGlobal, withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ApiMessage } from '../../api/types';
|
||||||
|
import { LoadMoreDirection } from '../../types';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import { selectChatMessage } from '../../modules/selectors';
|
||||||
|
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||||
|
import { getUserFullName } from '../../modules/helpers';
|
||||||
|
import renderText from '../common/helpers/renderText';
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { formatIntegerCompact } from '../../util/textFormat';
|
||||||
|
import { unique } from '../../util/iteratees';
|
||||||
|
|
||||||
|
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import Avatar from '../common/Avatar';
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
|
import ReactionStaticEmoji from '../common/ReactionStaticEmoji';
|
||||||
|
import Loading from '../ui/Loading';
|
||||||
|
|
||||||
|
import './ReactorListModal.scss';
|
||||||
|
|
||||||
|
const MIN_REACTIONS_COUNT_FOR_FILTERS = 10;
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StateProps = Pick<ApiMessage, 'reactors' | 'reactions' | 'seenByUserIds'> & {
|
||||||
|
chatId?: string;
|
||||||
|
messageId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||||
|
isOpen,
|
||||||
|
reactors,
|
||||||
|
reactions,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
seenByUserIds,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
loadReactors,
|
||||||
|
closeReactorListModal,
|
||||||
|
openChat,
|
||||||
|
} = getDispatch();
|
||||||
|
|
||||||
|
// No need for expensive global updates on users, so we avoid them
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [isClosing, startClosing, stopClosing] = useFlag(false);
|
||||||
|
const [chosenTab, setChosenTab] = useState<string | undefined>(undefined);
|
||||||
|
const canShowFilters = reactors && reactions && reactors.count >= MIN_REACTIONS_COUNT_FOR_FILTERS
|
||||||
|
&& reactions.results.length > 1;
|
||||||
|
const chatIdRef = useRef<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isClosing && !isOpen) {
|
||||||
|
stopClosing();
|
||||||
|
setChosenTab(undefined);
|
||||||
|
}
|
||||||
|
}, [isClosing, isOpen, stopClosing]);
|
||||||
|
|
||||||
|
const handleCloseAnimationEnd = useCallback(() => {
|
||||||
|
if (chatIdRef.current) {
|
||||||
|
openChat({ id: chatIdRef.current });
|
||||||
|
}
|
||||||
|
closeReactorListModal();
|
||||||
|
}, [closeReactorListModal, openChat]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
startClosing();
|
||||||
|
}, [startClosing]);
|
||||||
|
|
||||||
|
const handleClick = useCallback((userId: string) => {
|
||||||
|
chatIdRef.current = userId;
|
||||||
|
handleClose();
|
||||||
|
}, [handleClose]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
loadReactors({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
});
|
||||||
|
}, [chatId, loadReactors, messageId]);
|
||||||
|
|
||||||
|
const allReactions = useMemo(() => {
|
||||||
|
return reactors?.reactions ? unique(reactors.reactions.map((l) => l.reaction)) : [];
|
||||||
|
}, [reactors?.reactions]);
|
||||||
|
|
||||||
|
const userIds = useMemo(() => {
|
||||||
|
if (chosenTab) {
|
||||||
|
return reactors?.reactions.filter((l) => l.reaction === chosenTab).map((l) => l.userId);
|
||||||
|
}
|
||||||
|
return unique(reactors?.reactions.map((l) => l.userId).concat(seenByUserIds || []) || []);
|
||||||
|
}, [chosenTab, reactors?.reactions, seenByUserIds]);
|
||||||
|
|
||||||
|
const [viewportIds, getMore] = useInfiniteScroll(
|
||||||
|
handleLoadMore, userIds, reactors && reactors.nextOffset === undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMore?.({ direction: LoadMoreDirection.Backwards });
|
||||||
|
}, [getMore]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen && !isClosing}
|
||||||
|
onClose={handleClose}
|
||||||
|
className="ReactorListModal narrow"
|
||||||
|
title={lang('Reactions')}
|
||||||
|
onCloseAnimationEnd={handleCloseAnimationEnd}
|
||||||
|
>
|
||||||
|
{canShowFilters && (
|
||||||
|
<div className="Reactions">
|
||||||
|
<Button
|
||||||
|
className={buildClassName(!chosenTab && 'chosen')}
|
||||||
|
size="tiny"
|
||||||
|
ripple
|
||||||
|
onClick={() => setChosenTab(undefined)}
|
||||||
|
>
|
||||||
|
<i className="icon-reaction-filled" />
|
||||||
|
{reactors?.count && formatIntegerCompact(reactors.count)}
|
||||||
|
</Button>
|
||||||
|
{allReactions.map((reaction) => {
|
||||||
|
const count = reactions?.results.find((l) => l.reaction === reaction)?.count;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={buildClassName(chosenTab === reaction && 'chosen')}
|
||||||
|
size="tiny"
|
||||||
|
ripple
|
||||||
|
onClick={() => setChosenTab(reaction)}
|
||||||
|
>
|
||||||
|
<ReactionStaticEmoji reaction={reaction} className="reaction-filter-emoji" />
|
||||||
|
{count && formatIntegerCompact(count)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
{viewportIds?.length ? (
|
||||||
|
<InfiniteScroll
|
||||||
|
className="reactor-list custom-scroll"
|
||||||
|
items={viewportIds}
|
||||||
|
onLoadMore={getMore}
|
||||||
|
>
|
||||||
|
{viewportIds?.map(
|
||||||
|
(userId) => {
|
||||||
|
const user = usersById[userId];
|
||||||
|
const fullName = getUserFullName(user);
|
||||||
|
const reaction = reactors?.reactions.find((l) => l.userId === userId)?.reaction;
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={userId}
|
||||||
|
className="chat-item-clickable reactors-list-item"
|
||||||
|
onClick={() => handleClick(userId)}
|
||||||
|
>
|
||||||
|
<Avatar user={user} size="medium" />
|
||||||
|
<div className="title">
|
||||||
|
<h3 dir="auto">{fullName && renderText(fullName)}</h3>
|
||||||
|
</div>
|
||||||
|
{reaction && <ReactionStaticEmoji className="reactors-list-emoji" reaction={reaction} />}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</InfiniteScroll>
|
||||||
|
) : <Loading />}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="confirm-dialog-button"
|
||||||
|
isText
|
||||||
|
onClick={closeReactorListModal}
|
||||||
|
>
|
||||||
|
{lang('Close')}
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const { chatId, messageId } = global.reactorModal || {};
|
||||||
|
const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
reactions: message?.reactions,
|
||||||
|
reactors: message?.reactors,
|
||||||
|
seenByUserIds: message?.seenByUserIds,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(ReactorListModal));
|
||||||
@ -7,8 +7,8 @@ import {
|
|||||||
const REM = 16; // px
|
const REM = 16; // px
|
||||||
const MAX_TOOLBAR_WIDTH = 32 * REM;
|
const MAX_TOOLBAR_WIDTH = 32 * REM;
|
||||||
const MAX_MESSAGES_LIST_WIDTH = 45.5 * REM;
|
const MAX_MESSAGES_LIST_WIDTH = 45.5 * REM;
|
||||||
const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM;
|
export const SIDE_COLUMN_MAX_WIDTH = 26.5 * REM;
|
||||||
const MIN_LEFT_COLUMN_WIDTH = 18 * REM;
|
export const MIN_LEFT_COLUMN_WIDTH = 18 * REM;
|
||||||
const UNPIN_BUTTON_WIDTH = 16.125 * REM;
|
const UNPIN_BUTTON_WIDTH = 16.125 * REM;
|
||||||
|
|
||||||
export default function calculateMiddleFooterTransforms(windowWidth: number, canPost?: boolean) {
|
export default function calculateMiddleFooterTransforms(windowWidth: number, canPost?: boolean) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, {
|
|||||||
import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn';
|
import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { MessageListType } from '../../../global/types';
|
import { MessageListType } from '../../../global/types';
|
||||||
import { ApiMessage } from '../../../api/types';
|
import { ApiAvailableReaction, ApiMessage } from '../../../api/types';
|
||||||
import { IAlbum, IAnchorPosition } from '../../../types';
|
import { IAlbum, IAnchorPosition } from '../../../types';
|
||||||
import {
|
import {
|
||||||
selectActiveDownloadIds,
|
selectActiveDownloadIds,
|
||||||
@ -13,12 +13,16 @@ import {
|
|||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectIsMessageProtected,
|
selectIsMessageProtected,
|
||||||
} from '../../../modules/selectors';
|
} from '../../../modules/selectors';
|
||||||
import { isChatGroup, isOwnMessage } from '../../../modules/helpers';
|
import {
|
||||||
import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX } from '../../../config';
|
isActionMessage, isChatChannel,
|
||||||
|
isChatGroup, isOwnMessage, areReactionsEmpty, isUserId,
|
||||||
|
} from '../../../modules/helpers';
|
||||||
|
import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||||
import { getDayStartAt } from '../../../util/dateFormat';
|
import { getDayStartAt } from '../../../util/dateFormat';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
||||||
import ReportMessageModal from '../../common/ReportMessageModal';
|
import ReportMessageModal from '../../common/ReportMessageModal';
|
||||||
@ -26,6 +30,8 @@ import PinMessageModal from '../../common/PinMessageModal';
|
|||||||
import MessageContextMenu from './MessageContextMenu';
|
import MessageContextMenu from './MessageContextMenu';
|
||||||
import CalendarModal from '../../common/CalendarModal';
|
import CalendarModal from '../../common/CalendarModal';
|
||||||
|
|
||||||
|
const START_SIZE = 2 * REM;
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
chatUsername?: string;
|
chatUsername?: string;
|
||||||
@ -38,11 +44,15 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
noOptions?: boolean;
|
noOptions?: boolean;
|
||||||
canSendNow?: boolean;
|
canSendNow?: boolean;
|
||||||
canReschedule?: boolean;
|
canReschedule?: boolean;
|
||||||
canReply?: boolean;
|
canReply?: boolean;
|
||||||
canPin?: boolean;
|
canPin?: boolean;
|
||||||
|
canShowReactionsCount?: boolean;
|
||||||
|
canShowReactionList?: boolean;
|
||||||
|
canRemoveReaction?: boolean;
|
||||||
canUnpin?: boolean;
|
canUnpin?: boolean;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
canReport?: boolean;
|
canReport?: boolean;
|
||||||
@ -51,14 +61,18 @@ type StateProps = {
|
|||||||
canFaveSticker?: boolean;
|
canFaveSticker?: boolean;
|
||||||
canUnfaveSticker?: boolean;
|
canUnfaveSticker?: boolean;
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
|
isPrivate?: boolean;
|
||||||
|
hasFullInfo?: boolean;
|
||||||
canCopyLink?: boolean;
|
canCopyLink?: boolean;
|
||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
activeDownloads: number[];
|
activeDownloads: number[];
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
|
enabledReactions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||||
|
availableReactions,
|
||||||
isOpen,
|
isOpen,
|
||||||
messageListType,
|
messageListType,
|
||||||
chatUsername,
|
chatUsername,
|
||||||
@ -69,13 +83,19 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
noOptions,
|
noOptions,
|
||||||
canSendNow,
|
canSendNow,
|
||||||
|
hasFullInfo,
|
||||||
canReschedule,
|
canReschedule,
|
||||||
canReply,
|
canReply,
|
||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
canDelete,
|
canDelete,
|
||||||
canReport,
|
canReport,
|
||||||
|
canShowReactionsCount,
|
||||||
|
canShowReactionList,
|
||||||
|
canRemoveReaction,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
enabledReactions,
|
||||||
|
isPrivate,
|
||||||
canForward,
|
canForward,
|
||||||
canFaveSticker,
|
canFaveSticker,
|
||||||
canUnfaveSticker,
|
canUnfaveSticker,
|
||||||
@ -100,6 +120,10 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
cancelMessageMediaDownload,
|
cancelMessageMediaDownload,
|
||||||
loadSeenBy,
|
loadSeenBy,
|
||||||
openSeenByModal,
|
openSeenByModal,
|
||||||
|
sendReaction,
|
||||||
|
openReactorListModal,
|
||||||
|
loadFullChat,
|
||||||
|
loadReactors,
|
||||||
} = getDispatch();
|
} = getDispatch();
|
||||||
|
|
||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
@ -115,7 +139,26 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [loadSeenBy, isOpen, message.chatId, message.id, canShowSeenBy]);
|
}, [loadSeenBy, isOpen, message.chatId, message.id, canShowSeenBy]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canShowReactionsCount && isOpen) {
|
||||||
|
loadReactors({ chatId: message.chatId, messageId: message.id });
|
||||||
|
}
|
||||||
|
}, [canShowReactionsCount, isOpen, loadReactors, message.chatId, message.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasFullInfo && !isPrivate && isOpen) {
|
||||||
|
loadFullChat({ chatId: message.chatId });
|
||||||
|
}
|
||||||
|
}, [hasFullInfo, isOpen, isPrivate, loadFullChat, message.chatId]);
|
||||||
|
|
||||||
const seenByRecentUsers = useMemo(() => {
|
const seenByRecentUsers = useMemo(() => {
|
||||||
|
if (message.reactions?.recentReactions?.length) {
|
||||||
|
// No need for expensive global updates on users, so we avoid them
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
|
||||||
|
return message.reactions?.recentReactions?.slice(0, 3).map(({ userId }) => usersById[userId]).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
if (!message.seenByUserIds) {
|
if (!message.seenByUserIds) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -123,7 +166,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
// No need for expensive global updates on users, so we avoid them
|
// No need for expensive global updates on users, so we avoid them
|
||||||
const usersById = getGlobal().users.byId;
|
const usersById = getGlobal().users.byId;
|
||||||
return message.seenByUserIds?.slice(0, 3).map((id) => usersById[id]).filter(Boolean);
|
return message.seenByUserIds?.slice(0, 3).map((id) => usersById[id]).filter(Boolean);
|
||||||
}, [message.seenByUserIds]);
|
}, [message.reactions?.recentReactions, message.seenByUserIds]);
|
||||||
|
|
||||||
const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id))
|
const isDownloading = album ? album.messages.some((msg) => activeDownloads.includes(msg.id))
|
||||||
: activeDownloads.includes(message.id);
|
: activeDownloads.includes(message.id);
|
||||||
@ -231,6 +274,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
openSeenByModal({ chatId: message.chatId, messageId: message.id });
|
openSeenByModal({ chatId: message.chatId, messageId: message.id });
|
||||||
}, [closeMenu, message.chatId, message.id, openSeenByModal]);
|
}, [closeMenu, message.chatId, message.id, openSeenByModal]);
|
||||||
|
|
||||||
|
const handleOpenReactorListModal = useCallback(() => {
|
||||||
|
closeMenu();
|
||||||
|
openReactorListModal({ chatId: message.chatId, messageId: message.id });
|
||||||
|
}, [closeMenu, openReactorListModal, message.chatId, message.id]);
|
||||||
|
|
||||||
const handleRescheduleMessage = useCallback((date: Date) => {
|
const handleRescheduleMessage = useCallback((date: Date) => {
|
||||||
rescheduleMessage({
|
rescheduleMessage({
|
||||||
chatId: message.chatId,
|
chatId: message.chatId,
|
||||||
@ -255,6 +303,13 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [album, message, closeMenu, isDownloading, cancelMessageMediaDownload, downloadMessageMedia]);
|
}, [album, message, closeMenu, isDownloading, cancelMessageMediaDownload, downloadMessageMedia]);
|
||||||
|
|
||||||
|
const handleSendReaction = useCallback((reaction: string | undefined, x: number, y: number) => {
|
||||||
|
sendReaction({
|
||||||
|
chatId: message.chatId, messageId: message.id, reaction, x, y, startSize: START_SIZE,
|
||||||
|
});
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, message.chatId, message.id, sendReaction]);
|
||||||
|
|
||||||
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
||||||
|
|
||||||
if (noOptions) {
|
if (noOptions) {
|
||||||
@ -269,9 +324,15 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={['ContextMenuContainer', transitionClassNames].join(' ')}>
|
<div className={['ContextMenuContainer', transitionClassNames].join(' ')}>
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
|
availableReactions={availableReactions}
|
||||||
message={message}
|
message={message}
|
||||||
|
isPrivate={isPrivate}
|
||||||
isOpen={isMenuOpen}
|
isOpen={isMenuOpen}
|
||||||
|
enabledReactions={enabledReactions}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
|
canShowReactionsCount={canShowReactionsCount}
|
||||||
|
canShowReactionList={canShowReactionList}
|
||||||
|
canRemoveReaction={canRemoveReaction}
|
||||||
canSendNow={canSendNow}
|
canSendNow={canSendNow}
|
||||||
canReschedule={canReschedule}
|
canReschedule={canReschedule}
|
||||||
canReply={canReply}
|
canReply={canReply}
|
||||||
@ -306,6 +367,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onCopyLink={handleCopyLink}
|
onCopyLink={handleCopyLink}
|
||||||
onDownload={handleDownloadClick}
|
onDownload={handleDownloadClick}
|
||||||
onShowSeenBy={handleOpenSeenByModal}
|
onShowSeenBy={handleOpenSeenByModal}
|
||||||
|
onSendReaction={handleSendReaction}
|
||||||
|
onShowReactors={handleOpenReactorListModal}
|
||||||
/>
|
/>
|
||||||
<DeleteMessageModal
|
<DeleteMessageModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
@ -361,15 +424,22 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
const isPinned = messageListType === 'pinned';
|
const isPinned = messageListType === 'pinned';
|
||||||
const isScheduled = messageListType === 'scheduled';
|
const isScheduled = messageListType === 'scheduled';
|
||||||
|
const isChannel = chat && isChatChannel(chat);
|
||||||
const canShowSeenBy = Boolean(chat
|
const canShowSeenBy = Boolean(chat
|
||||||
&& isChatGroup(chat)
|
&& isChatGroup(chat)
|
||||||
&& isOwnMessage(message)
|
&& isOwnMessage(message)
|
||||||
&& chat.membersCount
|
&& chat.membersCount
|
||||||
&& chat.membersCount < SEEN_BY_MEMBERS_CHAT_MAX
|
&& chat.membersCount < SEEN_BY_MEMBERS_CHAT_MAX
|
||||||
&& message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE);
|
&& message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE);
|
||||||
|
const isPrivate = chat && isUserId(chat.id);
|
||||||
|
const isAction = isActionMessage(message);
|
||||||
|
const canShowReactionsCount = !isChannel && !isScheduled && !isAction && !isPrivate && message.reactions
|
||||||
|
&& !areReactionsEmpty(message.reactions) && message.reactions.canSeeList;
|
||||||
|
const canRemoveReaction = isPrivate && message.reactions?.results?.some((l) => l.isChosen);
|
||||||
const isProtected = selectIsMessageProtected(global, message);
|
const isProtected = selectIsMessageProtected(global, message);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
availableReactions: global.availableReactions,
|
||||||
noOptions,
|
noOptions,
|
||||||
canSendNow: isScheduled,
|
canSendNow: isScheduled,
|
||||||
canReschedule: isScheduled,
|
canReschedule: isScheduled,
|
||||||
@ -388,6 +458,12 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canDownload: !isProtected && canDownload,
|
canDownload: !isProtected && canDownload,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
|
enabledReactions: chat?.fullInfo?.enabledReactions,
|
||||||
|
isPrivate,
|
||||||
|
hasFullInfo: Boolean(chat?.fullInfo),
|
||||||
|
canShowReactionsCount,
|
||||||
|
canShowReactionList: !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
|
canRemoveReaction,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ContextMenuContainer));
|
)(ContextMenuContainer));
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
--background-color: var(--color-background);
|
--background-color: var(--color-background);
|
||||||
--hover-color: var(--color-reply-hover);
|
--hover-color: var(--color-reply-hover);
|
||||||
|
--color-reaction: var(--color-message-reaction);
|
||||||
|
--hover-color-reaction: var(--color-message-reaction-hover);
|
||||||
--active-color: var(--color-reply-active);
|
--active-color: var(--color-reply-active);
|
||||||
--max-width: 29rem;
|
--max-width: 29rem;
|
||||||
--accent-color: var(--color-primary);
|
--accent-color: var(--color-primary);
|
||||||
@ -53,6 +55,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-reaction {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: -0.5rem;
|
||||||
|
bottom: -0.375rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0;
|
||||||
|
transition: transform 0.2s ease-out, opacity 0.2s ease-out;
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-delay: unset;
|
||||||
|
transform: scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactionStaticEmoji {
|
||||||
|
width: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.last-in-list .quick-reaction:hover {
|
||||||
|
transform: translateY(-0.1875rem) scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.own .quick-reaction {
|
||||||
|
right: auto;
|
||||||
|
left: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&.last-in-group {
|
&.last-in-group {
|
||||||
margin-bottom: 0.625rem;
|
margin-bottom: 0.625rem;
|
||||||
}
|
}
|
||||||
@ -82,6 +120,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.has-active-reaction {
|
||||||
|
.message-content-wrapper {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.own) {
|
&:not(.own) {
|
||||||
padding-left: 2.5rem;
|
padding-left: 2.5rem;
|
||||||
|
|
||||||
@ -102,6 +146,8 @@
|
|||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
--background-color: var(--color-background-own);
|
--background-color: var(--color-background-own);
|
||||||
--hover-color: var(--color-reply-own-hover);
|
--hover-color: var(--color-reply-own-hover);
|
||||||
|
--color-reaction: var(--color-message-reaction-own);
|
||||||
|
--hover-color-reaction: var(--color-message-reaction-hover-own);
|
||||||
--active-color: var(--color-reply-own-active);
|
--active-color: var(--color-reply-own-active);
|
||||||
--max-width: 30rem;
|
--max-width: 30rem;
|
||||||
--accent-color: var(--color-accent-own);
|
--accent-color: var(--color-accent-own);
|
||||||
|
|||||||
@ -8,14 +8,14 @@ import React, {
|
|||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { MessageListType } from '../../../global/types';
|
import { ActiveEmojiInteraction, ActiveReaction, MessageListType } from '../../../global/types';
|
||||||
import {
|
import {
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageOutgoingStatus,
|
ApiMessageOutgoingStatus,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
ApiChat,
|
ApiChat,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiThreadInfo,
|
ApiThreadInfo, ApiAvailableReaction,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import {
|
import {
|
||||||
AudioOrigin, FocusDirection, IAlbum, ISettings,
|
AudioOrigin, FocusDirection, IAlbum, ISettings,
|
||||||
@ -45,7 +45,13 @@ import {
|
|||||||
selectAllowedMessageActions,
|
selectAllowedMessageActions,
|
||||||
selectIsDownloading,
|
selectIsDownloading,
|
||||||
selectThreadInfo,
|
selectThreadInfo,
|
||||||
|
selectAnimatedEmojiEffect,
|
||||||
|
selectAnimatedEmojiSound,
|
||||||
|
selectMessageIdsByGroupId,
|
||||||
|
selectLocalAnimatedEmoji,
|
||||||
selectIsMessageProtected,
|
selectIsMessageProtected,
|
||||||
|
selectLocalAnimatedEmojiEffect,
|
||||||
|
selectDefaultReaction,
|
||||||
} from '../../../modules/selectors';
|
} from '../../../modules/selectors';
|
||||||
import {
|
import {
|
||||||
getMessageContent,
|
getMessageContent,
|
||||||
@ -60,6 +66,7 @@ import {
|
|||||||
getMessageSingleEmoji,
|
getMessageSingleEmoji,
|
||||||
getSenderTitle,
|
getSenderTitle,
|
||||||
getUserColorKey,
|
getUserColorKey,
|
||||||
|
areReactionsEmpty,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||||
@ -98,6 +105,9 @@ import Album from './Album';
|
|||||||
import RoundVideo from './RoundVideo';
|
import RoundVideo from './RoundVideo';
|
||||||
import InlineButtons from './InlineButtons';
|
import InlineButtons from './InlineButtons';
|
||||||
import CommentButton from './CommentButton';
|
import CommentButton from './CommentButton';
|
||||||
|
import Reactions from './Reactions';
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
import LocalAnimatedEmoji from '../../common/LocalAnimatedEmoji';
|
||||||
|
|
||||||
import './Message.scss';
|
import './Message.scss';
|
||||||
|
|
||||||
@ -119,6 +129,7 @@ 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;
|
||||||
@ -136,6 +147,7 @@ type StateProps = {
|
|||||||
isThreadTop?: boolean;
|
isThreadTop?: boolean;
|
||||||
shouldHideReply?: boolean;
|
shouldHideReply?: boolean;
|
||||||
replyMessage?: ApiMessage;
|
replyMessage?: ApiMessage;
|
||||||
|
reactionsMessage?: ApiMessage;
|
||||||
replyMessageSender?: ApiUser | ApiChat;
|
replyMessageSender?: ApiUser | ApiChat;
|
||||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
uploadProgress?: number;
|
uploadProgress?: number;
|
||||||
@ -153,6 +165,10 @@ type StateProps = {
|
|||||||
highlight?: string;
|
highlight?: string;
|
||||||
isSingleEmoji?: boolean;
|
isSingleEmoji?: boolean;
|
||||||
animatedEmoji?: ApiSticker;
|
animatedEmoji?: ApiSticker;
|
||||||
|
localSticker?: string;
|
||||||
|
localEffect?: string;
|
||||||
|
animatedEmojiEffect?: ApiSticker;
|
||||||
|
animatedEmojiSoundId?: string;
|
||||||
isInSelectMode?: boolean;
|
isInSelectMode?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isGroupSelected?: boolean;
|
isGroupSelected?: boolean;
|
||||||
@ -164,10 +180,13 @@ type StateProps = {
|
|||||||
shouldLoopStickers?: boolean;
|
shouldLoopStickers?: boolean;
|
||||||
autoLoadFileMaxSizeMb: number;
|
autoLoadFileMaxSizeMb: number;
|
||||||
threadInfo?: ApiThreadInfo;
|
threadInfo?: ApiThreadInfo;
|
||||||
|
defaultReaction?: string;
|
||||||
|
activeReaction?: ActiveReaction;
|
||||||
|
activeEmojiInteraction?: ActiveEmojiInteraction;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const NBSP = '\u00A0';
|
const NBSP = '\u00A0';
|
||||||
const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover';
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const APPENDIX_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="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>' };
|
const APPENDIX_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="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>' };
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
@ -185,6 +204,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
noAvatars,
|
noAvatars,
|
||||||
withAvatar,
|
withAvatar,
|
||||||
withSenderName,
|
withSenderName,
|
||||||
|
areReactionsInMeta,
|
||||||
|
reactionsMessage,
|
||||||
noComments,
|
noComments,
|
||||||
appearanceOrder,
|
appearanceOrder,
|
||||||
isFirstInGroup,
|
isFirstInGroup,
|
||||||
@ -216,10 +237,18 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
highlight,
|
highlight,
|
||||||
animatedEmoji,
|
animatedEmoji,
|
||||||
|
localSticker,
|
||||||
|
localEffect,
|
||||||
|
animatedEmojiEffect,
|
||||||
|
animatedEmojiSoundId,
|
||||||
isInSelectMode,
|
isInSelectMode,
|
||||||
isSelected,
|
isSelected,
|
||||||
isGroupSelected,
|
isGroupSelected,
|
||||||
threadId,
|
threadId,
|
||||||
|
defaultReaction,
|
||||||
|
activeReaction,
|
||||||
|
activeEmojiInteraction,
|
||||||
|
availableReactions,
|
||||||
messageListType,
|
messageListType,
|
||||||
isPinnedList,
|
isPinnedList,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
@ -239,6 +268,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const bottomMarkerRef = useRef<HTMLDivElement>(null);
|
const bottomMarkerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -288,6 +319,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const {
|
const {
|
||||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice,
|
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice,
|
||||||
} = getMessageContent(message);
|
} = getMessageContent(message);
|
||||||
|
const hasReactionButtons = !areReactionsInMeta && reactionsMessage?.reactions
|
||||||
|
&& !areReactionsEmpty(reactionsMessage.reactions);
|
||||||
const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape));
|
const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape));
|
||||||
const isContextMenuShown = contextMenuPosition !== undefined;
|
const isContextMenuShown = contextMenuPosition !== undefined;
|
||||||
const signature = (
|
const signature = (
|
||||||
@ -308,6 +341,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
));
|
));
|
||||||
const avatarPeer = forwardInfo && (isChatWithSelf || isRepliesChat || !sender) ? originSender : sender;
|
const avatarPeer = forwardInfo && (isChatWithSelf || isRepliesChat || !sender) ? originSender : sender;
|
||||||
const senderPeer = forwardInfo ? originSender : sender;
|
const senderPeer = forwardInfo ? originSender : sender;
|
||||||
|
const hasAnimatedEmoji = localSticker || animatedEmoji;
|
||||||
|
const areReactionsOutside = hasReactionButtons
|
||||||
|
&& (asForwarded || customShape || ((photo || video || hasAnimatedEmoji) && !textParts));
|
||||||
|
|
||||||
const selectMessage = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
const selectMessage = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
||||||
toggleMessageSelection({
|
toggleMessageSelection({
|
||||||
@ -324,7 +360,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
handleContextMenu,
|
handleContextMenu,
|
||||||
handleDoubleClick,
|
handleDoubleClick,
|
||||||
handleContentDoubleClick,
|
handleContentDoubleClick,
|
||||||
|
handleMouseMove,
|
||||||
|
handleSendQuickReaction,
|
||||||
|
handleMouseLeave,
|
||||||
isSwiped,
|
isSwiped,
|
||||||
|
isQuickReactionVisible,
|
||||||
|
handleDocumentGroupMouseEnter,
|
||||||
} = useOuterHandlers(
|
} = useOuterHandlers(
|
||||||
selectMessage,
|
selectMessage,
|
||||||
ref,
|
ref,
|
||||||
@ -335,6 +376,11 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
Boolean(isProtected),
|
Boolean(isProtected),
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
handleBeforeContextMenu,
|
handleBeforeContextMenu,
|
||||||
|
chatId,
|
||||||
|
isContextMenuShown,
|
||||||
|
contentRef,
|
||||||
|
isOwn,
|
||||||
|
isInDocumentGroup && !isLastInDocumentGroup,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -395,6 +441,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) && 'has-active-reaction',
|
||||||
);
|
);
|
||||||
const contentClassName = buildContentClassName(message, {
|
const contentClassName = buildContentClassName(message, {
|
||||||
hasReply,
|
hasReply,
|
||||||
@ -410,6 +457,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
threadInfo && (!isInDocumentGroup || isLastInDocumentGroup) && messageListType === 'thread' && !noComments
|
threadInfo && (!isInDocumentGroup || isLastInDocumentGroup) && messageListType === 'thread' && !noComments
|
||||||
);
|
);
|
||||||
const withAppendix = contentClassName.includes('has-appendix');
|
const withAppendix = contentClassName.includes('has-appendix');
|
||||||
|
const withQuickReaction = !IS_TOUCH_ENV && defaultReaction && (!isInDocumentGroup || isLastInDocumentGroup);
|
||||||
|
|
||||||
useEnsureMessage(
|
useEnsureMessage(
|
||||||
isRepliesChat && message.replyToChatId ? message.replyToChatId : chatId,
|
isRepliesChat && message.replyToChatId ? message.replyToChatId : chatId,
|
||||||
@ -474,6 +522,21 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMeta(withReactionOffset = false) {
|
||||||
|
return (
|
||||||
|
<MessageMeta
|
||||||
|
message={message}
|
||||||
|
withReactionOffset={withReactionOffset}
|
||||||
|
withReactions={areReactionsInMeta}
|
||||||
|
outgoingStatus={outgoingStatus}
|
||||||
|
signature={signature}
|
||||||
|
onClick={handleMetaClick}
|
||||||
|
activeReaction={activeReaction}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'content-inner',
|
'content-inner',
|
||||||
@ -482,7 +545,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
noMediaCorners && 'no-media-corners',
|
noMediaCorners && 'no-media-corners',
|
||||||
);
|
);
|
||||||
const hasCustomAppendix = isLastInGroup && !textParts && !asForwarded && !hasThread;
|
const hasCustomAppendix = isLastInGroup && !textParts && !asForwarded && !hasThread;
|
||||||
const shouldInlineMeta = !webPage && !animatedEmoji && textParts;
|
const shouldInlineMeta = !webPage && !hasAnimatedEmoji && textParts;
|
||||||
const textContentClass = buildClassName(
|
const textContentClass = buildClassName(
|
||||||
'text-content',
|
'text-content',
|
||||||
shouldInlineMeta && 'with-meta',
|
shouldInlineMeta && 'with-meta',
|
||||||
@ -513,10 +576,31 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
{animatedEmoji && (
|
{animatedEmoji && (
|
||||||
<AnimatedEmoji
|
<AnimatedEmoji
|
||||||
size="small"
|
size="small"
|
||||||
|
isOwn={isOwn}
|
||||||
sticker={animatedEmoji}
|
sticker={animatedEmoji}
|
||||||
|
effect={animatedEmojiEffect}
|
||||||
|
soundId={animatedEmojiSoundId}
|
||||||
observeIntersection={observeIntersectionForMedia}
|
observeIntersection={observeIntersectionForMedia}
|
||||||
lastSyncTime={lastSyncTime}
|
lastSyncTime={lastSyncTime}
|
||||||
forceLoadPreview={isLocal}
|
forceLoadPreview={isLocal}
|
||||||
|
messageId={messageId}
|
||||||
|
chatId={chatId}
|
||||||
|
activeEmojiInteraction={activeEmojiInteraction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{localSticker && (
|
||||||
|
<LocalAnimatedEmoji
|
||||||
|
size="small"
|
||||||
|
isOwn={isOwn}
|
||||||
|
localSticker={localSticker}
|
||||||
|
localEffect={localEffect}
|
||||||
|
soundId={animatedEmojiSoundId}
|
||||||
|
observeIntersection={observeIntersectionForMedia}
|
||||||
|
lastSyncTime={lastSyncTime}
|
||||||
|
forceLoadPreview={isLocal}
|
||||||
|
messageId={messageId}
|
||||||
|
chatId={chatId}
|
||||||
|
activeEmojiInteraction={activeEmojiInteraction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAlbum && (
|
{isAlbum && (
|
||||||
@ -605,19 +689,25 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
{poll && (
|
{poll && (
|
||||||
<Poll message={message} poll={poll} onSendVote={handleVoteSend} />
|
<Poll message={message} poll={poll} onSendVote={handleVoteSend} />
|
||||||
)}
|
)}
|
||||||
{!animatedEmoji && textParts && (
|
{!hasAnimatedEmoji && textParts && (
|
||||||
<p className={textContentClass} dir="auto">
|
<p className={textContentClass} dir="auto">
|
||||||
{textParts}
|
{textParts}
|
||||||
{shouldInlineMeta && (
|
{shouldInlineMeta && (
|
||||||
<MessageMeta
|
<>
|
||||||
message={message}
|
{hasReactionButtons && !areReactionsOutside ? (
|
||||||
outgoingStatus={outgoingStatus}
|
<Reactions
|
||||||
signature={signature}
|
activeReaction={activeReaction}
|
||||||
onClick={handleMetaClick}
|
message={reactionsMessage!}
|
||||||
/>
|
metaChildren={renderMeta(true)}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: renderMeta()}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{webPage && (
|
{webPage && (
|
||||||
<WebPage
|
<WebPage
|
||||||
message={message}
|
message={message}
|
||||||
@ -705,7 +795,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseEnter : undefined}
|
onMouseEnter={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseEnter : undefined}
|
||||||
onMouseLeave={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseLeave : undefined}
|
onMouseMove={withQuickReaction ? handleMouseMove : undefined}
|
||||||
|
onMouseLeave={(withQuickReaction || (isInDocumentGroup && !isLastInDocumentGroup)) ? handleMouseLeave : undefined}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={bottomMarkerRef}
|
ref={bottomMarkerRef}
|
||||||
@ -734,6 +825,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
className={buildClassName('message-content-wrapper', contentClassName.includes('text') && 'can-select-text')}
|
className={buildClassName('message-content-wrapper', contentClassName.includes('text') && 'can-select-text')}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
ref={contentRef}
|
||||||
className={contentClassName}
|
className={contentClassName}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
style={style}
|
style={style}
|
||||||
@ -743,13 +835,17 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="message-title">{lang('ForwardedMessage')}</div>
|
<div className="message-title">{lang('ForwardedMessage')}</div>
|
||||||
)}
|
)}
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
{(!isInDocumentGroup || isLastInDocumentGroup) && !(!webPage && !animatedEmoji && textParts) && (
|
{(!isInDocumentGroup || isLastInDocumentGroup) && !(!webPage && !hasAnimatedEmoji && textParts) && (
|
||||||
<MessageMeta
|
<>
|
||||||
message={message}
|
{hasReactionButtons && !areReactionsOutside && (hasAnimatedEmoji || !textParts || webPage) ? (
|
||||||
outgoingStatus={outgoingStatus}
|
<Reactions
|
||||||
signature={signature}
|
activeReaction={activeReaction}
|
||||||
onClick={handleMetaClick}
|
message={reactionsMessage!}
|
||||||
/>
|
metaChildren={renderMeta(true)}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
) : renderMeta()}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{canShowActionButton && canForward ? (
|
{canShowActionButton && canForward ? (
|
||||||
<Button
|
<Button
|
||||||
@ -778,10 +874,26 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
{withAppendix && (
|
{withAppendix && (
|
||||||
<div className="svg-appendix" dangerouslySetInnerHTML={isOwn ? APPENDIX_OWN : APPENDIX_NOT_OWN} />
|
<div className="svg-appendix" dangerouslySetInnerHTML={isOwn ? APPENDIX_OWN : APPENDIX_NOT_OWN} />
|
||||||
)}
|
)}
|
||||||
|
{withQuickReaction && (
|
||||||
|
<div
|
||||||
|
className={buildClassName('quick-reaction', isQuickReactionVisible && !activeReaction && 'visible')}
|
||||||
|
onClick={handleSendQuickReaction}
|
||||||
|
>
|
||||||
|
<ReactionStaticEmoji reaction={defaultReaction!} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{message.inlineButtons && (
|
{message.inlineButtons && (
|
||||||
<InlineButtons message={message} onClick={clickInlineButton} />
|
<InlineButtons message={message} onClick={clickInlineButton} />
|
||||||
)}
|
)}
|
||||||
|
{areReactionsOutside && (
|
||||||
|
<Reactions
|
||||||
|
message={reactionsMessage!}
|
||||||
|
isOutside
|
||||||
|
activeReaction={activeReaction}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{contextMenuPosition && (
|
{contextMenuPosition && (
|
||||||
<ContextMenuContainer
|
<ContextMenuContainer
|
||||||
@ -799,35 +911,11 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleDocumentGroupMouseEnter(e: React.MouseEvent<HTMLDivElement>) {
|
|
||||||
const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget);
|
|
||||||
if (lastGroupElement) {
|
|
||||||
lastGroupElement.setAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDocumentGroupMouseLeave(e: React.MouseEvent<HTMLDivElement>) {
|
|
||||||
const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget);
|
|
||||||
if (lastGroupElement) {
|
|
||||||
lastGroupElement.removeAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastElementInDocumentGroup(element: Element) {
|
|
||||||
let current: Element | null = element;
|
|
||||||
|
|
||||||
do {
|
|
||||||
current = current.nextElementSibling;
|
|
||||||
} while (current && !current.classList.contains('last-in-document-group'));
|
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, ownProps): StateProps => {
|
(global, ownProps): StateProps => {
|
||||||
const { focusedMessage, forwardMessages, lastSyncTime } = global;
|
const { focusedMessage, forwardMessages, lastSyncTime } = global;
|
||||||
const {
|
const {
|
||||||
message, album, withSenderName, withAvatar, threadId, messageListType,
|
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup,
|
||||||
} = ownProps;
|
} = ownProps;
|
||||||
const {
|
const {
|
||||||
id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, threadInfo,
|
id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, threadInfo,
|
||||||
@ -884,6 +972,16 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
? selectThreadInfo(global, threadInfo.chatId, threadInfo.threadId) || threadInfo
|
? selectThreadInfo(global, threadInfo.chatId, threadInfo.threadId) || threadInfo
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const firstMessageInGroupId = message.groupedId
|
||||||
|
&& selectMessageIdsByGroupId(global, chatId, message.groupedId)?.[0];
|
||||||
|
const reactionsMessage = firstMessageInGroupId && !album
|
||||||
|
? (isLastInDocumentGroup
|
||||||
|
? selectChatMessage(global, chatId, firstMessageInGroupId)
|
||||||
|
: undefined)
|
||||||
|
: message;
|
||||||
|
|
||||||
|
const localSticker = singleEmoji ? selectLocalAnimatedEmoji(global, singleEmoji) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
chatUsername,
|
chatUsername,
|
||||||
@ -898,6 +996,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isProtected: selectIsMessageProtected(global, message),
|
isProtected: selectIsMessageProtected(global, message),
|
||||||
isFocused,
|
isFocused,
|
||||||
isForwarding,
|
isForwarding,
|
||||||
|
reactionsMessage,
|
||||||
isChatWithSelf,
|
isChatWithSelf,
|
||||||
isRepliesChat,
|
isRepliesChat,
|
||||||
isChannel,
|
isChannel,
|
||||||
@ -906,6 +1005,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
highlight,
|
highlight,
|
||||||
isSingleEmoji: Boolean(singleEmoji),
|
isSingleEmoji: Boolean(singleEmoji),
|
||||||
animatedEmoji: singleEmoji ? selectAnimatedEmoji(global, singleEmoji) : undefined,
|
animatedEmoji: singleEmoji ? selectAnimatedEmoji(global, singleEmoji) : undefined,
|
||||||
|
animatedEmojiEffect: singleEmoji && isUserId(chatId) ? selectAnimatedEmojiEffect(global, singleEmoji) : undefined,
|
||||||
|
animatedEmojiSoundId: singleEmoji ? selectAnimatedEmojiSound(global, singleEmoji) : undefined,
|
||||||
|
localSticker,
|
||||||
|
localEffect: localSticker && isUserId(chatId) ? selectLocalAnimatedEmojiEffect(localSticker) : undefined,
|
||||||
isInSelectMode: selectIsInSelectMode(global),
|
isInSelectMode: selectIsInSelectMode(global),
|
||||||
isSelected,
|
isSelected,
|
||||||
isGroupSelected: (
|
isGroupSelected: (
|
||||||
@ -922,6 +1025,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
||||||
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
||||||
...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }),
|
...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }),
|
||||||
|
defaultReaction: selectDefaultReaction(global, chatId),
|
||||||
|
activeReaction: global.activeReactions[id],
|
||||||
|
activeEmojiInteraction: global.activeEmojiInteraction,
|
||||||
|
availableReactions: global.availableReactions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Message));
|
)(Message));
|
||||||
|
|||||||
@ -2,11 +2,22 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.scrollable-content {
|
||||||
|
overflow: auto;
|
||||||
|
overflow: overlay;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
transform: scale(0.5);
|
transform: scale(0.5);
|
||||||
transition: opacity .15s cubic-bezier(0.2, 0, 0.2, 1), transform .15s cubic-bezier(0.2, 0, 0.2, 1) !important;
|
transition: opacity .15s cubic-bezier(0.2, 0, 0.2, 1), transform .15s cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||||
overflow: auto;
|
overflow: initial;
|
||||||
overflow: overlay;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.with-reactions .bubble {
|
||||||
|
margin-top: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
|
|||||||
@ -2,31 +2,39 @@ import React, {
|
|||||||
FC, memo, useCallback, useEffect, useRef,
|
FC, memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMessage, ApiUser } from '../../../api/types';
|
import { ApiAvailableReaction, ApiMessage, ApiUser } from '../../../api/types';
|
||||||
import { IAnchorPosition } from '../../../types';
|
import { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
import { getMessageCopyOptions } from './helpers/copyOptions';
|
import { getMessageCopyOptions } from './helpers/copyOptions';
|
||||||
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
|
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
|
||||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
|
||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
|
import ReactionSelector from './ReactionSelector';
|
||||||
|
|
||||||
import './MessageContextMenu.scss';
|
import './MessageContextMenu.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
anchor: IAnchorPosition;
|
anchor: IAnchorPosition;
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
canSendNow?: boolean;
|
canSendNow?: boolean;
|
||||||
|
enabledReactions?: string[];
|
||||||
canReschedule?: boolean;
|
canReschedule?: boolean;
|
||||||
canReply?: boolean;
|
canReply?: boolean;
|
||||||
canPin?: boolean;
|
canPin?: boolean;
|
||||||
canUnpin?: boolean;
|
canUnpin?: boolean;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
canReport?: boolean;
|
canReport?: boolean;
|
||||||
|
canShowReactionsCount?: boolean;
|
||||||
|
canShowReactionList?: boolean;
|
||||||
|
canRemoveReaction?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
canForward?: boolean;
|
canForward?: boolean;
|
||||||
canFaveSticker?: boolean;
|
canFaveSticker?: boolean;
|
||||||
@ -34,6 +42,7 @@ type OwnProps = {
|
|||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
canCopyLink?: boolean;
|
canCopyLink?: boolean;
|
||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
|
isPrivate?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
@ -55,13 +64,20 @@ type OwnProps = {
|
|||||||
onCopyLink?: () => void;
|
onCopyLink?: () => void;
|
||||||
onDownload?: () => void;
|
onDownload?: () => void;
|
||||||
onShowSeenBy?: () => void;
|
onShowSeenBy?: () => void;
|
||||||
|
onShowReactors?: () => void;
|
||||||
|
onSendReaction: (reaction: string | undefined, x: number, y: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCROLLBAR_WIDTH = 10;
|
const SCROLLBAR_WIDTH = 10;
|
||||||
|
const REACTION_BUBBLE_EXTRA_WIDTH = 32;
|
||||||
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
const MessageContextMenu: FC<OwnProps> = ({
|
const MessageContextMenu: FC<OwnProps> = ({
|
||||||
|
availableReactions,
|
||||||
isOpen,
|
isOpen,
|
||||||
message,
|
message,
|
||||||
|
isPrivate,
|
||||||
|
enabledReactions,
|
||||||
anchor,
|
anchor,
|
||||||
canSendNow,
|
canSendNow,
|
||||||
canReschedule,
|
canReschedule,
|
||||||
@ -80,6 +96,9 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canDownload,
|
canDownload,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
|
canShowReactionsCount,
|
||||||
|
canRemoveReaction,
|
||||||
|
canShowReactionList,
|
||||||
seenByRecentUsers,
|
seenByRecentUsers,
|
||||||
onReply,
|
onReply,
|
||||||
onEdit,
|
onEdit,
|
||||||
@ -98,10 +117,18 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onCopyLink,
|
onCopyLink,
|
||||||
onDownload,
|
onDownload,
|
||||||
onShowSeenBy,
|
onShowSeenBy,
|
||||||
|
onShowReactors,
|
||||||
|
onSendReaction,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||||
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
|
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
|
||||||
|
const noReactions = !isPrivate && !enabledReactions?.length;
|
||||||
|
const withReactions = canShowReactionList && !noReactions;
|
||||||
|
|
||||||
|
const [isReady, markIsReady, unmarkIsReady] = useFlag();
|
||||||
|
|
||||||
const getTriggerElement = useCallback(() => {
|
const getTriggerElement = useCallback(() => {
|
||||||
return document.querySelector(`.Transition__slide--active > .MessageList div[data-message-id="${message.id}"]`);
|
return document.querySelector(`.Transition__slide--active > .MessageList div[data-message-id="${message.id}"]`);
|
||||||
@ -117,6 +144,21 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRemoveReaction = useCallback(() => {
|
||||||
|
onSendReaction(undefined, 0, 0);
|
||||||
|
}, [onSendReaction]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
unmarkIsReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
markIsReady();
|
||||||
|
}, ANIMATION_DURATION);
|
||||||
|
}, [isOpen, markIsReady, unmarkIsReady]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, style, menuStyle, withScroll,
|
positionX, positionY, style, menuStyle, withScroll,
|
||||||
} = useContextMenuPosition(
|
} = useContextMenuPosition(
|
||||||
@ -126,10 +168,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
getMenuElement,
|
getMenuElement,
|
||||||
SCROLLBAR_WIDTH,
|
SCROLLBAR_WIDTH,
|
||||||
(document.querySelector('.MiddleHeader') as HTMLElement).offsetHeight,
|
(document.querySelector('.MiddleHeader') as HTMLElement).offsetHeight,
|
||||||
|
withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
disableScrolling(withScroll ? menuRef.current : undefined);
|
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
|
||||||
|
|
||||||
return enableScrolling;
|
return enableScrolling;
|
||||||
}, [withScroll]);
|
}, [withScroll]);
|
||||||
@ -144,51 +187,79 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
positionY={positionY}
|
positionY={positionY}
|
||||||
style={style}
|
style={style}
|
||||||
menuStyle={menuStyle}
|
menuStyle={menuStyle}
|
||||||
className="MessageContextMenu fluid"
|
className={buildClassName(
|
||||||
|
'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
|
||||||
|
)}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
{canShowReactionList && (
|
||||||
{canReschedule && (
|
<ReactionSelector
|
||||||
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
enabledReactions={enabledReactions}
|
||||||
|
onSendReaction={onSendReaction}
|
||||||
|
isPrivate={isPrivate}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
isReady={isReady}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{canReply && <MenuItem icon="reply" onClick={onReply}>{lang('Reply')}</MenuItem>}
|
|
||||||
{canEdit && <MenuItem icon="edit" onClick={onEdit}>{lang('Edit')}</MenuItem>}
|
<div
|
||||||
{canFaveSticker && (
|
className="scrollable-content custom-scroll"
|
||||||
<MenuItem icon="favorite" onClick={onFaveSticker}>{lang('AddToFavorites')}</MenuItem>
|
// @ts-ignore teact feature
|
||||||
)}
|
style={menuStyle}
|
||||||
{canUnfaveSticker && (
|
ref={scrollableRef}
|
||||||
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{lang('Stickers.RemoveFromFavorites')}</MenuItem>
|
>
|
||||||
)}
|
{canRemoveReaction && <MenuItem icon="reactions" onClick={handleRemoveReaction}>Remove Reaction</MenuItem>}
|
||||||
{canCopy && copyOptions.map((options) => (
|
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
||||||
<MenuItem key={options.label} icon="copy" onClick={options.handler}>{lang(options.label)}</MenuItem>
|
{canReschedule && (
|
||||||
))}
|
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
||||||
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
)}
|
||||||
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
{canReply && <MenuItem icon="reply" onClick={onReply}>{lang('Reply')}</MenuItem>}
|
||||||
{canDownload && (
|
{canEdit && <MenuItem icon="edit" onClick={onEdit}>{lang('Edit')}</MenuItem>}
|
||||||
<MenuItem icon="download" onClick={onDownload}>
|
{canFaveSticker && (
|
||||||
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
<MenuItem icon="favorite" onClick={onFaveSticker}>{lang('AddToFavorites')}</MenuItem>
|
||||||
</MenuItem>
|
)}
|
||||||
)}
|
{canUnfaveSticker && (
|
||||||
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{lang('Stickers.RemoveFromFavorites')}</MenuItem>
|
||||||
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
)}
|
||||||
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
{canCopy && copyOptions.map((options) => (
|
||||||
{canShowSeenBy && (
|
<MenuItem key={options.label} icon="copy" onClick={options.handler}>{lang(options.label)}</MenuItem>
|
||||||
<MenuItem icon="group" onClick={onShowSeenBy} disabled={!message.seenByUserIds?.length}>
|
))}
|
||||||
{message.seenByUserIds?.length
|
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
||||||
? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i')
|
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
||||||
: lang('Conversation.ContextMenuNoViews')}
|
{canDownload && (
|
||||||
<div className="avatars">
|
<MenuItem icon="download" onClick={onDownload}>
|
||||||
{seenByRecentUsers?.map((user) => (
|
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
||||||
<Avatar
|
</MenuItem>
|
||||||
size="micro"
|
)}
|
||||||
user={user}
|
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
||||||
/>
|
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
||||||
))}
|
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
||||||
</div>
|
{(canShowSeenBy || canShowReactionsCount) && (
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
)}
|
icon={canShowReactionsCount ? 'reactions' : 'group'}
|
||||||
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
||||||
|
disabled={!canShowReactionsCount && !message.seenByUserIds?.length}
|
||||||
|
>
|
||||||
|
{canShowReactionsCount && message.reactors?.count ? (
|
||||||
|
canShowSeenBy && message.seenByUserIds?.length
|
||||||
|
? lang('Chat.OutgoingContextMixedReactionCount', [message.reactors.count, message.seenByUserIds.length])
|
||||||
|
: lang('Chat.ContextReactionCount', message.reactors.count, 'i'))
|
||||||
|
: (message.seenByUserIds?.length
|
||||||
|
? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i')
|
||||||
|
: lang('Conversation.ContextMenuNoViews'))}
|
||||||
|
<div className="avatars">
|
||||||
|
{seenByRecentUsers?.map((user) => (
|
||||||
|
<Avatar
|
||||||
|
size="micro"
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
||||||
|
</div>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,12 @@
|
|||||||
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-signature,
|
.message-signature,
|
||||||
.message-views {
|
.message-views {
|
||||||
|
|||||||
@ -2,31 +2,42 @@ import React, {
|
|||||||
FC, memo, useMemo,
|
FC, memo, useMemo,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types';
|
import { ApiAvailableReaction, ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types';
|
||||||
|
import { 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';
|
||||||
|
|
||||||
import MessageOutgoingStatus from '../../common/MessageOutgoingStatus';
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import MessageOutgoingStatus from '../../common/MessageOutgoingStatus';
|
||||||
|
import ReactionAnimatedEmoji from './ReactionAnimatedEmoji';
|
||||||
|
|
||||||
import './MessageMeta.scss';
|
import './MessageMeta.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
|
withReactions?: boolean;
|
||||||
|
withReactionOffset?: boolean;
|
||||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
|
activeReaction?: ActiveReaction;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageMeta: FC<OwnProps> = ({
|
const MessageMeta: FC<OwnProps> = ({
|
||||||
message, outgoingStatus, signature, onClick,
|
message, outgoingStatus, signature, onClick, withReactions,
|
||||||
|
activeReaction, withReactionOffset, availableReactions,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [isActivated, markActivated] = useFlag();
|
const [isActivated, markActivated] = useFlag();
|
||||||
|
|
||||||
|
const reactions = withReactions && message.reactions?.results.filter((l) => l.count > 0);
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (!isActivated) return undefined;
|
if (!isActivated) return undefined;
|
||||||
const createDateTime = formatDateTimeToString(message.date * 1000, lang.code);
|
const createDateTime = formatDateTimeToString(message.date * 1000, lang.code);
|
||||||
@ -47,7 +58,19 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
}, [isActivated, lang, message]);
|
}, [isActivated, lang, message]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="MessageMeta" dir={lang.isRtl ? 'rtl' : 'ltr'} onClick={onClick}>
|
<span
|
||||||
|
className={buildClassName('MessageMeta', withReactionOffset && 'reactions-offset')}
|
||||||
|
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{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">
|
||||||
|
|||||||
50
src/components/middle/message/ReactionAnimatedEmoji.scss
Normal file
50
src/components/middle/message/ReactionAnimatedEmoji.scss
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/components/middle/message/ReactionAnimatedEmoji.tsx
Normal file
92
src/components/middle/message/ReactionAnimatedEmoji.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ActiveReaction } from '../../../global/types';
|
||||||
|
import { ApiAvailableReaction, ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
|
|
||||||
|
import './ReactionAnimatedEmoji.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
reaction: string;
|
||||||
|
activeReaction?: ActiveReaction;
|
||||||
|
isInMeta?: boolean;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CENTER_ICON_SIZE = 30;
|
||||||
|
const EFFECT_SIZE = 100;
|
||||||
|
|
||||||
|
const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||||
|
reaction,
|
||||||
|
activeReaction,
|
||||||
|
isInMeta,
|
||||||
|
availableReactions,
|
||||||
|
}) => {
|
||||||
|
const { stopActiveReaction } = getDispatch();
|
||||||
|
|
||||||
|
const availableReaction = availableReactions?.find((r) => r.reaction === reaction);
|
||||||
|
const centerIconId = availableReaction?.centerIcon?.id;
|
||||||
|
const effectId = availableReaction?.aroundAnimation?.id;
|
||||||
|
const mediaDataCenterIcon = useMedia(`sticker${centerIconId}`, !centerIconId, ApiMediaFormat.Lottie);
|
||||||
|
const mediaDataEffect = useMedia(`sticker${effectId}`, !effectId, ApiMediaFormat.Lottie);
|
||||||
|
|
||||||
|
const shouldPlay = Boolean(activeReaction?.reaction === reaction && mediaDataCenterIcon && mediaDataEffect);
|
||||||
|
const {
|
||||||
|
shouldRender: shouldRenderAnimation,
|
||||||
|
transitionClassNames: animationClassNames,
|
||||||
|
} = useShowTransition(shouldPlay, undefined, true, 'slow');
|
||||||
|
|
||||||
|
const handleEnded = useCallback(() => {
|
||||||
|
stopActiveReaction({ messageId: activeReaction?.messageId, reaction });
|
||||||
|
}, [activeReaction?.messageId, reaction, stopActiveReaction]);
|
||||||
|
|
||||||
|
const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag();
|
||||||
|
const shouldRenderStatic = !shouldPlay || !isAnimationLoaded;
|
||||||
|
|
||||||
|
const className = buildClassName(
|
||||||
|
'ReactionAnimatedEmoji',
|
||||||
|
isInMeta && 'in-meta',
|
||||||
|
shouldRenderAnimation && 'is-animating',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{shouldRenderStatic && <ReactionStaticEmoji reaction={reaction} />}
|
||||||
|
{shouldRenderAnimation && (
|
||||||
|
<>
|
||||||
|
<AnimatedSticker
|
||||||
|
key={centerIconId}
|
||||||
|
id={`reaction_emoji_${centerIconId}`}
|
||||||
|
className={animationClassNames}
|
||||||
|
size={CENTER_ICON_SIZE}
|
||||||
|
animationData={mediaDataCenterIcon as AnyLiteral}
|
||||||
|
play
|
||||||
|
noLoop
|
||||||
|
onLoad={markAnimationLoaded}
|
||||||
|
onEnded={unmarkAnimationLoaded}
|
||||||
|
/>
|
||||||
|
<AnimatedSticker
|
||||||
|
key={effectId}
|
||||||
|
id={`reaction_effect_${effectId}`}
|
||||||
|
className={buildClassName('effect', animationClassNames)}
|
||||||
|
size={EFFECT_SIZE}
|
||||||
|
animationData={mediaDataEffect as AnyLiteral}
|
||||||
|
play
|
||||||
|
noLoop
|
||||||
|
onEnded={handleEnded}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactionAnimatedEmoji);
|
||||||
79
src/components/middle/message/ReactionButton.tsx
Normal file
79
src/components/middle/message/ReactionButton.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useMemo,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch, getGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiAvailableReaction, ApiMessage, ApiReactionCount, ApiUser,
|
||||||
|
} from '../../../api/types';
|
||||||
|
import { ActiveReaction } from '../../../global/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
|
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import ReactionAnimatedEmoji from './ReactionAnimatedEmoji';
|
||||||
|
|
||||||
|
import './Reactions.scss';
|
||||||
|
|
||||||
|
const MAX_REACTORS_AVATARS = 3;
|
||||||
|
|
||||||
|
const ReactionButton: FC<{
|
||||||
|
reaction: ApiReactionCount;
|
||||||
|
message: ApiMessage;
|
||||||
|
activeReaction?: ActiveReaction;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
}> = ({
|
||||||
|
reaction,
|
||||||
|
message,
|
||||||
|
activeReaction,
|
||||||
|
availableReactions,
|
||||||
|
}) => {
|
||||||
|
const { sendReaction } = getDispatch();
|
||||||
|
|
||||||
|
const { recentReactions } = message.reactions!;
|
||||||
|
|
||||||
|
const recentReactors = useMemo(() => {
|
||||||
|
if (!recentReactions || reaction.count > MAX_REACTORS_AVATARS) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need for expensive global updates on users, so we avoid them
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
|
||||||
|
return recentReactions
|
||||||
|
.filter((recentReaction) => recentReaction.reaction === reaction.reaction)
|
||||||
|
.map((recentReaction) => usersById[recentReaction.userId])
|
||||||
|
.filter(Boolean) as ApiUser[];
|
||||||
|
}, [reaction, recentReactions]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
sendReaction({
|
||||||
|
reaction: reaction.isChosen ? undefined : reaction.reaction,
|
||||||
|
chatId: message.chatId,
|
||||||
|
messageId: message.id,
|
||||||
|
});
|
||||||
|
}, [message, reaction, sendReaction]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={buildClassName(reaction.isChosen && 'chosen')}
|
||||||
|
size="tiny"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<ReactionAnimatedEmoji
|
||||||
|
activeReaction={activeReaction}
|
||||||
|
reaction={reaction.reaction}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
{recentReactors?.length ? (
|
||||||
|
<div className="avatars">
|
||||||
|
{recentReactors.map((user) => <Avatar user={user} size="micro" />)}
|
||||||
|
</div>
|
||||||
|
) : formatIntegerCompact(reaction.count)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactionButton);
|
||||||
91
src/components/middle/message/ReactionSelector.scss
Normal file
91
src/components/middle/message/ReactionSelector.scss
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
.ReactionSelector {
|
||||||
|
position: absolute;
|
||||||
|
height: 3rem;
|
||||||
|
background: var(--color-background);
|
||||||
|
min-width: 3rem;
|
||||||
|
max-width: calc(100% + 5rem);
|
||||||
|
z-index: 100;
|
||||||
|
border-radius: 3rem;
|
||||||
|
filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow));
|
||||||
|
right: -3rem;
|
||||||
|
top: -3.5rem;
|
||||||
|
|
||||||
|
.bubble-big {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
right: 1.5rem;
|
||||||
|
bottom: -0.5rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-background);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-small {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
right: 1.25rem;
|
||||||
|
bottom: -1.25rem;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-safari & {
|
||||||
|
filter: none;
|
||||||
|
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-safari .bubble-small, body.is-safari .bubble-big {
|
||||||
|
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
padding: 0 1rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow: overlay;
|
||||||
|
overflow-y: hidden;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
min-width: 2rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactionStaticEmoji {
|
||||||
|
width: 2rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AnimatedSticker {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/components/middle/message/ReactionSelector.tsx
Normal file
122
src/components/middle/message/ReactionSelector.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useLayoutEffect, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiAvailableReaction, ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import { getTouchY } from '../../../util/scrollLock';
|
||||||
|
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
|
|
||||||
|
import './ReactionSelector.scss';
|
||||||
|
|
||||||
|
const REACTION_SIZE = 32;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
enabledReactions?: string[];
|
||||||
|
onSendReaction: (reaction: string, x: number, y: number) => void;
|
||||||
|
isPrivate?: boolean;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
isReady?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AvailableReaction: FC<{
|
||||||
|
reaction: ApiAvailableReaction;
|
||||||
|
isReady?: boolean;
|
||||||
|
onSendReaction: (reaction: string, x: number, y: number) => void;
|
||||||
|
}> = ({ reaction, onSendReaction, isReady }) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isActivated, activate, deactivate] = useFlag();
|
||||||
|
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady, ApiMediaFormat.Lottie);
|
||||||
|
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const { x, y } = containerRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
onSendReaction(reaction.reaction, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRenderPreview = !isAnimationLoaded;
|
||||||
|
const shouldRenderAnimated = mediaData;
|
||||||
|
const shouldPlay = isReady && isActivated;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="reaction"
|
||||||
|
onClick={handleClick}
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseEnter={isReady ? activate : undefined}
|
||||||
|
>
|
||||||
|
{shouldRenderPreview && <ReactionStaticEmoji reaction={reaction.reaction} />}
|
||||||
|
{shouldRenderAnimated && (
|
||||||
|
<AnimatedSticker
|
||||||
|
id={`select_${reaction.reaction}`}
|
||||||
|
animationData={mediaData as AnyLiteral}
|
||||||
|
play={shouldPlay}
|
||||||
|
noLoop
|
||||||
|
onEnded={deactivate}
|
||||||
|
size={REACTION_SIZE}
|
||||||
|
onLoad={markAnimationLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const ReactionSelector: FC<OwnProps> = ({
|
||||||
|
availableReactions,
|
||||||
|
enabledReactions,
|
||||||
|
onSendReaction,
|
||||||
|
isPrivate,
|
||||||
|
isReady,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const itemsScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isHorizontalScrollEnabled, enableHorizontalScroll] = useFlag(false);
|
||||||
|
useHorizontalScroll(itemsScrollRef.current, !isHorizontalScrollEnabled);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
enableHorizontalScroll();
|
||||||
|
}, [enableHorizontalScroll]);
|
||||||
|
|
||||||
|
const handleWheel = (e: React.WheelEvent | React.TouchEvent) => {
|
||||||
|
if (!itemsScrollRef) return;
|
||||||
|
const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e);
|
||||||
|
|
||||||
|
if (deltaY) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((!isPrivate && !enabledReactions?.length) || !availableReactions) return undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ReactionSelector" onWheelCapture={handleWheel} onTouchMove={handleWheel}>
|
||||||
|
<div className="bubble-big" />
|
||||||
|
<div className="bubble-small" />
|
||||||
|
<div className="items-wrapper">
|
||||||
|
<div className="items no-scrollbar" ref={itemsScrollRef}>
|
||||||
|
{availableReactions?.map((reaction) => {
|
||||||
|
if (reaction.isInactive
|
||||||
|
|| (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction)))) return undefined;
|
||||||
|
return (
|
||||||
|
<AvailableReaction
|
||||||
|
key={reaction.reaction}
|
||||||
|
isReady={isReady}
|
||||||
|
onSendReaction={onSendReaction}
|
||||||
|
reaction={reaction}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactionSelector);
|
||||||
84
src/components/middle/message/Reactions.scss
Normal file
84
src/components/middle/message/Reactions.scss
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
.Reactions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.Button {
|
||||||
|
--reaction-background: var(--color-reaction);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 1.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: auto;
|
||||||
|
margin: 0.125rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-color: var(--reaction-background) !important;
|
||||||
|
border-radius: 1.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--accent-color);
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.ReactionAnimatedEmoji, .icon-reaction-filled {
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
|
margin: 0 0 0 -0.25rem;
|
||||||
|
border: 0.0625rem solid var(--reaction-background);
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.chosen {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--reaction-background: var(--hover-color-reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-outside {
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-outside .Button {
|
||||||
|
--reaction-background: var(--pattern-color);
|
||||||
|
color: white;
|
||||||
|
.theme-dark & {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.chosen {
|
||||||
|
border-color: white;
|
||||||
|
|
||||||
|
.theme-dark & {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/components/middle/message/Reactions.tsx
Normal file
43
src/components/middle/message/Reactions.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiAvailableReaction, ApiMessage } from '../../../api/types';
|
||||||
|
import { ActiveReaction } from '../../../global/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import ReactionButton from './ReactionButton';
|
||||||
|
|
||||||
|
import './Reactions.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
message: ApiMessage;
|
||||||
|
isOutside?: boolean;
|
||||||
|
activeReaction?: ActiveReaction;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
metaChildren?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Reactions: FC<OwnProps> = ({
|
||||||
|
message,
|
||||||
|
isOutside,
|
||||||
|
activeReaction,
|
||||||
|
availableReactions,
|
||||||
|
metaChildren,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName('Reactions', isOutside && 'is-outside')}>
|
||||||
|
{message.reactions!.results.map((reaction) => (
|
||||||
|
<ReactionButton
|
||||||
|
key={reaction.reaction}
|
||||||
|
reaction={reaction}
|
||||||
|
message={message}
|
||||||
|
activeReaction={activeReaction}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{metaChildren}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Reactions);
|
||||||
@ -104,6 +104,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MessageMeta.reactions-offset {
|
||||||
|
position: relative;
|
||||||
|
top: .375rem;
|
||||||
|
bottom: auto !important;
|
||||||
|
float: right;
|
||||||
|
line-height: 1;
|
||||||
|
height: calc(var(--message-meta-height, 1rem));
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -.5rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
.MessageOutgoingStatus .Transition {
|
||||||
|
max-height: calc(var(--message-meta-height, 1rem));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-message-text-size="12"] & {
|
||||||
|
top: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-message-text-size="17"] & {
|
||||||
|
top: .4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-message-text-size="18"] &,
|
||||||
|
html[data-message-text-size="19"] & {
|
||||||
|
top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-message-text-size="20"] & {
|
||||||
|
top: .5625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.document:not(.text) {
|
&.document:not(.text) {
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
import React, { useEffect } from '../../../../lib/teact/teact';
|
import React, { useEffect, useRef } from '../../../../lib/teact/teact';
|
||||||
import { getDispatch } from '../../../../lib/teact/teactn';
|
import { getDispatch } from '../../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../../util/environment';
|
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../../util/environment';
|
||||||
@ -8,9 +8,14 @@ import { captureEvents, SwipeDirection } from '../../../../util/captureEvents';
|
|||||||
import useFlag from '../../../../hooks/useFlag';
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur';
|
import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur';
|
||||||
import stopEvent from '../../../../util/stopEvent';
|
import stopEvent from '../../../../util/stopEvent';
|
||||||
|
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
|
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
|
||||||
const SWIPE_ANIMATION_DURATION = 150;
|
const SWIPE_ANIMATION_DURATION = 150;
|
||||||
|
const QUICK_REACTION_DOUBLE_TAP_DELAY = 200;
|
||||||
|
const QUICK_REACTION_AREA_WIDTH = 3 * REM;
|
||||||
|
const QUICK_REACTION_AREA_HEIGHT = Number(REM);
|
||||||
|
const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover';
|
||||||
|
|
||||||
export default function useOuterHandlers(
|
export default function useOuterHandlers(
|
||||||
selectMessage: (e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
|
selectMessage: (e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
|
||||||
@ -22,20 +27,53 @@ export default function useOuterHandlers(
|
|||||||
isProtected: boolean,
|
isProtected: boolean,
|
||||||
onContextMenu: (e: React.MouseEvent) => void,
|
onContextMenu: (e: React.MouseEvent) => void,
|
||||||
handleBeforeContextMenu: (e: React.MouseEvent) => void,
|
handleBeforeContextMenu: (e: React.MouseEvent) => void,
|
||||||
|
chatId: string,
|
||||||
|
isContextMenuShown: boolean,
|
||||||
|
contentRef: RefObject<HTMLDivElement>,
|
||||||
|
isOwn: boolean,
|
||||||
|
shouldHandleMouseLeave: boolean,
|
||||||
) {
|
) {
|
||||||
const { setReplyingToId } = getDispatch();
|
const { setReplyingToId, sendDefaultReaction } = getDispatch();
|
||||||
|
|
||||||
|
const [isQuickReactionVisible, markQuickReactionVisible, unmarkQuickReactionVisible] = useFlag();
|
||||||
const [isSwiped, markSwiped, unmarkSwiped] = useFlag();
|
const [isSwiped, markSwiped, unmarkSwiped] = useFlag();
|
||||||
|
const doubleTapTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
function handleMouseDown(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function handleMouseDown(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
preventMessageInputBlur(e);
|
preventMessageInputBlur(e);
|
||||||
handleBeforeContextMenu(e);
|
handleBeforeContextMenu(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function handleMouseMove(e: React.MouseEvent) {
|
||||||
if (isInSelectMode) {
|
const container = contentRef.current;
|
||||||
selectMessage(e);
|
if (!container) return;
|
||||||
} else if (IS_ANDROID) {
|
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
const {
|
||||||
|
x, width, y, height,
|
||||||
|
} = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
const isVisibleX = Math.abs((isOwn ? (clientX - x) : (x + width - clientX))) < QUICK_REACTION_AREA_WIDTH;
|
||||||
|
const isVisibleY = Math.abs(y + height - clientY) < QUICK_REACTION_AREA_HEIGHT;
|
||||||
|
if (isVisibleX && isVisibleY) {
|
||||||
|
markQuickReactionVisible();
|
||||||
|
} else {
|
||||||
|
unmarkQuickReactionVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSendQuickReaction(e: React.MouseEvent) {
|
||||||
|
const { x, y } = e.currentTarget.getBoundingClientRect();
|
||||||
|
sendDefaultReaction({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTap(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
if (IS_ANDROID) {
|
||||||
const target = e.target as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
if (!target.classList.contains('text-content') && !target.classList.contains('Message')) {
|
if (!target.classList.contains('text-content') && !target.classList.contains('Message')) {
|
||||||
return;
|
return;
|
||||||
@ -51,6 +89,38 @@ export default function useOuterHandlers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDoubleTap(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
const { pageX: x, pageY: y } = e;
|
||||||
|
|
||||||
|
sendDefaultReaction({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
if (isInSelectMode) {
|
||||||
|
selectMessage(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IS_TOUCH_ENV) return;
|
||||||
|
|
||||||
|
if (doubleTapTimeoutRef.current) {
|
||||||
|
clearInterval(doubleTapTimeoutRef.current);
|
||||||
|
doubleTapTimeoutRef.current = undefined;
|
||||||
|
handleDoubleTap(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doubleTapTimeoutRef.current = setTimeout(() => {
|
||||||
|
doubleTapTimeoutRef.current = undefined;
|
||||||
|
handleTap(e);
|
||||||
|
}, QUICK_REACTION_DOUBLE_TAP_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
if (IS_ANDROID) {
|
if (IS_ANDROID) {
|
||||||
if ((e.target as HTMLElement).matches('a[href]')) {
|
if ((e.target as HTMLElement).matches('a[href]')) {
|
||||||
@ -65,6 +135,8 @@ export default function useOuterHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleContainerDoubleClick() {
|
function handleContainerDoubleClick() {
|
||||||
|
if (IS_TOUCH_ENV) return;
|
||||||
|
|
||||||
setReplyingToId({ messageId });
|
setReplyingToId({ messageId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +145,7 @@ export default function useOuterHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_TOUCH_ENV || isInSelectMode || !canReply) {
|
if (!IS_TOUCH_ENV || isInSelectMode || !canReply || isContextMenuShown) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +176,14 @@ export default function useOuterHandlers(
|
|||||||
startedAt = undefined;
|
startedAt = undefined;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped, canReply]);
|
}, [
|
||||||
|
containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped, canReply, isContextMenuShown,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleMouseLeave(e: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
unmarkQuickReactionVisible();
|
||||||
|
if (shouldHandleMouseLeave) handleDocumentGroupMouseLeave(e);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleMouseDown: !isInSelectMode ? handleMouseDown : undefined,
|
handleMouseDown: !isInSelectMode ? handleMouseDown : undefined,
|
||||||
@ -112,6 +191,35 @@ export default function useOuterHandlers(
|
|||||||
handleContextMenu: !isInSelectMode ? handleContextMenu : (isProtected ? stopEvent : undefined),
|
handleContextMenu: !isInSelectMode ? handleContextMenu : (isProtected ? stopEvent : undefined),
|
||||||
handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined,
|
handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined,
|
||||||
handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined,
|
handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined,
|
||||||
|
handleMouseMove,
|
||||||
|
handleSendQuickReaction,
|
||||||
|
handleMouseLeave,
|
||||||
isSwiped,
|
isSwiped,
|
||||||
|
isQuickReactionVisible,
|
||||||
|
handleDocumentGroupMouseEnter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDocumentGroupMouseEnter(e: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget);
|
||||||
|
if (lastGroupElement) {
|
||||||
|
lastGroupElement.setAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDocumentGroupMouseLeave(e: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget);
|
||||||
|
if (lastGroupElement) {
|
||||||
|
lastGroupElement.removeAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastElementInDocumentGroup(element: Element) {
|
||||||
|
let current: Element | null = element;
|
||||||
|
|
||||||
|
do {
|
||||||
|
current = current.nextElementSibling;
|
||||||
|
} while (current && !current.classList.contains('last-in-document-group'));
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|||||||
@ -123,6 +123,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
case ManagementScreens.ChatAdministrators:
|
case ManagementScreens.ChatAdministrators:
|
||||||
case ManagementScreens.ChannelSubscribers:
|
case ManagementScreens.ChannelSubscribers:
|
||||||
case ManagementScreens.GroupMembers:
|
case ManagementScreens.GroupMembers:
|
||||||
|
case ManagementScreens.Reactions:
|
||||||
setManagementScreen(ManagementScreens.Initial);
|
setManagementScreen(ManagementScreens.Initial);
|
||||||
break;
|
break;
|
||||||
case ManagementScreens.GroupUserPermissionsCreate:
|
case ManagementScreens.GroupUserPermissionsCreate:
|
||||||
|
|||||||
@ -17,10 +17,7 @@ import {
|
|||||||
selectUser,
|
selectUser,
|
||||||
} from '../../modules/selectors';
|
} from '../../modules/selectors';
|
||||||
import {
|
import {
|
||||||
getCanAddContact,
|
getCanAddContact, isChatAdmin, isChatChannel, isUserId,
|
||||||
isChatAdmin,
|
|
||||||
isChatChannel,
|
|
||||||
isUserId,
|
|
||||||
} from '../../modules/helpers';
|
} from '../../modules/helpers';
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
@ -84,6 +81,7 @@ enum HeaderContent {
|
|||||||
GifSearch,
|
GifSearch,
|
||||||
PollResults,
|
PollResults,
|
||||||
AddingMembers,
|
AddingMembers,
|
||||||
|
ManageReactions,
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightHeader: FC<OwnProps & StateProps> = ({
|
const RightHeader: FC<OwnProps & StateProps> = ({
|
||||||
@ -195,6 +193,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
HeaderContent.ManageGroupMembers
|
HeaderContent.ManageGroupMembers
|
||||||
) : managementScreen === ManagementScreens.GroupAddAdmins ? (
|
) : managementScreen === ManagementScreens.GroupAddAdmins ? (
|
||||||
HeaderContent.ManageGroupAddAdmins
|
HeaderContent.ManageGroupAddAdmins
|
||||||
|
) : managementScreen === ManagementScreens.Reactions ? (
|
||||||
|
HeaderContent.ManageReactions
|
||||||
) : undefined // Never reached
|
) : undefined // Never reached
|
||||||
) : undefined; // When column is closed
|
) : undefined; // When column is closed
|
||||||
|
|
||||||
@ -278,6 +278,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
case HeaderContent.MemberList:
|
case HeaderContent.MemberList:
|
||||||
case HeaderContent.ManageGroupMembers:
|
case HeaderContent.ManageGroupMembers:
|
||||||
return <h3>{lang('GroupMembers')}</h3>;
|
return <h3>{lang('GroupMembers')}</h3>;
|
||||||
|
case HeaderContent.ManageReactions:
|
||||||
|
return <h3>{lang('Reactions')}</h3>;
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type StateProps = {
|
|||||||
progress?: ManagementProgress;
|
progress?: ManagementProgress;
|
||||||
isSignaturesShown: boolean;
|
isSignaturesShown: boolean;
|
||||||
canChangeInfo?: boolean;
|
canChangeInfo?: boolean;
|
||||||
|
availableReactionsCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHANNEL_TITLE_EMPTY = 'Channel title can\'t be empty';
|
const CHANNEL_TITLE_EMPTY = 'Channel title can\'t be empty';
|
||||||
@ -46,6 +47,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
progress,
|
progress,
|
||||||
isSignaturesShown,
|
isSignaturesShown,
|
||||||
canChangeInfo,
|
canChangeInfo,
|
||||||
|
availableReactionsCount,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onClose,
|
onClose,
|
||||||
isActive,
|
isActive,
|
||||||
@ -92,6 +94,10 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
onScreenSelect(ManagementScreens.Discussion);
|
onScreenSelect(ManagementScreens.Discussion);
|
||||||
}, [onScreenSelect]);
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
|
const handleClickReactions = useCallback(() => {
|
||||||
|
onScreenSelect(ManagementScreens.Reactions);
|
||||||
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
const handleClickAdministrators = useCallback(() => {
|
const handleClickAdministrators = useCallback(() => {
|
||||||
onScreenSelect(ManagementScreens.ChatAdministrators);
|
onScreenSelect(ManagementScreens.ChatAdministrators);
|
||||||
}, [onScreenSelect]);
|
}, [onScreenSelect]);
|
||||||
@ -148,6 +154,8 @@ 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;
|
||||||
|
|
||||||
if (chat.isRestricted) {
|
if (chat.isRestricted) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -202,6 +210,17 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
|||||||
<span className="title">{lang('ChannelAdministrators')}</span>
|
<span className="title">{lang('ChannelAdministrators')}</span>
|
||||||
<span className="subtitle">{adminsCount}</span>
|
<span className="subtitle">{adminsCount}</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
icon="reactions"
|
||||||
|
multiline
|
||||||
|
onClick={handleClickReactions}
|
||||||
|
disabled={!canChangeInfo}
|
||||||
|
>
|
||||||
|
<span className="title">{lang('Reactions')}</span>
|
||||||
|
<span className="subtitle" dir="auto">
|
||||||
|
{enabledReactionsCount}/{availableReactionsCount}
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
<div className="ListItem no-selection narrow">
|
<div className="ListItem no-selection narrow">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSignaturesShown}
|
checked={isSignaturesShown}
|
||||||
@ -261,6 +280,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
progress,
|
progress,
|
||||||
isSignaturesShown,
|
isSignaturesShown,
|
||||||
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
||||||
|
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ManageChannel));
|
)(ManageChannel));
|
||||||
|
|||||||
@ -40,6 +40,7 @@ type StateProps = {
|
|||||||
hasLinkedChannel: boolean;
|
hasLinkedChannel: boolean;
|
||||||
canChangeInfo?: boolean;
|
canChangeInfo?: boolean;
|
||||||
canBanUsers?: boolean;
|
canBanUsers?: boolean;
|
||||||
|
availableReactionsCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GROUP_TITLE_EMPTY = 'Group title can\'t be empty';
|
const GROUP_TITLE_EMPTY = 'Group title can\'t be empty';
|
||||||
@ -59,6 +60,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onClose,
|
onClose,
|
||||||
isActive,
|
isActive,
|
||||||
|
availableReactionsCount,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
togglePreHistoryHidden,
|
togglePreHistoryHidden,
|
||||||
@ -100,6 +102,10 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
onScreenSelect(ManagementScreens.Discussion);
|
onScreenSelect(ManagementScreens.Discussion);
|
||||||
}, [onScreenSelect]);
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
|
const handleClickReactions = useCallback(() => {
|
||||||
|
onScreenSelect(ManagementScreens.Reactions);
|
||||||
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
const handleClickPermissions = useCallback(() => {
|
const handleClickPermissions = useCallback(() => {
|
||||||
onScreenSelect(ManagementScreens.GroupPermissions);
|
onScreenSelect(ManagementScreens.GroupPermissions);
|
||||||
}, [onScreenSelect]);
|
}, [onScreenSelect]);
|
||||||
@ -154,6 +160,8 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
togglePreHistoryHidden({ chatId: chat.id, isEnabled: !isPreHistoryHidden });
|
togglePreHistoryHidden({ chatId: chat.id, isEnabled: !isPreHistoryHidden });
|
||||||
}, [chat, togglePreHistoryHidden]);
|
}, [chat, togglePreHistoryHidden]);
|
||||||
|
|
||||||
|
const enabledReactionsCount = chat.fullInfo?.enabledReactions?.length || 0;
|
||||||
|
|
||||||
const enabledPermissionsCount = useMemo(() => {
|
const enabledPermissionsCount = useMemo(() => {
|
||||||
if (!chat.defaultBannedRights) {
|
if (!chat.defaultBannedRights) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -257,6 +265,18 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
|||||||
{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT}
|
{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT}
|
||||||
</span>
|
</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
icon="reactions"
|
||||||
|
multiline
|
||||||
|
onClick={handleClickReactions}
|
||||||
|
disabled={!canChangeInfo}
|
||||||
|
>
|
||||||
|
<span className="title">{lang('Reactions')}</span>
|
||||||
|
<span className="subtitle" dir="auto">
|
||||||
|
{enabledReactionsCount}/{availableReactionsCount}
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="admin"
|
icon="admin"
|
||||||
multiline
|
multiline
|
||||||
@ -332,6 +352,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
hasLinkedChannel,
|
hasLinkedChannel,
|
||||||
canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'),
|
canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'),
|
||||||
canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'),
|
canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'),
|
||||||
|
availableReactionsCount: global.availableReactions?.filter((l) => !l.isInactive).length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ManageGroup));
|
)(ManageGroup));
|
||||||
|
|||||||
131
src/components/right/management/ManageReactions.tsx
Normal file
131
src/components/right/management/ManageReactions.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ApiAvailableReaction, ApiChat } from '../../../api/types';
|
||||||
|
|
||||||
|
import { selectChat } from '../../../modules/selectors';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
|
import Spinner from '../../ui/Spinner';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
enabledReactions?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||||
|
availableReactions,
|
||||||
|
enabledReactions,
|
||||||
|
chat,
|
||||||
|
isActive,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { setChatEnabledReactions } = getDispatch();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [isTouched, setIsTouched] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [localEnabledReactions, setLocalEnabledReactions] = useState(enabledReactions);
|
||||||
|
|
||||||
|
useHistoryBack(isActive, onClose);
|
||||||
|
|
||||||
|
const handleSaveReactions = useCallback(() => {
|
||||||
|
if (!chat) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
setChatEnabledReactions({
|
||||||
|
chatId: chat.id,
|
||||||
|
enabledReactions: localEnabledReactions,
|
||||||
|
});
|
||||||
|
}, [chat, localEnabledReactions, setChatEnabledReactions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsTouched(false);
|
||||||
|
setLocalEnabledReactions(enabledReactions || []);
|
||||||
|
}, [enabledReactions]);
|
||||||
|
|
||||||
|
const handleReactionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!chat || !availableReactions) return;
|
||||||
|
|
||||||
|
const { name, checked } = e.currentTarget;
|
||||||
|
const newEnabledReactions = name === 'all' ? (checked ? availableReactions.map((l) => l.reaction) : [])
|
||||||
|
: (!checked
|
||||||
|
? localEnabledReactions.filter((l) => l !== name)
|
||||||
|
: [...localEnabledReactions, name]);
|
||||||
|
|
||||||
|
setLocalEnabledReactions(newEnabledReactions);
|
||||||
|
setIsTouched(true);
|
||||||
|
}, [availableReactions, chat, localEnabledReactions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Management">
|
||||||
|
<div className="custom-scroll">
|
||||||
|
<div className="section">
|
||||||
|
<div className="ListItem no-selection">
|
||||||
|
<Checkbox
|
||||||
|
name="all"
|
||||||
|
checked={!localEnabledReactions || localEnabledReactions.length > 0}
|
||||||
|
label={lang('EnableReactions')}
|
||||||
|
onChange={handleReactionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{availableReactions?.filter((l) => !l.isInactive).map(({ reaction, title }) => (
|
||||||
|
<div className="ListItem no-selection">
|
||||||
|
<Checkbox
|
||||||
|
name={reaction}
|
||||||
|
checked={!localEnabledReactions || localEnabledReactions?.includes(reaction)}
|
||||||
|
disabled={localEnabledReactions?.length === 0}
|
||||||
|
label={(
|
||||||
|
<div className="Reaction">
|
||||||
|
<ReactionStaticEmoji reaction={reaction} />
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
onChange={handleReactionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FloatingActionButton
|
||||||
|
isShown={isTouched}
|
||||||
|
onClick={handleSaveReactions}
|
||||||
|
ariaLabel={lang('Save')}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner color="white" />
|
||||||
|
) : (
|
||||||
|
<i className="icon-check" />
|
||||||
|
)}
|
||||||
|
</FloatingActionButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const chat = selectChat(global, chatId)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabledReactions: chat.fullInfo?.enabledReactions,
|
||||||
|
availableReactions: global.availableReactions,
|
||||||
|
chat,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(ManageReactions));
|
||||||
@ -55,6 +55,16 @@
|
|||||||
.ListItem {
|
.ListItem {
|
||||||
margin: 0 -.75rem;
|
margin: 0 -.75rem;
|
||||||
|
|
||||||
|
.Reaction {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactionStaticEmoji {
|
||||||
|
width: 1.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import ManageGroupRecentActions from './ManageGroupRecentActions';
|
|||||||
import ManageGroupAdminRights from './ManageGroupAdminRights';
|
import ManageGroupAdminRights from './ManageGroupAdminRights';
|
||||||
import ManageGroupMembers from './ManageGroupMembers';
|
import ManageGroupMembers from './ManageGroupMembers';
|
||||||
import ManageGroupUserPermissionsCreate from './ManageGroupUserPermissionsCreate';
|
import ManageGroupUserPermissionsCreate from './ManageGroupUserPermissionsCreate';
|
||||||
|
import ManageReactions from './ManageReactions';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -239,6 +240,15 @@ const Management: FC<OwnProps & StateProps> = ({
|
|||||||
onChatMemberSelect={onChatMemberSelect}
|
onChatMemberSelect={onChatMemberSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case ManagementScreens.Reactions:
|
||||||
|
return (
|
||||||
|
<ManageReactions
|
||||||
|
chatId={chatId}
|
||||||
|
isActive={isActive}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined; // Never reached
|
return undefined; // Never reached
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export const PROFILE_PHOTOS_LIMIT = 40;
|
|||||||
export const PROFILE_SENSITIVE_AREA = 500;
|
export const PROFILE_SENSITIVE_AREA = 500;
|
||||||
export const COMMON_CHATS_LIMIT = 100;
|
export const COMMON_CHATS_LIMIT = 100;
|
||||||
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
||||||
|
export const REACTION_LIST_LIMIT = 100;
|
||||||
|
|
||||||
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
||||||
export const ALL_CHATS_PRELOAD_DISABLED = false;
|
export const ALL_CHATS_PRELOAD_DISABLED = false;
|
||||||
|
|||||||
@ -198,6 +198,10 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
if (!cached.messages.sponsoredByChatId) {
|
if (!cached.messages.sponsoredByChatId) {
|
||||||
cached.messages.sponsoredByChatId = {};
|
cached.messages.sponsoredByChatId = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.activeReactions) {
|
||||||
|
cached.activeReactions = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache() {
|
function updateCache() {
|
||||||
@ -243,6 +247,7 @@ function updateCache() {
|
|||||||
settings: reduceSettings(global),
|
settings: reduceSettings(global),
|
||||||
chatFolders: reduceChatFolders(global),
|
chatFolders: reduceChatFolders(global),
|
||||||
groupCalls: reduceGroupCalls(global),
|
groupCalls: reduceGroupCalls(global),
|
||||||
|
availableReactions: reduceAvailableReactions(global),
|
||||||
};
|
};
|
||||||
|
|
||||||
const json = JSON.stringify(reducedGlobal);
|
const json = JSON.stringify(reducedGlobal);
|
||||||
@ -346,3 +351,8 @@ function reduceGroupCalls(global: GlobalState): GlobalState['groupCalls'] {
|
|||||||
isFallbackConfirmOpen: undefined,
|
isFallbackConfirmOpen: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reduceAvailableReactions(global: GlobalState): GlobalState['availableReactions'] {
|
||||||
|
return global.availableReactions
|
||||||
|
?.map((r) => pick(r, ['reaction', 'staticIcon', 'title', 'isInactive']));
|
||||||
|
}
|
||||||
|
|||||||
@ -184,6 +184,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
twoFaSettings: {},
|
twoFaSettings: {},
|
||||||
|
activeReactions: {},
|
||||||
|
|
||||||
shouldShowContextMenuHint: true,
|
shouldShowContextMenuHint: true,
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import {
|
|||||||
ApiCountryCode,
|
ApiCountryCode,
|
||||||
ApiCountry,
|
ApiCountry,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
|
ApiAvailableReaction,
|
||||||
|
ApiAppConfig,
|
||||||
ApiSponsoredMessage,
|
ApiSponsoredMessage,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import {
|
import {
|
||||||
@ -61,6 +63,23 @@ export interface MessageList {
|
|||||||
type: MessageListType;
|
type: MessageListType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActiveEmojiInteraction {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
messageId?: number;
|
||||||
|
endX?: number;
|
||||||
|
endY?: number;
|
||||||
|
startSize?: number;
|
||||||
|
reaction?: string;
|
||||||
|
animatedEffect?: string;
|
||||||
|
isReversed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActiveReaction {
|
||||||
|
messageId?: number;
|
||||||
|
reaction?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Thread {
|
export interface Thread {
|
||||||
listedIds?: number[];
|
listedIds?: number[];
|
||||||
outlyingIds?: number[];
|
outlyingIds?: number[];
|
||||||
@ -86,6 +105,7 @@ export interface ServiceNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GlobalState = {
|
export type GlobalState = {
|
||||||
|
appConfig?: ApiAppConfig;
|
||||||
isChatInfoShown: boolean;
|
isChatInfoShown: boolean;
|
||||||
isLeftColumnShown: boolean;
|
isLeftColumnShown: boolean;
|
||||||
isPollModalOpen?: boolean;
|
isPollModalOpen?: boolean;
|
||||||
@ -212,6 +232,11 @@ export type GlobalState = {
|
|||||||
messageId: number;
|
messageId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reactorModal?: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
};
|
||||||
|
|
||||||
fileUploads: {
|
fileUploads: {
|
||||||
byMessageLocalId: Record<string, {
|
byMessageLocalId: Record<string, {
|
||||||
progress: number;
|
progress: number;
|
||||||
@ -254,6 +279,7 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
animatedEmojis?: ApiStickerSet;
|
animatedEmojis?: ApiStickerSet;
|
||||||
|
animatedEmojiEffects?: ApiStickerSet;
|
||||||
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
||||||
|
|
||||||
gifs: {
|
gifs: {
|
||||||
@ -305,6 +331,10 @@ export type GlobalState = {
|
|||||||
globalUserIds?: string[];
|
globalUserIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
activeEmojiInteraction?: ActiveEmojiInteraction;
|
||||||
|
activeReactions: Record<number, ActiveReaction>;
|
||||||
|
|
||||||
localTextSearch: {
|
localTextSearch: {
|
||||||
byChatThreadKey: Record<string, {
|
byChatThreadKey: Record<string, {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@ -483,7 +513,8 @@ export type ActionTypes = (
|
|||||||
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
||||||
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||||
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
||||||
'openSeenByModal' | 'closeSeenByModal' |
|
'openSeenByModal' | 'closeSeenByModal' | 'closeReactorListModal' |
|
||||||
|
'openReactorListModal' |
|
||||||
// auth
|
// auth
|
||||||
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
||||||
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
||||||
@ -495,7 +526,7 @@ export type ActionTypes = (
|
|||||||
'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' |
|
||||||
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' | 'setChatEnabledReactions' |
|
||||||
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
|
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
|
||||||
// messages
|
// messages
|
||||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||||
@ -503,9 +534,12 @@ export type ActionTypes = (
|
|||||||
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
||||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' | 'sendReaction' |
|
||||||
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
||||||
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' |
|
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' | 'loadAvailableReactions' |
|
||||||
|
'stopActiveEmojiInteraction' | 'interactWithAnimatedEmoji' | 'loadReactors' | 'setDefaultReaction' |
|
||||||
|
'sendDefaultReaction' | 'sendEmojiInteraction' | 'sendWatchingEmojiInteraction' | 'loadMessageReactions' |
|
||||||
|
'stopActiveReaction' |
|
||||||
// downloads
|
// downloads
|
||||||
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
@ -541,7 +575,7 @@ export type ActionTypes = (
|
|||||||
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
|
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
|
||||||
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
|
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
|
||||||
'loadContentSettings' | 'updateContentSettings' |
|
'loadContentSettings' | 'updateContentSettings' |
|
||||||
'loadCountryList' | 'ensureTimeFormat' |
|
'loadCountryList' | 'ensureTimeFormat' | 'loadAppConfig' |
|
||||||
// stickers & GIFs
|
// stickers & GIFs
|
||||||
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
||||||
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user