diff --git a/Telegram/Resources/langs/rewrites/en.json b/Telegram/Resources/langs/rewrites/en.json index d78c2efc0..5c46ef234 100644 --- a/Telegram/Resources/langs/rewrites/en.json +++ b/Telegram/Resources/langs/rewrites/en.json @@ -105,7 +105,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", @@ -127,6 +141,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 0e5d7c585..175d86d3d 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" @@ -68,7 +69,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, @@ -166,6 +168,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); @@ -180,15 +183,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), @@ -201,7 +230,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); @@ -211,7 +242,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), @@ -223,10 +256,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) @@ -330,7 +368,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; } @@ -515,4 +555,73 @@ 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; + } + const auto topicRootId = action.replyTo.messageId + ? action.replyTo.topicRootId + : 0; + 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, + action.mtpReplyTo(), + 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 73026cc88..6bf47c79f 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 "api/api_websites.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" +#include "data/data_poll.h" #include "data/data_web_page.h" #include "data/data_folder.h" #include "data/data_forum_topic.h" @@ -3170,6 +3172,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(); @@ -3229,6 +3237,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>(); @@ -3307,9 +3316,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)); @@ -3318,6 +3332,501 @@ 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 topicRootId = action.replyTo.topicRootId; + const auto topMsgId = (topicRootId == kGeneralId) + ? MsgId(0) + : 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 + action.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 + action.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, + action.mtpReplyTo(), + 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 webpage = (!item->media() || !item->media()->webpage()) + ? Data::WebPageDraft{ .removed = true } + : Data::WebPageDraft{ + .id = item->media()->webpage()->id, + }; + + auto message = MessageToSend(action); + message.textWithTags = TextWithTags{ + item->originalText().text, + TextUtilities::ConvertEntitiesToTextTags(item->originalText().entities) + }; + message.action.clearDraft = false; + message.webPage = webpage; + + 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, @@ -3570,7 +4079,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; @@ -3591,7 +4103,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); @@ -3731,6 +4247,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { draftTopicRootId, UnixtimeFromMsgId(response.outerMsgId)); } + if (doneCallback) { + doneCallback(result, response.requestId); + } }; const auto fail = [=]( const MTP::Error &error, @@ -3785,7 +4304,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { isFirst = false; } - finishForwarding(action); + if (!forwarding) { + finishForwarding(action); + } } void ApiWrap::sendBotStart( diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 192e14f28..10e630598 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, @@ -334,7 +338,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 a9c92c745..1e76d0dd7 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1154,7 +1154,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 dff268c26..b933ed88d 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 "api/api_premium.h" #include "base/random.h" #include "lang/lang_keys.h" @@ -17,20 +19,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/scroll_area.h" -#include "ui/widgets/fields/input_field.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" #include "history/history_item_helpers.h" #include "history/view/history_view_element.h" #include "history/view/history_view_context_menu.h" // CopyPostLink. +#include "window/window_peer_menu.h" #include "settings/settings_premium.h" #include "window/window_session_controller.h" #include "boxes/peer_list_controllers.h" @@ -51,11 +54,52 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "styles/style_layers.h" #include "styles/style_boxes.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( @@ -65,6 +109,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; @@ -80,6 +126,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; @@ -172,6 +221,8 @@ private: base::flat_set> _selected; Fn, bool)> _peerSelectedChangedCallback; + Fn _submitRequest; + Fn _goToChatRequest; bool _searching = false; QString _lastQuery; @@ -181,6 +232,7 @@ private: rpl::event_stream _scrollToRequests; rpl::event_stream<> _searchRequests; + bool _hadSelection = false; }; ShareBox::ShareBox(QWidget*, Descriptor &&descriptor) @@ -259,7 +311,31 @@ 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.sendersCount = _descriptor.forwardOptions.sendersCount; + _forwardOptions.captionsCount = _descriptor.forwardOptions.captionsCount; + _forwardOptions.dropNames = (forwardOptions != Data::ForwardOptions::PreserveInfo); + _forwardOptions.dropCaptions = (forwardOptions == Data::ForwardOptions::NoNamesAndCaptions); + _groupOptions = groupOptions; + + updateAdditionalTitle(); _inner = setInnerWidget( object_ptr(this, _descriptor, uiShow()), @@ -282,11 +358,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( @@ -314,6 +401,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(), @@ -449,6 +547,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); } @@ -475,33 +575,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), - _forwardOptions, - [=](Ui::ForwardOptions value) { _forwardOptions = value; }, - _menu->lifetime()); - - _menu->addSeparator(); - } - const auto result = SendMenu::FillSendMenu( _menu.get(), sendMenuType(), @@ -517,14 +590,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.sendersCount - = _descriptor.forwardOptions.sendersCount; - _forwardOptions.captionsCount - = _descriptor.forwardOptions.captionsCount; send->setAcceptBoth(); send->clicks( @@ -539,6 +618,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); @@ -587,7 +864,8 @@ void ShareBox::submit(Api::SendOptions options) { _inner->selected(), _comment->entity()->getTextWithAppliedMarkdown(), options, - forwardOptions); + forwardOptions, + _groupOptions); } } @@ -616,14 +894,29 @@ void ShareBox::copyLink() const { } } +void ShareBox::goToChat(not_null thread) { + if (_descriptor.goToChatCallback) { + const auto forwardOptions = (_forwardOptions.captionsCount + && _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(); } @@ -1159,6 +1452,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(); + } } } @@ -1166,6 +1464,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; @@ -1259,6 +1576,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, @@ -1286,6 +1611,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(); @@ -1457,6 +1786,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; }; @@ -1587,11 +1930,19 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( state->requests.insert(threadHistory->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(); @@ -1670,6 +2021,7 @@ void FastShareMessage( .premiumRequiredError = SharePremiumRequiredError(), }), Ui::LayerOption::CloseOther); + */ } auto SharePremiumRequiredError() diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index d5cd22ffa..ad99afc09 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; @@ -80,19 +82,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; @@ -103,6 +111,8 @@ public: int sendersCount = 0; int captionsCount = 0; bool show = false; + bool hasMedia = false; + bool isShare = true; } forwardOptions; HistoryView::ScheduleBoxStyleArgs scheduleBoxStyle; @@ -127,6 +137,7 @@ private: void submitScheduled(); void submitWhenOnline(); void copyLink() const; + void goToChat(not_null thread); bool searchByUsername(bool useCache = false); SendMenu::Type sendMenuType() const; @@ -136,6 +147,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; @@ -159,7 +172,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 a34bd5cb3..9dc9daa8a 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -134,7 +134,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 e5b1f5be6..b5c380462 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -1452,6 +1452,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 c33c9cbd0..cc372a316 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -254,6 +254,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 eee8ac7e1..7dd6fcf91 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" @@ -463,6 +464,11 @@ PollData *Media::poll() const { return nullptr; } + +const LocationPoint *Media::geoPoint() const { + return nullptr; +} + const WallPaper *Media::paper() const { return nullptr; } @@ -1365,6 +1371,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 b2323af8a..d1c8f990a 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -145,6 +145,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 paperForBoth() const; virtual FullStoryId storyId() const; @@ -344,6 +345,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 f5a9765c0..4bfb59088 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; @@ -692,7 +694,8 @@ not_null History::addNewLocalMessage( const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + HistoryMessageMarkupData &&markup, + uint64 newGroupId) { return addNewItem( makeMessage( id, @@ -704,7 +707,8 @@ not_null History::addNewLocalMessage( postAuthor, document, caption, - std::move(markup)), + std::move(markup), + newGroupId), true); } @@ -718,7 +722,8 @@ not_null History::addNewLocalMessage( const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + HistoryMessageMarkupData &&markup, + uint64 newGroupId) { return addNewItem( makeMessage( id, @@ -730,7 +735,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 adeb63fc2..3493ef8a3 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 @@ -176,7 +184,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, @@ -187,7 +196,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 15fbde7b5..5261cd34f 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. @@ -615,7 +616,8 @@ HistoryItem::HistoryItem( const QString &postAuthor, not_null document, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) + HistoryMessageMarkupData &&markup, + uint64 groupedId) : HistoryItem( history, id, @@ -638,6 +640,12 @@ HistoryItem::HistoryItem( spoiler, /*ttlSeconds = */0); setText(caption); + if (groupedId) { + setGroupId(MessageGroupId::FromRaw( + history->peer->id, + groupedId, + flags & MessageFlag::IsOrWasScheduled)); + } } HistoryItem::HistoryItem( @@ -651,7 +659,8 @@ HistoryItem::HistoryItem( const QString &postAuthor, not_null photo, const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) + HistoryMessageMarkupData &&markup, + uint64 groupedId) : HistoryItem( history, id, @@ -668,6 +677,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( @@ -2327,7 +2342,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(); @@ -2342,6 +2358,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 3c5df4f25..1b0a33ef7 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -126,7 +126,7 @@ public: const TextWithEntities &textWithEntities, const MTPMessageMedia &media, HistoryMessageMarkupData &&markup, - uint64 groupedId); + uint64 groupedId = 0); HistoryItem( // Local service message. not_null history, MsgId id, @@ -155,7 +155,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, @@ -167,7 +168,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, @@ -428,7 +430,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 fb60bf9a7..9416948a4 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -74,7 +74,7 @@ QString GetErrorTextForSending( } 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 5ebb3f0de..2e40f464c 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -103,6 +103,7 @@ struct SendingErrorRequest { const Data::Story *story = 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 cae7f875b..fd867c36b 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" @@ -6498,11 +6499,14 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { Window::SectionShow::Way::Forward, _editMsgId); } else if (isReadyToForward) { - if (e->button() != Qt::LeftButton) { - _forwardPanel->editToNextOption(); - } else { - _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 if (_replyTo && ((e->modifiers() & Qt::ControlModifier) || (e->button() != Qt::LeftButton))) { @@ -6517,6 +6521,116 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { } } +void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) { + if (_menu) { + return; + } + if (_inDetails) { + 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()); + } + } +} + void HistoryWidget::editDraftOptions() { Expects(_history != nullptr); @@ -7291,10 +7405,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 3fa34ac2b..d6286d1e5 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -299,6 +299,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; @@ -821,6 +822,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 13c6dace6..2d00a9fc8 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,8 @@ 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/layers/generic_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" @@ -241,6 +242,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(); @@ -297,6 +299,7 @@ void ForwardPanel::editOptions(std::shared_ptr show) { }, optionsChanged, changeRecipient)); + */ } void ForwardPanel::editToNextOption() { diff --git a/Telegram/SourceFiles/kotato/kotato_settings.cpp b/Telegram/SourceFiles/kotato/kotato_settings.cpp index ddb6abc2a..1983d944e 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp @@ -366,12 +366,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 d5fc13f62..5b2094bd5 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings_menu.cpp @@ -48,6 +48,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) { @@ -298,6 +346,71 @@ void SetupKotatoForward(not_null container) { Ui::AddSkip(container); Ui::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); + + Ui::AddSkip(container); + Ui::AddDividerText(container, rktr("ktg_forward_force_old_unquoted_desc")); + Ui::AddSkip(container); + + SettingsMenuJsonSwitch(ktg_settings_forward_retain_selection, forward_retain_selection); + SettingsMenuJsonSwitch(ktg_settings_forward_chat_on_click, forward_on_click); Ui::AddSkip(container); Ui::AddDividerText(container, rktr("ktg_settings_forward_chat_on_click_description")); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 0f1541ae8..fdca0a8d7 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -537,6 +537,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/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index deb291cc1..5f7a7a3a4 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -77,7 +77,8 @@ namespace Media::Stories { std::vector> &&result, TextWithTags &&comment, Api::SendOptions options, - Data::ForwardOptions forwardOptions) { + Data::ForwardOptions forwardOptions, + Data::GroupingOptions groupingOptions) { if (state->requests) { return; // Share clicked already. } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 2f4dcc711..dff24fa26 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" @@ -52,9 +53,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_session_controller.h" @@ -77,10 +82,13 @@ 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_saved_sublist.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" @@ -91,6 +99,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 @@ -1765,10 +1775,227 @@ 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 sendersCount = ItemsForwardSendersCount(items); + const auto captionsCount = ItemsForwardCaptionsCount(items); + const auto hasOnlyForcedForwardedInfo = captionsCount + ? 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 = { + .sendersCount = sendersCount, + .captionsCount = captionsCount, + .show = !hasOnlyForcedForwardedInfo, + .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 itemsList = owner->idsToItems(draft.ids); @@ -2082,16 +2309,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( @@ -2100,7 +2318,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 53db41bc6..0c213fc40 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -273,8 +273,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