diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 6710e8034..207c62ac8 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -35,7 +35,7 @@ export { reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage, - fetchOutboxReadDate, + fetchOutboxReadDate, exportMessageLink, deleteSavedHistory, } from './messages'; diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index b02562f04..c63f48cd9 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1927,3 +1927,21 @@ export async function fetchOutboxReadDate({ chat, messageId }: { chat: ApiChat; return { date: result.date }; } + +export async function exportMessageLink({ + id, chat, shouldIncludeThread, shouldIncludeGrouped, +}: { + id: number; + chat: ApiChat; + shouldIncludeThread?: boolean; + shouldIncludeGrouped?: boolean; +}) { + const result = await invokeRequest(new GramJs.channels.ExportMessageLink({ + channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, + id, + thread: shouldIncludeThread || undefined, + grouped: shouldIncludeGrouped || undefined, + })); + + return result?.link; +} diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 7d97a774c..653d6d018 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -4,11 +4,18 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; -import type { - ApiAvailableReaction, ApiChatReactions, ApiMessage, ApiReaction, ApiStickerSet, ApiStickerSetInfo, ApiThreadInfo, -} from '../../../api/types'; import type { MessageListType, TabState } from '../../../global/types'; -import type { IAlbum, IAnchorPosition } from '../../../types'; +import type { IAlbum, IAnchorPosition, ThreadId } from '../../../types'; +import { + type ApiAvailableReaction, + type ApiChatReactions, + type ApiMessage, + type ApiReaction, + type ApiStickerSet, + type ApiStickerSetInfo, + type ApiThreadInfo, + MAIN_THREAD_ID, +} from '../../../api/types'; import { PREVIEW_AVATAR_COUNT, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { @@ -38,7 +45,6 @@ import { selectIsPremiumPurchaseBlocked, selectIsReactionPickerOpen, selectMessageCustomEmojiSets, - selectMessageLink, selectMessageTranslations, selectRequestedChatTranslationLanguage, selectRequestedMessageTranslationLanguage, @@ -77,6 +83,7 @@ export type OwnProps = { }; type StateProps = { + threadId?: ThreadId; availableReactions?: ApiAvailableReaction[]; topReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[]; @@ -120,13 +127,13 @@ type StateProps = { maxUniqueReactions?: number; canPlayAnimatedEmojis?: boolean; isReactionPickerOpen?: boolean; - messageLink?: string; isInSavedMessages?: boolean; }; const selection = window.getSelection(); const ContextMenuContainer: FC = ({ + threadId, availableReactions, topReactions, defaultTagReactions, @@ -178,7 +185,6 @@ const ContextMenuContainer: FC = ({ canShowOriginal, canSelectLanguage, isReactionPickerOpen, - messageLink, isInSavedMessages, onClose, onCloseAnimationEnd, @@ -213,6 +219,7 @@ const ContextMenuContainer: FC = ({ openMessageReactionPicker, openPremiumModal, loadOutboxReadDate, + copyMessageLink, } = getActions(); const lang = useLang(); @@ -459,7 +466,12 @@ const ContextMenuContainer: FC = ({ }); const handleCopyLink = useLastCallback(() => { - copyTextToClipboard(messageLink!); + copyMessageLink({ + chatId: message.chatId, + messageId: message.id, + shouldIncludeThread: threadId !== MAIN_THREAD_ID, + shouldIncludeGrouped: true, // TODO: Provide correct value when ability to target specific message is added + }); closeMenu(); }); @@ -728,11 +740,11 @@ export default memo(withGlobal( : undefined; const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage); const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId); - const messageLink = selectMessageLink(global, message.chatId, threadId, message.id); const isInSavedMessages = selectIsChatWithSelf(global, message.chatId); return { + threadId, availableReactions, topReactions, defaultTagReactions: defaultTags, @@ -777,7 +789,6 @@ export default memo(withGlobal( isMessageTranslated: hasTranslation, canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), isReactionPickerOpen: selectIsReactionPickerOpen(global), - messageLink, isInSavedMessages, }; }, diff --git a/src/components/middle/message/helpers/copyOptions.ts b/src/components/middle/message/helpers/copyOptions.ts index d1efb52bb..4d4a80adc 100644 --- a/src/components/middle/message/helpers/copyOptions.ts +++ b/src/components/middle/message/helpers/copyOptions.ts @@ -103,11 +103,7 @@ export function getMessageCopyOptions( options.push({ label: 'lng_context_copy_message_link', icon: 'link', - handler: () => { - onCopyLink(); - - afterEffect?.(); - }, + handler: onCopyLink, }); } diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 36e707dbb..5c3289f66 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -33,6 +33,7 @@ import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, } from '../../../config'; +import { copyTextToClipboard } from '../../../util/clipboard'; import { isDeepLink } from '../../../util/deepLinkParser'; import { ensureProtocol } from '../../../util/ensureProtocol'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; @@ -53,6 +54,7 @@ import { getIsSavedDialog, getUserFullName, isChatChannel, + isChatSuperGroup, isDeletedUser, isMessageLocal, isServiceNotificationMessage, @@ -1900,6 +1902,49 @@ addActionHandler('loadOutboxReadDate', async (global, actions, payload): Promise } }); +addActionHandler('copyMessageLink', async (global, actions, payload): Promise => { + const { + chatId, messageId, shouldIncludeThread, shouldIncludeGrouped, tabId = getCurrentTabId(), + } = payload; + const chat = selectChat(global, chatId); + if (!chat) { + actions.showNotification({ + message: translate('ErrorOccurred'), + tabId, + }); + return; + } + + if (!isChatChannel(chat) && !isChatSuperGroup(chat)) { + actions.showNotification({ + message: translate('lng_filters_link_private_error'), + tabId, + }); + return; + } + + const link = await callApi('exportMessageLink', { + chat, + id: messageId, + shouldIncludeThread, + shouldIncludeGrouped, + }); + + if (!link) { + actions.showNotification({ + message: translate('ErrorOccurred'), + tabId, + }); + return; + } + + copyTextToClipboard(link); + actions.showNotification({ + message: translate('LinkCopied'), + tabId, + }); +}); + function countSortedIds(ids: number[], from: number, to: number) { let count = 0; diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index d5fe98073..5a2ed1666 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -1414,29 +1414,21 @@ export function selectCanTranslateMessage( && !chatRequestedLanguage; } -export function selectMessageLink( - global: T, chatId: string, threadId?: ThreadId, messageId?: number, +export function selectTopicLink( + global: T, chatId: string, topicId?: ThreadId, ) { const chat = selectChat(global, chatId); - if (!chat) { + if (!chat || !chat?.isForum) { return undefined; } const chatUsername = getMainUsername(chat); - const isChannelId = isChatChannel(chat) || isChatSuperGroup(chat); - const normalizedId = isChannelId ? chatId.replace('-100', '') : chatId.replace('-', ''); + const normalizedId = chatId.replace('-100', ''); const chatPart = chatUsername || `c/${normalizedId}`; - const threadPart = threadId && threadId !== MAIN_THREAD_ID ? `/${threadId}` : ''; - const messagePart = messageId ? `/${messageId}` : ''; - return `${TME_LINK_PREFIX}${chatPart}${threadPart}${messagePart}`; -} - -export function selectTopicLink( - global: T, chatId: string, topicId?: ThreadId, -) { - return selectMessageLink(global, chatId, topicId); + const topicPart = topicId && topicId !== MAIN_THREAD_ID ? `/${topicId}` : ''; + return `${TME_LINK_PREFIX}${chatPart}${topicPart}`; } export function selectMessageReplyInfo( diff --git a/src/global/types.ts b/src/global/types.ts index a88de4726..b16db9c1f 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -2054,6 +2054,12 @@ export interface ActionPayloads { markMentionsRead: { messageIds: number[]; } & WithTabId; + copyMessageLink: { + chatId: string; + messageId: number; + shouldIncludeThread?: boolean; + shouldIncludeGrouped?: boolean; + } & WithTabId; sendPollVote: { chatId: string; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 4f7822ef8..017186f36 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1455,6 +1455,7 @@ channels.joinChannel#24b524c5 channel:InputChannel = Updates; channels.leaveChannel#f836aa95 channel:InputChannel = Updates; channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector = Updates; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; +channels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink; channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; channels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates; channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = Bool; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index dafbc6975..3a693b2b7 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -202,6 +202,7 @@ "channels.leaveChannel", "channels.inviteToChannel", "channels.deleteChannel", + "channels.exportMessageLink", "channels.toggleSignatures", "channels.editBanned", "channels.readMessageContents",