Introduce Saved tags (#4244)

This commit is contained in:
Alexander Zinchuk 2024-02-23 14:06:06 +01:00
parent 4c71117b61
commit c4736edbb4
88 changed files with 1836 additions and 775 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

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

View File

@ -0,0 +1,3 @@
.subtitle {
margin-bottom: 1rem;
}

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

View File

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

View File

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

View File

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

View File

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

View File

@ -184,7 +184,7 @@ export default memo(withGlobal<OwnProps>(
return {
enabledReactions: selectChatFullInfo(global, chatId)?.enabledReactions,
availableReactions: global.availableReactions,
availableReactions: global.reactions.availableReactions,
chat,
};
},

View File

@ -202,6 +202,6 @@ export default memo(withGlobal<OwnProps>((global, { storyView }) => {
return {
peer,
availableReactions: global.availableReactions,
availableReactions: global.reactions.availableReactions,
};
})(StoryView));

View File

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

View File

@ -42,6 +42,8 @@ function useStoryPreloader(peerId?: string | string[], aroundStoryId?: number) {
format,
)
.then((result) => {
if (!result) return;
if (format === ApiMediaFormat.Progressive) {
preloadProgressive(result);
}

View File

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

View File

@ -112,5 +112,6 @@
&.in-portal {
z-index: var(--z-portal-menu);
position: absolute;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
const api = require('./api');
const LAYER = 172;
const LAYER = 173;
const tlobjects = {};
for (const tl of Object.values(api)) {

View File

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

View File

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

View File

@ -288,6 +288,9 @@
"messages.deleteSavedHistory",
"messages.getPinnedSavedDialogs",
"messages.toggleSavedDialogPin",
"messages.getSavedReactionTags",
"messages.updateSavedReactionTag",
"messages.getDefaultTagReactions",
"help.getPremiumPromo",
"channels.deactivateAllUsernames",
"channels.toggleForum",

View File

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

View File

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

View File

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

View File

@ -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"],

View File

@ -197,6 +197,11 @@ export type FontIconName =
| 'story-priority'
| 'story-reply'
| 'strikethrough'
| 'tag-add'
| 'tag-crossed'
| 'tag-filter'
| 'tag-name'
| 'tag'
| 'timer'
| 'transcribe'
| 'truck'