= ({
onLoadMore={getMore}
onKeyDown={handleKeyDown}
>
+ {hasTags && (
+
+ {tags.map((tag) => (
+
+ ))}
+
+ )}
{isOnTop && (
{!query ? (
@@ -189,16 +262,19 @@ const RightSearch: FC = ({
};
export default memo(withGlobal(
- (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(
totalCount,
foundIds,
isSavedMessages,
+ savedTags,
+ searchTag: savedTag,
+ isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
},
)(RightSearch));
diff --git a/src/components/right/management/ManageChannel.tsx b/src/components/right/management/ManageChannel.tsx
index 0d4b8c695..ecaac702e 100644
--- a/src/components/right/management/ManageChannel.tsx
+++ b/src/components/right/management/ManageChannel.tsx
@@ -376,7 +376,7 @@ export default memo(withGlobal(
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
canInvite: getHasAdminRight(chat, 'inviteUsers'),
exportedInvites: invites,
- availableReactions: global.availableReactions,
+ availableReactions: global.reactions.availableReactions,
};
},
)(ManageChannel));
diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx
index 001a0581e..da7f72d8b 100644
--- a/src/components/right/management/ManageGroup.tsx
+++ b/src/components/right/management/ManageGroup.tsx
@@ -509,7 +509,7 @@ export default memo(withGlobal(
canInvite: chat.isCreator || getHasAdminRight(chat, 'inviteUsers'),
exportedInvites: invites,
isChannelsPremiumLimitReached: limitReachedModal?.limit === 'channels',
- availableReactions: global.availableReactions,
+ availableReactions: global.reactions.availableReactions,
canEditForum,
};
},
diff --git a/src/components/right/management/ManageReactions.tsx b/src/components/right/management/ManageReactions.tsx
index 8a951375f..c660bbf2b 100644
--- a/src/components/right/management/ManageReactions.tsx
+++ b/src/components/right/management/ManageReactions.tsx
@@ -184,7 +184,7 @@ export default memo(withGlobal(
return {
enabledReactions: selectChatFullInfo(global, chatId)?.enabledReactions,
- availableReactions: global.availableReactions,
+ availableReactions: global.reactions.availableReactions,
chat,
};
},
diff --git a/src/components/story/StoryView.tsx b/src/components/story/StoryView.tsx
index 859298881..d42124569 100644
--- a/src/components/story/StoryView.tsx
+++ b/src/components/story/StoryView.tsx
@@ -202,6 +202,6 @@ export default memo(withGlobal((global, { storyView }) => {
return {
peer,
- availableReactions: global.availableReactions,
+ availableReactions: global.reactions.availableReactions,
};
})(StoryView));
diff --git a/src/components/story/StoryViewModal.tsx b/src/components/story/StoryViewModal.tsx
index c2e86bd06..092fee947 100644
--- a/src/components/story/StoryViewModal.tsx
+++ b/src/components/story/StoryViewModal.tsx
@@ -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));
diff --git a/src/components/story/hooks/useStoryPreloader.ts b/src/components/story/hooks/useStoryPreloader.ts
index b2d3d9a57..cb078d5d5 100644
--- a/src/components/story/hooks/useStoryPreloader.ts
+++ b/src/components/story/hooks/useStoryPreloader.ts
@@ -42,6 +42,8 @@ function useStoryPreloader(peerId?: string | string[], aroundStoryId?: number) {
format,
)
.then((result) => {
+ if (!result) return;
+
if (format === ApiMediaFormat.Progressive) {
preloadProgressive(result);
}
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 87c929bfb..908b109ba 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -205,7 +205,7 @@ const Button: FC = ({
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 ? (
diff --git a/src/components/ui/Menu.scss b/src/components/ui/Menu.scss
index 2ffdb91f2..44731c9d5 100644
--- a/src/components/ui/Menu.scss
+++ b/src/components/ui/Menu.scss
@@ -112,5 +112,6 @@
&.in-portal {
z-index: var(--z-portal-menu);
+ position: absolute;
}
}
diff --git a/src/global/actions/api/localSearch.ts b/src/global/actions/api/localSearch.ts
index 4a0ae58a6..4e8490474 100644
--- a/src/global/actions/api/localSearch.ts
+++ b/src/global/actions/api/localSearch.ts
@@ -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;
}
diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts
index 05be89a8e..793acf65f 100644
--- a/src/global/actions/api/reactions.ts
+++ b/src/global/actions/api/reactions.ts
@@ -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
=> {
- const result = await callApi('getAvailableReactions');
+ const result = await callApi('fetchAvailableReactions');
if (!result) {
return;
}
@@ -61,7 +62,10 @@ addActionHandler('loadAvailableReactions', async (global): Promise => {
global = getGlobal();
global = {
...global,
- availableReactions: result,
+ reactions: {
+ ...global.reactions,
+ availableReactions: result,
+ },
};
setGlobal(global);
@@ -144,6 +148,8 @@ addActionHandler('toggleReaction', async (global, actions, payload): Promise => {
- 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 => {
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 => {
- 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 => {
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 => {
global = getGlobal();
global = {
...global,
- recentReactions: [],
+ reactions: {
+ ...global.reactions,
+ recentReactions: [],
+ },
+ };
+ setGlobal(global);
+});
+
+addActionHandler('loadDefaultTagReactions', async (global): Promise => {
+ 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 => {
+ 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 => {
+ 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);
});
diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts
index 426f95c1c..c7a6597ac 100644
--- a/src/global/actions/apiUpdaters/misc.ts
+++ b/src/global/actions/apiUpdaters/misc.ts
@@ -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;
diff --git a/src/global/actions/ui/localSearch.ts b/src/global/actions/ui/localSearch.ts
index d687f4ef9..92dc1fd16 100644
--- a/src/global/actions/ui/localSearch.ts
+++ b/src/global/actions/ui/localSearch.ts
@@ -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(
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;
}
diff --git a/src/global/cache.ts b/src/global/cache.ts
index eb4ee2f0a..dd78f5245 100644
--- a/src/global/cache.ts
+++ b/src/global/cache.ts
@@ -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(global: T) {
'topInlineBots',
'recentEmojis',
'recentCustomEmojis',
- 'topReactions',
- 'recentReactions',
'push',
'serviceNotifications',
'attachmentSettings',
@@ -282,6 +275,7 @@ export function serializeGlobal(global: T) {
'trustedBotIds',
'recentlyFoundChatIds',
'peerColors',
+ 'savedReactionTags',
]),
lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined,
customEmojis: reduceCustomEmojis(global),
@@ -291,7 +285,15 @@ export function serializeGlobal(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(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']));
}
diff --git a/src/global/helpers/reactions.ts b/src/global/helpers/reactions.ts
index 173d0251f..f4c31152d 100644
--- a/src/global/helpers/reactions.ts
+++ b/src/global/helpers/reactions.ts
@@ -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;
}
diff --git a/src/global/initialState.ts b/src/global/initialState.ts
index 50b26b39f..760659b0d 100644
--- a/src/global/initialState.ts
+++ b/src/global/initialState.ts
@@ -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: {},
diff --git a/src/global/reducers/localSearch.ts b/src/global/reducers/localSearch.ts
index 713bc7548..3d37e7783 100644
--- a/src/global/reducers/localSearch.ts
+++ b/src/global/reducers/localSearch.ts
@@ -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(
global: T,
chatId: string,
threadId: ThreadId,
- isActive: boolean,
query?: string,
...[tabId = getCurrentTabId()]: TabArgs
): T {
@@ -55,11 +54,29 @@ export function updateLocalTextSearch(
return replaceLocalTextSearch(global, chatThreadKey, {
...selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey],
- isActive,
query,
}, tabId);
}
+export function updateLocalTextSearchTag(
+ global: T,
+ chatId: string,
+ threadId: ThreadId,
+ tag?: ApiReaction,
+ ...[tabId = getCurrentTabId()]: TabArgs
+): 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(
global: T,
chatId: string,
diff --git a/src/global/reducers/reactions.ts b/src/global/reducers/reactions.ts
index 1f8f002ef..b2c11245e 100644
--- a/src/global/reducers/reactions.ts
+++ b/src/global/reducers/reactions.ts
@@ -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(
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
diff --git a/src/global/selectors/localSearch.ts b/src/global/selectors/localSearch.ts
index d94920747..22cf452f4 100644
--- a/src/global/selectors/localSearch.ts
+++ b/src/global/selectors/localSearch.ts
@@ -16,7 +16,7 @@ export function selectCurrentTextSearch(
const chatThreadKey = buildChatThreadKey(chatId, threadId);
const currentSearch = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey];
- if (!currentSearch || !currentSearch.isActive) {
+ if (!currentSearch || currentSearch.query === undefined) {
return undefined;
}
diff --git a/src/global/types.ts b/src/global/types.ts
index a6944b403..881264885 100644
--- a/src/global/types.ts
+++ b/src/global/types.ts
@@ -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;
@@ -945,8 +957,6 @@ export type GlobalState = {
};
};
- availableReactions?: ApiAvailableReaction[];
-
topPeers: {
userIds?: string[];
lastRequestedAt?: number;
@@ -997,6 +1007,11 @@ export type GlobalState = {
translations: {
byChatId: Record;
};
+
+ savedReactionTags?: {
+ byKey: Record;
+ 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;
diff --git a/src/hooks/useContextMenuHandlers.ts b/src/hooks/useContextMenuHandlers.ts
index ef368e6d1..7a61fd5f5 100644
--- a/src/hooks/useContextMenuHandlers.ts
+++ b/src/hooks/useContextMenuHandlers.ts
@@ -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);
diff --git a/src/hooks/useMenuPosition.ts b/src/hooks/useMenuPosition.ts
index 74571685f..90779ad8d 100644
--- a/src/hooks/useMenuPosition.ts
+++ b/src/hooks/useMenuPosition.ts
@@ -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();
diff --git a/src/lib/gramjs/tl/AllTLObjects.js b/src/lib/gramjs/tl/AllTLObjects.js
index 95c3c96e6..fbb01803d 100644
--- a/src/lib/gramjs/tl/AllTLObjects.js
+++ b/src/lib/gramjs/tl/AllTLObjects.js
@@ -1,6 +1,6 @@
const api = require('./api');
-const LAYER = 172;
+const LAYER = 173;
const tlobjects = {};
for (const tl of Object.values(api)) {
diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts
index f663d770d..7c21e80eb 100644
--- a/src/lib/gramjs/tl/api.d.ts
+++ b/src/lib/gramjs/tl/api.d.ts
@@ -11848,11 +11848,15 @@ namespace Api {
settings: Api.TypeCodeSettings;
};
export class SignUp extends Request, 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, messages.TypeSavedReactionTags> {
+ // flags: undefined;
+ peer?: Api.TypeInputPeer;
hash: long;
};
export class UpdateSavedReactionTag extends Request = 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;
diff --git a/src/styles/icons.scss b/src/styles/icons.scss
index e5b128566..2571103a1 100644
--- a/src/styles/icons.scss
+++ b/src/styles/icons.scss
@@ -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");
}
diff --git a/src/styles/icons.woff b/src/styles/icons.woff
index f1dbca30b..b342279b0 100644
Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ
diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2
index 6de97315d..871d36114 100644
Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 4488b4032..4da163622 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -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);
diff --git a/src/styles/themes.json b/src/styles/themes.json
index 1278b2a59..cc06e0c43 100644
--- a/src/styles/themes.json
+++ b/src/styles/themes.json
@@ -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"],
diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts
index ad85f4562..8d91a7400 100644
--- a/src/types/icons/font.ts
+++ b/src/types/icons/font.ts
@@ -197,6 +197,11 @@ export type FontIconName =
| 'story-priority'
| 'story-reply'
| 'strikethrough'
+ | 'tag-add'
+ | 'tag-crossed'
+ | 'tag-filter'
+ | 'tag-name'
+ | 'tag'
| 'timer'
| 'transcribe'
| 'truck'