diff --git a/Telegram/Resources/langs/rewrites/en.json b/Telegram/Resources/langs/rewrites/en.json index 6d2a6e786..70971d17a 100644 --- a/Telegram/Resources/langs/rewrites/en.json +++ b/Telegram/Resources/langs/rewrites/en.json @@ -118,7 +118,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", @@ -140,6 +154,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 6c2f78d07..44e8426b5 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" @@ -64,7 +65,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(); @@ -145,7 +148,6 @@ void SendExistingMedia( auto &histories = history->owner().histories(); const auto requestType = Data::Histories::RequestType::Send; histories.sendRequest(history, requestType, [=](Fn finish) { - const auto usedFileReference = media->fileReference(); history->sendRequestId = api->request(MTPmessages_SendMedia( MTP_flags(sendFlags), peer->input, @@ -157,12 +159,16 @@ void SendExistingMedia( sentEntities, MTP_int(message.action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()) - )).done([=](const MTPUpdates &result) { + )).done([=](const MTPUpdates &result, mtpRequestId requestId) { api->applyUpdates(result, randomId); + if (doneCallback) { + doneCallback(); + } finish(); }).fail([=](const MTP::Error &error) { if (error.code() == 400 && error.type().startsWith(qstr("FILE_REFERENCE_"))) { + const auto usedFileReference = media->fileReference(); api->refreshFileReference(origin, [=](const auto &result) { if (media->fileReference() != usedFileReference) { repeatRequest(repeatRequest); @@ -181,15 +187,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), @@ -202,7 +234,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); @@ -212,7 +246,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), @@ -224,10 +260,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) @@ -327,8 +368,11 @@ bool SendDice(MessageToSend &message) { MTP_vector(), MTP_int(message.action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()) - )).done([=](const MTPUpdates &result) { + )).done([=](const MTPUpdates &result, mtpRequestId requestId) { api->applyUpdates(result, randomId); + if (doneCallback) { + doneCallback(result, requestId); + } finish(); }).fail([=](const MTP::Error &error) { api->sendMessageFail(error, peer, randomId, newId); @@ -337,7 +381,9 @@ bool SendDice(MessageToSend &message) { ).send(); return history->sendRequestId; }); - api->finishForwarding(message.action); + if (!forwarding) { + api->finishForwarding(message.action); + } return true; } @@ -491,4 +537,71 @@ 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.clearDraft) { + sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; + history->clearLocalDraft(); + history->clearCloudDraft(); + } + 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) { + const auto replyTo = action.replyTo; + history->sendRequestId = api->request(MTPmessages_SendMedia( + MTP_flags(sendFlags), + peer->input, + MTP_int(replyTo), + 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 0e703dab8..801b048aa 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" @@ -35,6 +36,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_media_types.h" @@ -3016,6 +3018,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(); @@ -3066,6 +3074,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>(); @@ -3142,9 +3151,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)); @@ -3153,6 +3167,489 @@ 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; + } + + 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(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_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, @@ -3385,7 +3882,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; @@ -3394,7 +3894,12 @@ void ApiWrap::sendMessage(MessageToSend &&message) { action.generateLocal = true; sendAction(action); - if (!peer->canWrite() || Api::SendDice(message)) { + if (!peer->canWrite() + || Api::SendDice(message, [=] (const MTPUpdates &result, mtpRequestId requestId) { + if (doneCallback) { + doneCallback(result, requestId); + } + }, forwarding)) { return; } local().saveRecentSentHashtags(textWithTags.text); @@ -3509,11 +4014,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) { history->finishSavingCloudDraft( UnixtimeFromMsgId(response.outerMsgId)); } + if (doneCallback) { + doneCallback(result, response.requestId); + } finish(); }).fail([=]( const MTP::Error &error, const MTP::Response &response) { - if (error.type() == qstr("MESSAGE_EMPTY")) { + if (error.type() == qstr("MESSAGE_EMPTY") && !forwarding) { lastMessage->destroy(); } else { sendMessageFail(error, peer, randomId, newId); @@ -3529,7 +4037,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 d54a83d18..b8f7397aa 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -283,6 +283,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, @@ -326,7 +330,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 3b8e4a188..e38bd2dc0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1143,7 +1143,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 ddd06cb7d..0c4bbaf7d 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,18 +23,20 @@ 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 "chat_helpers/message_field.h" -#include "menu/menu_check_item.h" #include "menu/menu_send.h" #include "history/history.h" #include "history/history_message.h" #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_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -49,21 +53,65 @@ 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(QWidget *parent, const Descriptor &descriptor); void setPeerSelectedChangedCallback( Fn callback); + void setSubmitRequest(Fn callback); + void setGoToChatRequest(Fn callback); void peerUnselected(not_null peer); std::vector> selected() const; bool hasSelected() const; + Fn goToChatRequest() const; void peopleReceived( const QString &query, @@ -75,6 +123,8 @@ public: void activateSkipPage(int pageHeight, int direction); void updateFilter(QString filter = QString()); void selectActive(); + void tryGoToChat(); + void selectionMade(); rpl::producer scrollToRequests() const; rpl::producer<> searchRequests() const; @@ -154,6 +204,8 @@ private: base::flat_set> _selected; Fn _peerSelectedChangedCallback; + Fn _submitRequest; + Fn _goToChatRequest; bool _searching = false; QString _lastQuery; @@ -163,6 +215,7 @@ private: rpl::event_stream _scrollToRequests; rpl::event_stream<> _searchRequests; + bool _hadSelection = false; }; ShareBox::ShareBox(QWidget*, Descriptor &&descriptor) @@ -243,7 +296,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), @@ -266,11 +342,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( @@ -296,6 +383,17 @@ void ShareBox::prepare() { innerSelectedChanged(peer, 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(), @@ -431,6 +529,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); } @@ -455,34 +555,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, - new QAction(QString(), _menu->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(); - _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(), @@ -497,11 +569,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( @@ -516,6 +597,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); @@ -555,7 +834,8 @@ void ShareBox::submit(Api::SendOptions options) { _inner->selected(), _comment->entity()->getTextWithAppliedMarkdown(), options, - forwardOptions); + forwardOptions, + _groupOptions); } } @@ -581,14 +861,29 @@ void ShareBox::copyLink() { } } +void ShareBox::goToChat(not_null peer) { + if (_descriptor.goToChatCallback) { + const auto forwardOptions = (_forwardOptions.hasCaptions + && _forwardOptions.dropCaptions) + ? Data::ForwardOptions::NoNamesAndCaptions + : _forwardOptions.dropNames + ? Data::ForwardOptions::NoSenderNames + : Data::ForwardOptions::PreserveInfo; + _descriptor.goToChatCallback( + peer, + 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(); } @@ -1005,6 +1300,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(); + } } } @@ -1012,6 +1312,25 @@ void ShareBox::Inner::selectActive() { changeCheckState(getChatAtIndex(_active > 0 ? _active : 0)); } +void ShareBox::Inner::tryGoToChat() { + if (!_hadSelection + && _selected.size() == 1) { + if (_submitRequest && _selected.front()->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; @@ -1052,6 +1371,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, @@ -1073,6 +1400,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(); @@ -1220,6 +1551,10 @@ QString AppendShareGameScoreUrl( void FastShareMessage( not_null controller, not_null item) { + Window::ShowForwardMessagesBox( + controller, + item->history()->owner().itemOrItsGroup(item)); + /* struct ShareData { ShareData(not_null peer, MessageIdsList &&ids) : peer(peer) @@ -1421,6 +1756,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 e9ab80802..012ca7dd6 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -44,11 +44,13 @@ class IndexedList; namespace Data { enum class ForwardOptions; +enum class GroupingOptions; } // namespace Data namespace Ui { class MultiSelect; class InputField; +class DropdownMenu; struct ScrollToRequest; template class SlideWrap; @@ -73,14 +75,20 @@ public: std::vector>&&, TextWithTags&&, Api::SendOptions, - Data::ForwardOptions option)>; + Data::ForwardOptions option, + Data::GroupingOptions groupOption)>; using FilterCallback = Fn; + 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; @@ -91,6 +99,8 @@ public: int messagesCount = 0; bool show = false; bool hasCaptions = false; + bool hasMedia = false; + bool isShare = true; } forwardOptions; HistoryView::ScheduleBoxStyleArgs scheduleBoxStyle; }; @@ -111,6 +121,7 @@ private: void submitSilent(); void submitScheduled(); void copyLink(); + void goToChat(not_null peer); bool searchByUsername(bool useCache = false); SendMenu::Type sendMenuType() const; @@ -120,6 +131,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; @@ -145,7 +158,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 dd4745a35..e7f54d3e6 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "boxes/share_box.h" #include "history/view/history_view_schedule_box.h" +#include "history/history.h" #include "history/history_message.h" // GetErrorTextForSending. #include "data/data_histories.h" #include "data/data_session.h" @@ -135,7 +136,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 7386ce371..3c3c28498 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -1351,6 +1351,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 0b11ed013..7ca270237 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -243,6 +243,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(QLatin1String mime) const; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 121206723..89e248998 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" #include "history/history_location_manager.h" @@ -331,6 +332,10 @@ PollData *Media::poll() const { return nullptr; } +const LocationPoint *Media::geoPoint() const { + return nullptr; +} + bool Media::uploading() const { return false; } @@ -391,7 +396,7 @@ bool Media::forceForwardedInfo() const { return false; } -QString Media::errorTextForForward(not_null peer) const { +QString Media::errorTextForForward(not_null peer, bool unquoted) const { return QString(); } @@ -580,7 +585,7 @@ bool MediaPhoto::allowsEditMedia() const { return true; } -QString MediaPhoto::errorTextForForward(not_null peer) const { +QString MediaPhoto::errorTextForForward(not_null peer, bool unquoted) const { return Data::RestrictionError( peer, ChatRestriction::SendMedia @@ -908,7 +913,7 @@ bool MediaFile::dropForwardedInfo() const { return _document->isSong(); } -QString MediaFile::errorTextForForward(not_null peer) const { +QString MediaFile::errorTextForForward(not_null peer, bool unquoted) const { if (const auto sticker = _document->sticker()) { if (const auto error = Data::RestrictionError( peer, @@ -1134,6 +1139,10 @@ Data::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; @@ -1432,7 +1441,7 @@ TextForMimeData MediaGame::clipboardText() const { return TextForMimeData(); } -QString MediaGame::errorTextForForward(not_null peer) const { +QString MediaGame::errorTextForForward(not_null peer, bool unquoted) const { return Data::RestrictionError( peer, ChatRestriction::SendGames @@ -1582,10 +1591,13 @@ TextForMimeData MediaPoll::clipboardText() const { return TextForMimeData::Simple(text); } -QString MediaPoll::errorTextForForward(not_null peer) const { +QString MediaPoll::errorTextForForward(not_null peer, bool unquoted) const { if (_poll->publicVotes() && peer->isChannel() && !peer->isMegagroup()) { return tr::lng_restricted_send_public_polls(tr::now); } + if (unquoted && _poll->quiz() && !_poll->voted() && !_poll->closed()) { + return ktr("ktg_forward_quiz_unquoted"); + } return Data::RestrictionError( peer, ChatRestriction::SendPolls diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index c1925bba9..bee5afe16 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -90,6 +90,7 @@ public: virtual const Invoice *invoice() const; virtual Data::CloudImage *location() const; virtual PollData *poll() const; + virtual const LocationPoint *geoPoint() const; virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -111,7 +112,7 @@ public: virtual bool forwardedBecomesUnread() const; virtual bool dropForwardedInfo() const; virtual bool forceForwardedInfo() const; - virtual QString errorTextForForward(not_null peer) const; + virtual QString errorTextForForward(not_null peer, bool unquoted = false) const; [[nodiscard]] virtual bool consumeMessageText( const TextWithEntities &text); @@ -166,7 +167,7 @@ public: TextForMimeData clipboardText() const override; bool allowsEditCaption() const override; bool allowsEditMedia() const override; - QString errorTextForForward(not_null peer) const override; + QString errorTextForForward(not_null peer, bool unquoted = false) const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; @@ -207,7 +208,7 @@ public: bool allowsEditMedia() const override; bool forwardedBecomesUnread() const override; bool dropForwardedInfo() const override; - QString errorTextForForward(not_null peer) const override; + QString errorTextForForward(not_null peer, bool unquoted = false) const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; @@ -266,6 +267,7 @@ public: std::unique_ptr clone(not_null parent) override; Data::CloudImage *location() const override; + const LocationPoint *geoPoint() const override; ItemPreview toPreview(ToPreviewOptions options) const override; TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; @@ -366,7 +368,7 @@ public: TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; TextForMimeData clipboardText() const override; - QString errorTextForForward(not_null peer) const override; + QString errorTextForForward(not_null peer, bool unquoted = false) const override; bool dropForwardedInfo() const override; bool consumeMessageText(const TextWithEntities &text) override; @@ -428,7 +430,7 @@ public: TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; TextForMimeData clipboardText() const override; - QString errorTextForForward(not_null peer) const override; + QString errorTextForForward(not_null peer, bool unquoted = false) const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index af5d9f0ad..8119566ff 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -352,6 +352,7 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( return Data::ResolvedForwardDraft{ .items = owner().idsToItems(draft.ids), .options = draft.options, + .groupOptions = draft.groupOptions, }; } @@ -361,6 +362,7 @@ Data::ResolvedForwardDraft History::resolveForwardDraft() { setForwardDraft({ .ids = owner().itemsToIds(result.items), .options = result.options, + .groupOptions = result.groupOptions, }); } return result; @@ -615,7 +617,8 @@ not_null History::addNewLocalMessage( const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + HistoryMessageMarkupData &&markup, + uint64 newGroupId) { return addNewItem( makeMessage( id, @@ -627,7 +630,8 @@ not_null History::addNewLocalMessage( postAuthor, document, caption, - std::move(markup)), + std::move(markup), + newGroupId), true); } @@ -641,7 +645,8 @@ not_null History::addNewLocalMessage( const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + HistoryMessageMarkupData &&markup, + uint64 newGroupId) { return addNewItem( makeMessage( id, @@ -653,7 +658,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 948ccf2d9..18ab9abe9 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -51,14 +51,22 @@ enum class ForwardOptions { NoNamesAndCaptions, }; +enum class GroupingOptions { + GroupAsIs, + RegroupAll, + Separate, +}; + struct ForwardDraft { MessageIdsList ids; ForwardOptions options = ForwardOptions::PreserveInfo; + GroupingOptions groupOptions = GroupingOptions::GroupAsIs; }; struct ResolvedForwardDraft { HistoryItemsList items; ForwardOptions options = ForwardOptions::PreserveInfo; + GroupingOptions groupOptions = GroupingOptions::GroupAsIs; }; } // namespace Data @@ -193,7 +201,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, @@ -204,7 +213,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_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index 2ed2fa906..1613c714f 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/item_text_options.h" #include "core/ui_integration.h" +#include "window/window_peer_menu.h" #include "storage/storage_shared_media.h" #include "mtproto/mtproto_config.h" #include "data/notify/data_notify_settings.h" @@ -112,14 +113,15 @@ QString GetErrorTextForSending( not_null peer, const HistoryItemsList &items, const TextWithTags &comment, - bool ignoreSlowmodeCountdown) { + bool ignoreSlowmodeCountdown, + bool unquoted) { if (!peer->canWrite()) { return tr::lng_forward_cant(tr::now); } for (const auto &item : items) { if (const auto media = item->media()) { - const auto error = media->errorTextForForward(peer); + const auto error = media->errorTextForForward(peer, unquoted); if (!error.isEmpty() && error != qstr("skip")) { return error; } @@ -235,8 +237,9 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { QString GetErrorTextForSending( not_null peer, const HistoryItemsList &items, - bool ignoreSlowmodeCountdown) { - return GetErrorTextForSending(peer, items, {}, ignoreSlowmodeCountdown); + bool ignoreSlowmodeCountdown, + bool unquoted) { + return GetErrorTextForSending(peer, items, {}, ignoreSlowmodeCountdown, unquoted); } TextWithEntities DropCustomEmoji(TextWithEntities text) { @@ -531,7 +534,8 @@ HistoryMessage::HistoryMessage( const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) + HistoryMessageMarkupData &&markup, + uint64 newGroupId) : HistoryItem( history, id, @@ -551,6 +555,11 @@ HistoryMessage::HistoryMessage( document, skipPremiumEffect); setText(caption); + + if (newGroupId) { + setGroupId( + MessageGroupId::FromRaw(history->peer->id, newGroupId)); + } } HistoryMessage::HistoryMessage( @@ -564,7 +573,8 @@ HistoryMessage::HistoryMessage( const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) + HistoryMessageMarkupData &&markup, + uint64 newGroupId) : HistoryItem( history, id, @@ -580,6 +590,11 @@ HistoryMessage::HistoryMessage( _media = std::make_unique(this, photo); setText(caption); + + if (newGroupId) { + setGroupId( + MessageGroupId::FromRaw(history->peer->id, newGroupId)); + } } HistoryMessage::HistoryMessage( diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index 0b75679b7..bdaeb9214 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -43,12 +43,14 @@ void RequestDependentMessageData( [[nodiscard]] QString GetErrorTextForSending( not_null peer, const HistoryItemsList &items, - bool ignoreSlowmodeCountdown = false); + bool ignoreSlowmodeCountdown = false, + bool unquoted = false); [[nodiscard]] QString GetErrorTextForSending( not_null peer, const HistoryItemsList &items, const TextWithTags &comment, - bool ignoreSlowmodeCountdown = false); + bool ignoreSlowmodeCountdown = false, + bool unquoted = false); [[nodiscard]] TextWithEntities DropCustomEmoji(TextWithEntities text); class HistoryMessage final : public HistoryItem { @@ -95,7 +97,8 @@ public: const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); // local document + HistoryMessageMarkupData &&markup, + uint64 newGroupId = 0); // local document HistoryMessage( not_null history, MsgId id, @@ -107,7 +110,8 @@ public: const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); // local photo + HistoryMessageMarkupData &&markup, + uint64 newGroupId = 0); // local photo HistoryMessage( not_null history, MsgId id, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 57564557f..4441bcd56 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" @@ -40,7 +41,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" @@ -6087,12 +6088,61 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { st::historyReplyHeight).contains(e->pos()); if (_replyForwardPressed && !_fieldBarCancel->isHidden()) { updateField(); + } else if (_inReplyEditForward) { + if (readyToForward()) { + if (_toForward.items.empty() || e->button() != Qt::LeftButton) { + return; + } + const auto draft = std::move(_toForward); + session().data().cancelForwarding(_history); + auto list = session().data().itemsToIds(draft.items); + Window::ShowForwardMessagesBox(controller(), { + .ids = session().data().itemsToIds(draft.items), + .options = draft.options, + .groupOptions = draft.groupOptions, + }); + } else { + 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; - const auto now = _toForward.options; + using GroupingOptions = Data::GroupingOptions; const auto count = _toForward.items.size(); - const auto dropNames = (now != Options::PreserveInfo); + const auto hasMediaToGroup = [&] { + if (count > 1) { + auto grouppableMediaCount = 0; + for (const auto item : _toForward.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 : _toForward.items) { if (const auto media = item->media()) { @@ -6104,71 +6154,76 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { } return false; }(); - const auto hasOnlyForcedForwardedInfo = [&] { - if (hasCaptions) { - return false; - } - for (const auto item : _toForward.items) { - if (const auto media = item->media()) { - if (!media->forceForwardedInfo()) { - return false; - } - } else { - return false; - } - } - return true; - }(); - const auto dropCaptions = (now == Options::NoNamesAndCaptions); - const auto weak = Ui::MakeWeak(this); - const auto changeRecipient = crl::guard(weak, [=] { - if (_toForward.items.empty()) { - return; - } - const auto draft = std::move(_toForward); - session().data().cancelForwarding(_history); - auto list = session().data().itemsToIds(draft.items); - Window::ShowForwardMessagesBox(controller(), { - .ids = session().data().itemsToIds(draft.items), - .options = draft.options, - }); - }); - if (hasOnlyForcedForwardedInfo) { - changeRecipient(); - return; - } - const auto optionsChanged = crl::guard(weak, [=]( - Ui::ForwardOptions options) { - const auto newOptions = (options.hasCaptions - && options.dropCaptions) - ? Options::NoNamesAndCaptions - : options.dropNames - ? Options::NoSenderNames - : Options::PreserveInfo; + const auto addForwardOption = [=]( + Options newOptions, + const QString &langKey, + int settingsKey) { if (_history && _toForward.options != newOptions) { - _toForward.options = newOptions; - _history->setForwardDraft({ - .ids = session().data().itemsToIds(_toForward.items), - .options = newOptions, + _menu->addAction(ktr(langKey), [=] { + const auto error = GetErrorTextForSending( + _history->peer, + _toForward.items, + true, + newOptions != Options::PreserveInfo); + if (!error.isEmpty()) { + Ui::ShowMultilineToast({ + .text = { error } + }); + return; + } + _toForward.options = newOptions; + _history->setForwardDraft({ + .ids = session().data().itemsToIds(_toForward.items), + .options = newOptions, + .groupOptions = _toForward.groupOptions, + }); + updateField(); + if (::Kotato::JsonSettings::GetBool("forward_remember_mode")) { + ::Kotato::JsonSettings::Set("forward_mode", settingsKey); + ::Kotato::JsonSettings::Write(); + } }); - updateField(); } - }); - controller()->show(Box( - Ui::ForwardOptionsBox, - count, - Ui::ForwardOptions{ - .dropNames = dropNames, - .hasCaptions = hasCaptions, - .dropCaptions = dropCaptions, - }, - optionsChanged, - changeRecipient)); - } else { - controller()->showPeerHistory( - _peer, - Window::SectionShow::Way::Forward, - _editMsgId ? _editMsgId : replyToId()); + }; + + _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) { + if (_history && _toForward.groupOptions != newOptions) { + _menu->addAction(ktr(langKey), [=] { + _toForward.groupOptions = newOptions; + _history->setForwardDraft({ + .ids = session().data().itemsToIds(_toForward.items), + .options = _toForward.options, + .groupOptions = newOptions, + }); + updateForwardingTexts(); + 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()); } } } @@ -6780,10 +6835,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(); @@ -7588,6 +7650,8 @@ void HistoryWidget::updateForwardingTexts() { auto insertedPeers = base::flat_set>(); auto insertedNames = base::flat_set(); auto fullname = QString(); + auto hasMediaToGroup = false; + auto grouppableMediaCount = 0; auto names = std::vector(); names.reserve(_toForward.items.size()); for (const auto item : _toForward.items) { @@ -7608,8 +7672,20 @@ void HistoryWidget::updateForwardingTexts() { } else { Unexpected("Corrupt forwarded information in message."); } + if (!hasMediaToGroup) { + if (item->media() && item->media()->canBeGrouped()) { + grouppableMediaCount++; + } else { + grouppableMediaCount = 0; + } + if (grouppableMediaCount > 1) { + hasMediaToGroup = true; + } + } } - if (!keepNames) { + if (!keepCaptions) { + from = ktr("ktg_forward_sender_names_and_captions_removed"); + } else if (!keepNames) { from = tr::lng_forward_sender_names_removed(tr::now); } else if (names.size() > 2) { from = tr::lng_forwarding_from(tr::now, lt_count, names.size() - 1, lt_user, names[0]); @@ -7634,8 +7710,19 @@ void HistoryWidget::updateForwardingTexts() { text = DropCustomEmoji(std::move(text)); } } else { - text = Ui::Text::PlainLink( - tr::lng_forward_messages(tr::now, lt_count, count)); + auto forwardText = tr::lng_forward_messages(tr::now, lt_count, count); + + switch (_toForward.groupOptions) { + case Data::GroupingOptions::RegroupAll: + forwardText += ", " + ktr("ktg_forward_subtitle_group_all_media"); + break; + + case Data::GroupingOptions::Separate: + forwardText += ", " + ktr("ktg_forward_subtitle_separate_messages"); + break; + } + + text = Ui::Text::PlainLink(forwardText); } } _toForwardFrom.setText(st::msgNameStyle, from, Ui::NameTextOptions()); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 4c876f033..89542570a 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; @@ -793,6 +794,7 @@ private: int _topDelta = 0; + base::unique_qptr _menu; rpl::event_stream<> _cancelRequests; }; diff --git a/Telegram/SourceFiles/kotato/kotato_settings.cpp b/Telegram/SourceFiles/kotato/kotato_settings.cpp index f03a87083..82ca52ee5 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp @@ -406,6 +406,12 @@ 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, }}, { "auto_scroll_unfocused", { .type = SettingType::BoolSetting, .defaultValue = false, }}, @@ -415,6 +421,20 @@ const std::map> DefinitionMap { { "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 e5bd06de3..b076348ad 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp @@ -45,6 +45,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) { @@ -382,6 +430,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 20497fea1..bd6230270 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -506,7 +506,8 @@ bool MainWidget::setForwardDraft(PeerId peerId, Data::ForwardDraft &&draft) { const auto error = GetErrorTextForSending( peer, session().data().idsToItems(draft.ids), - true); + true, + draft.options != Data::ForwardOptions::PreserveInfo); if (!error.isEmpty()) { Ui::show(Ui::MakeInformBox(error), Ui::LayerOption::KeepOther); return false; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 6542db290..b1aa70cf6 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_bot_to_chat_box.h" #include "boxes/peers/add_participants_box.h" #include "boxes/peers/edit_contact_box.h" +#include "boxes/share_box.h" #include "ui/boxes/report_box.h" #include "ui/toast/toast.h" #include "ui/text/format_values.h" @@ -43,11 +44,15 @@ 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_item.h" #include "history/history_message.h" // GetErrorTextForSending. +#include "history/history_widget.h" +#include "history/view/history_view_element.h" #include "history/view/history_view_context_menu.h" #include "window/window_adaptive.h" // Adaptive::isThreeColumn #include "window/window_session_controller.h" @@ -65,9 +70,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_drafts.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" @@ -77,6 +85,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 namespace Window { @@ -1215,6 +1225,7 @@ void BlockSenderFromRepliesBox( Window::ClearReply{ id }); } +/* QPointer ShowForwardMessagesBox( not_null navigation, Data::ForwardDraft &&draft, @@ -1267,15 +1278,244 @@ QPointer ShowForwardMessagesBox( std::move(initBox)), Ui::LayerOption::KeepOther); return weak->data(); } +*/ + +QPointer ShowForwardMessagesBox( + not_null navigation, + Data::ForwardDraft &&draft, + FnMut &&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 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, + items, + comment, + false, /* ignoreSlowmodeCountdown */ + 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->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 peer : result) { + const auto history = owner->history(peer); + auto action = Api::SendAction(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 = [](PeerData *peer) { + return peer->canWrite(); + }; + auto copyLinkCallback = canCopyLink + ? Fn(std::move(copyCallback)) + : Fn(); + auto goToChatCallback = [navigation, data]( + PeerData *peer, + 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(peer->id, 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( not_null navigation, MessageIdsList &&items, FnMut &&successCallback) { + const auto options = [] { + 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; + } + }(); + return ShowForwardMessagesBox( navigation, - Data::ForwardDraft{ .ids = std::move(items) }, - std::move(successCallback)); + Data::ForwardDraft{ + .ids = std::move(items), + .options = options, + .groupOptions = groupOptions, + }, std::move(successCallback)); } QPointer ShowSendNowMessagesBox( diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 269e48773..c83e383ca 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -190,8 +190,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