Introduce Saved tags (#4244)
This commit is contained in:
parent
4c71117b61
commit
c4736edbb4
@ -7,6 +7,7 @@ import type {
|
||||
ApiReactionCount,
|
||||
ApiReactionEmoji,
|
||||
ApiReactions,
|
||||
ApiSavedReactionTag,
|
||||
} from '../../types';
|
||||
|
||||
import { buildApiDocument } from './messageContent';
|
||||
@ -14,10 +15,11 @@ import { getApiChatIdFromMtpPeer } from './peers';
|
||||
|
||||
export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions {
|
||||
const {
|
||||
recentReactions, results, canSeeList,
|
||||
recentReactions, results, canSeeList, reactionsAsTags,
|
||||
} = reactions;
|
||||
|
||||
return {
|
||||
areTags: reactionsAsTags,
|
||||
canSeeList,
|
||||
results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator),
|
||||
recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean),
|
||||
@ -82,6 +84,18 @@ export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | u
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildApiSavedReactionTag(tag: GramJs.SavedReactionTag): ApiSavedReactionTag | undefined {
|
||||
const { reaction, title, count } = tag;
|
||||
const apiReaction = buildApiReaction(reaction);
|
||||
if (!apiReaction) return undefined;
|
||||
|
||||
return {
|
||||
reaction: apiReaction,
|
||||
title,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction {
|
||||
const {
|
||||
selectAnimation, staticIcon, reaction, title, appearAnimation,
|
||||
|
||||
@ -91,10 +91,7 @@ export {
|
||||
requestCall, getDhConfig, confirmCall, sendSignalingData, acceptCall, discardCall, setCallRating, receivedCall,
|
||||
} from './calls';
|
||||
|
||||
export {
|
||||
getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList, clearRecentReactions,
|
||||
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction, fetchRecentReactions, fetchTopReactions,
|
||||
} from './reactions';
|
||||
export * from './reactions';
|
||||
|
||||
export {
|
||||
fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics,
|
||||
|
||||
@ -15,6 +15,7 @@ import type {
|
||||
ApiOnProgress,
|
||||
ApiPeer,
|
||||
ApiPoll,
|
||||
ApiReaction,
|
||||
ApiReportReason,
|
||||
ApiSendMessageAction,
|
||||
ApiSticker,
|
||||
@ -63,6 +64,7 @@ import {
|
||||
buildInputPeer,
|
||||
buildInputPoll,
|
||||
buildInputPollFromExisting,
|
||||
buildInputReaction,
|
||||
buildInputReplyTo,
|
||||
buildInputReportReason,
|
||||
buildInputStory,
|
||||
@ -1091,10 +1093,11 @@ export async function fetchDiscussionMessage({
|
||||
}
|
||||
|
||||
export async function searchMessagesLocal({
|
||||
chat, isSavedDialog, type, query, threadId, minDate, maxDate, ...pagination
|
||||
chat, isSavedDialog, savedTag, type, query, threadId, minDate, maxDate, ...pagination
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
isSavedDialog?: boolean;
|
||||
savedTag?: ApiReaction;
|
||||
type?: ApiMessageSearchType | ApiGlobalMessageSearchType;
|
||||
query?: string;
|
||||
threadId?: ThreadId;
|
||||
@ -1135,6 +1138,7 @@ export async function searchMessagesLocal({
|
||||
const result = await invokeRequest(new GramJs.messages.Search({
|
||||
peer: isSavedDialog ? new GramJs.InputPeerSelf() : peer,
|
||||
savedPeerId: isSavedDialog ? peer : undefined,
|
||||
savedReaction: savedTag && [buildInputReaction(savedTag)],
|
||||
topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
|
||||
filter,
|
||||
q: query || '',
|
||||
|
||||
@ -11,7 +11,12 @@ import {
|
||||
} from '../../../config';
|
||||
import { split } from '../../../util/iteratees';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/reactions';
|
||||
import {
|
||||
buildApiAvailableReaction,
|
||||
buildApiReaction,
|
||||
buildApiSavedReactionTag,
|
||||
buildMessagePeerReaction,
|
||||
} from '../apiBuilders/reactions';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
||||
import { addEntitiesToLocalDb } from '../helpers';
|
||||
@ -62,7 +67,7 @@ export function sendEmojiInteraction({
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAvailableReactions() {
|
||||
export async function fetchAvailableReactions() {
|
||||
const result = await invokeRequest(new GramJs.messages.GetAvailableReactions({}));
|
||||
|
||||
if (!result || result instanceof GramJs.messages.AvailableReactionsNotModified) {
|
||||
@ -202,3 +207,46 @@ export async function fetchRecentReactions({ hash = '0' }: { hash?: string }) {
|
||||
export function clearRecentReactions() {
|
||||
return invokeRequest(new GramJs.messages.ClearRecentReactions());
|
||||
}
|
||||
|
||||
export async function fetchDefaultTagReactions({ hash = '0' }: { hash?: string }) {
|
||||
const result = await invokeRequest(new GramJs.messages.GetDefaultTagReactions({
|
||||
hash: BigInt(hash),
|
||||
}));
|
||||
|
||||
if (!result || result instanceof GramJs.messages.ReactionsNotModified) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
hash: String(result.hash),
|
||||
reactions: result.reactions.map(buildApiReaction).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchSavedReactionTags({ hash = '0' }: { hash?: string }) {
|
||||
const result = await invokeRequest(new GramJs.messages.GetSavedReactionTags({ hash: BigInt(hash) }));
|
||||
|
||||
if (!result || result instanceof GramJs.messages.SavedReactionTagsNotModified) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
hash: String(result.hash),
|
||||
tags: result.tags.map(buildApiSavedReactionTag).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSavedReactionTag({
|
||||
reaction,
|
||||
title,
|
||||
}: {
|
||||
reaction: ApiReaction;
|
||||
title?: string;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.UpdateSavedReactionTag({
|
||||
reaction: buildInputReaction(reaction),
|
||||
title,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -947,6 +947,8 @@ export function updater(update: Update) {
|
||||
onUpdate({ '@type': 'updateRecentStickers' });
|
||||
} else if (update instanceof GramJs.UpdateRecentReactions) {
|
||||
onUpdate({ '@type': 'updateRecentReactions' });
|
||||
} else if (update instanceof GramJs.UpdateSavedReactionTags) {
|
||||
onUpdate({ '@type': 'updateSavedReactionTags' });
|
||||
} else if (update instanceof GramJs.UpdateMoveStickerSetToTop) {
|
||||
if (!update.masks) {
|
||||
onUpdate({
|
||||
|
||||
@ -543,6 +543,7 @@ export interface ApiMessage {
|
||||
|
||||
export interface ApiReactions {
|
||||
canSeeList?: boolean;
|
||||
areTags?: boolean;
|
||||
results: ApiReactionCount[];
|
||||
recentReactions?: ApiPeerReaction[];
|
||||
}
|
||||
@ -598,6 +599,14 @@ export type ApiReactionCustomEmoji = {
|
||||
|
||||
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
|
||||
|
||||
export type ApiReactionKey = `${string}-${string}`;
|
||||
|
||||
export type ApiSavedReactionTag = {
|
||||
reaction: ApiReaction;
|
||||
title?: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
interface ApiBaseThreadInfo {
|
||||
chatId: string;
|
||||
messagesCount: number;
|
||||
|
||||
@ -706,6 +706,10 @@ export type ApiUpdateGroupInvitePrivacyForbidden = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type ApiUpdateSavedReactionTags = {
|
||||
'@type': 'updateSavedReactionTags';
|
||||
};
|
||||
|
||||
export type ApiUpdate = (
|
||||
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
|
||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||
@ -724,7 +728,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
|
||||
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
|
||||
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
|
||||
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
||||
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
|
||||
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
|
||||
|
||||
1
src/assets/font-icons/tag-add.svg
Normal file
1
src/assets/font-icons/tag-add.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="24.518" fill="none"><g transform="translate(0 -2.999)"><path d="M10.838 3c-2.89 0-4.516-.066-6.025.703a6.452 6.452 0 0 0-2.819 2.82c-.77 1.51-.703 3.135-.703 6.026v2.064a1.29 1.29 0 0 0 1.29 1.291 1.29 1.29 0 0 0 1.29-1.29v-2.065c0-2.89.066-4.156.422-4.854a3.873 3.873 0 0 1 1.691-1.693c.699-.356 1.963-.422 4.854-.422h6.969c1.514 0 2.154.02 2.611.15.461.132.895.349 1.277.64.379.286.777.786 1.686 1.997l2.476 3.305c1 1.333 1.375 1.898 1.477 2.285.113.43.113.882 0 1.313-.102.386-.477.952-1.477 2.285l-2.476 3.304c-.909 1.212-1.307 1.711-1.686 1.998-.382.29-.816.507-1.277.64-.457.13-1.097.15-2.611.15h-4.905a1.29 1.29 0 0 0-1.289 1.289 1.29 1.29 0 0 0 1.29 1.29h4.904c1.514 0 2.387.019 3.32-.247.77-.22 1.49-.581 2.127-1.065.773-.586 1.283-1.297 2.191-2.508l2.477-3.302c1-1.333 1.625-2.1 1.908-3.176a5.17 5.17 0 0 0 0-2.63c-.283-1.076-.909-1.842-1.908-3.175L25.445 6.82c-.908-1.21-1.418-1.921-2.191-2.508a6.447 6.447 0 0 0-2.127-1.064C20.194 2.982 19.321 3 17.807 3Z" style="color:#000;fill:#000;stroke-linecap:round;-inkscape-stroke:none"/><circle cx="19.355" cy="14.613" r="2.581" fill="#000" style="stroke-width:1.29032"/><path d="M5.162 17.193a1.29 1.29 0 0 0-1.29 1.291v7.743a1.29 1.29 0 0 0 1.29 1.289 1.29 1.29 0 0 0 1.29-1.29v-7.742a1.29 1.29 0 0 0-1.29-1.29Z" style="color:#000;fill:#000;stroke-linecap:round;-inkscape-stroke:none"/><path d="M1.291 21.064A1.29 1.29 0 0 0 0 22.355a1.29 1.29 0 0 0 1.291 1.291h7.742a1.29 1.29 0 0 0 1.29-1.29 1.29 1.29 0 0 0-1.29-1.292Z" style="color:#000;fill:#000;stroke-linecap:round;-inkscape-stroke:none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/assets/font-icons/tag-crossed.svg
Normal file
1
src/assets/font-icons/tag-crossed.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="M4.185 5.457a6.45 6.45 0 0 1 .628-.367c.764-.39 1.59-.551 2.53-.628.914-.075 2.042-.075 3.44-.075h7.201c1.343 0 2.271 0 3.143.248.769.22 1.49.58 2.128 1.064.722.548 1.278 1.291 2.083 2.365l.107.142 2.478 3.304.143.191c.86 1.144 1.506 2.004 1.764 2.985a5.162 5.162 0 0 1 0 2.628c-.258.981-.905 1.841-1.764 2.985l-.143.191-2.478 3.304-.107.142c-.444.593-.813 1.085-1.175 1.499l1.266 1.266a1.29 1.29 0 0 1-1.825 1.824L.378 5.3a1.29 1.29 0 0 1 1.825-1.825zm1.89 1.89 16.258 16.258c.252-.303.573-.727 1.048-1.36l2.477-3.303c1.068-1.425 1.364-1.857 1.476-2.285.114-.43.114-.883 0-1.314-.112-.428-.408-.86-1.476-2.285l-2.477-3.303c-.954-1.271-1.288-1.698-1.686-2a3.87 3.87 0 0 0-1.277-.638c-.48-.138-1.023-.15-2.612-.15H10.84c-1.467 0-2.49.002-3.285.067-.714.058-1.15.163-1.48.312Zm4.764 20.266h-.056c-1.398 0-2.526 0-3.44-.075-.94-.077-1.766-.239-2.53-.628a6.451 6.451 0 0 1-2.82-2.82c-.389-.764-.551-1.59-.628-2.53-.075-.914-.075-2.042-.075-3.44V13.88c0-1.398 0-2.526.075-3.44.058-.707.164-1.35.38-1.951l2.19 2.19c-.063.792-.064 1.807-.064 3.256v4.13c0 1.466.001 2.488.066 3.284.064.781.183 1.23.356 1.57a3.87 3.87 0 0 0 1.692 1.691c.34.174.788.292 1.569.356.796.065 1.818.066 3.285.066H18.287l2.431 2.431c-.759.15-1.598.15-2.734.15h-.178ZM21.936 16a2.58 2.58 0 1 1-5.162 0 2.58 2.58 0 0 1 5.162 0z" style="stroke-width:1.29032"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/font-icons/tag-filter.svg
Normal file
1
src/assets/font-icons/tag-filter.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path d="M17.426 4.004a1.335 1.335 0 0 0-1.334 1.334 1.335 1.335 0 0 0 1.334 1.336h.734c1.566 0 2.229.02 2.701.154.478.136.925.36 1.32.66.392.297.805.816 1.745 2.069l2.562 3.416c1.034 1.378 1.422 1.963 1.528 2.363.117.446.117.914 0 1.36-.106.4-.494.984-1.528 2.363l-2.562 3.418c-.94 1.252-1.353 1.77-1.744 2.068-.396.3-.843.524-1.32.66-.473.135-1.136.154-2.702.154h-7.207c-2.99 0-4.3-.07-5.021-.437a4.004 4.004 0 0 1-1.75-1.75c-.368-.722-.436-2.03-.436-5.02v-1.468A1.335 1.335 0 0 0 2.41 15.35a1.335 1.335 0 0 0-1.334 1.334v1.468c0 2.99-.069 4.67.727 6.233A6.677 6.677 0 0 0 4.72 27.3c1.562.796 3.242.726 6.232.726h7.207c1.566 0 2.469.02 3.434-.256a6.675 6.675 0 0 0 2.2-1.101c.8-.607 1.327-1.339 2.267-2.592l2.562-3.418c1.034-1.378 1.68-2.171 1.973-3.285h.002a5.345 5.345 0 0 0 0-2.719h-.002c-.293-1.113-.94-1.906-1.973-3.285l-2.562-3.416c-.94-1.253-1.467-1.987-2.266-2.594a6.676 6.676 0 0 0-2.201-1.101c-.965-.276-1.868-.256-3.434-.256Z" style="color:#000;fill:#000;stroke-linecap:round;-inkscape-stroke:none"/><circle cx="19.762" cy="16.016" r="2.669" fill="#000" style="stroke-width:1.33467"/><path d="M3.77 0C2.488 0 1.42.824.994 1.854c-.426 1.03-.253 2.367.652 3.273l2.002 2.002a.333.333 0 0 1 .098.236v4.951c0 1.135.645 2.179 1.66 2.686l2.002 1c1.947.973 4.346-.509 4.346-2.686v-5.95c0-.09.035-.174.098-.237l2.002-2.002c.905-.906 1.076-2.244.65-3.273C14.077.824 13.012 0 11.73 0Zm0 2.67h7.96c.206 0 .253.073.307.205.055.132.073.218-.072.363L9.963 5.24a3.008 3.008 0 0 0-.879 2.125v5.951c0 .304-.21.435-.482.3L6.6 12.612a.326.326 0 0 1-.186-.297v-4.95c0-.797-.316-1.563-.879-2.126L3.533 3.238c-.145-.145-.127-.231-.072-.363.054-.132.103-.205.309-.205Z" style="color:#000;fill:#000;stroke-linecap:round;-inkscape-stroke:none"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
src/assets/font-icons/tag-name.svg
Normal file
1
src/assets/font-icons/tag-name.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path d="M10.977 3c-3.02 0-4.718-.07-6.295.734a6.748 6.748 0 0 0-2.948 2.948C.93 8.259 1 9.956 1 12.977v4.314c0 3.02-.07 4.72.734 6.297a6.744 6.744 0 0 0 2.948 2.945c1.577.804 3.274.737 6.295.737h7.28c1.583 0 2.495.018 3.47-.26a6.745 6.745 0 0 0 2.224-1.112c.808-.612 1.34-1.355 2.29-2.62l2.587-3.452c1.044-1.392 1.698-2.193 1.994-3.318.237-.9.237-1.847 0-2.746-.296-1.125-.95-1.926-1.994-3.319L26.24 6.99c-.949-1.265-1.481-2.006-2.289-2.619a6.745 6.745 0 0 0-2.224-1.111C20.752 2.98 19.84 3 18.257 3Zm0 2.697h7.28c1.583 0 2.252.018 2.73.155.482.137.934.364 1.333.668.396.3.813.824 1.762 2.09l2.59 3.45c1.044 1.393 1.434 1.983 1.54 2.387.12.45.12.923 0 1.373-.106.405-.496.997-1.54 2.389l-2.59 3.451c-.95 1.266-1.366 1.79-1.762 2.09-.4.303-.851.528-1.334.666-.477.136-1.146.156-2.728.156h-7.281c-3.02 0-4.341-.07-5.07-.441a4.044 4.044 0 0 1-1.768-1.768c-.372-.73-.442-2.052-.442-5.072v-4.314c0-3.02.07-4.341.442-5.07a4.044 4.044 0 0 1 1.767-1.768c.73-.372 2.05-.442 5.07-.442Z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="translate(0 .865)"/><path d="M14.482 7.719a1.348 1.348 0 0 0-1.234.808L8.529 19.313a1.348 1.348 0 0 0 .694 1.775A1.348 1.348 0 0 0 11 20.395l3.484-7.963 3.483 7.963a1.348 1.348 0 0 0 1.775.693 1.348 1.348 0 0 0 .695-1.776L15.72 8.527a1.348 1.348 0 0 0-1.237-.808Z" style="color:#000;fill:#000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none" transform="translate(0 .865)"/><path d="M11.111 15.135v2.697h6.743v-2.697z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="translate(0 .865)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/assets/font-icons/tag.svg
Normal file
1
src/assets/font-icons/tag.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path stroke="#000" stroke-width="2.696" d="M25.161 23.334c-.95 1.266-1.424 1.899-2.025 2.355a5.393 5.393 0 0 1-1.78.89c-.725.207-1.516.207-3.098.207h-7.28c-3.02 0-4.531 0-5.685-.588a5.393 5.393 0 0 1-2.357-2.357c-.587-1.153-.587-2.663-.587-5.684v-4.314c0-3.02 0-4.53.587-5.684a5.393 5.393 0 0 1 2.357-2.357c1.154-.588 2.664-.588 5.684-.588h7.281c1.582 0 2.373 0 3.099.207a5.393 5.393 0 0 1 1.779.89c.601.456 1.076 1.089 2.025 2.355l2.589 3.451c1.044 1.393 1.566 2.089 1.767 2.853a4.047 4.047 0 0 1 0 2.06c-.2.764-.723 1.46-1.767 2.853z"/><circle cx="19.876" cy="16" r="2.696" fill="#000" style="stroke-width:1.34825"/></svg>
|
||||
|
After Width: | Height: | Size: 700 B |
3
src/assets/premium/PremiumTags.svg
Normal file
3
src/assets/premium/PremiumTags.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9239 6.98158L23.2126 9.17218C22.7018 9.0588 22.1754 9.00001 21.6415 9.00001H12.4019C9.36436 9.00001 6.90192 11.4624 6.90192 14.5V20.2677C6.26941 19.7553 5.81176 19.0213 5.65931 18.1567L4.44377 11.263C4.10811 9.3594 5.3792 7.5441 7.28283 7.20844L16.3821 5.60399C18.029 5.31359 19.7158 5.82521 20.9239 6.98158ZM8.90198 21.0438V21.5C8.90198 23.433 10.469 25 12.402 25H21.6416C23.314 25 24.8863 24.2033 25.8752 22.8547L27.9175 20.0698C28.8209 18.8378 28.8209 17.1622 27.9175 15.9302L25.8752 13.1453C24.8863 11.7967 23.314 11 21.6416 11H12.402C12.1471 11 11.8986 11.0272 11.6593 11.079C10.083 11.4195 8.90192 12.8218 8.90192 14.5V21.0437C8.90194 21.0437 8.90196 21.0438 8.90198 21.0438ZM25.0021 18C25.0021 18.9665 24.2186 19.75 23.2521 19.75C22.2856 19.75 21.5021 18.9665 21.5021 18C21.5021 17.0335 22.2856 16.25 23.2521 16.25C24.2186 16.25 25.0021 17.0335 25.0021 18Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -52,7 +52,7 @@ export { default as StickerSetModal } from '../components/common/StickerSetModal
|
||||
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
|
||||
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
||||
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
||||
export { default as ReactionPicker } from '../components/middle/message/ReactionPicker';
|
||||
export { default as ReactionPicker } from '../components/middle/message/reactions/ReactionPicker';
|
||||
|
||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||
export { default as PollModal } from '../components/middle/composer/PollModal';
|
||||
|
||||
@ -240,10 +240,11 @@
|
||||
--color-interactive-element-hover: rgba(255, 255, 255, 0.1);
|
||||
--color-text: #fff;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
top: -3.875rem;
|
||||
transform: translateX(-50%);
|
||||
top: -0.75rem;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@ -259,6 +260,10 @@
|
||||
transform: scaleY(-1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ReactionSelector__hint {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -153,7 +153,7 @@ import SendAsMenu from '../middle/composer/SendAsMenu.async';
|
||||
import StickerTooltip from '../middle/composer/StickerTooltip.async';
|
||||
import SymbolMenuButton from '../middle/composer/SymbolMenuButton';
|
||||
import WebPagePreview from '../middle/composer/WebPagePreview';
|
||||
import ReactionSelector from '../middle/message/ReactionSelector';
|
||||
import ReactionSelector from '../middle/message/reactions/ReactionSelector';
|
||||
import Button from '../ui/Button';
|
||||
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
|
||||
import Spinner from '../ui/Spinner';
|
||||
@ -1521,6 +1521,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isReady={isReady}
|
||||
canBuyPremium={canBuyPremium}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
isInSavedMessages={isChatWithSelf}
|
||||
isInStoryViewer={isInStoryViewer}
|
||||
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
||||
onShowMore={handleReactionPickerOpen}
|
||||
className={reactionSelectorTransitonClassNames}
|
||||
@ -2001,8 +2003,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isInScheduledList = messageListType === 'scheduled';
|
||||
|
||||
return {
|
||||
availableReactions: type === 'story' ? global.availableReactions : undefined,
|
||||
topReactions: type === 'story' ? global.topReactions : undefined,
|
||||
availableReactions: type === 'story' ? global.reactions.availableReactions : undefined,
|
||||
topReactions: type === 'story' ? global.reactions.topReactions : undefined,
|
||||
isOnActiveTab: !tabState.isBlurred,
|
||||
editingMessage: selectEditingMessage(global, chatId, threadId, messageListType),
|
||||
draft,
|
||||
|
||||
@ -30,7 +30,6 @@ import {
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { pickTruthy, unique } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
|
||||
@ -76,6 +75,7 @@ type StateProps = {
|
||||
recentStatusEmojis?: ApiSticker[];
|
||||
topReactions?: ApiReaction[];
|
||||
recentReactions?: ApiReaction[];
|
||||
defaultTagReactions?: ApiReaction[];
|
||||
stickerSetsById: Record<string, ApiStickerSet>;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
addedCustomEmojiIds?: string[];
|
||||
@ -126,6 +126,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
withDefaultTopicIcons,
|
||||
defaultTopicIconsId,
|
||||
defaultStatusIconsId,
|
||||
defaultTagReactions,
|
||||
onCustomEmojiSelect,
|
||||
onReactionSelect,
|
||||
onContextMenuOpen,
|
||||
@ -168,13 +169,22 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
const areAddedLoaded = Boolean(addedCustomEmojiIds);
|
||||
|
||||
const allSets = useMemo(() => {
|
||||
if (!addedCustomEmojiIds) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
const defaultSets: StickerSetOrReactionsSetOrRecent[] = [];
|
||||
|
||||
if (isReactionPicker) {
|
||||
if (isReactionPicker && isSavedMessages) {
|
||||
if (defaultTagReactions?.length) {
|
||||
defaultSets.push({
|
||||
id: TOP_SYMBOL_SET_ID,
|
||||
accessHash: '',
|
||||
title: lang('PremiumPreviewTags'),
|
||||
reactions: defaultTagReactions,
|
||||
count: defaultTagReactions.length,
|
||||
isEmoji: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isReactionPicker && !isSavedMessages) {
|
||||
const topReactionsSlice = topReactions?.slice(0, TOP_REACTIONS_COUNT) || [];
|
||||
if (topReactionsSlice?.length) {
|
||||
defaultSets.push({
|
||||
@ -240,7 +250,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
const setIdsToDisplay = unique(addedCustomEmojiIds.concat(customEmojiFeaturedIds || []));
|
||||
const setIdsToDisplay = unique((addedCustomEmojiIds || []).concat(customEmojiFeaturedIds || []));
|
||||
|
||||
const setsToDisplay = Object.values(pickTruthy(stickerSetsById, setIdsToDisplay));
|
||||
|
||||
@ -251,7 +261,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
}, [
|
||||
addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||
customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions,
|
||||
defaultStatusIconsId, defaultTopicIconsId,
|
||||
defaultStatusIconsId, defaultTopicIconsId, isSavedMessages, defaultTagReactions,
|
||||
]);
|
||||
|
||||
const noPopulatedSets = useMemo(() => (
|
||||
@ -447,8 +457,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
},
|
||||
},
|
||||
recentCustomEmojis: recentCustomEmojiIds,
|
||||
recentReactions,
|
||||
topReactions,
|
||||
reactions: {
|
||||
availableReactions,
|
||||
recentReactions,
|
||||
topReactions,
|
||||
defaultTags,
|
||||
},
|
||||
} = global;
|
||||
|
||||
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
||||
@ -467,7 +481,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
defaultStatusIconsId: global.defaultStatusIconsId,
|
||||
topReactions: isReactionPicker ? topReactions : undefined,
|
||||
recentReactions: isReactionPicker ? recentReactions : undefined,
|
||||
availableReactions: isReactionPicker ? global.availableReactions : undefined,
|
||||
availableReactions: isReactionPicker ? availableReactions : undefined,
|
||||
defaultTagReactions: isReactionPicker ? defaultTags : undefined,
|
||||
};
|
||||
},
|
||||
)(CustomEmojiPicker));
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
RECENT_SYMBOL_SET_ID,
|
||||
STICKER_SIZE_PICKER,
|
||||
} from '../../config';
|
||||
import { getReactionUniqueKey } from '../../global/helpers';
|
||||
import { getReactionKey } from '../../global/helpers';
|
||||
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
@ -319,7 +319,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
</Button>
|
||||
)}
|
||||
{shouldRender && stickerSet.reactions?.map((reaction) => {
|
||||
const reactionId = getReactionUniqueKey(reaction);
|
||||
const reactionId = getReactionKey(reaction);
|
||||
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||
|
||||
return (
|
||||
|
||||
@ -209,14 +209,14 @@ const ReactionAnimatedEmoji = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { containerId }) => {
|
||||
const { availableReactions, genericEmojiEffects } = global;
|
||||
const { genericEmojiEffects, reactions } = global;
|
||||
const { activeReactions } = selectTabState(global);
|
||||
|
||||
const withEffects = selectPerformanceSettingsValue(global, 'reactionEffects');
|
||||
|
||||
return {
|
||||
activeReactions: activeReactions?.[containerId],
|
||||
availableReactions,
|
||||
availableReactions: reactions.availableReactions,
|
||||
genericEffects: genericEmojiEffects,
|
||||
withEffects,
|
||||
};
|
||||
|
||||
@ -65,10 +65,10 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global) => {
|
||||
const { availableReactions, config } = global;
|
||||
const { config, reactions } = global;
|
||||
|
||||
return {
|
||||
availableReactions,
|
||||
availableReactions: reactions.availableReactions,
|
||||
selectedReaction: config?.defaultReaction,
|
||||
};
|
||||
},
|
||||
|
||||
@ -175,7 +175,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
customEmojiSetIds: global.customEmojis.added.setIds,
|
||||
stickerSetsById: global.stickers.setsById,
|
||||
defaultReaction: global.config?.defaultReaction,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
};
|
||||
},
|
||||
|
||||
@ -72,7 +72,7 @@ import UnreadCount from '../common/UnreadCounter';
|
||||
import LeftColumn from '../left/LeftColumn';
|
||||
import MediaViewer from '../mediaViewer/MediaViewer.async';
|
||||
import AudioPlayer from '../middle/AudioPlayer';
|
||||
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
||||
import ReactionPicker from '../middle/message/reactions/ReactionPicker.async';
|
||||
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
|
||||
import MiddleColumn from '../middle/MiddleColumn';
|
||||
import AttachBotInstallModal from '../modals/attachBotInstall/AttachBotInstallModal.async';
|
||||
@ -265,11 +265,13 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
updatePageTitle,
|
||||
loadTopReactions,
|
||||
loadRecentReactions,
|
||||
loadDefaultTagReactions,
|
||||
loadFeaturedEmojiStickers,
|
||||
setIsElectronUpdateAvailable,
|
||||
loadPremiumSetStickers,
|
||||
loadAuthorizations,
|
||||
loadPeerColors,
|
||||
loadSavedReactionTags,
|
||||
} = getActions();
|
||||
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
@ -343,8 +345,10 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
checkAppVersion();
|
||||
loadTopReactions();
|
||||
loadRecentReactions();
|
||||
loadDefaultTagReactions();
|
||||
loadFeaturedEmojiStickers();
|
||||
loadAuthorizations();
|
||||
loadSavedReactionTags();
|
||||
}
|
||||
}, [isMasterTab, isSynced]);
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
||||
emoji_status: 'PremiumPreviewEmojiStatus',
|
||||
translations: 'PremiumPreviewTranslations',
|
||||
stories: 'PremiumPreviewStories',
|
||||
saved_tags: 'PremiumPreviewTags2',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
@ -56,6 +57,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
emoji_status: 'PremiumPreviewEmojiStatusDescription',
|
||||
translations: 'PremiumPreviewTranslationsDescription',
|
||||
stories: 'PremiumPreviewStoriesDescription',
|
||||
saved_tags: 'PremiumPreviewTagsDescription2',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_SECTIONS = [
|
||||
@ -73,6 +75,7 @@ export const PREMIUM_FEATURE_SECTIONS = [
|
||||
'animated_userpics',
|
||||
'emoji_status',
|
||||
'translations',
|
||||
'saved_tags',
|
||||
];
|
||||
|
||||
const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||
@ -84,6 +87,7 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||
'animated_userpics',
|
||||
'emoji_status',
|
||||
'translations',
|
||||
'saved_tags',
|
||||
];
|
||||
|
||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType,
|
||||
|
||||
@ -51,6 +51,7 @@ import PremiumReactions from '../../../assets/premium/PremiumReactions.svg';
|
||||
import PremiumSpeed from '../../../assets/premium/PremiumSpeed.svg';
|
||||
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
|
||||
import PremiumStickers from '../../../assets/premium/PremiumStickers.svg';
|
||||
import PremiumTags from '../../../assets/premium/PremiumTags.svg';
|
||||
import PremiumTranslate from '../../../assets/premium/PremiumTranslate.svg';
|
||||
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
|
||||
import PremiumVoice from '../../../assets/premium/PremiumVoice.svg';
|
||||
@ -73,6 +74,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
||||
animated_userpics: PremiumVideo,
|
||||
emoji_status: PremiumStatus,
|
||||
translations: PremiumTranslate,
|
||||
saved_tags: PremiumTags,
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
|
||||
@ -17,6 +17,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
#MobileSearch > .tags-subheader {
|
||||
--color-reaction: var(--color-background-secondary);
|
||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
||||
--text-color-reaction: var(--color-text-secondary);
|
||||
--color-reaction-chosen: var(--color-primary);
|
||||
--text-color-reaction-chosen: #FFFFFF;
|
||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
||||
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
left: 0;
|
||||
z-index: var(--z-mobile-search);
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
background: var(--color-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding-left: max(0.25rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
#MobileSearch > .footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@ -49,7 +73,7 @@
|
||||
}
|
||||
|
||||
#MobileSearch:not(.active) {
|
||||
.header, .footer {
|
||||
.header, .tags-subheader, .footer {
|
||||
// `display: none` will prevent synchronous focus on iOS
|
||||
transform: translateX(-999rem);
|
||||
}
|
||||
|
||||
@ -1,28 +1,36 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect,
|
||||
useMemo,
|
||||
useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat } from '../../api/types';
|
||||
import type {
|
||||
ApiChat, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
||||
} from '../../api/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
selectCurrentChat,
|
||||
selectChat,
|
||||
selectCurrentMessageList,
|
||||
selectCurrentTextSearch,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { getDayStartAt } from '../../util/dateFormat';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import { IS_IOS } from '../../util/windowEnvironment';
|
||||
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import SearchInput from '../ui/SearchInput';
|
||||
import SavedTagButton from './message/reactions/SavedTagButton';
|
||||
|
||||
import './MobileSearch.scss';
|
||||
|
||||
@ -35,9 +43,12 @@ type StateProps = {
|
||||
chat?: ApiChat;
|
||||
threadId?: ThreadId;
|
||||
query?: string;
|
||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
searchTag?: ApiReaction;
|
||||
totalCount?: number;
|
||||
foundIds?: number[];
|
||||
isHistoryCalendarOpen?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
||||
@ -47,22 +58,33 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
chat,
|
||||
threadId,
|
||||
query,
|
||||
savedTags,
|
||||
searchTag,
|
||||
totalCount,
|
||||
foundIds,
|
||||
isHistoryCalendarOpen,
|
||||
isCurrentUserPremium,
|
||||
}) => {
|
||||
const {
|
||||
setLocalTextSearchQuery,
|
||||
setLocalTextSearchTag,
|
||||
searchTextMessagesLocal,
|
||||
focusMessage,
|
||||
closeLocalTextSearch,
|
||||
openHistoryCalendar,
|
||||
openPremiumModal,
|
||||
loadSavedReactionTags,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tagsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
|
||||
const hasQueryData = Boolean(query || searchTag);
|
||||
|
||||
// Fix for iOS keyboard
|
||||
useEffect(() => {
|
||||
const { visualViewport } = window as any;
|
||||
@ -127,14 +149,41 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
searchInput.blur();
|
||||
}, [isHistoryCalendarOpen]);
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!savedTags) return undefined;
|
||||
return Object.values(savedTags);
|
||||
}, [savedTags]);
|
||||
|
||||
const hasTags = Boolean(tags?.length);
|
||||
const areTagsDisabled = hasTags && !isCurrentUserPremium;
|
||||
|
||||
useHorizontalScroll(tagsRef, !hasTags);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) loadSavedReactionTags();
|
||||
}, [hasTags, isActive]);
|
||||
|
||||
const handleMessageSearchQueryChange = useLastCallback((newQuery: string) => {
|
||||
setLocalTextSearchQuery({ query: newQuery });
|
||||
|
||||
if (newQuery.length) {
|
||||
if (hasQueryData) {
|
||||
runDebouncedForSearch(searchTextMessagesLocal);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTagClick = useLastCallback((tag: ApiReaction) => {
|
||||
if (areTagsDisabled) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalTextSearchTag({ tag });
|
||||
|
||||
runDebouncedForSearch(searchTextMessagesLocal);
|
||||
});
|
||||
|
||||
const handleUp = useLastCallback(() => {
|
||||
if (chat && foundIds) {
|
||||
const newFocusIndex = focusedIndex + 1;
|
||||
@ -172,9 +221,28 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
onChange={handleMessageSearchQueryChange}
|
||||
/>
|
||||
</div>
|
||||
{hasTags && (
|
||||
<div
|
||||
ref={tagsRef}
|
||||
className="tags-subheader custom-scroll-x no-scrollbar"
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<SavedTagButton
|
||||
containerId="mobile-search"
|
||||
key={getReactionKey(tag.reaction)}
|
||||
reaction={tag.reaction}
|
||||
tag={tag}
|
||||
withCount
|
||||
isDisabled={areTagsDisabled}
|
||||
isChosen={isSameReaction(tag.reaction, searchTag)}
|
||||
onClick={handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="footer">
|
||||
<div className="counter">
|
||||
{query ? (
|
||||
{hasQueryData ? (
|
||||
foundIds?.length ? (
|
||||
`${focusedIndex + 1} of ${totalCount}`
|
||||
) : foundIds && !foundIds.length ? (
|
||||
@ -220,15 +288,25 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const chat = selectCurrentChat(global);
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
if (!currentMessageList) {
|
||||
return {};
|
||||
}
|
||||
const { chatId, threadId } = currentMessageList;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { query, results } = selectCurrentTextSearch(global) || {};
|
||||
const { threadId } = selectCurrentMessageList(global) || {};
|
||||
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
|
||||
const { totalCount, foundIds } = results || {};
|
||||
|
||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||
|
||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
||||
|
||||
return {
|
||||
chat,
|
||||
query,
|
||||
@ -236,6 +314,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
threadId,
|
||||
foundIds,
|
||||
isHistoryCalendarOpen: Boolean(selectTabState(global).historyCalendarSelectedAt),
|
||||
savedTags,
|
||||
searchTag: savedTag,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
};
|
||||
},
|
||||
)(MobileSearchFooter));
|
||||
|
||||
@ -8,7 +8,7 @@ import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
import type { ApiAvailableReaction, ApiMessage, ApiReaction } from '../../api/types';
|
||||
import { LoadMoreDirection } from '../../types';
|
||||
|
||||
import { getReactionUniqueKey, isSameReaction } from '../../global/helpers';
|
||||
import { getReactionKey, isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
selectChatMessage,
|
||||
selectTabState,
|
||||
@ -162,7 +162,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
.find((reactionsCount) => isSameReaction(reactionsCount.reaction, reaction))?.count;
|
||||
return (
|
||||
<Button
|
||||
key={getReactionUniqueKey(reaction)}
|
||||
key={getReactionKey(reaction)}
|
||||
className={buildClassName(isSameReaction(chosenTab, reaction) && 'chosen')}
|
||||
size="tiny"
|
||||
ripple
|
||||
@ -201,7 +201,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
items.push(
|
||||
<ListItem
|
||||
key={`${peerId}-${getReactionUniqueKey(r.reaction)}`}
|
||||
key={`${peerId}-${getReactionKey(r.reaction)}`}
|
||||
className="chat-item-clickable reactors-list-item"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleClick(peerId)}
|
||||
@ -271,7 +271,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
reactions: message?.reactions,
|
||||
reactors: message?.reactors,
|
||||
seenByDates: message?.seenByDates,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
};
|
||||
},
|
||||
)(ReactorListModal));
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectCurrentMessageList,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsMessageProtected,
|
||||
selectIsMessageUnread,
|
||||
@ -78,6 +79,7 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
topReactions?: ApiReaction[];
|
||||
defaultTagReactions?: ApiReaction[];
|
||||
customEmojiSetsInfo?: ApiStickerSetInfo[];
|
||||
customEmojiSets?: ApiStickerSet[];
|
||||
noOptions?: boolean;
|
||||
@ -119,6 +121,7 @@ type StateProps = {
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
isReactionPickerOpen?: boolean;
|
||||
messageLink?: string;
|
||||
isInSavedMessages?: boolean;
|
||||
};
|
||||
|
||||
const selection = window.getSelection();
|
||||
@ -126,6 +129,7 @@ const selection = window.getSelection();
|
||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
availableReactions,
|
||||
topReactions,
|
||||
defaultTagReactions,
|
||||
isOpen,
|
||||
messageListType,
|
||||
message,
|
||||
@ -175,6 +179,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canSelectLanguage,
|
||||
isReactionPickerOpen,
|
||||
messageLink,
|
||||
isInSavedMessages,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
}) => {
|
||||
@ -206,6 +211,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
showOriginalMessage,
|
||||
openChatLanguageModal,
|
||||
openMessageReactionPicker,
|
||||
openPremiumModal,
|
||||
loadOutboxReadDate,
|
||||
} = getActions();
|
||||
|
||||
@ -480,9 +486,15 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleToggleReaction = useLastCallback((reaction: ApiReaction) => {
|
||||
toggleReaction({
|
||||
chatId: message.chatId, messageId: message.id, reaction, shouldAddToRecent: true,
|
||||
});
|
||||
if (isInSavedMessages && !isCurrentUserPremium) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
} else {
|
||||
toggleReaction({
|
||||
chatId: message.chatId, messageId: message.id, reaction, shouldAddToRecent: true,
|
||||
});
|
||||
}
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
@ -531,6 +543,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
isReactionPickerOpen={isReactionPickerOpen}
|
||||
availableReactions={availableReactions}
|
||||
topReactions={topReactions}
|
||||
defaultTagReactions={defaultTagReactions}
|
||||
message={message}
|
||||
isPrivate={isPrivate}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
@ -573,6 +586,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
customEmojiSets={customEmojiSets}
|
||||
isDownloading={isDownloading}
|
||||
seenByRecentPeers={seenByRecentPeers}
|
||||
isInSavedMessages={isInSavedMessages}
|
||||
noReplies={noReplies}
|
||||
onOpenThread={handleOpenThread}
|
||||
onReply={handleReply}
|
||||
@ -636,6 +650,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, messageListType, detectedLanguage }): StateProps => {
|
||||
const { threadId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
const { defaultTags, topReactions, availableReactions } = global.reactions;
|
||||
|
||||
const activeDownloads = selectActiveDownloads(global, message.chatId);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const {
|
||||
@ -713,9 +730,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId);
|
||||
const messageLink = selectMessageLink(global, message.chatId, threadId, message.id);
|
||||
|
||||
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
|
||||
|
||||
return {
|
||||
availableReactions: global.availableReactions,
|
||||
topReactions: global.topReactions,
|
||||
availableReactions,
|
||||
topReactions,
|
||||
defaultTagReactions: defaultTags,
|
||||
noOptions,
|
||||
canSendNow: isScheduled,
|
||||
canReschedule: isScheduled,
|
||||
@ -758,6 +778,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||
messageLink,
|
||||
isInSavedMessages,
|
||||
};
|
||||
},
|
||||
)(ContextMenuContainer));
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
--hover-color-reaction: var(--color-message-reaction-hover);
|
||||
--text-color-reaction: var(--accent-color);
|
||||
--color-reaction-chosen: var(--accent-color);
|
||||
--hover-color-reaction-chosen: var(--color-message-reaction-chosen-hover);
|
||||
--text-color-reaction-chosen: #FFFFFF;
|
||||
--hover-color-reaction-chosen: var(--color-message-reaction-chosen-hover);
|
||||
--active-color: var(--color-reply-active);
|
||||
--max-width: 29rem;
|
||||
--accent-color: var(--color-primary);
|
||||
@ -30,6 +30,15 @@
|
||||
--border-bottom-left-radius: var(--border-radius-messages);
|
||||
--border-bottom-right-radius: var(--border-radius-messages);
|
||||
|
||||
.theme-dark & {
|
||||
--color-reaction: rgb(255, 255, 255, 0.1);
|
||||
--hover-color-reaction: rgb(255, 255, 255, 0.2);
|
||||
--text-color-reaction: var(--color-text);
|
||||
|
||||
--color-reaction-chosen: #3390ec;
|
||||
--hover-color-reaction-chosen: #4096ec;
|
||||
}
|
||||
|
||||
@media (min-width: 1921px) {
|
||||
--max-width: calc(30vw - 1rem);
|
||||
}
|
||||
@ -184,8 +193,8 @@
|
||||
--hover-color-reaction: var(--color-message-reaction-hover-own);
|
||||
--text-color-reaction: var(--accent-color);
|
||||
--color-reaction-chosen: var(--accent-color);
|
||||
--hover-color-reaction-chosen: var(--color-message-reaction-chosen-hover-own);
|
||||
--text-color-reaction-chosen: var(--color-background);
|
||||
--hover-color-reaction-chosen: var(--color-message-reaction-chosen-hover-own);
|
||||
--active-color: var(--color-reply-own-active);
|
||||
--max-width: 30rem;
|
||||
--accent-color: var(--color-accent-own);
|
||||
@ -199,6 +208,16 @@
|
||||
--color-voice-transcribe: var(--color-voice-transcribe-button-own);
|
||||
--thumbs-background: var(--color-background-own);
|
||||
|
||||
.theme-dark & {
|
||||
--color-reaction: rgb(255, 255, 255, 0.1);
|
||||
--hover-color-reaction: rgb(255, 255, 255, 0.2);
|
||||
--text-color-reaction: var(--color-text);
|
||||
|
||||
--color-reaction-chosen: rgb(255, 255, 255, 0.75);
|
||||
--hover-color-reaction-chosen: rgb(255, 255, 255, 0.85);
|
||||
--text-color-reaction-chosen: rgb(62, 62, 62);
|
||||
}
|
||||
|
||||
@media (min-width: 1921px) {
|
||||
--max-width: 30vw;
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ import type {
|
||||
ApiMessageOutgoingStatus,
|
||||
ApiPeer,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
ApiSavedReactionTag,
|
||||
ApiThreadInfo,
|
||||
ApiTopic,
|
||||
ApiTypeStory,
|
||||
@ -165,7 +167,7 @@ import MessageMeta from './MessageMeta';
|
||||
import MessagePhoneCall from './MessagePhoneCall';
|
||||
import Photo from './Photo';
|
||||
import Poll from './Poll';
|
||||
import Reactions from './Reactions';
|
||||
import Reactions from './reactions/Reactions';
|
||||
import RoundVideo from './RoundVideo';
|
||||
import Sticker from './Sticker';
|
||||
import Story from './Story';
|
||||
@ -277,6 +279,7 @@ type StateProps = {
|
||||
isConnected: boolean;
|
||||
isLoadingComments?: boolean;
|
||||
shouldWarnAboutSvg?: boolean;
|
||||
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
};
|
||||
|
||||
type MetaPosition =
|
||||
@ -391,6 +394,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isConnected,
|
||||
getIsMessageListReady,
|
||||
shouldWarnAboutSvg,
|
||||
tags,
|
||||
onPinnedIntersectionChange,
|
||||
}) => {
|
||||
const {
|
||||
@ -946,6 +950,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
metaChildren={meta}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
noRecentReactors={isChannel}
|
||||
tags={tags}
|
||||
isCurrentUserPremium={isPremium}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1428,9 +1434,11 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
<Reactions
|
||||
message={reactionMessage!}
|
||||
isOutside
|
||||
isCurrentUserPremium={isPremium}
|
||||
maxWidth={reactionsMaxWidth}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
noRecentReactors={isChannel}
|
||||
tags={tags}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -1611,7 +1619,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb,
|
||||
shouldLoopStickers: selectShouldLoopStickers(global),
|
||||
repliesThreadInfo,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
defaultReaction: isMessageLocal(message) || messageListType === 'scheduled'
|
||||
? undefined : selectDefaultReaction(global, chatId),
|
||||
hasActiveReactions,
|
||||
@ -1644,6 +1652,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isResizingContainer,
|
||||
focusedQuote,
|
||||
}),
|
||||
tags: global.savedReactionTags?.byKey,
|
||||
};
|
||||
},
|
||||
)(Message));
|
||||
|
||||
@ -21,13 +21,15 @@
|
||||
.bubble {
|
||||
overflow: initial;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&.with-reactions .bubble {
|
||||
background: none !important;
|
||||
backdrop-filter: none !important;
|
||||
box-shadow: none;
|
||||
padding: 3.5rem 0 0 !important;
|
||||
}
|
||||
|
||||
&.with-reactions &_items {
|
||||
@ -37,6 +39,10 @@
|
||||
border-radius: var(--border-radius-default);
|
||||
padding: 0.25rem 0;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
margin-inline-end: 2.75rem;
|
||||
}
|
||||
|
||||
body.no-menu-blur & {
|
||||
background: var(--color-background);
|
||||
backdrop-filter: none;
|
||||
|
||||
@ -36,7 +36,7 @@ import Menu from '../../ui/Menu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import MenuSeparator from '../../ui/MenuSeparator';
|
||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||
import ReactionSelector from './ReactionSelector';
|
||||
import ReactionSelector from './reactions/ReactionSelector';
|
||||
import ReadTimeMenuItem from './ReadTimeMenuItem';
|
||||
|
||||
import './MessageContextMenu.scss';
|
||||
@ -45,6 +45,7 @@ type OwnProps = {
|
||||
isReactionPickerOpen?: boolean;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
topReactions?: ApiReaction[];
|
||||
defaultTagReactions?: ApiReaction[];
|
||||
isOpen: boolean;
|
||||
anchor: IAnchorPosition;
|
||||
targetHref?: string;
|
||||
@ -87,6 +88,7 @@ type OwnProps = {
|
||||
customEmojiSets?: ApiStickerSet[];
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
noTransition?: boolean;
|
||||
isInSavedMessages?: boolean;
|
||||
shouldRenderShowWhen?: boolean;
|
||||
canLoadReadDate?: boolean;
|
||||
onReply?: NoneToVoidFunction;
|
||||
@ -124,14 +126,15 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
const SCROLLBAR_WIDTH = 10;
|
||||
const REACTION_BUBBLE_EXTRA_WIDTH = 32;
|
||||
const REACTION_SELECTOR_WIDTH_REM = 19.25;
|
||||
const REACTION_SELECTOR_HEIGHT_REM = 3;
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
const MessageContextMenu: FC<OwnProps> = ({
|
||||
isReactionPickerOpen,
|
||||
availableReactions,
|
||||
topReactions,
|
||||
defaultTagReactions,
|
||||
isOpen,
|
||||
message,
|
||||
isPrivate,
|
||||
@ -174,6 +177,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
customEmojiSets,
|
||||
canPlayAnimatedEmojis,
|
||||
noTransition,
|
||||
isInSavedMessages,
|
||||
shouldRenderShowWhen,
|
||||
canLoadReadDate,
|
||||
onReply,
|
||||
@ -288,8 +292,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
return {
|
||||
extraPaddingX: SCROLLBAR_WIDTH,
|
||||
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
|
||||
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
|
||||
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
|
||||
topShiftY: withReactions && !isMobile ? -REACTION_SELECTOR_HEIGHT_REM * REM : 0,
|
||||
shouldAvoidNegativePosition: !isDesktop,
|
||||
menuElMinWidth: withReactions && isMobile ? REACTION_SELECTOR_WIDTH_REM * REM : undefined,
|
||||
};
|
||||
@ -343,6 +347,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
enabledReactions={enabledReactions}
|
||||
topReactions={topReactions}
|
||||
allAvailableReactions={availableReactions}
|
||||
defaultTagReactions={defaultTagReactions}
|
||||
currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined}
|
||||
maxUniqueReactions={maxUniqueReactions}
|
||||
onToggleReaction={onToggleReaction!}
|
||||
@ -350,8 +355,10 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
isReady={isReady}
|
||||
canBuyPremium={canBuyPremium}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
isInSavedMessages={isInSavedMessages}
|
||||
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
||||
onShowMore={handleOpenMessageReactionPicker}
|
||||
onClose={onClose}
|
||||
className={buildClassName(areItemsHidden && 'ReactionSelector-hidden')}
|
||||
/>
|
||||
)}
|
||||
@ -362,6 +369,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
areItemsHidden && 'MessageContextMenu_items-hidden',
|
||||
)}
|
||||
style={menuStyle}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
ref={scrollableRef}
|
||||
>
|
||||
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiReactionCount,
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isReactionChosen, isSameReaction } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import ReactionAnimatedEmoji from '../../common/reactions/ReactionAnimatedEmoji';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import './Reactions.scss';
|
||||
|
||||
const REACTION_SIZE = 1.25 * REM;
|
||||
|
||||
const ReactionButton: FC<{
|
||||
reaction: ApiReactionCount;
|
||||
message: ApiMessage;
|
||||
withRecentReactors?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
}> = ({
|
||||
reaction,
|
||||
message,
|
||||
withRecentReactors,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const { toggleReaction } = getActions();
|
||||
const { recentReactions } = message.reactions!;
|
||||
|
||||
const recentReactors = useMemo(() => {
|
||||
if (!withRecentReactors || !recentReactions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// No need for expensive global updates on chats or users, so we avoid them
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const usersById = getGlobal().users.byId;
|
||||
|
||||
return recentReactions
|
||||
.filter((recentReaction) => isSameReaction(recentReaction.reaction, reaction.reaction))
|
||||
.map((recentReaction) => usersById[recentReaction.peerId] || chatsById[recentReaction.peerId])
|
||||
.filter(Boolean) as ApiPeer[];
|
||||
}, [reaction.reaction, recentReactions, withRecentReactors]);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
toggleReaction({
|
||||
reaction: reaction.reaction,
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buildClassName(isReactionChosen(reaction) && 'chosen', 'message-reaction')}
|
||||
size="tiny"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ReactionAnimatedEmoji
|
||||
className="reaction-animated-emoji"
|
||||
containerId={getMessageKey(message)}
|
||||
reaction={reaction.reaction}
|
||||
size={REACTION_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
{recentReactors?.length ? (
|
||||
<AvatarList size="mini" peers={recentReactors} />
|
||||
) : (
|
||||
<AnimatedCounter text={formatIntegerCompact(reaction.count)} className="counter" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ReactionButton);
|
||||
@ -1,151 +0,0 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
import {
|
||||
canSendReaction, getReactionUniqueKey, isSameReaction, sortReactions,
|
||||
} from '../../../global/helpers';
|
||||
import buildClassName, { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import ReactionSelectorCustomReaction from './ReactionSelectorCustomReaction';
|
||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
|
||||
import './ReactionSelector.scss';
|
||||
|
||||
type OwnProps = {
|
||||
enabledReactions?: ApiChatReactions;
|
||||
isPrivate?: boolean;
|
||||
topReactions?: ApiReaction[];
|
||||
allAvailableReactions?: ApiAvailableReaction[];
|
||||
currentReactions?: ApiReactionCount[];
|
||||
maxUniqueReactions?: number;
|
||||
isReady?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
className?: string;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
onShowMore: (position: IAnchorPosition) => void;
|
||||
};
|
||||
|
||||
const cn = createClassNameBuilder('ReactionSelector');
|
||||
const REACTIONS_AMOUNT = 6;
|
||||
const FADE_IN_DELAY = 20;
|
||||
|
||||
const ReactionSelector: FC<OwnProps> = ({
|
||||
allAvailableReactions,
|
||||
topReactions,
|
||||
enabledReactions,
|
||||
currentReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isReady,
|
||||
canPlayAnimatedEmojis,
|
||||
className,
|
||||
onToggleReaction,
|
||||
onShowMore,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
|
||||
const availableReactions = useMemo(() => {
|
||||
const reactions = (enabledReactions?.type === 'some' && enabledReactions.allowed)
|
||||
|| allAvailableReactions?.map((reaction) => reaction.reaction);
|
||||
const filteredReactions = reactions?.map((reaction) => {
|
||||
const isCustomReaction = 'documentId' in reaction;
|
||||
const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction));
|
||||
if ((!isCustomReaction && !availableReaction) || availableReaction?.isInactive) return undefined;
|
||||
|
||||
if (!isPrivate && (!enabledReactions || !canSendReaction(reaction, enabledReactions))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions
|
||||
&& !currentReactions.some(({ reaction: currentReaction }) => isSameReaction(reaction, currentReaction))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return isCustomReaction ? reaction : availableReaction;
|
||||
}).filter(Boolean) || [];
|
||||
|
||||
return sortReactions(filteredReactions, topReactions);
|
||||
}, [allAvailableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions, topReactions]);
|
||||
|
||||
const reactionsToRender = useMemo(() => {
|
||||
// Component can fit one more if we do not need show more button
|
||||
return availableReactions.length === REACTIONS_AMOUNT + 1
|
||||
? availableReactions
|
||||
: availableReactions.slice(0, REACTIONS_AMOUNT);
|
||||
}, [availableReactions]);
|
||||
const withMoreButton = reactionsToRender.length < availableReactions.length;
|
||||
|
||||
const userReactionIndexes = useMemo(() => {
|
||||
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
||||
return new Set(chosenReactions.map(({ reaction }) => (
|
||||
reactionsToRender.findIndex((r) => r && isSameReaction('reaction' in r ? r.reaction : r, reaction))
|
||||
)));
|
||||
}, [currentReactions, reactionsToRender]);
|
||||
|
||||
const handleShowMoreClick = useLastCallback(() => {
|
||||
const bound = ref.current?.getBoundingClientRect() || { x: 0, y: 0 };
|
||||
onShowMore({
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
});
|
||||
});
|
||||
|
||||
if (!reactionsToRender.length) return undefined;
|
||||
|
||||
return (
|
||||
<div className={buildClassName(cn('&', lang.isRtl && 'isRtl'), className)} ref={ref}>
|
||||
<div className={cn('bubble-small', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items-wrapper')}>
|
||||
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{reactionsToRender.map((reaction, i) => (
|
||||
'reaction' in reaction ? (
|
||||
<ReactionSelectorReaction
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
noAppearAnimation={!canPlayAnimatedEmojis}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
/>
|
||||
) : (
|
||||
<ReactionSelectorCustomReaction
|
||||
key={getReactionUniqueKey(reaction)}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
noAppearAnimation={!canPlayAnimatedEmojis}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
style={`--_animation-delay: ${(REACTIONS_AMOUNT - i) * FADE_IN_DELAY}ms`}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{withMoreButton && (
|
||||
<Button
|
||||
color="translucent"
|
||||
className={cn('show-more')}
|
||||
onClick={handleShowMoreClick}
|
||||
>
|
||||
<i className="icon icon-down" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ReactionSelector);
|
||||
@ -1,62 +0,0 @@
|
||||
.ReactionSelectorReaction {
|
||||
--custom-emoji-size: 2rem;
|
||||
|
||||
margin-inline-start: 0.25rem;
|
||||
position: relative;
|
||||
min-width: 2rem;
|
||||
min-height: 2rem;
|
||||
|
||||
&:first-child {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
&__static-icon {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.AnimatedSticker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&--chosen::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-background-compact-menu-hover);
|
||||
}
|
||||
|
||||
&--custom {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&--custom-animated {
|
||||
animation: ReactionSelectorReaction--fade-in 0.2s ease-in-out forwards;
|
||||
animation-delay: var(--_animation-delay);
|
||||
}
|
||||
|
||||
@keyframes ReactionSelectorReaction--fade-in {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
.Reactions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.375rem;
|
||||
overflow: visible;
|
||||
max-width: calc(var(--max-width) + 2.25rem);
|
||||
|
||||
.Button {
|
||||
--custom-emoji-size: 1.25rem;
|
||||
--reaction-background: var(--color-reaction);
|
||||
--reaction-background-hover: var(--hover-color-reaction);
|
||||
--reaction-text-color: var(--text-color-reaction);
|
||||
|
||||
.theme-dark & {
|
||||
--reaction-background: rgb(255, 255, 255, 0.1);
|
||||
--reaction-background-hover: rgb(255, 255, 255, 0.2);
|
||||
--reaction-text-color: var(--color-text);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 1.875rem;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
padding: 0 0.375rem 0 0.25rem;
|
||||
background-color: var(--reaction-background) !important;
|
||||
border-radius: 1.75rem;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-transform: none;
|
||||
color: var(--reaction-text-color);
|
||||
overflow: visible;
|
||||
line-height: 1.75rem;
|
||||
|
||||
transition: background-color 150ms, color 150ms, backdrop-filter 150ms, filter 150ms;
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
|
||||
.Avatar {
|
||||
margin: 0;
|
||||
margin-inline-start: -0.25rem;
|
||||
border: 0.0625rem solid var(--reaction-background);
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message-reaction {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.reaction-animated-emoji {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
&.chosen {
|
||||
--reaction-background: var(--color-reaction-chosen);
|
||||
--reaction-background-hover: var(--hover-color-reaction-chosen);
|
||||
--reaction-text-color: var(--text-color-reaction-chosen);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.theme-dark & {
|
||||
--reaction-background: #3390ec;
|
||||
--reaction-background-hover: #4096ec;
|
||||
}
|
||||
|
||||
.theme-dark .own & {
|
||||
--reaction-background: rgb(255, 255, 255, 0.75);
|
||||
--reaction-background-hover: rgb(255, 255, 255, 0.85);
|
||||
--reaction-text-color: rgb(62 62 62);
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
margin-inline-end: 0.125rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--reaction-background: var(--reaction-background-hover) !important;
|
||||
|
||||
backdrop-filter: var(--reaction-background-hover-filter);
|
||||
|
||||
@supports not (backdrop-filter: var(--reaction-background-hover-filter)) {
|
||||
filter: var(--reaction-background-hover-filter);
|
||||
}
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-outside {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.own &.is-outside {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.Button {
|
||||
&:first-of-type {
|
||||
margin-inline-start: 0.125rem;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-inline-end: 0.125rem;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light &.is-outside .Button {
|
||||
--reaction-background: var(--pattern-color);
|
||||
--reaction-background-hover: var(--pattern-color);
|
||||
--reaction-background-hover-filter: brightness(115%);
|
||||
--reaction-text-color: white;
|
||||
|
||||
&.chosen {
|
||||
--reaction-background: rgb(255, 255, 255, 0.6);
|
||||
--reaction-background-hover: rgb(255, 255, 255, 0.75);
|
||||
--reaction-text-color: rgb(62 62 62);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getReactionUniqueKey } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ReactionButton from './ReactionButton';
|
||||
|
||||
import './Reactions.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
isOutside?: boolean;
|
||||
maxWidth?: number;
|
||||
metaChildren?: React.ReactNode;
|
||||
observeIntersection?: ObserveFn;
|
||||
noRecentReactors?: boolean;
|
||||
};
|
||||
|
||||
const MAX_RECENT_AVATARS = 3;
|
||||
|
||||
const Reactions: FC<OwnProps> = ({
|
||||
message,
|
||||
isOutside,
|
||||
maxWidth,
|
||||
metaChildren,
|
||||
observeIntersection,
|
||||
noRecentReactors,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const totalCount = useMemo(() => (
|
||||
message.reactions!.results.reduce((acc, reaction) => acc + reaction.count, 0)
|
||||
), [message]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('Reactions', isOutside && 'is-outside')}
|
||||
style={maxWidth ? `max-width: ${maxWidth}px` : undefined}
|
||||
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{message.reactions!.results.map((reaction) => (
|
||||
<ReactionButton
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
reaction={reaction}
|
||||
message={message}
|
||||
withRecentReactors={totalCount <= MAX_RECENT_AVATARS && !noRecentReactors}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
))}
|
||||
{metaChildren}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Reactions);
|
||||
@ -0,0 +1,118 @@
|
||||
// Hack: Increase selector specificity to override Button styles
|
||||
.root.root {
|
||||
--custom-emoji-size: 1.25rem;
|
||||
--reaction-background: var(--color-reaction);
|
||||
--reaction-background-hover: var(--hover-color-reaction);
|
||||
--reaction-text-color: var(--text-color-reaction);
|
||||
|
||||
&.chosen {
|
||||
--reaction-background: var(--color-reaction-chosen);
|
||||
--reaction-background-hover: var(--hover-color-reaction-chosen);
|
||||
--reaction-text-color: var(--text-color-reaction-chosen);
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 1.875rem;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
padding: 0 0.375rem 0 0.25rem;
|
||||
background-color: var(--reaction-background) !important;
|
||||
border-radius: 1.75rem;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-transform: none;
|
||||
color: var(--reaction-text-color);
|
||||
overflow: visible;
|
||||
line-height: 1.75rem;
|
||||
|
||||
gap: 0.125rem;
|
||||
|
||||
transition: background-color 150ms, color 150ms, backdrop-filter 150ms, filter 150ms !important;
|
||||
|
||||
&:hover {
|
||||
--reaction-background: var(--reaction-background-hover) !important;
|
||||
|
||||
backdrop-filter: var(--reaction-background-hover-filter);
|
||||
|
||||
@supports not (backdrop-filter: var(--reaction-background-hover-filter)) {
|
||||
filter: var(--reaction-background-hover-filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animated-emoji {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.tag.tag {
|
||||
position: relative;
|
||||
margin-right: 1rem;
|
||||
padding-inline: 0;
|
||||
justify-content: start;
|
||||
|
||||
border-radius: 0.375rem;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
// SVG has problems with backdrop-filter
|
||||
&:hover {
|
||||
backdrop-filter: unset;
|
||||
filter: var(--reaction-background-hover-filter);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--text-color-reaction-chosen);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.animated-emoji {
|
||||
margin: 0.25rem 0 0.25rem 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tail {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: -0.9375rem;
|
||||
z-index: -1;
|
||||
|
||||
.is-safari & {
|
||||
// Safari subpixel rendering be damned. May cause slight overlap, but it's better than a gap.
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
right: -14.8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tail-fill {
|
||||
fill: var(--reaction-background);
|
||||
transition: fill 150ms;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
font-size: 1rem;
|
||||
margin-inline-end: 0.375rem;
|
||||
}
|
||||
|
||||
.counter {
|
||||
margin-inline-end: 0.125rem;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
76
src/components/middle/message/reactions/ReactionButton.tsx
Normal file
76
src/components/middle/message/reactions/ReactionButton.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiPeer, ApiReaction, ApiReactionCount,
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isReactionChosen } from '../../../../global/helpers';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { formatIntegerCompact } from '../../../../util/textFormat';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedCounter from '../../../common/AnimatedCounter';
|
||||
import AvatarList from '../../../common/AvatarList';
|
||||
import ReactionAnimatedEmoji from '../../../common/reactions/ReactionAnimatedEmoji';
|
||||
import Button from '../../../ui/Button';
|
||||
|
||||
import styles from './ReactionButton.module.scss';
|
||||
|
||||
const REACTION_SIZE = 1.25 * REM;
|
||||
|
||||
const ReactionButton: FC<{
|
||||
reaction: ApiReactionCount;
|
||||
containerId: string;
|
||||
isOwnMessage?: boolean;
|
||||
recentReactors?: ApiPeer[];
|
||||
className?: string;
|
||||
chosenClassName?: string;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (reaction: ApiReaction) => void;
|
||||
}> = ({
|
||||
reaction,
|
||||
containerId,
|
||||
isOwnMessage,
|
||||
recentReactors,
|
||||
className,
|
||||
chosenClassName,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClick?.(reaction.reaction);
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
isOwnMessage && styles.own,
|
||||
isReactionChosen(reaction) && styles.chosen,
|
||||
isReactionChosen(reaction) && chosenClassName,
|
||||
className,
|
||||
)}
|
||||
size="tiny"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ReactionAnimatedEmoji
|
||||
className={styles.animatedEmoji}
|
||||
containerId={containerId}
|
||||
reaction={reaction.reaction}
|
||||
size={REACTION_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
{recentReactors?.length ? (
|
||||
<AvatarList size="mini" peers={recentReactors} />
|
||||
) : (
|
||||
<AnimatedCounter text={formatIntegerCompact(reaction.count)} className={styles.counter} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ReactionButton);
|
||||
@ -1,11 +1,11 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React from '../../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './ReactionPicker';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
import { Bundles } from '../../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
import useModuleLoader from '../../../../hooks/useModuleLoader';
|
||||
|
||||
const ReactionPickerAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
@ -1,31 +1,31 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiMessageEntity,
|
||||
ApiReaction, ApiReactionCustomEmoji, ApiSticker, ApiStory, ApiStorySkipped,
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
} from '../../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../../types';
|
||||
|
||||
import { getStoryKey, isUserId } from '../../../global/helpers';
|
||||
import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers';
|
||||
import {
|
||||
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium,
|
||||
selectPeerStory, selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { buildCustomEmojiHtml } from '../composer/helpers/customEmoji';
|
||||
} from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import { buildCustomEmojiHtml } from '../../composer/helpers/customEmoji';
|
||||
|
||||
import { getIsMobile } from '../../../hooks/useAppLayout';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import { getIsMobile } from '../../../../hooks/useAppLayout';
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../../hooks/useMenuPosition';
|
||||
|
||||
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||
import Menu from '../../ui/Menu';
|
||||
import CustomEmojiPicker from '../../../common/CustomEmojiPicker';
|
||||
import Menu from '../../../ui/Menu';
|
||||
import ReactionPickerLimited from './ReactionPickerLimited';
|
||||
|
||||
import styles from './ReactionPicker.module.scss';
|
||||
@ -174,7 +174,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
const selectedReactionIds = useMemo(() => {
|
||||
return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => {
|
||||
if (chosenOrder !== undefined) {
|
||||
acc.push('emoticon' in reaction ? reaction.emoticon : reaction.documentId);
|
||||
acc.push(getReactionKey(reaction));
|
||||
}
|
||||
|
||||
return acc;
|
||||
@ -202,6 +202,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
onClose={closeReactionPicker}
|
||||
>
|
||||
<CustomEmojiPicker
|
||||
chatId={renderedChatId}
|
||||
idPrefix="message-emoji-set-"
|
||||
isHidden={!isOpen || !(withCustomReactions || renderedStoryId)}
|
||||
loadAndPlay={Boolean(isOpen && withCustomReactions)}
|
||||
@ -1,18 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../global';
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiChatReactions, ApiReaction } from '../../../api/types';
|
||||
import type { ApiAvailableReaction, ApiChatReactions, ApiReaction } from '../../../../api/types';
|
||||
|
||||
import { getReactionUniqueKey, sortReactions } from '../../../global/helpers';
|
||||
import { selectChatFullInfo } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { getReactionKey, sortReactions } from '../../../../global/helpers';
|
||||
import { selectChatFullInfo } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useWindowSize from '../../../hooks/window/useWindowSize';
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useWindowSize from '../../../../hooks/window/useWindowSize';
|
||||
|
||||
import ReactionEmoji from '../../common/ReactionEmoji';
|
||||
import ReactionEmoji from '../../../common/ReactionEmoji';
|
||||
|
||||
import styles from './ReactionPickerLimited.module.scss';
|
||||
|
||||
@ -87,7 +87,7 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
|
||||
{allAvailableReactions.map((reaction) => {
|
||||
const reactionId = getReactionUniqueKey(reaction);
|
||||
const reactionId = getReactionKey(reaction);
|
||||
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||
|
||||
return (
|
||||
@ -111,7 +111,7 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const { availableReactions, topReactions } = global;
|
||||
const { availableReactions, topReactions } = global.reactions;
|
||||
const { enabledReactions } = selectChatFullInfo(global, chatId) || {};
|
||||
|
||||
return {
|
||||
@ -1,16 +1,12 @@
|
||||
.ReactionSelector {
|
||||
position: absolute;
|
||||
height: 2.5rem;
|
||||
position: relative;
|
||||
min-width: 3rem;
|
||||
max-width: calc(100% + 4rem);
|
||||
z-index: 100;
|
||||
border-radius: 3rem;
|
||||
right: -2rem;
|
||||
top: 0.5rem;
|
||||
max-width: fit-content;
|
||||
right: 0;
|
||||
top: -0.5rem;
|
||||
|
||||
&--isRtl {
|
||||
right: auto;
|
||||
left: -3rem;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@ -24,17 +20,12 @@
|
||||
&__bubble-small,
|
||||
&__items-wrapper {
|
||||
background: var(--color-background);
|
||||
filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow));
|
||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||
|
||||
body:not(.no-menu-blur) & {
|
||||
background: var(--color-background-compact-menu);
|
||||
backdrop-filter: blur(25px);
|
||||
}
|
||||
|
||||
body.is-safari & {
|
||||
filter: none;
|
||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&__bubble-big {
|
||||
@ -84,7 +75,7 @@
|
||||
&__items-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 3rem;
|
||||
border-radius: 1.25rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
width: fit-content;
|
||||
@ -92,13 +83,25 @@
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
&__reactions {
|
||||
padding: 0 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
align-items: center;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
&__show-more {
|
||||
207
src/components/middle/message/reactions/ReactionSelector.tsx
Normal file
207
src/components/middle/message/reactions/ReactionSelector.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
||||
} from '../../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../../types';
|
||||
|
||||
import {
|
||||
canSendReaction, getReactionKey, isSameReaction, sortReactions,
|
||||
} from '../../../../global/helpers';
|
||||
import buildClassName, { createClassNameBuilder } from '../../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import Button from '../../../ui/Button';
|
||||
import Link from '../../../ui/Link';
|
||||
import ReactionSelectorCustomReaction from './ReactionSelectorCustomReaction';
|
||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
|
||||
import './ReactionSelector.scss';
|
||||
|
||||
type OwnProps = {
|
||||
enabledReactions?: ApiChatReactions;
|
||||
isPrivate?: boolean;
|
||||
topReactions?: ApiReaction[];
|
||||
defaultTagReactions?: ApiReaction[];
|
||||
allAvailableReactions?: ApiAvailableReaction[];
|
||||
currentReactions?: ApiReactionCount[];
|
||||
maxUniqueReactions?: number;
|
||||
isReady?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
className?: string;
|
||||
isInSavedMessages?: boolean;
|
||||
isInStoryViewer?: boolean;
|
||||
onClose?: NoneToVoidFunction;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
onShowMore: (position: IAnchorPosition) => void;
|
||||
};
|
||||
|
||||
const cn = createClassNameBuilder('ReactionSelector');
|
||||
const REACTIONS_AMOUNT = 7;
|
||||
const FADE_IN_DELAY = 20;
|
||||
|
||||
const ReactionSelector: FC<OwnProps> = ({
|
||||
allAvailableReactions,
|
||||
topReactions,
|
||||
defaultTagReactions,
|
||||
enabledReactions,
|
||||
currentReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isReady,
|
||||
canPlayAnimatedEmojis,
|
||||
className,
|
||||
isCurrentUserPremium,
|
||||
isInSavedMessages,
|
||||
isInStoryViewer,
|
||||
onClose,
|
||||
onToggleReaction,
|
||||
onShowMore,
|
||||
}) => {
|
||||
const { openPremiumModal } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
|
||||
const areReactionsLocked = isInSavedMessages && !isCurrentUserPremium && !isInStoryViewer;
|
||||
|
||||
const availableReactions = useMemo(() => {
|
||||
const reactions = isInSavedMessages ? defaultTagReactions
|
||||
: (enabledReactions?.type === 'some' ? enabledReactions.allowed
|
||||
: allAvailableReactions?.map((reaction) => reaction.reaction));
|
||||
const filteredReactions = reactions?.map((reaction) => {
|
||||
const isCustomReaction = 'documentId' in reaction;
|
||||
const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction));
|
||||
if ((!isCustomReaction && !availableReaction) || availableReaction?.isInactive) return undefined;
|
||||
|
||||
if (!isPrivate && (!enabledReactions || !canSendReaction(reaction, enabledReactions))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions
|
||||
&& !currentReactions.some(({ reaction: currentReaction }) => isSameReaction(reaction, currentReaction))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return isCustomReaction ? reaction : availableReaction;
|
||||
}).filter(Boolean) || [];
|
||||
|
||||
return sortReactions(filteredReactions, topReactions);
|
||||
}, [
|
||||
allAvailableReactions, currentReactions, defaultTagReactions, enabledReactions, isInSavedMessages, isPrivate,
|
||||
maxUniqueReactions, topReactions,
|
||||
]);
|
||||
|
||||
const reactionsToRender = useMemo(() => {
|
||||
// Component can fit one more if we do not need show more button
|
||||
return availableReactions.length === REACTIONS_AMOUNT + 1
|
||||
? availableReactions
|
||||
: availableReactions.slice(0, REACTIONS_AMOUNT);
|
||||
}, [availableReactions]);
|
||||
const withMoreButton = reactionsToRender.length < availableReactions.length;
|
||||
|
||||
const userReactionIndexes = useMemo(() => {
|
||||
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
||||
return new Set(chosenReactions.map(({ reaction }) => (
|
||||
reactionsToRender.findIndex((r) => r && isSameReaction('reaction' in r ? r.reaction : r, reaction))
|
||||
)));
|
||||
}, [currentReactions, reactionsToRender]);
|
||||
|
||||
const handleShowMoreClick = useLastCallback(() => {
|
||||
const bound = ref.current?.getBoundingClientRect() || { x: 0, y: 0 };
|
||||
onShowMore({
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
});
|
||||
});
|
||||
|
||||
const handleOpenPremiumModal = useLastCallback(() => {
|
||||
onClose?.();
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
});
|
||||
|
||||
const hintText = useMemo(() => {
|
||||
if (isInSavedMessages) {
|
||||
if (!isCurrentUserPremium) {
|
||||
const text = lang('lng_subscribe_tag_about');
|
||||
const parts = text.split('{link}');
|
||||
return (
|
||||
<span>
|
||||
{parts[0]}
|
||||
<Link isPrimary onClick={handleOpenPremiumModal}>
|
||||
{lang('lng_subscribe_tag_link')}
|
||||
</Link>
|
||||
{parts[1]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return lang('SavedTagReactionsHint2');
|
||||
}
|
||||
|
||||
if (isInStoryViewer) {
|
||||
return lang('StoryReactionsHint');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang]);
|
||||
|
||||
if (!reactionsToRender.length) return undefined;
|
||||
|
||||
return (
|
||||
<div className={buildClassName(cn('&', lang.isRtl && 'isRtl'), className)} ref={ref}>
|
||||
<div className={cn('bubble-small', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items-wrapper')}>
|
||||
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items')}>
|
||||
{hintText && <div className={cn('hint')}>{hintText}</div>}
|
||||
<div className={cn('reactions')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{reactionsToRender.map((reaction, i) => (
|
||||
'reaction' in reaction ? (
|
||||
<ReactionSelectorReaction
|
||||
key={getReactionKey(reaction.reaction)}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
noAppearAnimation={!canPlayAnimatedEmojis}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
isLocked={areReactionsLocked}
|
||||
/>
|
||||
) : (
|
||||
<ReactionSelectorCustomReaction
|
||||
key={getReactionKey(reaction)}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
noAppearAnimation={!canPlayAnimatedEmojis}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
isLocked={areReactionsLocked}
|
||||
style={`--_animation-delay: ${(REACTIONS_AMOUNT - i) * FADE_IN_DELAY}ms`}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{withMoreButton && (
|
||||
<Button
|
||||
color="translucent"
|
||||
className={cn('show-more')}
|
||||
onClick={handleShowMoreClick}
|
||||
>
|
||||
<i className="icon icon-down" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ReactionSelector);
|
||||
@ -1,14 +1,15 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiReaction, ApiReactionCustomEmoji } from '../../../api/types';
|
||||
import type { ApiReaction, ApiReactionCustomEmoji } from '../../../../api/types';
|
||||
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import CustomEmoji from '../../../common/CustomEmoji';
|
||||
import Icon from '../../../common/Icon';
|
||||
|
||||
import './ReactionSelectorReaction.scss';
|
||||
import styles from './ReactionSelectorReaction.module.scss';
|
||||
|
||||
const REACTION_SIZE = 2 * REM;
|
||||
|
||||
@ -18,17 +19,17 @@ type OwnProps = {
|
||||
isReady?: boolean;
|
||||
noAppearAnimation?: boolean;
|
||||
style?: string;
|
||||
isLocked?: boolean;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
};
|
||||
|
||||
const cn = createClassNameBuilder('ReactionSelectorReaction');
|
||||
|
||||
const ReactionSelectorCustomReaction: FC<OwnProps> = ({
|
||||
reaction,
|
||||
chosen,
|
||||
isReady,
|
||||
noAppearAnimation,
|
||||
style,
|
||||
isLocked,
|
||||
onToggleReaction,
|
||||
}) => {
|
||||
function handleClick() {
|
||||
@ -37,12 +38,12 @@ const ReactionSelectorCustomReaction: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'&',
|
||||
'custom',
|
||||
chosen && 'chosen',
|
||||
!noAppearAnimation && isReady && 'custom-animated',
|
||||
noAppearAnimation && 'visible',
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
styles.custom,
|
||||
chosen && styles.chosen,
|
||||
!noAppearAnimation && isReady && styles.customAnimated,
|
||||
noAppearAnimation && styles.visible,
|
||||
)}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
@ -51,6 +52,9 @@ const ReactionSelectorCustomReaction: FC<OwnProps> = ({
|
||||
documentId={reaction.documentId}
|
||||
size={REACTION_SIZE}
|
||||
/>
|
||||
{isLocked && (
|
||||
<Icon className={styles.lock} name="lock-badge" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
.root {
|
||||
--custom-emoji-size: 2rem;
|
||||
|
||||
margin-inline-start: 0.25rem;
|
||||
position: relative;
|
||||
min-width: 2rem;
|
||||
min-height: 2rem;
|
||||
|
||||
&:first-child {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
:global(.AnimatedSticker) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.custom-animated {
|
||||
animation: custom-fade-in 0.2s ease-in-out forwards;
|
||||
animation-delay: var(--_animation-delay);
|
||||
}
|
||||
|
||||
.chosen::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-background-compact-menu-hover);
|
||||
}
|
||||
|
||||
.static-icon {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.lock {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.125rem;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-background-compact-menu);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes custom-fade-in {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../../api/types';
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../../../api/types';
|
||||
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import useMedia from '../../../../hooks/useMedia';
|
||||
|
||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||
import AnimatedSticker from '../../../common/AnimatedSticker';
|
||||
import Icon from '../../../common/Icon';
|
||||
|
||||
import './ReactionSelectorReaction.scss';
|
||||
import styles from './ReactionSelectorReaction.module.scss';
|
||||
|
||||
const REACTION_SIZE = 2 * REM;
|
||||
|
||||
@ -20,16 +21,16 @@ type OwnProps = {
|
||||
isReady?: boolean;
|
||||
chosen?: boolean;
|
||||
noAppearAnimation?: boolean;
|
||||
isLocked?: boolean;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
};
|
||||
|
||||
const cn = createClassNameBuilder('ReactionSelectorReaction');
|
||||
|
||||
const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
reaction,
|
||||
isReady,
|
||||
noAppearAnimation,
|
||||
chosen,
|
||||
isLocked,
|
||||
onToggleReaction,
|
||||
}) => {
|
||||
const mediaAppearData = useMedia(`sticker${reaction.appearAnimation?.id}`, !isReady || noAppearAnimation);
|
||||
@ -46,13 +47,13 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('&', chosen && 'chosen')}
|
||||
className={buildClassName(styles.root, chosen && styles.chosen)}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={isReady && !isFirstPlay ? activate : undefined}
|
||||
>
|
||||
{noAppearAnimation && (
|
||||
<img
|
||||
className={cn('static-icon')}
|
||||
className={styles.staticIcon}
|
||||
src={staticIconData}
|
||||
alt={reaction.reaction.emoticon}
|
||||
draggable={false}
|
||||
@ -81,6 +82,9 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
forceAlways
|
||||
/>
|
||||
)}
|
||||
{isLocked && (
|
||||
<Icon className={styles.lock} name="lock-badge" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
31
src/components/middle/message/reactions/Reactions.scss
Normal file
31
src/components/middle/message/reactions/Reactions.scss
Normal file
@ -0,0 +1,31 @@
|
||||
.Reactions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.375rem;
|
||||
overflow: visible;
|
||||
max-width: calc(var(--max-width) + 2.25rem);
|
||||
|
||||
&.is-outside {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.own &.is-outside {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.theme-light &.is-outside .message-reaction {
|
||||
--reaction-background: var(--pattern-color);
|
||||
--reaction-background-hover: var(--pattern-color);
|
||||
--reaction-background-hover-filter: brightness(115%);
|
||||
--reaction-text-color: white;
|
||||
|
||||
&.chosen {
|
||||
--reaction-background: rgb(255, 255, 255, 0.6);
|
||||
--reaction-background-hover: rgb(255, 255, 255, 0.75);
|
||||
--reaction-text-color: rgb(62 62 62);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
src/components/middle/message/reactions/Reactions.tsx
Normal file
172
src/components/middle/message/reactions/Reactions.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage,
|
||||
ApiPeer,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
ApiSavedReactionTag,
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getReactionKey, isReactionChosen } from '../../../../global/helpers';
|
||||
import { selectPeer } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { getMessageKey } from '../../../../util/messageKey';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import ReactionButton from './ReactionButton';
|
||||
import SavedTagButton from './SavedTagButton';
|
||||
|
||||
import './Reactions.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
isOutside?: boolean;
|
||||
maxWidth?: number;
|
||||
metaChildren?: React.ReactNode;
|
||||
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
isCurrentUserPremium?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
noRecentReactors?: boolean;
|
||||
};
|
||||
|
||||
const MAX_RECENT_AVATARS = 3;
|
||||
|
||||
const Reactions: FC<OwnProps> = ({
|
||||
message,
|
||||
isOutside,
|
||||
maxWidth,
|
||||
metaChildren,
|
||||
observeIntersection,
|
||||
noRecentReactors,
|
||||
isCurrentUserPremium,
|
||||
tags,
|
||||
}) => {
|
||||
const {
|
||||
toggleReaction,
|
||||
setLocalTextSearchTag,
|
||||
searchTextMessagesLocal,
|
||||
openPremiumModal,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const { results, areTags, recentReactions } = message.reactions!;
|
||||
|
||||
const totalCount = useMemo(() => (
|
||||
results.reduce((acc, reaction) => acc + reaction.count, 0)
|
||||
), [results]);
|
||||
|
||||
const recentReactorsByReactionKey = useMemo(() => {
|
||||
const global = getGlobal();
|
||||
|
||||
return recentReactions?.reduce((acc, recentReaction) => {
|
||||
const { reaction, peerId } = recentReaction;
|
||||
const key = getReactionKey(reaction);
|
||||
const peer = selectPeer(global, peerId);
|
||||
|
||||
if (!peer) return acc;
|
||||
|
||||
const peers = acc[key] || [];
|
||||
peers.push(peer);
|
||||
acc[key] = peers;
|
||||
return acc;
|
||||
}, {} as Record<ApiReactionKey, ApiPeer[]>);
|
||||
}, [recentReactions]);
|
||||
|
||||
const props = useMemo(() => {
|
||||
const messageKey = getMessageKey(message);
|
||||
return results.map((reaction) => {
|
||||
const reactionKey = getReactionKey(reaction.reaction);
|
||||
const recentReactors = recentReactorsByReactionKey?.[reactionKey];
|
||||
const shouldHideRecentReactors = totalCount > MAX_RECENT_AVATARS || noRecentReactors;
|
||||
const tag = areTags ? tags?.[reactionKey] : undefined;
|
||||
|
||||
return {
|
||||
reaction,
|
||||
reactionKey,
|
||||
messageKey,
|
||||
recentReactors: !shouldHideRecentReactors ? recentReactors : undefined,
|
||||
isChosen: isReactionChosen(reaction),
|
||||
tag,
|
||||
};
|
||||
});
|
||||
}, [message, noRecentReactors, recentReactorsByReactionKey, results, areTags, tags, totalCount]);
|
||||
|
||||
const handleClick = useLastCallback((reaction: ApiReaction) => {
|
||||
if (areTags) {
|
||||
if (!isCurrentUserPremium) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalTextSearchTag({ tag: reaction });
|
||||
searchTextMessagesLocal();
|
||||
return;
|
||||
}
|
||||
|
||||
toggleReaction({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
reaction,
|
||||
});
|
||||
});
|
||||
|
||||
const handleRemoveReaction = useLastCallback((reaction: ApiReaction) => {
|
||||
toggleReaction({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
reaction,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('Reactions', isOutside && 'is-outside')}
|
||||
style={maxWidth ? `max-width: ${maxWidth}px` : undefined}
|
||||
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{props.map(({
|
||||
reaction, recentReactors, messageKey, reactionKey, isChosen, tag,
|
||||
}) => (
|
||||
areTags ? (
|
||||
<SavedTagButton
|
||||
key={reactionKey}
|
||||
className="message-reaction"
|
||||
chosenClassName="chosen"
|
||||
containerId={messageKey}
|
||||
isOwnMessage={message.isOutgoing}
|
||||
isChosen={isChosen}
|
||||
reaction={reaction.reaction}
|
||||
tag={tag}
|
||||
withContextMenu={isCurrentUserPremium}
|
||||
onClick={handleClick}
|
||||
onRemove={handleRemoveReaction}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
) : (
|
||||
<ReactionButton
|
||||
key={reactionKey}
|
||||
className="message-reaction"
|
||||
chosenClassName="chosen"
|
||||
containerId={messageKey}
|
||||
isOwnMessage={message.isOutgoing}
|
||||
recentReactors={recentReactors}
|
||||
reaction={reaction}
|
||||
onClick={handleClick}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{metaChildren}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Reactions);
|
||||
193
src/components/middle/message/reactions/SavedTagButton.tsx
Normal file
193
src/components/middle/message/reactions/SavedTagButton.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiReaction, ApiSavedReactionTag,
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../../hooks/useMenuPosition';
|
||||
|
||||
import ReactionAnimatedEmoji from '../../../common/reactions/ReactionAnimatedEmoji';
|
||||
import PromptDialog from '../../../modals/prompt/PromptDialog';
|
||||
import Button from '../../../ui/Button';
|
||||
import Menu from '../../../ui/Menu';
|
||||
import MenuItem from '../../../ui/MenuItem';
|
||||
|
||||
import styles from './ReactionButton.module.scss';
|
||||
|
||||
const REACTION_SIZE = 1.25 * REM;
|
||||
const TITLE_MAX_LENGTH = 15;
|
||||
|
||||
const SavedTagButton: FC<{
|
||||
reaction: ApiReaction;
|
||||
tag?: ApiSavedReactionTag;
|
||||
containerId: string;
|
||||
isChosen?: boolean;
|
||||
isOwnMessage?: boolean;
|
||||
withCount?: boolean;
|
||||
className?: string;
|
||||
chosenClassName?: string;
|
||||
isDisabled?: boolean;
|
||||
withContextMenu?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (reaction: ApiReaction) => void;
|
||||
onRemove?: (reaction: ApiReaction) => void;
|
||||
}> = ({
|
||||
reaction,
|
||||
tag,
|
||||
containerId,
|
||||
isChosen,
|
||||
isOwnMessage,
|
||||
className,
|
||||
chosenClassName,
|
||||
withCount,
|
||||
isDisabled,
|
||||
withContextMenu,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { editSavedReactionTag } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
const [isRenamePromptOpen, openRenamePrompt, closeRenamePrompt] = useFlag();
|
||||
|
||||
const { title, count } = tag || {};
|
||||
const hasText = Boolean(title || (withCount && count));
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClick?.(reaction);
|
||||
});
|
||||
|
||||
const handleRemoveClick = useLastCallback(() => {
|
||||
onRemove?.(reaction);
|
||||
});
|
||||
|
||||
const handleRenameTag = useLastCallback((newTitle: string) => {
|
||||
editSavedReactionTag({
|
||||
reaction,
|
||||
title: newTitle,
|
||||
});
|
||||
closeRenamePrompt();
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, !withContextMenu);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
|
||||
const getLayout = () => ({ withPortal: true, shouldAvoidNegativePosition: true });
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
if (withCount && count === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
styles.tag,
|
||||
isOwnMessage && styles.own,
|
||||
isChosen && styles.chosen,
|
||||
isChosen && chosenClassName,
|
||||
isDisabled && styles.disabled,
|
||||
className,
|
||||
)}
|
||||
size="tiny"
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
ref={ref}
|
||||
>
|
||||
<ReactionAnimatedEmoji
|
||||
className={styles.animatedEmoji}
|
||||
containerId={containerId}
|
||||
reaction={reaction}
|
||||
size={REACTION_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
{hasText && (
|
||||
<span className={styles.tagText}>
|
||||
{title && <span>{title}</span>}
|
||||
{withCount && <span>{count}</span>}
|
||||
</span>
|
||||
)}
|
||||
<svg
|
||||
className={styles.tail}
|
||||
width="15"
|
||||
height="30"
|
||||
viewBox="0 0 15 30"
|
||||
>
|
||||
<path className={styles.tailFill} d="m 0,30 c 3.1855,0 6.1803,-1.5176 8.0641,-4.0864 l 5.835,-7.9568 c 1.2906,-1.7599 1.2906,-4.1537 0,-5.9136 L 8.0641,4.08636 C 6.1803,1.51761 3.1855,0 0,0" />
|
||||
</svg>
|
||||
<PromptDialog
|
||||
isOpen={isRenamePromptOpen}
|
||||
maxLength={TITLE_MAX_LENGTH}
|
||||
title={lang(tag?.title ? 'SavedTagRenameTag' : 'SavedTagLabelTag')}
|
||||
subtitle={lang('SavedTagLabelTagText')}
|
||||
placeholder={lang('SavedTagLabelPlaceholder')}
|
||||
initialValue={tag?.title}
|
||||
onClose={closeRenamePrompt}
|
||||
onSubmit={handleRenameTag}
|
||||
/>
|
||||
{withContextMenu && contextMenuPosition && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
autoClose
|
||||
withPortal
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
>
|
||||
<MenuItem icon="tag-filter" onClick={handleClick}>
|
||||
{lang('SavedTagFilterByTag')}
|
||||
</MenuItem>
|
||||
<MenuItem icon="tag-name" onClick={openRenamePrompt}>
|
||||
{lang(tag?.title ? 'SavedTagRenameTag' : 'SavedTagLabelTag')}
|
||||
</MenuItem>
|
||||
<MenuItem icon="tag-crossed" destructive onClick={handleRemoveClick}>
|
||||
{lang('SavedTagRemoveTag')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SavedTagButton);
|
||||
3
src/components/modals/prompt/PromptDialog.module.scss
Normal file
3
src/components/modals/prompt/PromptDialog.module.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.subtitle {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
79
src/components/modals/prompt/PromptDialog.tsx
Normal file
79
src/components/modals/prompt/PromptDialog.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { memo, useState } from '../../../lib/teact/teact';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './PromptDialog.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
placeholder: string;
|
||||
submitText?: string;
|
||||
maxLength?: number;
|
||||
initialValue?: string;
|
||||
onClose: NoneToVoidFunction;
|
||||
onSubmit: (text: string) => void;
|
||||
};
|
||||
|
||||
const PromptDialog = ({
|
||||
isOpen,
|
||||
title,
|
||||
subtitle,
|
||||
placeholder,
|
||||
submitText,
|
||||
maxLength,
|
||||
initialValue = '',
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
|
||||
const [text, setText] = useState(initialValue);
|
||||
|
||||
const handleTextChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setText(e.target.value);
|
||||
});
|
||||
|
||||
const handleSubmit = useLastCallback(() => {
|
||||
onSubmit(text);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="narrow"
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isSlim
|
||||
>
|
||||
{Boolean(subtitle) && (
|
||||
<div className={styles.subtitle}>
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
<InputText
|
||||
placeholder={placeholder}
|
||||
value={text}
|
||||
onChange={handleTextChange}
|
||||
maxLength={maxLength}
|
||||
teactExperimentControlled
|
||||
/>
|
||||
<div className="dialog-buttons mt-2">
|
||||
<Button className="confirm-dialog-button" onClick={handleSubmit}>
|
||||
{submitText || lang('Save')}
|
||||
</Button>
|
||||
<Button className="confirm-dialog-button" isText onClick={onClose}>
|
||||
{lang('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PromptDialog);
|
||||
@ -11,4 +11,19 @@
|
||||
unicode-bidi: plaintext;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.search-tags {
|
||||
--color-reaction: var(--color-background-secondary);
|
||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
||||
--text-color-reaction: var(--color-text-secondary);
|
||||
--color-reaction-chosen: var(--color-primary);
|
||||
--text-color-reaction-chosen: #FFFFFF;
|
||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
||||
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
|
||||
margin-top: 0.25rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +1,40 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback,
|
||||
useEffect, useMemo, useRef,
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiMessage, ApiPeer } from '../../api/types';
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
||||
} from '../../api/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
|
||||
import { REPLIES_USER_ID } from '../../config';
|
||||
import { ANONYMOUS_USER_ID, REPLIES_USER_ID } from '../../config';
|
||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
selectChatMessages,
|
||||
selectCurrentTextSearch,
|
||||
selectForwardedSender,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectSender,
|
||||
} from '../../global/selectors';
|
||||
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import { renderMessageSummary } from '../common/helpers/renderMessageText';
|
||||
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
import Avatar from '../common/Avatar';
|
||||
import FullNameTitle from '../common/FullNameTitle';
|
||||
import LastMessageMeta from '../common/LastMessageMeta';
|
||||
import SavedTagButton from '../middle/message/reactions/SavedTagButton';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem from '../ui/ListItem';
|
||||
|
||||
@ -43,11 +50,16 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
messagesById?: Record<number, ApiMessage>;
|
||||
query?: string;
|
||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
searchTag?: ApiReaction;
|
||||
totalCount?: number;
|
||||
foundIds?: number[];
|
||||
isSavedMessages?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
||||
|
||||
const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
@ -56,16 +68,24 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
query,
|
||||
totalCount,
|
||||
foundIds,
|
||||
savedTags,
|
||||
searchTag,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
searchTextMessagesLocal,
|
||||
setLocalTextSearchTag,
|
||||
focusMessage,
|
||||
openPremiumModal,
|
||||
loadSavedReactionTags,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tagsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
useHistoryBack({
|
||||
@ -83,14 +103,45 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
return enableDirectTextInput;
|
||||
}, [isActive]);
|
||||
|
||||
const handleSearchTextMessagesLocal = useCallback(() => {
|
||||
searchTextMessagesLocal();
|
||||
}, [searchTextMessagesLocal]);
|
||||
const tags = useMemo(() => {
|
||||
if (!savedTags) return undefined;
|
||||
return Object.values(savedTags);
|
||||
}, [savedTags]);
|
||||
|
||||
const hasTags = Boolean(tags?.length);
|
||||
const areTagsDisabled = hasTags && !isCurrentUserPremium;
|
||||
|
||||
useHorizontalScroll(tagsRef, !hasTags);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) loadSavedReactionTags();
|
||||
}, [hasTags, isActive]);
|
||||
|
||||
const handleSearchTextMessagesLocal = useLastCallback(() => {
|
||||
runDebouncedForSearch(searchTextMessagesLocal);
|
||||
});
|
||||
|
||||
const handleTagClick = useLastCallback((tag: ApiReaction) => {
|
||||
if (areTagsDisabled) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSameReaction(tag, searchTag)) {
|
||||
setLocalTextSearchTag({ tag: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalTextSearchTag({ tag });
|
||||
handleSearchTextMessagesLocal();
|
||||
});
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(handleSearchTextMessagesLocal, foundIds);
|
||||
|
||||
const viewportResults = useMemo(() => {
|
||||
if (!query || !viewportIds?.length || !messagesById) {
|
||||
if ((!query && !searchTag) || !viewportIds?.length || !messagesById) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
@ -102,23 +153,22 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const global = getGlobal();
|
||||
|
||||
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID)
|
||||
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID || chatId === ANONYMOUS_USER_ID)
|
||||
? selectForwardedSender(global, message) : undefined;
|
||||
const messageSender = selectSender(global, message);
|
||||
|
||||
const senderPeer = originalSender || messageSender;
|
||||
|
||||
if (!senderPeer) {
|
||||
return undefined;
|
||||
}
|
||||
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
|
||||
|
||||
return {
|
||||
message,
|
||||
senderPeer,
|
||||
hiddenForwardTitle,
|
||||
onClick: () => focusMessage({ chatId, threadId, messageId: id }),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}, [query, viewportIds, messagesById, isSavedMessages, chatId, threadId]);
|
||||
}, [query, searchTag, viewportIds, messagesById, isSavedMessages, chatId, threadId]);
|
||||
|
||||
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
|
||||
const foundResult = viewportResults?.[index === -1 ? 0 : index];
|
||||
@ -128,10 +178,11 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
}, '.ListItem-button', true);
|
||||
|
||||
const renderSearchResult = ({
|
||||
message, senderPeer, onClick,
|
||||
message, senderPeer, hiddenForwardTitle, onClick,
|
||||
}: {
|
||||
message: ApiMessage;
|
||||
senderPeer: ApiPeer;
|
||||
senderPeer?: ApiPeer;
|
||||
hiddenForwardTitle?: string;
|
||||
onClick: NoneToVoidFunction;
|
||||
}) => {
|
||||
const text = renderMessageSummary(lang, message, undefined, query);
|
||||
@ -145,10 +196,12 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<Avatar
|
||||
peer={senderPeer}
|
||||
text={hiddenForwardTitle}
|
||||
/>
|
||||
<div className="info">
|
||||
<div className="search-result-message-top">
|
||||
<FullNameTitle peer={senderPeer} withEmojiStatus />
|
||||
{senderPeer && <FullNameTitle peer={senderPeer} withEmojiStatus />}
|
||||
{!senderPeer && hiddenForwardTitle}
|
||||
<LastMessageMeta message={message} />
|
||||
</div>
|
||||
<div className="subtitle" dir="auto">
|
||||
@ -170,6 +223,26 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
onLoadMore={getMore}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{hasTags && (
|
||||
<div
|
||||
ref={tagsRef}
|
||||
className="search-tags custom-scroll-x no-scrollbar"
|
||||
key="search-tags"
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<SavedTagButton
|
||||
containerId="local-search"
|
||||
key={getReactionKey(tag.reaction)}
|
||||
reaction={tag.reaction}
|
||||
tag={tag}
|
||||
withCount
|
||||
isDisabled={areTagsDisabled}
|
||||
isChosen={isSameReaction(tag.reaction, searchTag)}
|
||||
onClick={handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isOnTop && (
|
||||
<p key="helper-text" className="helper-text" dir="auto">
|
||||
{!query ? (
|
||||
@ -189,16 +262,19 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
(global, { chatId, threadId }): StateProps => {
|
||||
const messagesById = selectChatMessages(global, chatId);
|
||||
if (!messagesById) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { query, results } = selectCurrentTextSearch(global) || {};
|
||||
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
|
||||
const { totalCount, foundIds } = results || {};
|
||||
|
||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||
|
||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
||||
|
||||
return {
|
||||
messagesById,
|
||||
@ -206,6 +282,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
totalCount,
|
||||
foundIds,
|
||||
isSavedMessages,
|
||||
savedTags,
|
||||
searchTag: savedTag,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
};
|
||||
},
|
||||
)(RightSearch));
|
||||
|
||||
@ -376,7 +376,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
||||
canInvite: getHasAdminRight(chat, 'inviteUsers'),
|
||||
exportedInvites: invites,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
};
|
||||
},
|
||||
)(ManageChannel));
|
||||
|
||||
@ -509,7 +509,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canInvite: chat.isCreator || getHasAdminRight(chat, 'inviteUsers'),
|
||||
exportedInvites: invites,
|
||||
isChannelsPremiumLimitReached: limitReachedModal?.limit === 'channels',
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
canEditForum,
|
||||
};
|
||||
},
|
||||
|
||||
@ -184,7 +184,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
enabledReactions: selectChatFullInfo(global, chatId)?.enabledReactions,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
chat,
|
||||
};
|
||||
},
|
||||
|
||||
@ -202,6 +202,6 @@ export default memo(withGlobal<OwnProps>((global, { storyView }) => {
|
||||
|
||||
return {
|
||||
peer,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
};
|
||||
})(StoryView));
|
||||
|
||||
@ -287,7 +287,7 @@ export default memo(withGlobal((global) => {
|
||||
story: story && 'content' in story ? story : undefined,
|
||||
nextOffset,
|
||||
isLoading,
|
||||
availableReactions: global.availableReactions,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
};
|
||||
})(StoryViewModal));
|
||||
|
||||
@ -42,6 +42,8 @@ function useStoryPreloader(peerId?: string | string[], aroundStoryId?: number) {
|
||||
format,
|
||||
)
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
|
||||
if (format === ApiMediaFormat.Progressive) {
|
||||
preloadProgressive(result);
|
||||
}
|
||||
|
||||
@ -205,7 +205,7 @@ const Button: FC<OwnProps> = ({
|
||||
title={ariaLabel}
|
||||
tabIndex={tabIndex}
|
||||
dir={isRtl ? 'rtl' : undefined}
|
||||
style={buildStyle(style, backgroundImage && `background-image: url(${backgroundImage})`)}
|
||||
style={buildStyle(style, backgroundImage && `background-image: url(${backgroundImage})`) || undefined}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div>
|
||||
|
||||
@ -112,5 +112,6 @@
|
||||
|
||||
&.in-portal {
|
||||
z-index: var(--z-portal-menu);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { getIsSavedDialog } from '../../helpers';
|
||||
import { getIsSavedDialog, isSameReaction } from '../../helpers';
|
||||
import {
|
||||
addActionHandler, getGlobal, setGlobal,
|
||||
} from '../../index';
|
||||
@ -36,14 +36,14 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
|
||||
|
||||
const chat = realChatId ? selectChat(global, realChatId) : undefined;
|
||||
let currentSearch = selectCurrentTextSearch(global, tabId);
|
||||
if (!chat || !currentSearch || !threadId) {
|
||||
if (!chat || !threadId || !currentSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { query, results } = currentSearch;
|
||||
const { query, results, savedTag } = currentSearch;
|
||||
const offsetId = results?.nextOffsetId;
|
||||
|
||||
if (!query) {
|
||||
if (!query && !savedTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
|
||||
limit: MESSAGE_SEARCH_SLICE,
|
||||
offsetId,
|
||||
isSavedDialog,
|
||||
savedTag,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@ -71,7 +72,7 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
|
||||
global = getGlobal();
|
||||
|
||||
currentSearch = selectCurrentTextSearch(global, tabId);
|
||||
if (!currentSearch || query !== currentSearch.query) {
|
||||
if (!currentSearch || query !== currentSearch.query || !isSameReaction(savedTag, currentSearch.savedTag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -3,13 +3,14 @@ import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { GENERAL_REFETCH_INTERVAL } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { buildCollectionByCallback, buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import requestActionTimeout from '../../../util/requestActionTimeout';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
getReactionKey,
|
||||
getUserReactions,
|
||||
isMessageLocal,
|
||||
isSameReaction,
|
||||
@ -37,7 +38,7 @@ const INTERACTION_RANDOM_OFFSET = 40;
|
||||
let interactionLocalId = 0;
|
||||
|
||||
addActionHandler('loadAvailableReactions', async (global): Promise<void> => {
|
||||
const result = await callApi('getAvailableReactions');
|
||||
const result = await callApi('fetchAvailableReactions');
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@ -61,7 +62,10 @@ addActionHandler('loadAvailableReactions', async (global): Promise<void> => {
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
availableReactions: result,
|
||||
reactions: {
|
||||
...global.reactions,
|
||||
availableReactions: result,
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
|
||||
@ -144,6 +148,8 @@ addActionHandler('toggleReaction', async (global, actions, payload): Promise<voi
|
||||
return;
|
||||
}
|
||||
|
||||
const isInSaved = selectIsChatWithSelf(global, chatId);
|
||||
|
||||
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
|
||||
const documentGroupFirstMessageId = isInDocumentGroup
|
||||
? selectMessageIdsByGroupId(global, chatId, message.groupedId!)![0]
|
||||
@ -181,6 +187,10 @@ addActionHandler('toggleReaction', async (global, actions, payload): Promise<voi
|
||||
reactions,
|
||||
shouldAddToRecent,
|
||||
});
|
||||
|
||||
if (isInSaved) {
|
||||
actions.loadSavedReactionTags();
|
||||
}
|
||||
} catch (error) {
|
||||
global = getGlobal();
|
||||
global = addMessageReaction(global, message, userReactions);
|
||||
@ -446,7 +456,9 @@ addActionHandler('readAllReactions', (global, actions, payload): ActionReturnTyp
|
||||
});
|
||||
|
||||
addActionHandler('loadTopReactions', async (global): Promise<void> => {
|
||||
const result = await callApi('fetchTopReactions', {});
|
||||
const result = await callApi('fetchTopReactions', {
|
||||
hash: global.reactions.hash.topReactions,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@ -454,13 +466,22 @@ addActionHandler('loadTopReactions', async (global): Promise<void> => {
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
topReactions: result.reactions,
|
||||
reactions: {
|
||||
...global.reactions,
|
||||
topReactions: result.reactions,
|
||||
hash: {
|
||||
...global.reactions.hash,
|
||||
topReactions: result.hash,
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadRecentReactions', async (global): Promise<void> => {
|
||||
const result = await callApi('fetchRecentReactions', {});
|
||||
const result = await callApi('fetchRecentReactions', {
|
||||
hash: global.reactions.hash.recentReactions,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@ -468,7 +489,14 @@ addActionHandler('loadRecentReactions', async (global): Promise<void> => {
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
recentReactions: result.reactions,
|
||||
reactions: {
|
||||
...global.reactions,
|
||||
recentReactions: result.reactions,
|
||||
hash: {
|
||||
...global.reactions.hash,
|
||||
recentReactions: result.hash,
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
@ -482,7 +510,89 @@ addActionHandler('clearRecentReactions', async (global): Promise<void> => {
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
recentReactions: [],
|
||||
reactions: {
|
||||
...global.reactions,
|
||||
recentReactions: [],
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadDefaultTagReactions', async (global): Promise<void> => {
|
||||
const result = await callApi('fetchDefaultTagReactions', {
|
||||
hash: global.reactions.hash.defaultTags,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
reactions: {
|
||||
...global.reactions,
|
||||
defaultTags: result.reactions,
|
||||
hash: {
|
||||
...global.reactions.hash,
|
||||
defaultTags: result.hash,
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadSavedReactionTags', async (global): Promise<void> => {
|
||||
const { hash } = global.savedReactionTags || {};
|
||||
|
||||
const result = await callApi('fetchSavedReactionTags', { hash });
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
const tagsByKey = buildCollectionByCallback(result.tags, (tag) => ([getReactionKey(tag.reaction), tag]));
|
||||
|
||||
global = {
|
||||
...global,
|
||||
savedReactionTags: {
|
||||
hash: result.hash,
|
||||
byKey: tagsByKey,
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('editSavedReactionTag', async (global, actions, payload): Promise<void> => {
|
||||
const { reaction, title } = payload;
|
||||
|
||||
const result = await callApi('updateSavedReactionTag', { reaction, title });
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
const tagsByKey = global.savedReactionTags?.byKey;
|
||||
if (!tagsByKey) return;
|
||||
|
||||
const key = getReactionKey(reaction);
|
||||
const tag = tagsByKey[key];
|
||||
|
||||
const newTag = {
|
||||
...tag,
|
||||
title,
|
||||
};
|
||||
|
||||
global = {
|
||||
...global,
|
||||
savedReactionTags: {
|
||||
...global.savedReactionTags!,
|
||||
byKey: {
|
||||
...tagsByKey,
|
||||
[key]: newTag,
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -63,6 +63,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
actions.loadRecentEmojiStatuses();
|
||||
break;
|
||||
|
||||
case 'updateSavedReactionTags':
|
||||
actions.loadSavedReactionTags();
|
||||
break;
|
||||
|
||||
case 'updateMoveStickerSetToTop': {
|
||||
const oldOrder = update.isCustomEmoji ? global.customEmojis.added.setIds : global.stickers.added.setIds;
|
||||
if (!oldOrder) return global;
|
||||
|
||||
@ -2,12 +2,13 @@ import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { buildChatThreadKey } from '../../helpers';
|
||||
import { buildChatThreadKey, isSameReaction } from '../../helpers';
|
||||
import { addActionHandler } from '../../index';
|
||||
import {
|
||||
replaceLocalTextSearchResults,
|
||||
updateLocalMediaSearchType,
|
||||
updateLocalTextSearch,
|
||||
updateLocalTextSearchTag,
|
||||
} from '../../reducers';
|
||||
import { selectCurrentMessageList, selectTabState } from '../../selectors';
|
||||
|
||||
@ -18,7 +19,7 @@ addActionHandler('openLocalTextSearch', (global, actions, payload): ActionReturn
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateLocalTextSearch(global, chatId, threadId, true, undefined, tabId);
|
||||
return updateLocalTextSearch(global, chatId, threadId, '', tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeLocalTextSearch', (global, actions, payload): ActionReturnType => {
|
||||
@ -41,7 +42,27 @@ addActionHandler('setLocalTextSearchQuery', (global, actions, payload): ActionRe
|
||||
global = replaceLocalTextSearchResults(global, chatId, threadId, MEMO_EMPTY_ARRAY, undefined, undefined, tabId);
|
||||
}
|
||||
|
||||
global = updateLocalTextSearch(global, chatId, threadId, true, query, tabId);
|
||||
global = updateLocalTextSearch(global, chatId, threadId, query, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('setLocalTextSearchTag', (global, actions, payload): ActionReturnType => {
|
||||
const { tag, tabId = getCurrentTabId() } = payload!;
|
||||
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||
const { savedTag } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
|
||||
|
||||
if (!isSameReaction(tag, savedTag)) {
|
||||
global = replaceLocalTextSearchResults(global, chatId, threadId, MEMO_EMPTY_ARRAY, undefined, undefined, tabId);
|
||||
}
|
||||
|
||||
global = updateLocalTextSearchTag(global, chatId, threadId, tag, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
@ -65,7 +86,8 @@ export function closeLocalTextSearch<T extends GlobalState>(
|
||||
return global;
|
||||
}
|
||||
|
||||
global = updateLocalTextSearch(global, chatId, threadId, false, undefined, tabId);
|
||||
global = updateLocalTextSearchTag(global, chatId, threadId, undefined, tabId);
|
||||
global = updateLocalTextSearch(global, chatId, threadId, undefined, tabId);
|
||||
global = replaceLocalTextSearchResults(global, chatId, threadId, undefined, undefined, undefined, tabId);
|
||||
return global;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable eslint-multitab-tt/no-immediate-global */
|
||||
import { addCallback, removeCallback } from '../lib/teact/teactn';
|
||||
|
||||
import type { ApiMessage } from '../api/types';
|
||||
import type { ApiAvailableReaction, ApiMessage } from '../api/types';
|
||||
import type { ActionReturnType, GlobalState, MessageList } from './types';
|
||||
import { MAIN_THREAD_ID } from '../api/types';
|
||||
|
||||
@ -181,15 +181,6 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
cached.appConfig.limits = DEFAULT_LIMITS;
|
||||
}
|
||||
|
||||
if (typeof cached.config?.defaultReaction === 'string') {
|
||||
cached.config.defaultReaction = { emoticon: cached.config.defaultReaction };
|
||||
}
|
||||
|
||||
if (typeof cached.availableReactions?.[0].reaction === 'string') {
|
||||
cached.availableReactions = cached.availableReactions
|
||||
.map((r) => ({ ...r, reaction: { emoticon: r.reaction as unknown as string } }));
|
||||
}
|
||||
|
||||
if (!cached.archiveSettings) {
|
||||
cached.archiveSettings = initialState.archiveSettings;
|
||||
}
|
||||
@ -224,6 +215,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.fileUploads.byMessageKey) {
|
||||
cached.fileUploads.byMessageKey = {};
|
||||
}
|
||||
|
||||
if (!cached.reactions) {
|
||||
cached.reactions = initialState.reactions;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache() {
|
||||
@ -269,8 +264,6 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
|
||||
'topInlineBots',
|
||||
'recentEmojis',
|
||||
'recentCustomEmojis',
|
||||
'topReactions',
|
||||
'recentReactions',
|
||||
'push',
|
||||
'serviceNotifications',
|
||||
'attachmentSettings',
|
||||
@ -282,6 +275,7 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
|
||||
'trustedBotIds',
|
||||
'recentlyFoundChatIds',
|
||||
'peerColors',
|
||||
'savedReactionTags',
|
||||
]),
|
||||
lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined,
|
||||
customEmojis: reduceCustomEmojis(global),
|
||||
@ -291,7 +285,15 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
|
||||
settings: reduceSettings(global),
|
||||
chatFolders: reduceChatFolders(global),
|
||||
groupCalls: reduceGroupCalls(global),
|
||||
availableReactions: reduceAvailableReactions(global),
|
||||
reactions: {
|
||||
...pick(global.reactions, [
|
||||
'defaultTags',
|
||||
'recentReactions',
|
||||
'topReactions',
|
||||
'hash',
|
||||
]),
|
||||
availableReactions: reduceAvailableReactions(global.reactions.availableReactions),
|
||||
},
|
||||
passcode: pick(global.passcode, [
|
||||
'isScreenLocked',
|
||||
'hasPasscode',
|
||||
@ -536,7 +538,7 @@ function reduceGroupCalls<T extends GlobalState>(global: T): GlobalState['groupC
|
||||
};
|
||||
}
|
||||
|
||||
function reduceAvailableReactions(global: GlobalState): GlobalState['availableReactions'] {
|
||||
return global.availableReactions
|
||||
function reduceAvailableReactions(availableReactions?: ApiAvailableReaction[]): ApiAvailableReaction[] | undefined {
|
||||
return availableReactions
|
||||
?.map((r) => pick(r, ['reaction', 'staticIcon', 'title', 'isInactive']));
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
ApiMessage,
|
||||
ApiReaction,
|
||||
ApiReactionCount,
|
||||
ApiReactionKey,
|
||||
ApiReactions,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState } from '../types';
|
||||
@ -22,20 +23,20 @@ export function areReactionsEmpty(reactions: ApiReactions) {
|
||||
return !reactions.results.some(({ count }) => count > 0);
|
||||
}
|
||||
|
||||
export function getReactionKey(reaction: ApiReaction): ApiReactionKey {
|
||||
if ('emoticon' in reaction) {
|
||||
return `emoji-${reaction.emoticon}`;
|
||||
}
|
||||
|
||||
return `document-${reaction.documentId}`;
|
||||
}
|
||||
|
||||
export function isSameReaction(first?: ApiReaction, second?: ApiReaction) {
|
||||
if (!first || !second) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('emoticon' in first && 'emoticon' in second) {
|
||||
return first.emoticon === second.emoticon;
|
||||
}
|
||||
|
||||
if ('documentId' in first && 'documentId' in second) {
|
||||
return first.documentId === second.documentId;
|
||||
}
|
||||
|
||||
return false;
|
||||
return getReactionKey(first) === getReactionKey(second);
|
||||
}
|
||||
|
||||
export function canSendReaction(reaction: ApiReaction, chatReactions: ApiChatReactions) {
|
||||
@ -71,14 +72,6 @@ export function getUserReactions(message: ApiMessage): ApiReaction[] {
|
||||
.map((r) => r.reaction) || [];
|
||||
}
|
||||
|
||||
export function getReactionUniqueKey(reaction: ApiReaction) {
|
||||
if ('emoticon' in reaction) {
|
||||
return reaction.emoticon;
|
||||
}
|
||||
|
||||
return reaction.documentId;
|
||||
}
|
||||
|
||||
export function isReactionChosen(reaction: ApiReactionCount) {
|
||||
return reaction.chosenOrder !== undefined;
|
||||
}
|
||||
|
||||
@ -149,8 +149,13 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
|
||||
recentEmojis: ['grinning', 'kissing_heart', 'christmas_tree', 'brain', 'trophy', 'duck', 'cherries'],
|
||||
recentCustomEmojis: ['5377305978079288312'],
|
||||
topReactions: [],
|
||||
recentReactions: [],
|
||||
|
||||
reactions: {
|
||||
defaultTags: [],
|
||||
topReactions: [],
|
||||
recentReactions: [],
|
||||
hash: {},
|
||||
},
|
||||
|
||||
stickers: {
|
||||
setsById: {},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessageSearchType } from '../../api/types';
|
||||
import type { ApiMessageSearchType, ApiReaction } from '../../api/types';
|
||||
import type { SharedMediaType, ThreadId } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
@ -9,8 +9,8 @@ import { selectTabState } from '../selectors';
|
||||
import { updateTabState } from './tabs';
|
||||
|
||||
interface TextSearchParams {
|
||||
isActive: boolean;
|
||||
query?: string;
|
||||
savedTag?: ApiReaction;
|
||||
results?: {
|
||||
totalCount?: number;
|
||||
nextOffsetId?: number;
|
||||
@ -47,7 +47,6 @@ export function updateLocalTextSearch<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
isActive: boolean,
|
||||
query?: string,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
@ -55,11 +54,29 @@ export function updateLocalTextSearch<T extends GlobalState>(
|
||||
|
||||
return replaceLocalTextSearch(global, chatThreadKey, {
|
||||
...selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey],
|
||||
isActive,
|
||||
query,
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function updateLocalTextSearchTag<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
tag?: ApiReaction,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||
|
||||
const currentSearch = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey];
|
||||
const query = currentSearch?.query || '';
|
||||
|
||||
return replaceLocalTextSearch(global, chatThreadKey, {
|
||||
...currentSearch,
|
||||
query,
|
||||
savedTag: tag,
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function replaceLocalTextSearchResults<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
SIDE_COLUMN_MAX_WIDTH,
|
||||
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
|
||||
import { updateReactionCount } from '../helpers';
|
||||
import { selectSendAs, selectTabState } from '../selectors';
|
||||
import { selectIsChatWithSelf, selectSendAs, selectTabState } from '../selectors';
|
||||
import { updateChat } from './chats';
|
||||
import { updateChatMessage } from './messages';
|
||||
|
||||
@ -42,7 +42,8 @@ export function subtractXForEmojiInteraction(global: GlobalState, x: number) {
|
||||
export function addMessageReaction<T extends GlobalState>(
|
||||
global: T, message: ApiMessage, userReactions: ApiReaction[],
|
||||
): T {
|
||||
const currentReactions = message.reactions || { results: [] };
|
||||
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
|
||||
const currentReactions = message.reactions || { results: [], areTags: isInSavedMessages };
|
||||
const currentSendAs = selectSendAs(global, message.chatId);
|
||||
|
||||
// Update UI without waiting for server response
|
||||
|
||||
@ -16,7 +16,7 @@ export function selectCurrentTextSearch<T extends GlobalState>(
|
||||
|
||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||
const currentSearch = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey];
|
||||
if (!currentSearch || !currentSearch.isActive) {
|
||||
if (!currentSearch || currentSearch.query === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -46,8 +46,10 @@ import type {
|
||||
ApiPostStatistics,
|
||||
ApiPremiumPromo,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
ApiReceipt,
|
||||
ApiReportReason,
|
||||
ApiSavedReactionTag,
|
||||
ApiSendMessageAction,
|
||||
ApiSession,
|
||||
ApiSessionData,
|
||||
@ -345,8 +347,8 @@ export type TabState = {
|
||||
|
||||
localTextSearch: {
|
||||
byChatThreadKey: Record<string, {
|
||||
isActive: boolean;
|
||||
query?: string;
|
||||
savedTag?: ApiReaction;
|
||||
results?: {
|
||||
totalCount?: number;
|
||||
nextOffsetId?: number;
|
||||
@ -872,8 +874,18 @@ export type GlobalState = {
|
||||
|
||||
recentEmojis: string[];
|
||||
recentCustomEmojis: string[];
|
||||
topReactions: ApiReaction[];
|
||||
recentReactions: ApiReaction[];
|
||||
|
||||
reactions: {
|
||||
topReactions: ApiReaction[];
|
||||
recentReactions: ApiReaction[];
|
||||
defaultTags: ApiReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
hash: {
|
||||
topReactions?: string;
|
||||
recentReactions?: string;
|
||||
defaultTags?: string;
|
||||
};
|
||||
};
|
||||
|
||||
stickers: {
|
||||
setsById: Record<string, ApiStickerSet>;
|
||||
@ -945,8 +957,6 @@ export type GlobalState = {
|
||||
};
|
||||
};
|
||||
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
|
||||
topPeers: {
|
||||
userIds?: string[];
|
||||
lastRequestedAt?: number;
|
||||
@ -997,6 +1007,11 @@ export type GlobalState = {
|
||||
translations: {
|
||||
byChatId: Record<string, ChatTranslatedMessages>;
|
||||
};
|
||||
|
||||
savedReactionTags?: {
|
||||
byKey: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
hash: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CallSound = (
|
||||
@ -1197,6 +1212,9 @@ export interface ActionPayloads {
|
||||
setLocalTextSearchQuery: {
|
||||
query?: string;
|
||||
} & WithTabId;
|
||||
setLocalTextSearchTag: {
|
||||
tag: ApiReaction | undefined;
|
||||
} & WithTabId;
|
||||
setLocalMediaSearchType: {
|
||||
mediaType: SharedMediaType;
|
||||
} & WithTabId;
|
||||
@ -2080,7 +2098,13 @@ export interface ActionPayloads {
|
||||
loadTopReactions: undefined;
|
||||
loadRecentReactions: undefined;
|
||||
loadAvailableReactions: undefined;
|
||||
loadDefaultTagReactions: undefined;
|
||||
clearRecentReactions: undefined;
|
||||
loadSavedReactionTags: undefined;
|
||||
editSavedReactionTag: {
|
||||
reaction: ApiReaction;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
loadMessageReactions: {
|
||||
chatId: string;
|
||||
|
||||
@ -49,6 +49,7 @@ const useContextMenuHandlers = (
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (contextMenuPosition) {
|
||||
return;
|
||||
@ -135,6 +136,7 @@ const useContextMenuHandlers = (
|
||||
if (isMenuDisabled) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
clearLongPressTimer();
|
||||
|
||||
timer = window.setTimeout(() => emulateContextMenuEvent(e), LONG_TAP_DURATION_MS);
|
||||
|
||||
@ -5,10 +5,10 @@ import type { IAnchorPosition } from '../types';
|
||||
interface Layout {
|
||||
extraPaddingX?: number;
|
||||
extraTopPadding?: number;
|
||||
marginSides?: number;
|
||||
extraMarginTop?: number;
|
||||
menuElMinWidth?: number;
|
||||
deltaX?: number;
|
||||
topShiftY?: number;
|
||||
shouldAvoidNegativePosition?: boolean;
|
||||
withPortal?: boolean;
|
||||
isDense?: boolean; // Allows you to place the menu as close to the edges of the area as possible
|
||||
@ -51,8 +51,8 @@ export default function useMenuPosition(
|
||||
const {
|
||||
extraPaddingX = 0,
|
||||
extraTopPadding = 0,
|
||||
marginSides = 0,
|
||||
extraMarginTop = 0,
|
||||
topShiftY = 0,
|
||||
menuElMinWidth = 0,
|
||||
deltaX = 0,
|
||||
shouldAvoidNegativePosition = false,
|
||||
@ -83,22 +83,13 @@ export default function useMenuPosition(
|
||||
}
|
||||
setPositionX(horizontalPosition);
|
||||
|
||||
if (marginSides
|
||||
&& horizontalPosition === 'right' && (x + extraPaddingX + marginSides >= rootRect.width + rootRect.left)) {
|
||||
x -= marginSides;
|
||||
}
|
||||
|
||||
if (marginSides && horizontalPosition === 'left') {
|
||||
if (x + extraPaddingX + marginSides + menuRect.width >= rootRect.width + rootRect.left) {
|
||||
x -= marginSides;
|
||||
} else if (x - marginSides <= 0) {
|
||||
x += marginSides;
|
||||
}
|
||||
}
|
||||
x += deltaX;
|
||||
|
||||
if (isDense || (y + menuRect.height < rootRect.height + rootRect.top)) {
|
||||
const yWithTopShift = y + topShiftY;
|
||||
|
||||
if (isDense || (yWithTopShift + menuRect.height < rootRect.height + rootRect.top)) {
|
||||
verticalPosition = 'top';
|
||||
y = yWithTopShift;
|
||||
} else {
|
||||
verticalPosition = 'bottom';
|
||||
|
||||
@ -106,6 +97,7 @@ export default function useMenuPosition(
|
||||
y = rootRect.top + rootRect.height;
|
||||
}
|
||||
}
|
||||
|
||||
setPositionY(verticalPosition);
|
||||
|
||||
const triggerRect = triggerEl.getBoundingClientRect();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const api = require('./api');
|
||||
|
||||
const LAYER = 172;
|
||||
const LAYER = 173;
|
||||
const tlobjects = {};
|
||||
|
||||
for (const tl of Object.values(api)) {
|
||||
|
||||
8
src/lib/gramjs/tl/api.d.ts
vendored
8
src/lib/gramjs/tl/api.d.ts
vendored
@ -11848,11 +11848,15 @@ namespace Api {
|
||||
settings: Api.TypeCodeSettings;
|
||||
};
|
||||
export class SignUp extends Request<Partial<{
|
||||
// flags: undefined;
|
||||
noJoinedNotifications?: true;
|
||||
phoneNumber: string;
|
||||
phoneCodeHash: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}>, auth.TypeAuthorization> {
|
||||
// flags: undefined;
|
||||
noJoinedNotifications?: true;
|
||||
phoneNumber: string;
|
||||
phoneCodeHash: string;
|
||||
firstName: string;
|
||||
@ -14706,8 +14710,12 @@ namespace Api {
|
||||
order: Api.TypeInputDialogPeer[];
|
||||
};
|
||||
export class GetSavedReactionTags extends Request<Partial<{
|
||||
// flags: undefined;
|
||||
peer?: Api.TypeInputPeer;
|
||||
hash: long;
|
||||
}>, messages.TypeSavedReactionTags> {
|
||||
// flags: undefined;
|
||||
peer?: Api.TypeInputPeer;
|
||||
hash: long;
|
||||
};
|
||||
export class UpdateSavedReactionTag extends Request<Partial<{
|
||||
|
||||
@ -1215,7 +1215,7 @@ invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
|
||||
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
|
||||
auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode;
|
||||
auth.signUp#80eee427 phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;
|
||||
auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;
|
||||
auth.signIn#8d52a951 flags:# phone_number:string phone_code_hash:string phone_code:flags.0?string email_verification:flags.1?EmailVerification = auth.Authorization;
|
||||
auth.logOut#3e72ba19 = auth.LoggedOut;
|
||||
auth.resetAuthorizations#9fab0d1a = Bool;
|
||||
@ -1412,6 +1412,9 @@ messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int a
|
||||
messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory;
|
||||
messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs;
|
||||
messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
|
||||
messages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long = messages.SavedReactionTags;
|
||||
messages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool;
|
||||
messages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions;
|
||||
messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
||||
|
||||
@ -288,6 +288,9 @@
|
||||
"messages.deleteSavedHistory",
|
||||
"messages.getPinnedSavedDialogs",
|
||||
"messages.toggleSavedDialogPin",
|
||||
"messages.getSavedReactionTags",
|
||||
"messages.updateSavedReactionTag",
|
||||
"messages.getDefaultTagReactions",
|
||||
"help.getPremiumPromo",
|
||||
"channels.deactivateAllUsernames",
|
||||
"channels.toggleForum",
|
||||
|
||||
@ -1662,7 +1662,7 @@ invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;
|
||||
invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X;
|
||||
|
||||
auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode;
|
||||
auth.signUp#80eee427 phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;
|
||||
auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;
|
||||
auth.signIn#8d52a951 flags:# phone_number:string phone_code_hash:string phone_code:flags.0?string email_verification:flags.1?EmailVerification = auth.Authorization;
|
||||
auth.logOut#3e72ba19 = auth.LoggedOut;
|
||||
auth.resetAuthorizations#9fab0d1a = Bool;
|
||||
@ -2002,7 +2002,7 @@ messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:
|
||||
messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs;
|
||||
messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
|
||||
messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector<InputDialogPeer> = Bool;
|
||||
messages.getSavedReactionTags#761ddacf hash:long = messages.SavedReactionTags;
|
||||
messages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long = messages.SavedReactionTags;
|
||||
messages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool;
|
||||
messages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions;
|
||||
messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate;
|
||||
|
||||
@ -227,33 +227,38 @@ $icons-map: (
|
||||
"story-priority": "\f1c4",
|
||||
"story-reply": "\f1c5",
|
||||
"strikethrough": "\f1c6",
|
||||
"timer": "\f1c7",
|
||||
"transcribe": "\f1c8",
|
||||
"truck": "\f1c9",
|
||||
"unarchive": "\f1ca",
|
||||
"underlined": "\f1cb",
|
||||
"unlock-badge": "\f1cc",
|
||||
"unlock": "\f1cd",
|
||||
"unmute": "\f1ce",
|
||||
"unpin": "\f1cf",
|
||||
"unread": "\f1d0",
|
||||
"up": "\f1d1",
|
||||
"user-filled": "\f1d2",
|
||||
"user-online": "\f1d3",
|
||||
"user": "\f1d4",
|
||||
"video-outlined": "\f1d5",
|
||||
"video-stop": "\f1d6",
|
||||
"video": "\f1d7",
|
||||
"view-once": "\f1d8",
|
||||
"voice-chat": "\f1d9",
|
||||
"volume-1": "\f1da",
|
||||
"volume-2": "\f1db",
|
||||
"volume-3": "\f1dc",
|
||||
"web": "\f1dd",
|
||||
"webapp": "\f1de",
|
||||
"word-wrap": "\f1df",
|
||||
"zoom-in": "\f1e0",
|
||||
"zoom-out": "\f1e1",
|
||||
"tag-add": "\f1c7",
|
||||
"tag-crossed": "\f1c8",
|
||||
"tag-filter": "\f1c9",
|
||||
"tag-name": "\f1ca",
|
||||
"tag": "\f1cb",
|
||||
"timer": "\f1cc",
|
||||
"transcribe": "\f1cd",
|
||||
"truck": "\f1ce",
|
||||
"unarchive": "\f1cf",
|
||||
"underlined": "\f1d0",
|
||||
"unlock-badge": "\f1d1",
|
||||
"unlock": "\f1d2",
|
||||
"unmute": "\f1d3",
|
||||
"unpin": "\f1d4",
|
||||
"unread": "\f1d5",
|
||||
"up": "\f1d6",
|
||||
"user-filled": "\f1d7",
|
||||
"user-online": "\f1d8",
|
||||
"user": "\f1d9",
|
||||
"video-outlined": "\f1da",
|
||||
"video-stop": "\f1db",
|
||||
"video": "\f1dc",
|
||||
"view-once": "\f1dd",
|
||||
"voice-chat": "\f1de",
|
||||
"volume-1": "\f1df",
|
||||
"volume-2": "\f1e0",
|
||||
"volume-3": "\f1e1",
|
||||
"web": "\f1e2",
|
||||
"webapp": "\f1e3",
|
||||
"word-wrap": "\f1e4",
|
||||
"zoom-in": "\f1e5",
|
||||
"zoom-out": "\f1e6",
|
||||
);
|
||||
|
||||
.icon-active-sessions::before {
|
||||
@ -850,6 +855,21 @@ $icons-map: (
|
||||
.icon-strikethrough::before {
|
||||
content: map.get($icons-map, "strikethrough");
|
||||
}
|
||||
.icon-tag-add::before {
|
||||
content: map.get($icons-map, "tag-add");
|
||||
}
|
||||
.icon-tag-crossed::before {
|
||||
content: map.get($icons-map, "tag-crossed");
|
||||
}
|
||||
.icon-tag-filter::before {
|
||||
content: map.get($icons-map, "tag-filter");
|
||||
}
|
||||
.icon-tag-name::before {
|
||||
content: map.get($icons-map, "tag-name");
|
||||
}
|
||||
.icon-tag::before {
|
||||
content: map.get($icons-map, "tag");
|
||||
}
|
||||
.icon-timer::before {
|
||||
content: map.get($icons-map, "timer");
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -326,7 +326,7 @@ body:not(.is-ios) {
|
||||
--color-background-compact-menu-hover: rgb(0, 0, 0, 0.4);
|
||||
--color-background-menu-separator: rgba(255, 255, 255, 0.102);
|
||||
--color-background-secondary: rgb(15, 15, 15);
|
||||
--color-background-secondary-accent: rgb(16, 15, 16);
|
||||
--color-background-secondary-accent: rgb(25, 25, 25);
|
||||
--color-background-own: rgb(118, 106, 200);
|
||||
--color-background-own-apple: rgb(118, 106, 200);
|
||||
--color-background-selected: rgb(44, 44, 44);
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"--color-background-compact-menu-reactions": ["#FFFFFFEB", "#212121DD"],
|
||||
"--color-background-compact-menu-hover": ["#00000011", "#00000066"],
|
||||
"--color-background-secondary": ["#f4f4f5", "#0F0F0F"],
|
||||
"--color-background-secondary-accent": ["#E4E4E5", "#100f10"],
|
||||
"--color-background-secondary-accent": ["#E4E4E5", "#191919"],
|
||||
"--color-background-own": ["#EEFFDE", "#766AC8"],
|
||||
"--color-background-own-apple": ["#DCF8C5", "#766AC8"],
|
||||
"--color-background-selected": ["#F4F4F5", "#2C2C2C"],
|
||||
|
||||
@ -197,6 +197,11 @@ export type FontIconName =
|
||||
| 'story-priority'
|
||||
| 'story-reply'
|
||||
| 'strikethrough'
|
||||
| 'tag-add'
|
||||
| 'tag-crossed'
|
||||
| 'tag-filter'
|
||||
| 'tag-name'
|
||||
| 'tag'
|
||||
| 'timer'
|
||||
| 'transcribe'
|
||||
| 'truck'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user