diff --git a/Telegram/Resources/langs/rewrites/en.json b/Telegram/Resources/langs/rewrites/en.json index 7e7140b10..40b4e2101 100644 --- a/Telegram/Resources/langs/rewrites/en.json +++ b/Telegram/Resources/langs/rewrites/en.json @@ -108,7 +108,21 @@ "ktg_group_id_copied": "Group ID copied to clipboard.", "ktg_supergroup_id_copied": "Supergroup ID copied to clipboard.", "ktg_channel_id_copied": "Channel ID copied to clipboard.", + "ktg_forward_go_to_chat": "Go to chat", "ktg_settings_forward": "Forward", + "ktg_settings_forward_retain_selection": "Retain selection after forward", + "ktg_settings_forward_chat_on_click": "Open chat on click", + "ktg_settings_forward_chat_on_click_description": "You can hold Ctrl to select multiple chats regardless of this option.", + "ktg_forward_menu_quoted": "Quoted", + "ktg_forward_menu_unquoted": "Unquoted with captions", + "ktg_forward_menu_uncaptioned": "Unquoted without captions", + "ktg_forward_menu_default_albums": "Preserve albums", + "ktg_forward_menu_group_all_media": "Group all media", + "ktg_forward_menu_separate_messages": "Separate messages", + "ktg_forward_subtitle_unquoted": "unquoted", + "ktg_forward_subtitle_uncaptioned": "uncaptioned", + "ktg_forward_subtitle_group_all_media": "as albums", + "ktg_forward_subtitle_separate_messages": "one by one", "ktg_filters_exclude_not_owned": "Not owned", "ktg_filters_exclude_not_admin": "Not administrated", "ktg_filters_exclude_owned": "Owned", @@ -130,6 +144,20 @@ "ktg_filters_hide_all_chats_toast": "\"All Chats\" folder is hidden.\nYou can enable it back in Kotatogram Settings.", "ktg_filters_hide_edit_toast": "Edit button is hidden.\nYou can enable it back in Kotatogram Settings.", "ktg_settings_telegram_sites_autologin": "Auto-login on Telegram sites", + "ktg_forward_sender_names_and_captions_removed": "Sender names and captions removed", + "ktg_forward_remember_mode": "Remember forward mode", + "ktg_forward_mode": "Forward mode", + "ktg_forward_mode_quoted": "Quoted", + "ktg_forward_mode_unquoted": "Unquoted", + "ktg_forward_mode_uncaptioned": "Uncaptioned", + "ktg_forward_grouping_mode": "Grouping mode", + "ktg_forward_grouping_mode_preserve_albums": "Same as original", + "ktg_forward_grouping_mode_regroup": "Regroup media", + "ktg_forward_grouping_mode_regroup_desc": "Unquoted and uncaptioned only", + "ktg_forward_grouping_mode_separate": "Separate", + "ktg_forward_force_old_unquoted": "Old unquoted forward method", + "ktg_forward_force_old_unquoted_desc": "Old method copies messages content on client rather than server. Currently it's used only for \"Regroup media\" grouping mode, since new one doesn't support it. If for some reason unquoted forward doesn't work correctly, try switching this option.", + "ktg_forward_quiz_unquoted": "Sorry, quizzes that are currently open and unvoted on cannot be forwarded unquoted.", "ktg_in_app_update_disabled": "In-app updater is disabled.", "dummy_last_string": "" } diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index dfba1d930..37c334847 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "data/data_document.h" #include "data/data_photo.h" +#include "data/data_location.h" #include "data/data_channel.h" // ChannelData::addsSignature. #include "data/data_user.h" // UserData::name #include "data/data_session.h" @@ -65,7 +66,9 @@ void SendExistingMedia( not_null media, Fn inputMedia, Data::FileOrigin origin, - std::optional localMessageId) { + std::optional localMessageId, + Fn doneCallback = nullptr, + bool forwarding = false) { const auto history = message.action.history; const auto peer = history->peer; const auto session = &history->session(); @@ -146,7 +149,6 @@ void SendExistingMedia( const auto performRequest = [=](const auto &repeatRequest) -> void { auto &histories = history->owner().histories(); - const auto usedFileReference = media->fileReference(); histories.sendPreparedMessage( history, message.action.replyTo, @@ -168,6 +170,7 @@ void SendExistingMedia( }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 && error.type().startsWith(u"FILE_REFERENCE_"_q)) { + const auto usedFileReference = media->fileReference(); api->refreshFileReference(origin, [=](const auto &result) { if (media->fileReference() != usedFileReference) { repeatRequest(repeatRequest); @@ -182,15 +185,41 @@ void SendExistingMedia( }; performRequest(performRequest); - api->finishForwarding(message.action); + if (!forwarding) { + api->finishForwarding(message.action); + } } } // namespace +void SendWebDocument( + Api::MessageToSend &&message, + not_null document, + std::optional localMessageId, + Fn doneCallback, + bool forwarding) { + const auto inputMedia = [=] { + return MTP_inputMediaDocumentExternal( + MTP_flags(0), + MTP_string(document->url()), + MTPint()); // ttl_seconds + }; + SendExistingMedia( + std::move(message), + document, + inputMedia, + document->stickerOrGifOrigin(), + std::move(localMessageId), + (doneCallback ? std::move(doneCallback) : nullptr), + forwarding); +} + void SendExistingDocument( MessageToSend &&message, not_null document, - std::optional localMessageId) { + std::optional localMessageId, + Fn doneCallback, + bool forwarding) { const auto inputMedia = [=] { return MTP_inputMediaDocument( MTP_flags(0), @@ -203,7 +232,9 @@ void SendExistingDocument( document, inputMedia, document->stickerOrGifOrigin(), - std::move(localMessageId)); + std::move(localMessageId), + (doneCallback ? std::move(doneCallback) : nullptr), + forwarding); if (document->sticker()) { document->owner().stickers().incrementSticker(document); @@ -213,7 +244,9 @@ void SendExistingDocument( void SendExistingPhoto( MessageToSend &&message, not_null photo, - std::optional localMessageId) { + std::optional localMessageId, + Fn doneCallback, + bool forwarding) { const auto inputMedia = [=] { return MTP_inputMediaPhoto( MTP_flags(0), @@ -225,10 +258,15 @@ void SendExistingPhoto( photo, inputMedia, Data::FileOrigin(), - std::move(localMessageId)); + std::move(localMessageId), + (doneCallback ? std::move(doneCallback) : nullptr), + forwarding); } -bool SendDice(MessageToSend &message) { +bool SendDice( + MessageToSend &message, + Fn doneCallback, + bool forwarding) { const auto full = QStringView(message.textWithTags.text).trimmed(); auto length = 0; if (!Ui::Emoji::Find(full.data(), full.data() + full.size(), &length) @@ -338,7 +376,9 @@ bool SendDice(MessageToSend &message) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); }); - api->finishForwarding(message.action); + if (!forwarding) { + api->finishForwarding(message.action); + } return true; } @@ -508,4 +548,75 @@ void SendConfirmedFile( } } +void SendLocationPoint( + const Data::LocationPoint &data, + const SendAction &action, + Fn done, + Fn fail) { + const auto history = action.history; + const auto session = &history->session(); + const auto api = &session->api(); + const auto peer = history->peer; + api->sendAction(action); + + auto sendFlags = MTPmessages_SendMedia::Flags(0); + if (action.replyTo) { + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; + if (action.topicRootId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; + } + } + const auto topicRootId = action.topicRootId; + if (action.clearDraft) { + sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; + history->clearLocalDraft(topicRootId); + history->clearCloudDraft(topicRootId); + } + const auto sendAs = action.options.sendAs; + + if (sendAs) { + sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; + } + const auto silentPost = ShouldSendSilent(peer, action.options); + if (silentPost) { + sendFlags |= MTPmessages_SendMedia::Flag::f_silent; + } + if (action.options.scheduled) { + sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + } + auto &histories = history->owner().histories(); + const auto requestType = Data::Histories::RequestType::Send; + histories.sendRequest(history, requestType, [=](Fn finish) { + history->sendRequestId = api->request(MTPmessages_SendMedia( + MTP_flags(sendFlags), + peer->input, + MTP_int(action.replyTo), + MTP_int(topicRootId), + MTP_inputMediaGeoPoint( + MTP_inputGeoPoint( + MTP_flags(0), + MTP_double(data.lat()), + MTP_double(data.lon()), + MTP_int(0))), + MTP_string(), + MTP_long(base::RandomValue()), + MTPReplyMarkup(), + MTPVector(), + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + )).done([=](const MTPUpdates &result) mutable { + api->applyUpdates(result); + done(); + finish(); + }).fail([=](const MTP::Error &error) mutable { + if (fail) { + fail(error); + } + finish(); + }).afterRequest(history->sendRequestId + ).send(); + return history->sendRequestId; + }); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_sending.h b/Telegram/SourceFiles/api/api_sending.h index e17c66f3e..674ffca89 100644 --- a/Telegram/SourceFiles/api/api_sending.h +++ b/Telegram/SourceFiles/api/api_sending.h @@ -16,22 +16,44 @@ class PhotoData; class DocumentData; struct FileLoadResult; +namespace MTP { +class Error; +} // namespace MTP + +namespace Data { +class LocationPoint; +} // namespace Data + namespace Api { struct MessageToSend; struct SendAction; +void SendWebDocument( + MessageToSend &&message, + not_null document, + std::optional localMessageId = std::nullopt, + Fn doneCallback = nullptr, + bool forwarding = false); + void SendExistingDocument( MessageToSend &&message, not_null document, - std::optional localMessageId = std::nullopt); + std::optional localMessageId = std::nullopt, + Fn doneCallback = nullptr, + bool forwarding = false); void SendExistingPhoto( MessageToSend &&message, not_null photo, - std::optional localMessageId = std::nullopt); + std::optional localMessageId = std::nullopt, + Fn doneCallback = nullptr, + bool forwarding = false); -bool SendDice(MessageToSend &message); +bool SendDice( + MessageToSend &message, + Fn doneCallback = nullptr, + bool forwarding = false); void FillMessagePostFlags( const SendAction &action, @@ -42,4 +64,10 @@ void SendConfirmedFile( not_null session, const std::shared_ptr &file); +void SendLocationPoint( + const Data::LocationPoint &data, + const SendAction &action, + Fn done, + Fn fail); + } // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index c60f96862..4029a01a9 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "apiwrap.h" +#include "kotato/kotato_settings.h" #include "api/api_authorizations.h" #include "api/api_attached_stickers.h" #include "api/api_blocked_peers.h" @@ -36,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_drafts.h" #include "data/data_changes.h" #include "data/data_photo.h" +#include "data/data_poll.h" #include "data/data_web_page.h" #include "data/data_folder.h" #include "data/data_forum_topic.h" @@ -3134,6 +3136,12 @@ void ApiWrap::forwardMessages( Data::ResolvedForwardDraft &&draft, const SendAction &action, FnMut &&successCallback) { + if (draft.options != Data::ForwardOptions::PreserveInfo + && (draft.groupOptions == Data::GroupingOptions::RegroupAll + || ::Kotato::JsonSettings::GetBool("forward_force_old_unquoted"))) { + forwardMessagesUnquoted(std::move(draft), action, std::move(successCallback)); + return; + } Expects(!draft.items.empty()); auto &histories = _session->data().histories(); @@ -3191,6 +3199,7 @@ void ApiWrap::forwardMessages( } auto forwardFrom = draft.items.front()->history()->peer; + auto forwardGroupId = draft.items.front()->groupId(); auto ids = QVector(); auto randomIds = QVector(); auto localIds = std::shared_ptr>(); @@ -3269,9 +3278,14 @@ void ApiWrap::forwardMessages( localIds->emplace(randomId, newId); } const auto newFrom = item->history()->peer; - if (forwardFrom != newFrom) { + const auto newGroupId = item->groupId(); + if (item != draft.items.front() && + ((draft.groupOptions == Data::GroupingOptions::GroupAsIs + && (forwardGroupId != newGroupId || forwardFrom != newFrom)) + || draft.groupOptions == Data::GroupingOptions::Separate)) { sendAccumulated(); forwardFrom = newFrom; + forwardGroupId = newGroupId; } ids.push_back(MTP_int(item->id)); randomIds.push_back(MTP_long(randomId)); @@ -3280,6 +3294,499 @@ void ApiWrap::forwardMessages( _session->data().sendHistoryChangeNotifications(); } +void ApiWrap::forwardMessagesUnquoted( + Data::ResolvedForwardDraft &&draft, + const SendAction &action, + FnMut &&successCallback) { + Expects(!draft.items.empty()); + + auto &histories = _session->data().histories(); + + struct SharedCallback { + int requestsLeft = 0; + FnMut callback; + }; + + enum LastGroupType { + None, + Music, + Documents, + Medias, + }; + const auto shared = successCallback + ? std::make_shared() + : std::shared_ptr(); + if (successCallback) { + shared->callback = std::move(successCallback); + } + + const auto count = int(draft.items.size()); + const auto history = action.history; + const auto peer = history->peer; + + histories.readInbox(history); + + const auto anonymousPost = peer->amAnonymous(); + const auto silentPost = ShouldSendSilent(peer, action.options); + const auto sendAs = action.options.sendAs; + const auto self = _session->user(); + const auto messageFromId = sendAs + ? sendAs->id + : anonymousPost + ? PeerId(0) + : self->id; + + auto flags = MessageFlags(); + auto sendFlags = MTPmessages_ForwardMessages::Flags(0); + FillMessagePostFlags(action, peer, flags); + if (silentPost) { + sendFlags |= MTPmessages_ForwardMessages::Flag::f_silent; + } + if (action.options.scheduled) { + flags |= MessageFlag::IsOrWasScheduled; + sendFlags |= MTPmessages_ForwardMessages::Flag::f_schedule_date; + } + if (sendAs) { + sendFlags |= MTPmessages_ForwardMessages::Flag::f_send_as; + } + + const auto kGeneralId = Data::ForumTopic::kGeneralId; + const auto topMsgId = (action.topicRootId == kGeneralId) + ? MsgId(0) + : action.topicRootId; + if (topMsgId) { + sendFlags |= MTPmessages_ForwardMessages::Flag::f_top_msg_id; + } + + auto forwardFrom = draft.items.front()->history()->peer; + auto currentGroupId = draft.items.front()->groupId(); + auto lastGroup = LastGroupType::None; + auto ids = QVector(); + auto randomIds = QVector(); + auto fromIter = draft.items.begin(); + auto toIter = draft.items.begin(); + auto messageGroupCount = 0; + auto messagePostAuthor = peer->isBroadcast() ? _session->user()->name() : QString(); + + const auto needNextGroup = [&] (not_null item) { + auto lastGroupCheck = false; + if (item->media() && item->media()->canBeGrouped()) { + lastGroupCheck = lastGroup != ((item->media()->photo() + || (item->media()->document() + && item->media()->document()->isVideoFile())) + ? LastGroupType::Medias + : (item->media()->document() + && item->media()->document()->isSharedMediaMusic()) + ? LastGroupType::Music + : LastGroupType::Documents); + } else { + lastGroupCheck = lastGroup != LastGroupType::None; + } + + switch (draft.groupOptions) { + case Data::GroupingOptions::GroupAsIs: + return forwardFrom != item->history()->peer + || !currentGroupId + || currentGroupId != item->groupId() + || lastGroupCheck + || messageGroupCount >= 10; + + case Data::GroupingOptions::RegroupAll: + return lastGroupCheck + || messageGroupCount >= 10; + + case Data::GroupingOptions::Separate: + return true; + + default: + Unexpected("draft.groupOptions in ApiWrap::forwardMessagesUnquoted::needNextGroup."); + } + + return false; + }; + + const auto isGrouped = [&] { + return lastGroup != LastGroupType::None + && messageGroupCount > 1 + && messageGroupCount <= 10; + }; + + const auto forwardQuotedSingle = [&] (not_null item) { + if (shared) { + ++shared->requestsLeft; + } + + auto currentIds = QVector(); + currentIds.push_back(MTP_int(item->id)); + + auto currentRandomId = MTP_long(randomIds.takeFirst()); + auto currentRandomIds = QVector(); + currentRandomIds.push_back(currentRandomId); + + const auto requestType = Data::Histories::RequestType::Send; + histories.sendRequest(history, requestType, [=](Fn finish) { + history->sendRequestId = request(MTPmessages_ForwardMessages( + MTP_flags(sendFlags), + forwardFrom->input, + MTP_vector(currentIds), + MTP_vector(currentRandomIds), + peer->input, + MTP_int(topMsgId), + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + if (shared && !--shared->requestsLeft) { + shared->callback(); + } + finish(); + }).fail([=](const MTP::Error &error) { + sendMessageFail(error, peer); + finish(); + }).afterRequest( + history->sendRequestId + ).send(); + return history->sendRequestId; + }); + }; + + const auto forwardAlbumUnquoted = [&] { + if (shared) { + ++shared->requestsLeft; + } + + const auto medias = std::make_shared>(); + const auto mediaInputs = std::make_shared>(); + const auto mediaRefs = std::make_shared>(); + mediaInputs->reserve(ids.size()); + mediaRefs->reserve(ids.size()); + + const auto newGroupId = base::RandomValue(); + auto msgFlags = NewMessageFlags(peer); + + FillMessagePostFlags(action, peer, msgFlags); + + if (action.options.scheduled) { + msgFlags |= MessageFlag::IsOrWasScheduled; + } + + for (auto i = fromIter, e = toIter; i != e; i++) { + const auto item = *i; + const auto media = item->media(); + medias->push_back(media); + + const auto inputMedia = media->photo() + ? MTP_inputMediaPhoto(MTP_flags(0), media->photo()->mtpInput(), MTPint()) + : MTP_inputMediaDocument(MTP_flags(0), media->document()->mtpInput(), MTPint(), MTPstring()); + auto caption = (draft.options != Data::ForwardOptions::NoNamesAndCaptions) + ? item->originalText() + : TextWithEntities(); + auto sentEntities = Api::EntitiesToMTP( + _session, + caption.entities, + Api::ConvertOption::SkipLocal); + + const auto flags = !sentEntities.v.isEmpty() + ? MTPDinputSingleMedia::Flag::f_entities + : MTPDinputSingleMedia::Flag(0); + + const auto newId = FullMsgId( + peer->id, + _session->data().nextLocalMessageId()); + auto randomId = randomIds.takeFirst(); + + mediaInputs->push_back(MTP_inputSingleMedia( + MTP_flags(flags), + inputMedia, + MTP_long(randomId), + MTP_string(caption.text), + sentEntities)); + + _session->data().registerMessageRandomId(randomId, newId); + + if (const auto photo = media->photo()) { + history->addNewLocalMessage( + newId.msg, + msgFlags, + 0, // viaBotId + 0, // replyTo + HistoryItem::NewMessageDate(action.options.scheduled), + messageFromId, + messagePostAuthor, + photo, + caption, + HistoryMessageMarkupData(), + newGroupId); + } else if (const auto document = media->document()) { + history->addNewLocalMessage( + newId.msg, + msgFlags, + 0, // viaBotId + 0, // replyTo + HistoryItem::NewMessageDate(action.options.scheduled), + messageFromId, + messagePostAuthor, + document, + caption, + HistoryMessageMarkupData(), + newGroupId); + } + } + + const auto finalFlags = MTPmessages_SendMultiMedia::Flags(0) + | (action.options.silent + ? MTPmessages_SendMultiMedia::Flag::f_silent + : MTPmessages_SendMultiMedia::Flag(0)) + | (action.options.scheduled + ? MTPmessages_SendMultiMedia::Flag::f_schedule_date + : MTPmessages_SendMultiMedia::Flag(0)); + + const auto requestType = Data::Histories::RequestType::Send; + auto performRequest = [=, &histories](const auto &repeatRequest) -> void { + mediaRefs->clear(); + for (auto i = medias->begin(), e = medias->end(); i != e; i++) { + const auto media = *i; + mediaRefs->push_back(media->photo() + ? media->photo()->fileReference() + : media->document()->fileReference()); + } + histories.sendRequest(history, requestType, [=](Fn finish) { + history->sendRequestId = request(MTPmessages_SendMultiMedia( + MTP_flags(finalFlags), + peer->input, + MTPint(), + MTP_int(topMsgId), + MTP_vector(*mediaInputs), + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + if (shared && !--shared->requestsLeft) { + shared->callback(); + } + finish(); + }).fail([=](const MTP::Error &error) { + if (error.code() == 400 + && error.type().startsWith(qstr("FILE_REFERENCE_"))) { + auto refreshRequests = mediaRefs->size(); + auto index = 0; + auto wasUpdated = false; + for (auto i = medias->begin(), e = medias->end(); i != e; i++) { + const auto media = *i; + const auto origin = media->document() + ? media->document()->stickerOrGifOrigin() + : Data::FileOrigin(); + const auto usedFileReference = mediaRefs->value(index); + + refreshFileReference(origin, [=, &refreshRequests, &wasUpdated](const auto &result) { + const auto currentMediaReference = media->photo() + ? media->photo()->fileReference() + : media->document()->fileReference(); + + if (currentMediaReference != usedFileReference) { + wasUpdated = true; + } + + if (refreshRequests > 0) { + refreshRequests--; + return; + } + + if (wasUpdated) { + repeatRequest(repeatRequest); + } else { + sendMessageFail(error, peer); + } + }); + index++; + } + } else { + sendMessageFail(error, peer); + } + finish(); + }).afterRequest( + history->sendRequestId + ).send(); + return history->sendRequestId; + }); + }; + performRequest(performRequest); + }; + + const auto forwardMediaUnquoted = [&] (not_null item) { + if (shared) { + ++shared->requestsLeft; + } + const auto media = item->media(); + + auto message = MessageToSend(action); + const auto caption = (draft.options != Data::ForwardOptions::NoNamesAndCaptions + && !media->geoPoint() + && !media->sharedContact()) + ? item->originalText() + : TextWithEntities(); + + message.textWithTags = TextWithTags{ + caption.text, + TextUtilities::ConvertEntitiesToTextTags(caption.entities) + }; + message.action.clearDraft = false; + + auto doneCallback = [=] () { + if (shared && !--shared->requestsLeft) { + shared->callback(); + } + }; + + if (media->poll()) { + const auto poll = *(media->poll()); + _polls->create(poll, + message.action, + std::move(doneCallback), + nullptr); + } else if (media->geoPoint()) { + const auto location = *(media->geoPoint()); + Api::SendLocationPoint( + location, + message.action, + std::move(doneCallback), + nullptr); + } else if (media->sharedContact()) { + const auto contact = media->sharedContact(); + shareContact( + contact->phoneNumber, + contact->firstName, + contact->lastName, + message.action); + } else if (media->photo()) { + Api::SendExistingPhoto( + std::move(message), + media->photo(), + std::nullopt, + std::move(doneCallback), + true); // forwarding + } else if (media->document()) { + Api::SendExistingDocument( + std::move(message), + media->document(), + std::nullopt, + std::move(doneCallback), + true); // forwarding + } else { + Unexpected("Media type in ApiWrap::forwardMessages."); + } + }; + + const auto forwardDiceUnquoted = [&] (not_null item) { + if (shared) { + ++shared->requestsLeft; + } + const auto dice = dynamic_cast(item->media()); + if (!dice) { + Unexpected("Non-dice in ApiWrap::forwardMessages."); + } + + auto message = MessageToSend(action); + message.textWithTags.text = dice->emoji(); + message.action.clearDraft = false; + + Api::SendDice(message, [=] (const MTPUpdates &result, mtpRequestId requestId) { + if (shared && !--shared->requestsLeft) { + shared->callback(); + } + }, true); // forwarding + }; + + const auto forwardMessageUnquoted = [&] (not_null item) { + if (shared) { + ++shared->requestsLeft; + } + const auto media = item->media(); + + const auto webPageId = (!media || !media->webpage()) + ? CancelledWebPageId + : media->webpage()->id; + + auto message = MessageToSend(action); + message.textWithTags = TextWithTags{ + item->originalText().text, + TextUtilities::ConvertEntitiesToTextTags(item->originalText().entities) + }; + message.action.clearDraft = false; + message.webPageId = webPageId; + + session().api().sendMessage( + std::move(message), + [=] (const MTPUpdates &result, mtpRequestId requestId) { + if (shared && !--shared->requestsLeft) { + shared->callback(); + } + }, true); // forwarding + }; + + const auto sendAccumulated = [&] { + if (isGrouped()) { + forwardAlbumUnquoted(); + } else { + for (auto i = fromIter, e = toIter; i != e; i++) { + const auto item = *i; + const auto media = item->media(); + + if (media && !media->webpage()) { + if (const auto dice = dynamic_cast(media)) { + forwardDiceUnquoted(item); + } else if ((media->poll() && !history->peer->isUser()) + || media->geoPoint() + || media->sharedContact() + || media->photo() + || media->document()) { + forwardMediaUnquoted(item); + } else { + forwardQuotedSingle(item); + } + } else { + forwardMessageUnquoted(item); + } + } + } + + ids.resize(0); + randomIds.resize(0); + }; + + ids.reserve(count); + randomIds.reserve(count); + for (auto i = draft.items.begin(), e = draft.items.end(); i != e; /* ++i is in the end */) { + const auto item = *i; + const auto randomId = base::RandomValue(); + if (needNextGroup(item)) { + sendAccumulated(); + messageGroupCount = 0; + forwardFrom = item->history()->peer; + currentGroupId = item->groupId(); + fromIter = i; + } + ids.push_back(MTP_int(item->id)); + randomIds.push_back(randomId); + if (item->media() && item->media()->canBeGrouped()) { + lastGroup = ((item->media()->photo() + || (item->media()->document() + && item->media()->document()->isVideoFile())) + ? LastGroupType::Medias + : (item->media()->document() + && item->media()->document()->isSharedMediaMusic()) + ? LastGroupType::Music + : LastGroupType::Documents); + } else { + lastGroup = LastGroupType::None; + } + toIter = ++i; + messageGroupCount++; + } + sendAccumulated(); + _session->data().sendHistoryChangeNotifications(); +} + void ApiWrap::shareContact( const QString &phone, const QString &firstName, @@ -3520,7 +4027,10 @@ void ApiWrap::cancelLocalItem(not_null item) { } } -void ApiWrap::sendMessage(MessageToSend &&message) { +void ApiWrap::sendMessage( + MessageToSend &&message, + Fn doneCallback, + bool forwarding) { const auto history = message.action.history; const auto peer = history->peer; auto &textWithTags = message.textWithTags; @@ -3540,7 +4050,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) { : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) - || Api::SendDice(message)) { + || Api::SendDice(message, [=] (const MTPUpdates &result, mtpRequestId requestId) { + if (doneCallback) { + doneCallback(result, requestId); + } + }, forwarding)) { return; } local().saveRecentSentHashtags(textWithTags.text); @@ -3661,6 +4175,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { topicRootId, UnixtimeFromMsgId(response.outerMsgId)); } + if (doneCallback) { + doneCallback(result, response.requestId); + } }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.type() == u"MESSAGE_EMPTY"_q) { lastMessage->destroy(); @@ -3675,7 +4192,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { }); } - finishForwarding(action); + if (!forwarding) { + finishForwarding(action); + } } void ApiWrap::sendBotStart( diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index e747f0f07..668823506 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -287,6 +287,10 @@ public: Data::ResolvedForwardDraft &&draft, const SendAction &action, FnMut &&successCallback = nullptr); + void forwardMessagesUnquoted( + Data::ResolvedForwardDraft &&draft, + const SendAction &action, + FnMut &&successCallback = nullptr); void shareContact( const QString &phone, const QString &firstName, @@ -330,7 +334,10 @@ public: void cancelLocalItem(not_null item); - void sendMessage(MessageToSend &&message); + void sendMessage( + MessageToSend &&message, + Fn doneCallback = nullptr, + bool forwarding = false); void sendBotStart( not_null bot, PeerData *chat = nullptr, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 00f9eac3f..366b748bf 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1138,7 +1138,8 @@ object_ptr ShareInviteLinkBox( std::vector> &&result, TextWithTags &&comment, Api::SendOptions options, - Data::ForwardOptions) { + Data::ForwardOptions, + Data::GroupingOptions) { if (*sending || result.empty()) { return; } diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index d23e6142c..39d6fbc14 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/share_box.h" +#include "kotato/kotato_lang.h" +#include "kotato/kotato_settings.h" #include "base/random.h" #include "dialogs/dialogs_indexed_list.h" #include "lang/lang_keys.h" @@ -21,13 +23,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/input_fields.h" +#include "ui/widgets/menu/menu_action.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/dropdown_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" #include "chat_helpers/message_field.h" -#include "menu/menu_check_item.h" #include "menu/menu_send.h" #include "history/history.h" #include "history/history_item.h" @@ -35,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" // HistoryView::Context. #include "history/view/history_view_context_menu.h" // CopyPostLink. #include "history/view/history_view_schedule_box.h" +#include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "boxes/peer_list_controllers.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -53,11 +57,52 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_info.h" #include "styles/style_menu_icons.h" +#include "styles/style_media_player.h" #include #include +namespace { + +class ForwardOptionItem final : public Ui::Menu::Action { +public: + using Ui::Menu::Action::Action; + + void init(bool checked) { + enableMouseSelecting(); + + AbstractButton::setDisabled(true); + + _checkView = std::make_unique(st::defaultToggle, false); + _checkView->checkedChanges( + ) | rpl::start_with_next([=](bool checked) { + setIcon(checked ? &st::mediaPlayerMenuCheck : nullptr); + }, lifetime()); + + _checkView->setLocked(checked); + _checkView->setChecked(checked, anim::type::normal); + AbstractButton::clicks( + ) | rpl::start_with_next([=] { + if (!_checkView->isLocked()) { + _checkView->setChecked( + !_checkView->checked(), + anim::type::normal); + } + }, lifetime()); + } + + not_null checkView() const { + return _checkView.get(); + } + +private: + std::unique_ptr _checkView; +}; + +} // namespace + class ShareBox::Inner final : public Ui::RpWidget { public: Inner( @@ -67,6 +112,8 @@ public: void setPeerSelectedChangedCallback( Fn thread, bool selected)> callback); + void setSubmitRequest(Fn callback); + void setGoToChatRequest(Fn callback); void peerUnselected(not_null peer); [[nodiscard]] std::vector> selected() const; @@ -82,6 +129,9 @@ public: void activateSkipPage(int pageHeight, int direction); void updateFilter(QString filter = QString()); void selectActive(); + void tryGoToChat(); + void selectionMade(); + Fn goToChatRequest() const; rpl::producer scrollToRequests() const; rpl::producer<> searchRequests() const; @@ -167,6 +217,8 @@ private: base::flat_set> _selected; Fn, bool)> _peerSelectedChangedCallback; + Fn _submitRequest; + Fn _goToChatRequest; bool _searching = false; QString _lastQuery; @@ -176,6 +228,7 @@ private: rpl::event_stream _scrollToRequests; rpl::event_stream<> _searchRequests; + bool _hadSelection = false; }; ShareBox::ShareBox(QWidget*, Descriptor &&descriptor) @@ -253,7 +306,30 @@ void ShareBox::prepare() { _select->resizeToWidth(st::boxWideWidth); Ui::SendPendingMoveResizeEvents(_select); - setTitle(tr::lng_share_title()); + setTitle(_descriptor.forwardOptions.isShare ? tr::lng_share_title() : tr::lng_selected_forward()); + + const auto forwardOptions = [] { + switch (::Kotato::JsonSettings::GetInt("forward_mode")) { + case 1: return Data::ForwardOptions::NoSenderNames; + case 2: return Data::ForwardOptions::NoNamesAndCaptions; + default: return Data::ForwardOptions::PreserveInfo; + } + }(); + + const auto groupOptions = [] { + switch (::Kotato::JsonSettings::GetInt("forward_grouping_mode")) { + case 1: return Data::GroupingOptions::RegroupAll; + case 2: return Data::GroupingOptions::Separate; + default: return Data::GroupingOptions::GroupAsIs; + } + }(); + + _forwardOptions.hasCaptions = _descriptor.forwardOptions.hasCaptions; + _forwardOptions.dropNames = (forwardOptions != Data::ForwardOptions::PreserveInfo); + _forwardOptions.dropCaptions = (forwardOptions == Data::ForwardOptions::NoNamesAndCaptions); + _groupOptions = groupOptions; + + updateAdditionalTitle(); _inner = setInnerWidget( object_ptr(this, _descriptor, uiShow()), @@ -276,11 +352,22 @@ void ShareBox::prepare() { }); _select->setResizedCallback([=] { updateScrollSkips(); }); _select->setSubmittedCallback([=](Qt::KeyboardModifiers modifiers) { - if (modifiers.testFlag(Qt::ControlModifier) + if ((modifiers.testFlag(Qt::ControlModifier) + && !::Kotato::JsonSettings::GetBool("forward_on_click")) || modifiers.testFlag(Qt::MetaModifier)) { submit({}); + } else if (modifiers.testFlag(Qt::ShiftModifier)) { + if (_inner->selected().size() == 1 && _inner->goToChatRequest()) { + _inner->goToChatRequest()(); + } } else { _inner->selectActive(); + if (!modifiers.testFlag(Qt::ControlModifier) + || ::Kotato::JsonSettings::GetBool("forward_on_click")) { + _inner->tryGoToChat(); + } else { + _inner->selectionMade(); + } } }); rpl::combine( @@ -308,6 +395,17 @@ void ShareBox::prepare() { innerSelectedChanged(thread, checked); }); + _inner->setSubmitRequest([=] { + submit({}); + }); + + if (_descriptor.goToChatCallback) { + _inner->setGoToChatRequest([=] { + const auto singleChat = _inner->selected().at(0); + goToChat(singleChat); + }); + } + Ui::Emoji::SuggestionsController::Init( getDelegate()->outerContainer(), _comment->entity(), @@ -443,6 +541,8 @@ void ShareBox::keyPressEvent(QKeyEvent *e) { _inner->activateSkipPage(contentHeight(), -1); } else if (e->key() == Qt::Key_PageDown) { _inner->activateSkipPage(contentHeight(), 1); + } else if (e->key() == Qt::Key_Escape && !_select->getQuery().isEmpty()) { + _select->clearQuery(); } else { BoxContent::keyPressEvent(e); } @@ -469,34 +569,6 @@ void ShareBox::showMenu(not_null parent) { } _menu.emplace(parent, st::popupMenuWithIcons); - if (_descriptor.forwardOptions.show) { - auto createView = [&](rpl::producer &&text, bool checked) { - auto item = base::make_unique_q( - _menu->menu(), - st::popupMenuWithIcons.menu, - Ui::CreateChild(_menu->menu().get()), - nullptr, - nullptr); - std::move( - text - ) | rpl::start_with_next([action = item->action()](QString text) { - action->setText(text); - }, item->lifetime()); - item->init(checked); - const auto view = item->checkView(); - _menu->addAction(std::move(item)); - return view; - }; - Ui::FillForwardOptions( - std::move(createView), - _descriptor.forwardOptions.messagesCount, - _forwardOptions, - [=](Ui::ForwardOptions value) { _forwardOptions = value; }, - _menu->lifetime()); - - _menu->addSeparator(); - } - const auto result = SendMenu::FillSendMenu( _menu.get(), sendMenuType(), @@ -512,11 +584,20 @@ void ShareBox::showMenu(not_null parent) { void ShareBox::createButtons() { clearButtons(); + if (!_descriptor.forwardOptions.isShare && _descriptor.forwardOptions.show) { + const auto moreButton = addTopButton(st::infoTopBarMenu); + moreButton->setClickedCallback([=] { showForwardMenu(moreButton.data()); }); + } + if (_hasSelected) { + if (_descriptor.goToChatCallback && _inner->selected().size() == 1) { + const auto singleChat = _inner->selected().at(0); + addLeftButton(rktr("ktg_forward_go_to_chat"), [=] { goToChat(singleChat); }); + } + const auto send = addButton(tr::lng_share_confirm(), [=] { submit({}); }); - _forwardOptions.hasCaptions = _descriptor.forwardOptions.hasCaptions; send->setAcceptBoth(); send->clicks( @@ -531,6 +612,204 @@ void ShareBox::createButtons() { addButton(tr::lng_cancel(), [=] { closeBox(); }); } +bool ShareBox::showForwardMenu(not_null button) { + if (_topMenu) { + _topMenu->hideAnimated(Ui::InnerDropdown::HideOption::IgnoreShow); + return true; + } + + _topMenu = base::make_unique_q(window()); + const auto weak = _topMenu.get(); + _topMenu->setHiddenCallback([=] { + weak->deleteLater(); + if (_topMenu == weak) { + button->setForceRippled(false); + } + }); + _topMenu->setShowStartCallback([=] { + if (_topMenu == weak) { + button->setForceRippled(true); + } + }); + _topMenu->setHideStartCallback([=] { + if (_topMenu == weak) { + button->setForceRippled(false); + } + }); + button->installEventFilter(_topMenu); + + auto createView = [&](rpl::producer &&text, bool checked) { + auto item = base::make_unique_q( + _topMenu->menu(), + st::popupMenuWithIcons.menu, + new QAction(QString(), _topMenu->menu()), + nullptr, + nullptr); + std::move( + text + ) | rpl::start_with_next([action = item->action()](QString text) { + action->setText(text); + }, item->lifetime()); + item->init(checked); + const auto view = item->checkView(); + _topMenu->addAction(std::move(item)); + return view; + }; + + const auto forwardOptions = (_forwardOptions.dropCaptions) + ? Data::ForwardOptions::NoNamesAndCaptions + : _forwardOptions.dropNames + ? Data::ForwardOptions::NoSenderNames + : Data::ForwardOptions::PreserveInfo; + + const auto quoted = createView( + rktr("ktg_forward_menu_quoted"), + forwardOptions == Data::ForwardOptions::PreserveInfo); + const auto noNames = createView( + rktr("ktg_forward_menu_unquoted"), + forwardOptions == Data::ForwardOptions::NoSenderNames); + const auto noCaptions = createView( + rktr("ktg_forward_menu_uncaptioned"), + forwardOptions == Data::ForwardOptions::NoNamesAndCaptions); + + const auto onForwardOptionChange = [=, this](int mode, bool value) { + if (value) { + quoted->setLocked(mode == 0 && value); + noNames->setLocked(mode == 1 && value); + noCaptions->setLocked(mode == 2 && value); + quoted->setChecked(quoted->isLocked(), anim::type::normal); + noNames->setChecked(noNames->isLocked(), anim::type::normal); + noCaptions->setChecked(noCaptions->isLocked(), anim::type::normal); + _forwardOptions.dropNames = (mode != 0 && value); + _forwardOptions.dropCaptions = (mode == 2 && value); + if (::Kotato::JsonSettings::GetBool("forward_remember_mode")) { + ::Kotato::JsonSettings::Set("forward_mode", mode); + ::Kotato::JsonSettings::Write(); + } + updateAdditionalTitle(); + } + }; + + quoted->checkedChanges( + ) | rpl::start_with_next([=](bool value) { + onForwardOptionChange(0, value); + }, _topMenu->lifetime()); + + noNames->checkedChanges( + ) | rpl::start_with_next([=](bool value) { + onForwardOptionChange(1, value); + }, _topMenu->lifetime()); + + noCaptions->checkedChanges( + ) | rpl::start_with_next([=](bool value) { + onForwardOptionChange(2, value); + }, _topMenu->lifetime()); + + if (_descriptor.forwardOptions.hasMedia) { + _topMenu->addSeparator(); + + const auto groupAsIs = createView( + rktr("ktg_forward_menu_default_albums"), + _groupOptions == Data::GroupingOptions::GroupAsIs); + const auto groupAll = createView( + rktr("ktg_forward_menu_group_all_media"), + _groupOptions == Data::GroupingOptions::RegroupAll); + const auto groupNone = createView( + rktr("ktg_forward_menu_separate_messages"), + _groupOptions == Data::GroupingOptions::Separate); + + const auto onGroupOptionChange = [=, this](int mode, bool value) { + if (value) { + groupAsIs->setLocked(mode == 0 && value); + groupAll->setLocked(mode == 1 && value); + groupNone->setLocked(mode == 2 && value); + groupAsIs->setChecked(groupAsIs->isLocked(), anim::type::normal); + groupAll->setChecked(groupAll->isLocked(), anim::type::normal); + groupNone->setChecked(groupNone->isLocked(), anim::type::normal); + _groupOptions = (mode == 2) + ? Data::GroupingOptions::Separate + : (mode == 1) + ? Data::GroupingOptions::RegroupAll + : Data::GroupingOptions::GroupAsIs; + if (::Kotato::JsonSettings::GetBool("forward_remember_mode")) { + ::Kotato::JsonSettings::Set("forward_grouping_mode", mode); + ::Kotato::JsonSettings::Write(); + } + updateAdditionalTitle(); + } + }; + + groupAsIs->checkedChanges( + ) | rpl::start_with_next([=](bool value) { + onGroupOptionChange(0, value); + }, _topMenu->lifetime()); + + groupAll->checkedChanges( + ) | rpl::start_with_next([=](bool value) { + onGroupOptionChange(1, value); + }, _topMenu->lifetime()); + + groupNone->checkedChanges( + ) | rpl::start_with_next([=](bool value) { + onGroupOptionChange(2, value); + }, _topMenu->lifetime()); + } + + const auto parentTopLeft = window()->mapToGlobal(QPoint()); + const auto buttonTopLeft = button->mapToGlobal(QPoint()); + const auto parentRect = QRect(parentTopLeft, window()->size()); + const auto buttonRect = QRect(buttonTopLeft, button->size()); + _topMenu->move( + buttonRect.x() + buttonRect.width() - _topMenu->width() - parentRect.x(), + buttonRect.y() + buttonRect.height() - parentRect.y() - style::ConvertScale(18)); + _topMenu->showAnimated(Ui::PanelAnimation::Origin::TopRight); + + return true; +} + +void ShareBox::updateAdditionalTitle() { + if (!_descriptor.forwardOptions.show || _descriptor.forwardOptions.isShare) { + return; + } + + QString result; + + const auto forwardOptions = (_forwardOptions.dropCaptions) + ? Data::ForwardOptions::NoNamesAndCaptions + : _forwardOptions.dropNames + ? Data::ForwardOptions::NoSenderNames + : Data::ForwardOptions::PreserveInfo; + + switch (forwardOptions) { + case Data::ForwardOptions::NoSenderNames: + result += ktr("ktg_forward_subtitle_unquoted"); + break; + + case Data::ForwardOptions::NoNamesAndCaptions: + result += ktr("ktg_forward_subtitle_uncaptioned"); + break; + } + + if (_descriptor.forwardOptions.hasMedia + && _groupOptions != Data::GroupingOptions::GroupAsIs) { + if (!result.isEmpty()) { + result += ", "; + } + + switch (_groupOptions) { + case Data::GroupingOptions::RegroupAll: + result += ktr("ktg_forward_subtitle_group_all_media"); + break; + + case Data::GroupingOptions::Separate: + result += ktr("ktg_forward_subtitle_separate_messages"); + break; + } + } + + setAdditionalTitle(rpl::single(result)); +} + void ShareBox::applyFilterUpdate(const QString &query) { scrollToY(0); _inner->updateFilter(query); @@ -579,7 +858,8 @@ void ShareBox::submit(Api::SendOptions options) { _inner->selected(), _comment->entity()->getTextWithAppliedMarkdown(), options, - forwardOptions); + forwardOptions, + _groupOptions); } } @@ -608,14 +888,29 @@ void ShareBox::copyLink() { } } +void ShareBox::goToChat(not_null thread) { + if (_descriptor.goToChatCallback) { + const auto forwardOptions = (_forwardOptions.hasCaptions + && _forwardOptions.dropCaptions) + ? Data::ForwardOptions::NoNamesAndCaptions + : _forwardOptions.dropNames + ? Data::ForwardOptions::NoSenderNames + : Data::ForwardOptions::PreserveInfo; + _descriptor.goToChatCallback( + thread, + forwardOptions, + _groupOptions); + } +} + void ShareBox::selectedChanged() { auto hasSelected = _inner->hasSelected(); if (_hasSelected != hasSelected) { _hasSelected = hasSelected; - createButtons(); _comment->toggle(_hasSelected, anim::type::normal); _comment->resizeToWidth(st::boxWideWidth); } + createButtons(); update(); } @@ -1050,6 +1345,11 @@ void ShareBox::Inner::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { updateUpon(e->pos()); changeCheckState(getChatAtIndex(_upon)); + if (!e->modifiers().testFlag(Qt::ControlModifier)) { + tryGoToChat(); + } else { + selectionMade(); + } } } @@ -1057,6 +1357,25 @@ void ShareBox::Inner::selectActive() { changeCheckState(getChatAtIndex(_active > 0 ? _active : 0)); } +void ShareBox::Inner::tryGoToChat() { + if (!_hadSelection + && _selected.size() == 1) { + if (_submitRequest && _selected.front()->peer()->isSelf()) { + _submitRequest(); + } else if (_goToChatRequest + && ::Kotato::JsonSettings::GetBool("forward_on_click")) { + _goToChatRequest(); + } + _hadSelection = true; + } +} + +void ShareBox::Inner::selectionMade() { + if (!_hadSelection) { + _hadSelection = true; + } +} + void ShareBox::Inner::resizeEvent(QResizeEvent *e) { _columnSkip = (width() - _columnCount * _st.item.checkbox.imageRadius * 2) / float64(_columnCount + 1); _rowWidthReal = _st.item.checkbox.imageRadius * 2 + _columnSkip; @@ -1150,6 +1469,14 @@ void ShareBox::Inner::setPeerSelectedChangedCallback( _peerSelectedChangedCallback = std::move(callback); } +void ShareBox::Inner::setSubmitRequest(Fn callback) { + _submitRequest = std::move(callback); +} + +void ShareBox::Inner::setGoToChatRequest(Fn callback) { + _goToChatRequest = std::move(callback); +} + void ShareBox::Inner::changePeerCheckState( not_null chat, bool checked, @@ -1177,6 +1504,10 @@ bool ShareBox::Inner::hasSelected() const { return _selected.size(); } +Fn ShareBox::Inner::goToChatRequest() const { + return _goToChatRequest; +} + void ShareBox::Inner::updateFilter(QString filter) { _lastQuery = filter.toLower().trimmed(); @@ -1334,6 +1665,20 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( std::shared_ptr show, not_null history, MessageIdsList msgIds) { + return [=]( + std::vector> &&result, + TextWithTags &&comment, + Api::SendOptions options, + Data::ForwardOptions forwardOptions, + Data::GroupingOptions groupingOptions) { + const auto window = history->session().tryResolveWindow(); + if (window) { + Window::ShowForwardMessagesBox( + window, + Data::ForwardDraft{ msgIds, forwardOptions, groupingOptions }); + } + }; + /* struct State final { base::flat_set requests; }; @@ -1457,11 +1802,19 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( state->requests.insert(history->sendRequestId); } }; + */ } void FastShareMessage( not_null controller, not_null item) { + const auto history = item->history(); + const auto owner = &history->owner(); + const auto msgIds = owner->itemOrItsGroup(item); + Window::ShowForwardMessagesBox( + controller, + Data::ForwardDraft{ msgIds }); + /* const auto show = controller->uiShow(); const auto history = item->history(); const auto owner = &history->owner(); @@ -1534,6 +1887,7 @@ void FastShareMessage( }, }), Ui::LayerOption::CloseOther); + */ } void ShareGameScoreByHash( diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index b13638577..1ccb7d783 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -46,12 +46,14 @@ class IndexedList; namespace Data { enum class ForwardOptions; +enum class GroupingOptions; class Thread; } // namespace Data namespace Ui { class MultiSelect; class InputField; +class DropdownMenu; struct ScrollToRequest; template class SlideWrap; @@ -76,19 +78,25 @@ public: std::vector>&&, TextWithTags&&, Api::SendOptions, - Data::ForwardOptions)>; + Data::ForwardOptions option, + Data::GroupingOptions groupOption)>; using FilterCallback = Fn)>; [[nodiscard]] static SubmitCallback DefaultForwardCallback( std::shared_ptr show, not_null history, MessageIdsList msgIds); + using GoToChatCallback = Fn; struct Descriptor { not_null session; CopyCallback copyCallback; SubmitCallback submitCallback; FilterCallback filterCallback; + GoToChatCallback goToChatCallback; object_ptr bottomWidget = { nullptr }; rpl::producer copyLinkText; const style::MultiSelect *stMultiSelect = nullptr; @@ -99,6 +107,8 @@ public: int messagesCount = 0; bool show = false; bool hasCaptions = false; + bool hasMedia = false; + bool isShare = true; } forwardOptions; HistoryView::ScheduleBoxStyleArgs scheduleBoxStyle; }; @@ -120,6 +130,7 @@ private: void submitScheduled(); void submitWhenOnline(); void copyLink(); + void goToChat(not_null thread); bool searchByUsername(bool useCache = false); SendMenu::Type sendMenuType() const; @@ -129,6 +140,8 @@ private: void applyFilterUpdate(const QString &query); void selectedChanged(); void createButtons(); + bool showForwardMenu(not_null button); + void updateAdditionalTitle(); int getTopScrollSkip() const; int getBottomScrollSkip() const; int contentHeight() const; @@ -152,7 +165,9 @@ private: object_ptr _bottomWidget; base::unique_qptr _menu; + base::unique_qptr _topMenu; Ui::ForwardOptions _forwardOptions; + Data::GroupingOptions _groupOptions; class Inner; QPointer _inner; diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 59ed26a62..138622eef 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -135,7 +135,8 @@ object_ptr ShareInviteLinkBox( std::vector> &&result, TextWithTags &&comment, Api::SendOptions options, - Data::ForwardOptions) { + Data::ForwardOptions, + Data::GroupingOptions) { if (*sending || result.empty()) { return; } diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 2adfe69e4..2ca670895 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -1396,6 +1396,10 @@ void DocumentData::refreshFileReference(const QByteArray &value) { _videoThumbnail.location.refreshFileReference(value); } +QString DocumentData::url() const { + return _url; +} + QString DocumentData::filename() const { return _filename; } diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 42d49058d..61e94146c 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -252,6 +252,7 @@ public: // to (this) received from the server "same" document. void collectLocalData(not_null local); + [[nodiscard]] QString url() const; [[nodiscard]] QString filename() const; [[nodiscard]] QString mimeString() const; [[nodiscard]] bool hasMimeType(const QString &mime) const; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 4e311e1f1..3c7e2e57f 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_media_types.h" +#include "kotato/kotato_lang.h" #include "history/history.h" #include "history/history_item.h" // CreateMedia. #include "history/history_location_manager.h" @@ -400,6 +401,11 @@ PollData *Media::poll() const { return nullptr; } + +const LocationPoint *Media::geoPoint() const { + return nullptr; +} + const WallPaper *Media::paper() const { return nullptr; } @@ -1246,6 +1252,10 @@ CloudImage *MediaLocation::location() const { return _location; } +const LocationPoint *MediaLocation::geoPoint() const { + return &_point; +} + ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const { const auto type = tr::lng_maps_point(tr::now); const auto hasMiniImages = false; diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 985e2c8aa..22dc1baff 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -109,6 +109,7 @@ public: virtual const Invoice *invoice() const; virtual CloudImage *location() const; virtual PollData *poll() const; + virtual const LocationPoint *geoPoint() const; virtual const WallPaper *paper() const; virtual bool uploading() const; @@ -295,6 +296,7 @@ public: std::unique_ptr clone(not_null parent) override; CloudImage *location() const override; + const LocationPoint *geoPoint() const override; ItemPreview toPreview(ToPreviewOptions options) const override; TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 262af2bec..7f242a738 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -370,6 +370,7 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( return Data::ResolvedForwardDraft{ .items = owner().idsToItems(draft.ids), .options = draft.options, + .groupOptions = draft.groupOptions, }; } @@ -381,6 +382,7 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( setForwardDraft(topicRootId, { .ids = owner().itemsToIds(result.items), .options = result.options, + .groupOptions = result.groupOptions, }); } return result; @@ -685,7 +687,8 @@ not_null History::addNewLocalMessage( const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + HistoryMessageMarkupData &&markup, + uint64 newGroupId) { return addNewItem( makeMessage( id, @@ -697,7 +700,8 @@ not_null History::addNewLocalMessage( postAuthor, document, caption, - std::move(markup)), + std::move(markup), + newGroupId), true); } @@ -711,7 +715,8 @@ not_null History::addNewLocalMessage( const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + HistoryMessageMarkupData &&markup, + uint64 newGroupId) { return addNewItem( makeMessage( id, @@ -723,7 +728,8 @@ not_null History::addNewLocalMessage( postAuthor, photo, caption, - std::move(markup)), + std::move(markup), + newGroupId), true); } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index d2b80be10..8dee554fb 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -42,9 +42,16 @@ enum class ForwardOptions { NoNamesAndCaptions, }; +enum class GroupingOptions { + GroupAsIs, + RegroupAll, + Separate, +}; + struct ForwardDraft { MessageIdsList ids; ForwardOptions options = ForwardOptions::PreserveInfo; + GroupingOptions groupOptions = GroupingOptions::GroupAsIs; friend inline auto operator<=>( const ForwardDraft&, @@ -56,6 +63,7 @@ using ForwardDrafts = base::flat_map; struct ResolvedForwardDraft { HistoryItemsList items; ForwardOptions options = ForwardOptions::PreserveInfo; + GroupingOptions groupOptions = GroupingOptions::GroupAsIs; }; } // namespace Data @@ -174,7 +182,8 @@ public: const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + HistoryMessageMarkupData &&markup, + uint64 newGroupId = 0); not_null addNewLocalMessage( MsgId id, MessageFlags flags, @@ -185,7 +194,8 @@ public: const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + HistoryMessageMarkupData &&markup, + uint64 newGroupId = 0); not_null addNewLocalMessage( MsgId id, MessageFlags flags, diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 8cdf3d2c4..3f57a45de 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_item.h" +#include "kotato/kotato_lang.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "calls/calls_instance.h" // Core::App().calls().joinGroupCall. @@ -548,7 +549,8 @@ HistoryItem::HistoryItem( const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) + HistoryMessageMarkupData &&markup, + uint64 groupedId) : HistoryItem( history, id, @@ -570,6 +572,12 @@ HistoryItem::HistoryItem( skipPremiumEffect, spoiler); setText(caption); + if (groupedId) { + setGroupId(MessageGroupId::FromRaw( + history->peer->id, + groupedId, + flags & MessageFlag::IsOrWasScheduled)); + } } HistoryItem::HistoryItem( @@ -583,7 +591,8 @@ HistoryItem::HistoryItem( const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) + HistoryMessageMarkupData &&markup, + uint64 groupedId) : HistoryItem( history, id, @@ -600,6 +609,12 @@ HistoryItem::HistoryItem( const auto spoiler = false; _media = std::make_unique(this, photo, spoiler); setText(caption); + if (groupedId) { + setGroupId(MessageGroupId::FromRaw( + history->peer->id, + groupedId, + flags & MessageFlag::IsOrWasScheduled)); + } } HistoryItem::HistoryItem( @@ -2038,7 +2053,8 @@ bool HistoryItem::requiresSendInlineRight() const { } std::optional HistoryItem::errorTextForForward( - not_null to) const { + not_null to, + bool isUnquotedForward) const { const auto requiredRight = requiredSendRight(); const auto requiresInline = requiresSendInlineRight(); const auto peer = to->peer(); @@ -2053,6 +2069,13 @@ std::optional HistoryItem::errorTextForForward( && _media->poll()->publicVotes() && peer->isBroadcast()) { return tr::lng_restricted_send_public_polls(tr::now); + } else if (isUnquotedForward + && _media + && _media->poll() + && _media->poll()->quiz() + && !_media->poll()->voted() + && !_media->poll()->closed()) { + return ktr("ktg_forward_quiz_unquoted"); } else if (!Data::CanSend(to, requiredRight, false)) { return tr::lng_forward_cant(tr::now); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b26793963..61fb8b949 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -125,7 +125,7 @@ public: const TextWithEntities &textWithEntities, const MTPMessageMedia &media, HistoryMessageMarkupData &&markup, - uint64 groupedId); + uint64 groupedId = 0); HistoryItem( // Local service message. not_null history, MsgId id, @@ -154,7 +154,8 @@ public: const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + HistoryMessageMarkupData &&markup, + uint64 groupedId = 0); HistoryItem( // Local document. not_null history, MsgId id, @@ -166,7 +167,8 @@ public: const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + HistoryMessageMarkupData &&markup, + uint64 groupedId = 0); HistoryItem( // Local game. not_null history, MsgId id, @@ -411,7 +413,8 @@ public: [[nodiscard]] ChatRestriction requiredSendRight() const; [[nodiscard]] bool requiresSendInlineRight() const; [[nodiscard]] std::optional errorTextForForward( - not_null to) const; + not_null to, + bool isUnquotedForward) const; [[nodiscard]] const HistoryMessageTranslation *translation() const; [[nodiscard]] bool translationShowRequiresCheck(LanguageId to) const; bool translationShowRequiresRequest(LanguageId to); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 6a8491d6c..9fda350b2 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -66,7 +66,7 @@ QString GetErrorTextForSending( : peer->owner().history(peer); if (request.forward) { for (const auto &item : *request.forward) { - if (const auto error = item->errorTextForForward(thread)) { + if (const auto error = item->errorTextForForward(thread, request.isUnquotedForward)) { return *error; } } diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 46b772517..cd26e9d00 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -88,6 +88,7 @@ struct SendingErrorRequest { const HistoryItemsList *forward = nullptr; const TextWithTags *text = nullptr; bool ignoreSlowmodeCountdown = false; + bool isUnquotedForward = false; }; [[nodiscard]] QString GetErrorTextForSending( not_null peer, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index c52cb5ceb..6bb9fac68 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_widget.h" #include "kotato/kotato_settings.h" +#include "kotato/kotato_lang.h" #include "api/api_editing.h" #include "api/api_bot.h" #include "api/api_chat_participants.h" @@ -38,7 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/message_sending_animation_controller.h" #include "ui/text/text_utilities.h" // Ui::Text::ToUpper #include "ui/text/format_values.h" -#include "ui/chat/forward_options_box.h" +//#include "ui/chat/forward_options_box.h" #include "ui/chat/message_bar.h" #include "ui/chat/attach/attach_send_files_way.h" #include "ui/chat/choose_send_as.h" @@ -6211,12 +6212,137 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { crl::guard(_list, [=] { cancelEdit(); })); } else if (_inReplyEditForward) { if (readyToForward()) { - _forwardPanel->editOptions(controller()->uiShow()); + if (_forwardPanel->items().empty() || e->button() != Qt::LeftButton) { + return; + } + + _history->setForwardDraft(MsgId(), {}); + Window::ShowForwardMessagesBox(controller(), Data::ForwardDraft{ + .ids = session().data().itemsToIds(_forwardPanel->items()) + }); } else { - controller()->showPeerHistory( - _peer, - Window::SectionShow::Way::Forward, - _editMsgId ? _editMsgId : replyToId()); + Ui::showPeerHistory(_peer, _editMsgId ? _editMsgId : replyToId()); + } + } +} + +void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) { + if (_menu) { + return; + } + const auto hasSecondLayer = (_editMsgId + || _replyToId + || readyToForward() + || _kbReplyTo); + _replyForwardPressed = hasSecondLayer && QRect( + 0, + _field->y() - st::historySendPadding - st::historyReplyHeight, + st::historyReplySkip, + st::historyReplyHeight).contains(e->pos()); + if (_replyForwardPressed && !_fieldBarCancel->isHidden()) { + return; + } else if (_inReplyEditForward) { + if (readyToForward()) { + using Options = Data::ForwardOptions; + using GroupingOptions = Data::GroupingOptions; + const auto count = _forwardPanel->items().size(); + const auto hasMediaToGroup = [&] { + if (count > 1) { + auto grouppableMediaCount = 0; + for (const auto item : _forwardPanel->items()) { + if (item->media() && item->media()->canBeGrouped()) { + grouppableMediaCount++; + } else { + grouppableMediaCount = 0; + } + if (grouppableMediaCount > 1) { + return true; + } + } + } + return false; + }(); + const auto hasCaptions = [&] { + for (const auto item : _forwardPanel->items()) { + if (const auto media = item->media()) { + if (!item->originalText().text.isEmpty() + && media->allowsEditCaption()) { + return true; + } + } + } + return false; + }(); + const auto addForwardOption = [=]( + Options newOptions, + const QString &langKey, + int settingsKey) { + const auto draft = _history->resolveForwardDraft(MsgId()); + if (_history && draft.options != newOptions) { + _menu->addAction(ktr(langKey), [=] { + const auto error = GetErrorTextForSending( + _history->peer, + { + .topicRootId = MsgId(), + .forward = &_forwardPanel->items(), + .ignoreSlowmodeCountdown = true, + .isUnquotedForward = newOptions != Options::PreserveInfo, + }); + if (!error.isEmpty()) { + controller()->showToast(error); + return; + } + _history->setForwardDraft(MsgId(), { + .ids = session().data().itemsToIds(_forwardPanel->items()), + .options = newOptions, + .groupOptions = draft.groupOptions, + }); + updateField(); + if (::Kotato::JsonSettings::GetBool("forward_remember_mode")) { + ::Kotato::JsonSettings::Set("forward_mode", settingsKey); + ::Kotato::JsonSettings::Write(); + } + }); + } + }; + + _menu = base::make_unique_q(this); + + addForwardOption(Options::PreserveInfo, "ktg_forward_menu_quoted", 0); + addForwardOption(Options::NoSenderNames, "ktg_forward_menu_unquoted", 1); + if (hasCaptions) { + addForwardOption(Options::NoNamesAndCaptions, "ktg_forward_menu_uncaptioned", 2); + } + + if (hasMediaToGroup && count > 1) { + const auto addGroupingOption = [=]( + GroupingOptions newOptions, + const QString &langKey, + int settingsKey) { + const auto draft = _history->resolveForwardDraft(MsgId()); + if (_history && draft.groupOptions != newOptions) { + _menu->addAction(ktr(langKey), [=] { + _history->setForwardDraft(MsgId(), { + .ids = session().data().itemsToIds(_forwardPanel->items()), + .options = draft.options, + .groupOptions = newOptions, + }); + updateField(); + if (::Kotato::JsonSettings::GetBool("forward_remember_mode")) { + ::Kotato::JsonSettings::Set("forward_grouping_mode", settingsKey); + ::Kotato::JsonSettings::Write(); + } + }); + } + }; + + _menu->addSeparator(); + addGroupingOption(GroupingOptions::GroupAsIs, "ktg_forward_menu_default_albums", 0); + addGroupingOption(GroupingOptions::RegroupAll, "ktg_forward_menu_group_all_media", 1); + addGroupingOption(GroupingOptions::Separate, "ktg_forward_menu_separate_messages", 2); + } + + _menu->popup(QCursor::pos()); } } } @@ -6908,10 +7034,17 @@ bool HistoryWidget::sendExistingDocument( return false; } - Api::SendExistingDocument( - Api::MessageToSend(prepareSendAction(options)), - document, - localId); + if (document->hasRemoteLocation()) { + Api::SendExistingDocument( + Api::MessageToSend(prepareSendAction(options)), + document, + localId); + } else { + Api::SendWebDocument( + Api::MessageToSend(prepareSendAction(options)), + document, + localId); + } if (_fieldAutocomplete->stickersShown()) { clearFieldText(); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 46d8287b9..27123f055 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -286,6 +286,7 @@ protected: void resizeEvent(QResizeEvent *e) override; void keyPressEvent(QKeyEvent *e) override; void mousePressEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; void paintEvent(QPaintEvent *e) override; void leaveEventHook(QEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; @@ -810,6 +811,7 @@ private: int _topDelta = 0; + base::unique_qptr _menu; rpl::event_stream<> _cancelRequests; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index 30e3b26e0..99beb22b8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_media_types.h" #include "data/data_forum_topic.h" #include "main/main_session.h" -#include "ui/chat/forward_options_box.h" +//#include "ui/chat/forward_options_box.h" #include "ui/effects/spoiler_mess.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" @@ -219,6 +219,7 @@ bool ForwardPanel::empty() const { } void ForwardPanel::editOptions(std::shared_ptr show) { + /* using Options = Data::ForwardOptions; const auto now = _data.options; const auto count = _data.items.size(); @@ -296,6 +297,7 @@ void ForwardPanel::editOptions(std::shared_ptr show) { }, optionsChanged, changeRecipient)); + */ } void ForwardPanel::paint( diff --git a/Telegram/SourceFiles/kotato/kotato_settings.cpp b/Telegram/SourceFiles/kotato/kotato_settings.cpp index 730579d6c..f06520b9c 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp @@ -373,12 +373,32 @@ const std::map> DefinitionMap { { "profile_top_mute", { .type = SettingType::BoolSetting, .defaultValue = false, }}, + { "forward_retain_selection", { + .type = SettingType::BoolSetting, + .defaultValue = false, }}, + { "forward_on_click", { + .type = SettingType::BoolSetting, + .defaultValue = false, }}, { "folders/local", { .scope = SettingScope::Account, .type = SettingType::QJsonArraySetting, }}, { "telegram_sites_autologin", { .type = SettingType::BoolSetting, .defaultValue = true, }}, + { "forward_remember_mode", { + .type = SettingType::BoolSetting, + .defaultValue = true, }}, + { "forward_mode", { + .type = SettingType::IntSetting, + .defaultValue = 0, + .limitHandler = IntLimit(0, 2), }}, + { "forward_grouping_mode", { + .type = SettingType::IntSetting, + .defaultValue = 0, + .limitHandler = IntLimit(0, 2), }}, + { "forward_force_old_unquoted", { + .type = SettingType::BoolSetting, + .defaultValue = false, }}, }; using OldOptionKey = QString; diff --git a/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp index 32bbe158b..a682a2680 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp @@ -47,6 +47,54 @@ namespace Settings { namespace { +QString ForwardModeLabel(int mode) { + switch (mode) { + case 0: + return ktr("ktg_forward_mode_quoted"); + + case 1: + return ktr("ktg_forward_mode_unquoted"); + + case 2: + return ktr("ktg_forward_mode_uncaptioned"); + + default: + Unexpected("Boost in Settings::ForwardModeLabel."); + } + return QString(); +} + +QString GroupingModeLabel(int mode) { + switch (mode) { + case 0: + return ktr("ktg_forward_grouping_mode_preserve_albums"); + + case 1: + return ktr("ktg_forward_grouping_mode_regroup"); + + case 2: + return ktr("ktg_forward_grouping_mode_separate"); + + default: + Unexpected("Boost in Settings::GroupingModeLabel."); + } + return QString(); +} + +QString GroupingModeDescription(int mode) { + switch (mode) { + case 0: + case 2: + return QString(); + + case 1: + return ktr("ktg_forward_grouping_mode_regroup_desc"); + + default: + Unexpected("Boost in Settings::GroupingModeLabel."); + } + return QString(); +} QString NetBoostLabel(int boost) { switch (boost) { @@ -297,6 +345,71 @@ void SetupKotatoForward(not_null container) { AddSkip(container); AddSubsectionTitle(container, rktr("ktg_settings_forward")); + SettingsMenuJsonSwitch(ktg_forward_remember_mode, forward_remember_mode); + + auto forwardModeText = rpl::single( + ForwardModeLabel(::Kotato::JsonSettings::GetInt("forward_mode")) + ) | rpl::then( + ::Kotato::JsonSettings::Events( + "forward_mode" + ) | rpl::map([] { + return ForwardModeLabel(::Kotato::JsonSettings::GetInt("forward_mode")); + }) + ); + + AddButtonWithLabel( + container, + rktr("ktg_forward_mode"), + forwardModeText, + st::settingsButtonNoIcon + )->addClickHandler([=] { + Ui::show(Box<::Kotato::RadioBox>( + ktr("ktg_forward_mode"), + ::Kotato::JsonSettings::GetInt("forward_mode"), + 3, + ForwardModeLabel, + [=] (int value) { + ::Kotato::JsonSettings::Set("forward_mode", value); + ::Kotato::JsonSettings::Write(); + }, false)); + }); + + auto forwardGroupingModeText = rpl::single( + GroupingModeLabel(::Kotato::JsonSettings::GetInt("forward_grouping_mode")) + ) | rpl::then( + ::Kotato::JsonSettings::Events( + "forward_grouping_mode" + ) | rpl::map([] { + return GroupingModeLabel(::Kotato::JsonSettings::GetInt("forward_grouping_mode")); + }) + ); + + AddButtonWithLabel( + container, + rktr("ktg_forward_grouping_mode"), + forwardGroupingModeText, + st::settingsButtonNoIcon + )->addClickHandler([=] { + Ui::show(Box<::Kotato::RadioBox>( + ktr("ktg_forward_grouping_mode"), + ::Kotato::JsonSettings::GetInt("forward_grouping_mode"), + 3, + GroupingModeLabel, + GroupingModeDescription, + [=] (int value) { + ::Kotato::JsonSettings::Set("forward_grouping_mode", value); + ::Kotato::JsonSettings::Write(); + }, false)); + }); + + SettingsMenuJsonSwitch(ktg_forward_force_old_unquoted, forward_force_old_unquoted); + + AddSkip(container); + AddDividerText(container, rktr("ktg_forward_force_old_unquoted_desc")); + AddSkip(container); + + SettingsMenuJsonSwitch(ktg_settings_forward_retain_selection, forward_retain_selection); + SettingsMenuJsonSwitch(ktg_settings_forward_chat_on_click, forward_on_click); AddSkip(container); AddDividerText(container, rktr("ktg_settings_forward_chat_on_click_description")); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 4b46807d0..4596f8449 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -567,6 +567,7 @@ bool MainWidget::setForwardDraft( .topicRootId = topicRootId, .forward = &items, .ignoreSlowmodeCountdown = true, + .isUnquotedForward = draft.options != Data::ForwardOptions::PreserveInfo, }); if (!error.isEmpty()) { _controller->show(Ui::MakeInformBox(error)); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 3921a21cb..c935f7485 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_participants_box.h" #include "boxes/peers/edit_forum_topic_box.h" #include "boxes/peers/edit_contact_box.h" +#include "boxes/share_box.h" #include "calls/calls_instance.h" #include "inline_bots/bot_attach_web_view.h" // InlineBots::PeerType. #include "ui/boxes/report_box.h" @@ -53,9 +54,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_blocked_peers.h" #include "api/api_chat_filters.h" #include "api/api_polls.h" +#include "api/api_sending.h" #include "api/api_updates.h" +#include "api/api_text_entities.h" #include "mtproto/mtproto_config.h" #include "history/history.h" +#include "history/history_widget.h" +#include "history/view/history_view_element.h" #include "history/history_item_helpers.h" // GetErrorTextForSending. #include "history/view/history_view_context_menu.h" #include "window/window_adaptive.h" // Adaptive::isThreeColumn @@ -76,9 +81,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_user.h" +#include "data/data_game.h" +#include "data/data_web_page.h" #include "data/data_scheduled_messages.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" +#include "data/data_file_origin.h" #include "dialogs/dialogs_key.h" #include "core/application.h" #include "export/export_manager.h" @@ -89,6 +97,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_window.h" // st::windowMinWidth #include "styles/style_menu_icons.h" +#include +#include #include #include @@ -1654,10 +1664,230 @@ QPointer ShowChooseRecipientBox( return weak->data(); } +QPointer ShowForwardMessagesBox( + not_null navigation, + Data::ForwardDraft &&draft, + Fn &&successCallback) { + struct ShareData { + ShareData(not_null peer, Data::ForwardDraft &&fwdDraft, FnMut &&callback) + : peer(peer) + , draft(std::move(fwdDraft)) + , submitCallback(std::move(callback)) { + } + not_null peer; + Data::ForwardDraft draft; + int requestsLeft = 0; + FnMut submitCallback; + }; + const auto weak = std::make_shared>(); + const auto firstItem = navigation->session().data().message(draft.ids[0]); + const auto history = firstItem->history(); + const auto topicRootId = history->topicRootId(); + const auto owner = &history->owner(); + const auto session = &history->session(); + const auto isGame = firstItem->getMessageBot() + && firstItem->media() + && (firstItem->media()->game() != nullptr); + + const auto items = history->owner().idsToItems(draft.ids); + const auto hasCaptions = ranges::any_of(items, [](auto item) { + return item->media() + && !item->originalText().text.isEmpty() + && item->media()->allowsEditCaption(); + }); + const auto hasOnlyForcedForwardedInfo = hasCaptions + ? false + : ranges::all_of(items, [](auto item) { + return item->media() && item->media()->forceForwardedInfo(); + }); + + const auto canCopyLink = [=] { + if (draft.ids.size() > 10) { + return false; + } + + const auto groupId = firstItem->groupId(); + + for (const auto item : items) { + if (groupId != item->groupId()) { + return false; + } + } + + return (firstItem->hasDirectLink() || isGame); + }(); + + const auto hasMediaForGrouping = [=] { + if (draft.ids.size() > 1) { + auto grouppableMediaCount = 0; + for (const auto item : items) { + if (item->media() && item->media()->canBeGrouped()) { + grouppableMediaCount++; + } else { + grouppableMediaCount = 0; + } + if (grouppableMediaCount > 1) { + return true; + } + } + } + return false; + }(); + + const auto data = std::make_shared(history->peer, std::move(draft), std::move(successCallback)); + + auto copyCallback = [=]() { + if (const auto item = owner->message(data->draft.ids[0])) { + if (item->hasDirectLink()) { + HistoryView::CopyPostLink( + navigation->parentController(), + item->fullId(), + HistoryView::Context::History); + } else if (const auto bot = item->getMessageBot()) { + if (const auto media = item->media()) { + if (const auto game = media->game()) { + const auto link = session->createInternalLinkFull( + bot->username() + + qsl("?game=") + + game->shortName); + + QGuiApplication::clipboard()->setText(link); + + Ui::Toast::Show(tr::lng_share_game_link_copied(tr::now)); + } + } + } + } + }; + auto submitCallback = [=]( + std::vector> &&result, + TextWithTags &&comment, + Api::SendOptions options, + Data::ForwardOptions forwardOptions, + Data::GroupingOptions groupOptions) { + if (data->requestsLeft > 0) { + return; // Share clicked already. + } + auto items = history->owner().idsToItems(data->draft.ids); + if (items.empty() || result.empty()) { + return; + } + + const auto error = [&] { + for (const auto peer : result) { + const auto error = GetErrorTextForSending( + peer, + { + .topicRootId = topicRootId, + .forward = &items, + .ignoreSlowmodeCountdown = false, + .isUnquotedForward = forwardOptions != Data::ForwardOptions::PreserveInfo, + }); + if (!error.isEmpty()) { + return std::make_pair(error, peer); + } + } + return std::make_pair(QString(), result.front()); + }(); + if (!error.first.isEmpty()) { + auto text = TextWithEntities(); + if (result.size() > 1) { + text.append( + Ui::Text::Bold(error.second->peer()->name()) + ).append("\n\n"); + } + text.append(error.first); + Ui::show( + Ui::MakeInformBox(text), + Ui::LayerOption::KeepOther); + return; + } + + const auto checkAndClose = [=] { + data->requestsLeft--; + if (!data->requestsLeft) { + Ui::Toast::Show(tr::lng_share_done(tr::now)); + Ui::hideLayer(); + } + }; + auto &api = owner->session().api(); + + data->draft.options = forwardOptions; + data->draft.groupOptions = groupOptions; + + for (const auto thread : result) { + auto action = Api::SendAction(thread); + const auto history = action.history; + action.options = options; + action.clearDraft = false; + + if (!comment.text.isEmpty()) { + auto message = ApiWrap::MessageToSend(action); + message.textWithTags = comment; + api.sendMessage(std::move(message)); + } + + data->requestsLeft++; + auto resolved = history->resolveForwardDraft(data->draft); + + api.forwardMessages(std::move(resolved), action, [=] { + checkAndClose(); + }); + } + if (data->submitCallback + && !::Kotato::JsonSettings::GetBool("forward_retain_selection")) { + data->submitCallback(); + } + }; + auto filterCallback = [](not_null thread) { + return Data::CanSendTexts(thread); + }; + auto copyLinkCallback = canCopyLink + ? Fn(std::move(copyCallback)) + : Fn(); + auto goToChatCallback = [navigation, data]( + Data::Thread* thread, + Data::ForwardOptions forwardOptions, + Data::GroupingOptions groupOptions) { + if (data->submitCallback + && !::Kotato::JsonSettings::GetBool("forward_retain_selection")) { + data->submitCallback(); + } + data->draft.options = forwardOptions; + data->draft.groupOptions = groupOptions; + navigation->parentController()->content()->setForwardDraft(thread, std::move(data->draft)); + }; + *weak = Ui::show( + Box(ShareBox::Descriptor{ + .session = session, + .copyCallback = std::move(copyLinkCallback), + .submitCallback = std::move(submitCallback), + .filterCallback = std::move(filterCallback), + .goToChatCallback = std::move(goToChatCallback), + .forwardOptions = { + .messagesCount = int(draft.ids.size()), + .show = !hasOnlyForcedForwardedInfo, + .hasCaptions = hasCaptions, + .hasMedia = hasMediaForGrouping, + .isShare = false, + }, + + }), + Ui::LayerOption::KeepOther); + return weak->data(); +} + QPointer ShowForwardMessagesBox( std::shared_ptr show, Data::ForwardDraft &&draft, Fn &&successCallback) { + const auto window = show->session().tryResolveWindow(); + return ShowForwardMessagesBox( + window, + std::move(draft), + std::move(successCallback)); + + /* const auto session = &show->session(); const auto owner = &session->data(); const auto msgIds = owner->itemsToIds(owner->idsToItems(draft.ids)); @@ -1963,16 +2193,7 @@ QPointer ShowForwardMessagesBox( }, state->box->lifetime()); return QPointer(state->box); -} - -QPointer ShowForwardMessagesBox( - not_null navigation, - Data::ForwardDraft &&draft, - Fn &&successCallback) { - return ShowForwardMessagesBox( - navigation->uiShow(), - std::move(draft), - std::move(successCallback)); + */ } QPointer ShowForwardMessagesBox( @@ -1981,7 +2202,7 @@ QPointer ShowForwardMessagesBox( Fn &&successCallback) { return ShowForwardMessagesBox( navigation, - Data::ForwardDraft{ .ids = std::move(items) }, + Data::ForwardDraft{ .ids = std::move(items) }, std::move(successCallback)); } diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 404f72080..399feffc4 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -212,8 +212,8 @@ PRIVATE ui/chat/chat_theme.h ui/chat/continuous_scroll.cpp ui/chat/continuous_scroll.h - ui/chat/forward_options_box.cpp - ui/chat/forward_options_box.h + #ui/chat/forward_options_box.cpp + #ui/chat/forward_options_box.h ui/chat/group_call_bar.cpp ui/chat/group_call_bar.h ui/chat/group_call_userpics.cpp