diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 7cc0af58ba..81b1ccba6b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -168,6 +168,10 @@ set(ayugram_files ayu/features/streamer_mode/streamer_mode.h ayu/features/messageshot/message_shot.cpp ayu/features/messageshot/message_shot.h + ayu/features/forward/ayu_forward.cpp + ayu/features/forward/ayu_forward.h + ayu/features/forward/ayu_sync.cpp + ayu/features/forward/ayu_sync.h ayu/data/messages_storage.cpp ayu/data/messages_storage.h ayu/data/entities.h diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 44b5427fd6..e47ddf991d 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -90,6 +90,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ayu/ayu_settings.h" #include "ayu/ayu_worker.h" #include "ayu/utils/telegram_helpers.h" +#include "ayu/features/forward/ayu_forward.h" namespace { @@ -3279,6 +3280,22 @@ void ApiWrap::forwardMessages( FnMut &&successCallback) { Expects(!draft.items.empty()); + const auto fullAyuForward = AyuForward::isFullAyuForwardNeeded(draft.items.front()); + if (fullAyuForward) { + crl::async([=] { + AyuForward::forwardMessages(_session, action, false, draft); + }); + return; + } + + const auto ayuIntelligentForwardNeeded = AyuForward::isAyuForwardNeeded(draft.items); + if (ayuIntelligentForwardNeeded) { + crl::async([=] { + AyuForward::intelligentForward(_session, action, draft); + }); + return; + } + auto &histories = _session->data().histories(); struct SharedCallback { @@ -3766,8 +3783,12 @@ void ApiWrap::sendMessage(MessageToSend &&message) { ? replyTo->topicRootId() : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); - if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) - || Api::SendDice(message)) { + + const bool canSendTexts = topic + ? Data::CanSendTexts(topic) + : Data::CanSendTexts(peer); + + if (!canSendTexts && !AyuForward::isForwarding(peer->id) || Api::SendDice(message)) { return; } local().saveRecentSentHashtags(textWithTags.text); diff --git a/Telegram/SourceFiles/ayu/features/forward/ayu_forward.cpp b/Telegram/SourceFiles/ayu/features/forward/ayu_forward.cpp new file mode 100644 index 0000000000..651666b543 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/forward/ayu_forward.cpp @@ -0,0 +1,402 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "ayu_forward.h" +#include "apiwrap.h" +#include "ayu_sync.h" +#include "lang_auto.h" +#include "ayu/utils/telegram_helpers.h" +#include "base/random.h" +#include "base/unixtime.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_photo.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "storage/file_download.h" +#include "storage/localimageloader.h" +#include "storage/storage_account.h" +#include "storage/storage_media_prepare.h" +#include "styles/style_boxes.h" +#include "ui/chat/attach/attach_prepare.h" +#include "ui/text/text_utilities.h" + +namespace AyuForward { + +std::unordered_map> forwardStates; + +bool isForwarding(const PeerId &id) { + const auto fwState = forwardStates.find(id); + if (id.value && fwState != forwardStates.end()) { + const auto state = *fwState->second; + + return state.state != ForwardState::State::Finished + && state.currentChunk < state.totalChunks + && !state.stopRequested + && state.totalChunks + && state.totalMessages; + } + return false; +} + +void cancelForward(const PeerId &id, const Main::Session &session) { + const auto fwState = forwardStates.find(id); + if (fwState != forwardStates.end()) { + fwState->second->stopRequested = true; + fwState->second->updateBottomBar(session, &id, ForwardState::State::Finished); + } +} + +std::pair stateName(const PeerId &id) { + const auto fwState = forwardStates.find(id); + + + if (fwState == forwardStates.end()) { + return std::make_pair(QString(), QString()); + } + + const auto state = fwState->second; + + QString messagesString = tr::ayu_AyuForwardStatusSentCount(tr::now, + lt_count1, + QString::number(state->sentMessages), + lt_count2, + QString::number(state->totalMessages) + + ); + + QString chunkString = tr::ayu_AyuForwardStatusChunkCount(tr::now, + lt_count1, + QString::number(state->currentChunk + 1), + lt_count2, + QString::number(state->totalChunks) + + ); + + const auto partString = state->totalChunks <= 1 ? messagesString : (messagesString + " • " + chunkString); + + QString status; + + if (state->state == ForwardState::State::Preparing) { + status = tr::ayu_AyuForwardStatusPreparing(tr::now); + } else if (state->state == ForwardState::State::Downloading) { + status = tr::ayu_AyuForwardStatusLoadingMedia(tr::now); + } else if (state->state == ForwardState::State::Sending) { + status = tr::ayu_AyuForwardStatusForwarding(tr::now); + } else { + // ForwardState::State::Finished + status = tr::ayu_AyuForwardStatusFinished(tr::now); + } + + + return std::make_pair(status, partString); +} + +void ForwardState::updateBottomBar(const Main::Session &session, const PeerId *peer, const State &st) { + state = st; + + session.changes().peerUpdated(session.data().peer(*peer), Data::PeerUpdate::Flag::Rights); +} + +static Ui::PreparedList prepareMedia(not_null session, + const std::vector> &items, + int &i, + std::vector> &groupMedia) { + const auto prepare = [&](not_null media) + { + groupMedia.emplace_back(media); + auto prepared = Ui::PreparedFile(AyuSync::filePath(session, media)); + Storage::PrepareDetails(prepared, st::sendMediaPreviewSize, PhotoSideLimit()); + return prepared; + }; + + const auto startItem = items[i]; + const auto media = startItem->media(); + const auto groupId = startItem->groupId(); + + Ui::PreparedList list; + list.files.emplace_back(prepare(media)); + + if (!groupId.value) { + return list; + } + + for (int k = i + 1; k < items.size(); ++k) { + const auto nextItem = items[k]; + if (nextItem->groupId() != groupId) { + break; + } + if (const auto nextMedia = nextItem->media()) { + list.files.emplace_back(prepare(nextMedia)); + i = k; + } + } + return list; +} + +void sendMedia( + not_null session, + std::shared_ptr bundle, + not_null primaryMedia, + Api::MessageToSend &&message, + bool sendImagesAsPhotos) { + if (const auto document = primaryMedia->document(); document && document->sticker()) { + AyuSync::sendStickerSync(session, message, document); + return; + } + + auto mediaType = [&] + { + if (const auto document = primaryMedia->document()) { + if (document->isVoiceMessage()) { + return SendMediaType::Audio; + } else if (document->isVideoMessage()) { + return SendMediaType::Round; + } + return SendMediaType::File; + } + return SendMediaType::Photo; + }(); + + if (mediaType == SendMediaType::Round || mediaType == SendMediaType::Audio) { + const auto path = bundle->groups.front().list.files.front().path; + + QFile file(path); + auto failed = false; + if (!file.open(QIODevice::ReadOnly)) { + LOG(("failed to open file for forward with reason: %1").arg(file.errorString())); + failed = true; + } + auto data = file.readAll(); + + if (!failed && data.size()) { + file.close(); + AyuSync::sendVoiceSync(session, + data, + primaryMedia->document()->duration(), + mediaType == SendMediaType::Round, + message.action); + return; + } + // at least try to send it as squared-video + } + + // workaround for media albums consisting of video and photos + if (sendImagesAsPhotos) { + mediaType = SendMediaType::Photo; + } + + for (auto &group : bundle->groups) { + AyuSync::sendDocumentSync( + session, + group, + mediaType, + std::move(message.textWithTags), + message.action); + } +} + +bool isAyuForwardNeeded(const std::vector> &items) { + for (const auto &item : items) { + if (isAyuForwardNeeded(item)) { + return true; + } + } + return false; +} + +bool isAyuForwardNeeded(not_null item) { + if (item->isDeleted() || item->isAyuNoForwards() || item->ttlDestroyAt()) { + return true; + } + return false; +} + +bool isFullAyuForwardNeeded(not_null item) { + return item->from()->isAyuNoForwards() || item->history()->peer->isAyuNoForwards(); +} + +struct ForwardChunk +{ + bool isAyuForwardNeeded; + std::vector> items; +}; + +void intelligentForward( + not_null session, + const Api::SendAction &action, + Data::ResolvedForwardDraft draft) { + const auto history = action.history; + history->setForwardDraft(action.replyTo.topicRootId, {}); + + const auto items = draft.items; + const auto peer = history->peer; + + auto chunks = std::vector(); + auto currentArray = std::vector>(); + + auto currentChunk = ForwardChunk({ + .isAyuForwardNeeded = isAyuForwardNeeded(items[0]), + .items = currentArray + }); + + for (const auto &item : items) { + if (isAyuForwardNeeded(item) != currentChunk.isAyuForwardNeeded) { + currentChunk.items = currentArray; + chunks.push_back(currentChunk); + + currentArray = std::vector>(); + + currentChunk = ForwardChunk({ + .isAyuForwardNeeded = isAyuForwardNeeded(item), + .items = currentArray + }); + } + currentArray.push_back(item); + } + + currentChunk.items = currentArray; + chunks.push_back(currentChunk); + + auto state = std::make_shared(chunks.size()); + forwardStates[peer->id] = state; + + + for (const auto &chunk : chunks) { + if (chunk.isAyuForwardNeeded) { + forwardMessages(session, action, true, Data::ResolvedForwardDraft(chunk.items)); + } else { + state->totalMessages = chunk.items.size(); + state->sentMessages = 0; + state->updateBottomBar(*session, &peer->id, ForwardState::State::Sending); + + AyuSync::forwardMessagesSync(session, chunk.items, action, draft.options); + + state->sentMessages = state->totalMessages; + + state->updateBottomBar(*session, &peer->id, ForwardState::State::Finished); + } + state->currentChunk++; + } + + state->updateBottomBar(*session, &peer->id, ForwardState::State::Finished); +} + +void forwardMessages( + not_null session, + const Api::SendAction &action, + bool forwardState, + Data::ResolvedForwardDraft draft) { + const auto items = draft.items; + const auto history = action.history; + const auto peer = history->peer; + + history->setForwardDraft(action.replyTo.topicRootId, {}); + + std::shared_ptr state; + + if (forwardState) { + state = std::make_shared(*forwardStates[peer->id]); + } else { + state = std::make_shared(1); + } + + forwardStates[peer->id] = state; + + std::unordered_map groupIds; + + std::vector> toBeDownloaded; + + + for (const auto item : items) { + if (mediaDownloadable(item->media())) { + toBeDownloaded.push_back(item); + } + + if (item->groupId()) { + const auto currentId = groupIds.find(item->groupId().value); + + if (currentId == groupIds.end()) { + groupIds[item->groupId().value] = base::RandomValue(); + } + } + } + state->totalMessages = items.size(); + if (!toBeDownloaded.empty()) { + state->state = ForwardState::State::Downloading; + state->updateBottomBar(*session, &peer->id, ForwardState::State::Downloading); + AyuSync::loadDocuments(session, toBeDownloaded); + } + + + state->sentMessages = 0; + state->updateBottomBar(*session, &peer->id, ForwardState::State::Sending); + + for (int i = 0; i < items.size(); i++) { + const auto item = items[i]; + + if (state->stopRequested) { + state->updateBottomBar(*session, &peer->id, ForwardState::State::Finished); + return; + } + + auto extractedText = extractText(item); + if (extractedText.empty() && !mediaDownloadable(item->media())) { + continue; + } + + auto message = Api::MessageToSend(Api::SendAction(session->data().history(peer->id))); + message.action.replyTo = action.replyTo; + + if (draft.options != Data::ForwardOptions::NoNamesAndCaptions) { + message.textWithTags = extractedText; + } + + if (!mediaDownloadable(item->media())) { + AyuSync::sendMessageSync(session, message); + } else if (const auto media = item->media()) { + if (media->poll()) { + AyuSync::sendMessageSync(session, message); + continue; + } + + std::vector> groupMedia; + auto preparedMedia = prepareMedia(session, items, i, groupMedia); + + Ui::SendFilesWay way; + way.setGroupFiles(true); + way.setSendImagesAsPhotos(false); + for (const auto &media2 : groupMedia) { + if (media2->photo()) { + way.setSendImagesAsPhotos(true); + break; + } + } + + auto groups = Ui::DivideByGroups( + std::move(preparedMedia), + way, + peer->slowmodeApplied()); + + auto bundle = Ui::PrepareFilesBundle( + std::move(groups), + way, + message.textWithTags, + false); + sendMedia(session, bundle, media, std::move(message), way.sendImagesAsPhotos()); + } + // if there are grouped messages + // "i" is incremented in prepareMedia + + state->sentMessages = i + 1; + state->updateBottomBar(*session, &peer->id, ForwardState::State::Sending); + } + state->updateBottomBar(*session, &peer->id, ForwardState::State::Finished); +} + +} // namespace AyuFeatures::AyuForward diff --git a/Telegram/SourceFiles/ayu/features/forward/ayu_forward.h b/Telegram/SourceFiles/ayu/features/forward/ayu_forward.h new file mode 100644 index 0000000000..cd582624f4 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/forward/ayu_forward.h @@ -0,0 +1,52 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include "history/history.h" +#include "main/main_session.h" + +namespace AyuForward { +bool isForwarding(const PeerId &id); +void cancelForward(const PeerId &id, const Main::Session &session); +std::pair stateName(const PeerId &id); + +class ForwardState +{ +public: + enum class State + { + Preparing, + Downloading, + Sending, + Finished + }; + void updateBottomBar(const Main::Session &session, const PeerId *peer, const State &st); + + int totalChunks; + int currentChunk; + int totalMessages; + int sentMessages; + + State state = State::Preparing; + bool stopRequested = false; + +}; + +bool isAyuForwardNeeded(const std::vector> &items); +bool isAyuForwardNeeded(not_null item); +bool isFullAyuForwardNeeded(not_null item); +void intelligentForward( + not_null session, + const Api::SendAction &action, + Data::ResolvedForwardDraft draft); +void forwardMessages( + not_null session, + const Api::SendAction &action, + bool forwardState, + Data::ResolvedForwardDraft draft); + +} diff --git a/Telegram/SourceFiles/ayu/features/forward/ayu_sync.cpp b/Telegram/SourceFiles/ayu/features/forward/ayu_sync.cpp new file mode 100644 index 0000000000..f8958c248b --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/forward/ayu_sync.cpp @@ -0,0 +1,326 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "ayu_sync.h" +#include "apiwrap.h" +#include "api/api_sending.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "core/file_utilities.h" +#include "data/data_document.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "storage/file_download_mtproto.h" +#include "storage/localimageloader.h" + +class TimedCountDownLatch +{ +public: + explicit TimedCountDownLatch(int count) + : count_(count) { + } + + void countDown() { + std::unique_lock lock(mutex_); + if (count_ > 0) { + count_--; + } + if (count_ == 0) { + cv_.notify_all(); + } + } + + bool await(std::chrono::milliseconds timeout) { + std::unique_lock lock(mutex_); + if (count_ == 0) { + return true; + } + return cv_.wait_for(lock, timeout, [this] { return count_ == 0; }); + } + +private: + std::mutex mutex_; + std::condition_variable cv_; + int count_; +}; + +namespace AyuSync { + +QString pathForSave(not_null session) { + const auto path = Core::App().settings().downloadPath(); + if (path.isEmpty()) { + return File::DefaultDownloadPath(session); + } + if (path == FileDialog::Tmp()) { + return session->local().tempDirectory(); + } + return path; +} + +QString filePath(not_null session, const Data::Media *media) { + if (const auto document = media->document()) { + if (!document->filename().isEmpty()) { + return pathForSave(session) + media->document()->filename(); + } + if (const auto name = document->filepath(true); !name.isEmpty()) { + return name; + } + if (document->isVoiceMessage()) { + return pathForSave(session) + "audio_" + QString::number(document->getDC()) + "_" + + QString::number(document->id) + ".ogg"; + } + if (document->isVideoMessage()) { + return pathForSave(session) + "round_" + QString::number(document->getDC()) + "_" + + QString::number(document->id) + ".mp4"; + } + } else if (const auto photo = media->photo()) { + return pathForSave(session) + QString::number(photo->getDC()) + "_" + QString::number(photo->id) + ".jpg"; + } + + return QString(); +} + +qint64 fileSize(not_null item) { + if (const auto path = filePath(&item->history()->session(), item->media()); !path.isEmpty()) { + QFile file(path); + if (file.exists()) { + auto size = file.size(); + return size; + } + } + return 0; +} + +void loadDocuments(not_null session, const std::vector> &items) { + for (const auto &item : items) { + if (const auto data = item->media()->document()) { + const auto size = fileSize(item); + + if (size == data->size) { + continue; + } + if (size && size < data->size) { + // in case there some unfinished file + QFile file(filePath(session, item->media())); + file.remove(); + } + + loadDocumentSync(session, data, item); + } else if (auto photo = item->media()->photo()) { + if (fileSize(item) == photo->imageByteSize(Data::PhotoSize::Large)) { + continue; + } + + loadPhotoSync(session, std::pair(photo, item->fullId())); + } + } +} + +void loadDocumentSync(not_null session, DocumentData *data, not_null item) { + auto latch = std::make_shared(1); + auto lifetime = std::make_shared(); + + data->save(Data::FileOriginMessage(item->fullId()), filePath(session, item->media())); + + rpl::single() | rpl::then( + session->downloaderTaskFinished() + ) | rpl::filter([&] + { + return data->status == FileDownloadFailed || fileSize(item) == data->size; + }) | rpl::start_with_next([&]() mutable + { + latch->countDown(); + base::take(lifetime)->destroy(); + }, + *lifetime); + + latch->await(std::chrono::minutes(5)); +} + +void forwardMessagesSync(not_null session, + const std::vector> &items, + const ApiWrap::SendAction &action, + Data::ForwardOptions options) { + auto latch = std::make_shared(1); + + crl::on_main([=, &latch] + { + session->api().forwardMessages(Data::ResolvedForwardDraft(items, options), + action, + [&] + { + latch->countDown(); + }); + }); + + + latch->await(std::chrono::minutes(1)); +} + +void loadPhotoSync(not_null session, const std::pair, FullMsgId> &photo) { + const auto folderPath = pathForSave(session); + const auto downloadPath = folderPath.isEmpty() ? Core::App().settings().downloadPath() : folderPath; + + const auto path = downloadPath.isEmpty() + ? File::DefaultDownloadPath(session) + : downloadPath == FileDialog::Tmp() + ? session->local().tempDirectory() + : downloadPath; + if (path.isEmpty()) { + return; + } + if (!QDir().mkpath(path)) { + return; + } + + const auto view = photo.first->createMediaView(); + if (!view) { + return; + } + view->wanted(Data::PhotoSize::Large, photo.second); + + const auto finalCheck = [=] + { + return !photo.first->loading(); + }; + + const auto saveToFiles = [=] + { + QDir directory(path); + const auto dir = directory.absolutePath(); + const auto nameBase = dir.endsWith('/') ? dir : dir + '/'; + const auto fullPath = nameBase + QString::number(photo.first->getDC()) + "_" + QString::number(photo.first->id) + + ".jpg"; + view->saveToFile(fullPath); + }; + + auto latch = std::make_shared(1); + auto lifetime = std::make_shared(); + + if (finalCheck()) { + saveToFiles(); + } else { + session->downloaderTaskFinished() | rpl::filter([&] + { + return finalCheck(); + }) | rpl::start_with_next([&]() mutable + { + saveToFiles(); + latch->countDown(); + base::take(lifetime)->destroy(); + }, + *lifetime); + } + + latch->await(std::chrono::minutes(5)); +} + +void sendMessageSync(not_null session, Api::MessageToSend &message) { + crl::on_main([=, &message] + { + // we cannot send events to objects + // owned by a different thread + // because sendMessage updates UI too + + session->api().sendMessage(std::move(message)); + }); + + + waitForMsgSync(session, message.action); +} + +void waitForMsgSync(not_null session, const Api::SendAction &action) { + auto latch = std::make_shared(1); + auto lifetime = std::make_shared(); + + + session->data().itemIdChanged() + | rpl::filter([&](const Data::Session::IdChange &update) + { + return action.history->peer->id == update.newId.peer; + }) | rpl::start_with_next([&] + { + latch->countDown(); + base::take(lifetime)->destroy(); + }, + *lifetime); + + latch->await(std::chrono::minutes(2)); +} + +void sendDocumentSync(not_null session, + Ui::PreparedGroup &group, + SendMediaType type, + TextWithTags &&caption, + const Api::SendAction &action) { + const auto size = group.list.files.size(); + auto latch = std::make_shared(size); + auto lifetime = std::make_shared(); + + auto groupId = std::make_shared(); + groupId->groupId = base::RandomValue(); + + crl::on_main([=, lst = std::move(group.list), caption = std::move(caption)]() mutable + { + session->api().sendFiles(std::move(lst), type, std::move(caption), groupId, action); + }); + + + // probably need to handle + // session->uploader().photoFailed() + // and + // session->uploader().documentFailed() + // too + + rpl::merge( + session->uploader().documentReady(), + session->uploader().photoReady() + ) | rpl::filter([&](const Storage::UploadedMedia &docOrPhoto) + { + return docOrPhoto.fullId.peer == action.history->peer->id; + }) | rpl::start_with_next([&] + { + latch->countDown(); + }, + *lifetime); + + latch->await(std::chrono::minutes(5 * size)); + base::take(lifetime)->destroy(); +} + +void sendStickerSync(not_null session, + Api::MessageToSend &message, + not_null document) { + auto &action = message.action; + crl::on_main([&] + { + Api::SendExistingDocument(std::move(message), document, std::nullopt); + }); + + waitForMsgSync(session, action); +} + +void sendVoiceSync(not_null session, + const QByteArray &data, + int64_t duration, + bool video, + const Api::SendAction &action) { + crl::on_main([&] + { + session->api().sendVoiceMessage(data, + QVector(), + duration, + video, + action); + }); + waitForMsgSync(session, action); +} + +} // namespace AyuSync diff --git a/Telegram/SourceFiles/ayu/features/forward/ayu_sync.h b/Telegram/SourceFiles/ayu/features/forward/ayu_sync.h new file mode 100644 index 0000000000..a10c5dc023 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/forward/ayu_sync.h @@ -0,0 +1,49 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include "apiwrap.h" +#include "base/random.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_photo.h" +#include "history/history_item.h" +#include "storage/file_download.h" +#include "storage/file_upload.h" +#include "storage/storage_account.h" +#include "ui/chat/attach/attach_prepare.h" + +namespace AyuSync { + +QString pathForSave(not_null session); +QString filePath(not_null session, const Data::Media *media); +void loadDocuments(not_null session, const std::vector> &items); +bool isMediaDownloadable(Data::Media *media); +void sendMessageSync(not_null session, Api::MessageToSend &message); + +void sendDocumentSync(not_null session, + Ui::PreparedGroup &group, + SendMediaType type, + TextWithTags &&caption, + const Api::SendAction &action); + +void sendStickerSync(not_null session, + Api::MessageToSend &message, + not_null document); +void waitForMsgSync(not_null session, const Api::SendAction &action); +void loadPhotoSync(not_null session, const std::pair, FullMsgId> &photos); +void loadDocumentSync(not_null session, DocumentData *data, not_null item); +void forwardMessagesSync(not_null session, + const std::vector> &items, + const ApiWrap::SendAction &action, + Data::ForwardOptions options); +void sendVoiceSync(not_null session, + const QByteArray &data, + int64_t duration, + bool video, + const Api::SendAction &action); +} diff --git a/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp b/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp index 478d393fdd..612cf70b9b 100644 --- a/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp +++ b/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp @@ -741,3 +741,34 @@ ID getUserIdFromPackId(uint64 id) { return ownerId; } + +TextWithTags extractText(not_null item) { + TextWithTags result; + + QString text; + if (const auto media = item->media()) { + if (const auto poll = media->poll()) { + text.append("\xF0\x9F\x93\x8A ") // 📊 + .append(poll->question.text).append("\n"); + for (const auto answer : poll->answers) { + text.append("• ").append(answer.text.text).append("\n"); + } + } + } + + result.tags = TextUtilities::ConvertEntitiesToTextTags(item->originalText().entities); + result.text = text.isEmpty() ? item->originalText().text : text; + return result; +} + +bool mediaDownloadable(Data::Media *media) { + if (!media + || media->webpage() || media->poll() || media->game() + || media->invoice() || media->location() || media->paper() + || media->giveawayStart() || media->giveawayResults() + || media->sharedContact() || media->call() + ) { + return false; + } + return true; +} diff --git a/Telegram/SourceFiles/ayu/utils/telegram_helpers.h b/Telegram/SourceFiles/ayu/utils/telegram_helpers.h index 7fc169cde8..9c000466c0 100644 --- a/Telegram/SourceFiles/ayu/utils/telegram_helpers.h +++ b/Telegram/SourceFiles/ayu/utils/telegram_helpers.h @@ -12,6 +12,8 @@ #include "dialogs/dialogs_main_list.h" #include "info/profile/info_profile_badge.h" #include "main/main_domain.h" +#include "data/data_poll.h" +#include "data/data_media_types.h" using UsernameResolverCallback = Fn; @@ -55,3 +57,6 @@ void searchById(ID userId, Main::Session *session, bool retry, const UsernameRes void searchById(ID userId, Main::Session *session, const UsernameResolverCallback &callback); ID getUserIdFromPackId(uint64 id); + +TextWithTags extractText(not_null item); +bool mediaDownloadable(Data::Media* media); \ No newline at end of file diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index fb02fa7a8f..7af67590c0 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -64,6 +64,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL // AyuGram includes #include "ayu/ayu_settings.h" #include "ayu/utils/telegram_helpers.h" +#include "ayu/features/forward/ayu_forward.h" class ShareBox::Inner final : public Ui::RpWidget { @@ -1671,6 +1672,48 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( result, msgIds); const auto requestType = Data::Histories::RequestType::Send; + + + // AyuGram-changed + const auto dismiss = [=] + { + if (show->valid()) { + show->hideLayer(); + } + }; + + + if (AyuForward::isFullAyuForwardNeeded(items.front())) { + crl::async([=]{ + for (const auto thread : result) { + AyuForward::forwardMessages( + &history->owner().session(), + Api::SendAction(thread, options), + false, + Data::ResolvedForwardDraft(items, forwardOptions)); + } + }); + + dismiss(); + return; + } + if (AyuForward::isAyuForwardNeeded(items)) { + crl::async([=] + { + for (const auto thread : result) { + AyuForward::intelligentForward( + &history->owner().session(), + Api::SendAction(thread, options), + Data::ResolvedForwardDraft(items, forwardOptions)); + } + }); + + dismiss(); + return; + } + // AyuGram-changed + + for (const auto thread : result) { if (!comment.text.isEmpty()) { auto message = Api::MessageToSend( diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index fb82472a0b..abf2fb7195 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -63,6 +63,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include "ayu/features/forward/ayu_forward.h" + namespace { using namespace Ui::Text; @@ -1280,6 +1282,69 @@ std::unique_ptr BoostsToLiftWriteRestriction( return result; } +std::unique_ptr AyuForwardWriteRestriction( + not_null parent, + const PeerId &peer, + const Main::Session &session) { + using namespace Ui; + + // status and part + const auto pair = AyuForward::stateName(peer); + + auto result = std::make_unique( + parent, + QString(), + st::historyComposeButton); + const auto raw = result.get(); + + const auto title = CreateChild( + raw, + pair.first, + st::frozenRestrictionTitle); + title->setTextColorOverride(st::historyComposeButton.color->c); + + + title->setAttribute(Qt::WA_TransparentForMouseEvents); + title->show(); + const auto subtitle = CreateChild( + raw, + pair.second, + st::frozenRestrictionSubtitle); + subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + subtitle->show(); + + + raw->sizeValue() | rpl::start_with_next([=](QSize size) { + + const auto toggle = [&](auto &&widget, bool shown) { + if (widget->isHidden() == shown) { + widget->setVisible(shown); + } + }; + const auto small = 2 * st::defaultDialogRow.photoSize; + const auto shown = (size.width() > small); + + toggle(title, shown); + toggle(subtitle, shown); + + const auto skip = st::defaultDialogRow.padding.left(); + const auto available = size.width() - skip * 2; + title->resizeToWidth(available); + subtitle->resizeToWidth(available); + const auto height = title->height() + subtitle->height(); + const auto top = (size.height() - height) / 2; + title->moveToLeft(skip, top, size.width()); + subtitle->moveToLeft(skip, top + title->height(), size.width()); + + }, title->lifetime()); + + raw->setClickedCallback([&] { + AyuForward::cancelForward(peer, session); + }); + + return result; +} + std::unique_ptr FrozenWriteRestriction( not_null parent, std::shared_ptr show, diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 4d7ef52b48..fc8aaea19c 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -202,7 +202,10 @@ enum class FrozenWriteRestrictionType { std::shared_ptr show, FrozenWriteRestrictionType type, FreezeInfoStyleOverride st = {}); - +std::unique_ptr AyuForwardWriteRestriction( + not_null parent, + const PeerId &peer, + const Main::Session &session); void SelectTextInFieldWithMargins( not_null field, const TextSelection &selection); diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 8761065915..602b32cceb 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -620,6 +620,10 @@ bool ChannelData::canAddAdmins() const { || (adminRights() & AdminRight::AddAdmins); } +bool ChannelData::isAyuNoForwards() const { + return flags() & Flag::AyuNoForwards; +} + bool ChannelData::allowsForwarding() const { return !(flags() & Flag::NoForwards); } diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index b4f99b5243..2ad833a8fd 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -74,6 +74,8 @@ enum class ChannelDataFlag : uint64 { StargiftsAvailable = (1ULL << 36), PaidMessagesAvailable = (1ULL << 37), AutoTranslation = (1ULL << 38), + + AyuNoForwards = (1ULL << 63), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -365,6 +367,7 @@ public: // Like in ChatData. [[nodiscard]] bool allowsForwarding() const; + [[nodiscard]] bool isAyuNoForwards() const; [[nodiscard]] bool canEditInformation() const; [[nodiscard]] bool canEditPermissions() const; [[nodiscard]] bool canEditUsername() const; diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index fc33c01708..25a14f56a1 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -64,6 +64,10 @@ ChatAdminRightsInfo ChatData::defaultAdminRights(not_null user) { | (isCreator ? Flag::AddAdmins : Flag(0))); } +bool ChatData::isAyuNoForwards() const { + return flags() & Flag::AyuNoForwards; +} + bool ChatData::allowsForwarding() const { return !(flags() & Flag::NoForwards); } diff --git a/Telegram/SourceFiles/data/data_chat.h b/Telegram/SourceFiles/data/data_chat.h index 5a285aeeb5..f7384cb6dc 100644 --- a/Telegram/SourceFiles/data/data_chat.h +++ b/Telegram/SourceFiles/data/data_chat.h @@ -23,6 +23,8 @@ enum class ChatDataFlag { CallNotEmpty = (1 << 6), CanSetUsername = (1 << 7), NoForwards = (1 << 8), + + AyuNoForwards = (1 << 31), }; inline constexpr bool is_flag_type(ChatDataFlag) { return true; }; using ChatDataFlags = base::flags; @@ -99,6 +101,7 @@ public: // Like in ChannelData. [[nodiscard]] bool allowsForwarding() const; + [[nodiscard]] bool isAyuNoForwards() const; [[nodiscard]] bool canEditInformation() const; [[nodiscard]] bool canEditPermissions() const; [[nodiscard]] bool canEditUsername() const; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 81b3330972..1a25babb7f 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -25,6 +25,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_widgets.h" +// AyuGram includes +#include "ayu/features/forward/ayu_forward.h" + + namespace { [[nodiscard]] ChatAdminRights ChatAdminRightsFlags( @@ -118,6 +122,9 @@ bool CanSendAnyOf( not_null peer, ChatRestrictions rights, bool forbidInForums) { + if (AyuForward::isForwarding(peer->id)) { + return false; + } if (peer->session().frozen() && !peer->isFreezeAppealChat()) { return false; @@ -180,6 +187,11 @@ bool CanSendAnyOf( SendError RestrictionError( not_null peer, ChatRestriction restriction) { + if (AyuForward::isForwarding(peer->id)) { + return SendError({ + .text = AyuForward::stateName(peer->id).first + "\n" + AyuForward::stateName(peer->id).second, + }); + } using Flag = ChatRestriction; if (peer->session().frozen() && !peer->isFreezeAppealChat()) { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 40a29139b0..c9436af405 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1386,6 +1386,18 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const { return nullptr; } +bool PeerData::isAyuNoForwards() const { + if (const auto user = asUser()) { + return false; + } else if (const auto channel = asChannel()) { + return channel->isAyuNoForwards(); + } else if (const auto chat = asChat()) { + return chat->isAyuNoForwards(); + } + return true; +} + + bool PeerData::allowsForwarding() const { if (isUser()) { return true; diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index b0563c42d5..5615c378a6 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -264,6 +264,7 @@ public: return _notify; } + [[nodiscard]] bool isAyuNoForwards() const; [[nodiscard]] bool allowsForwarding() const; [[nodiscard]] Data::RestrictionCheckResult amRestricted( ChatRestriction right) const; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index c1a6047da9..12aadef348 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -860,7 +860,8 @@ not_null Session::processChat(const MTPChat &data) { && chat->groupCall()->fullCount() > 0)) ? Flag::CallNotEmpty : Flag()) - | (data.is_noforwards() ? Flag::NoForwards : Flag()); + | (data.is_noforwards() ? Flag::NoForwards : Flag()) + | (data.is_ayuNoforwards() ? Flag::AyuNoForwards : Flag()); chat->setFlags((chat->flags() & ~flagsMask) | flagsSet); chat->count = data.vparticipants_count().v; @@ -1017,7 +1018,8 @@ not_null Session::processChat(const MTPChat &data) { && data.is_stories_hidden()) ? Flag::StoriesHidden : Flag()) - | (data.is_autotranslation() ? Flag::AutoTranslation : Flag()); + | (data.is_autotranslation() ? Flag::AutoTranslation : Flag()) + | (data.is_ayuNoforwards() ? Flag::AyuNoForwards : Flag()); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setBotVerifyDetailsIcon( data.vbot_verification_icon().value_or_empty()); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 9f0562e1b7..b87e1dcb21 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -351,6 +351,9 @@ enum class MessageFlag : uint64 { ReactionsAllowed = (1ULL << 50), HideDisplayDate = (1ULL << 51), + + + AyuNoForwards = (1ULL << 63), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 632fdae1ae..3e2a8983cb 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2785,7 +2785,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto itemId = item->fullId(); const auto blockSender = item->history()->peer->isRepliesChat(); if (isUponSelected != -2) { - if (item->allowsForward() && !item->isDeleted()) { + if (item->allowsForward()) { _menu->addAction(tr::lng_context_forward_msg(tr::now), [=] { forwardItem(itemId); }, &st::menuIconForward); @@ -3027,7 +3027,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }, &st::menuIconSelect); } else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->isRegular())) { if (isUponSelected != -2) { - if (canForward && !item->isDeleted()) { + if (canForward) { _menu->addAction(tr::lng_context_forward_msg(tr::now), [=] { forwardAsGroup(itemId); }, &st::menuIconForward); @@ -3954,7 +3954,7 @@ auto HistoryInner::getSelectionState() const if (selected.first->canDelete()) { ++result.canDeleteCount; } - if (selected.first->allowsForward() && !selected.first->isDeleted()) { + if (selected.first->allowsForward()) { ++result.canForwardCount; } } else if (selected.second.from != selected.second.to) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 980c748d66..0fe7853547 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1767,6 +1767,10 @@ bool HistoryItem::isSponsored() const { return _flags & MessageFlag::Sponsored; } +bool HistoryItem::isAyuNoForwards() const { + return _flags & MessageFlag::AyuNoForwards; +} + bool HistoryItem::skipNotification() const { if (isSilent() && (_flags & MessageFlag::IsContactSignUp)) { return true; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 17e635a32d..63822da59e 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -207,6 +207,9 @@ public: [[nodiscard]] bool isFromScheduled() const; [[nodiscard]] bool isScheduled() const; [[nodiscard]] bool isSponsored() const; + + [[nodiscard]] bool isAyuNoForwards() const; + [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; [[nodiscard]] BusinessShortcutId shortcutId() const; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 923fcbfd23..31710fa986 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -767,6 +767,7 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_views) ? Flag::HasViews : Flag()) // AyuGram: removed // | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag()) + | (flags & MTP::f_noforwards ? Flag::AyuNoForwards : Flag()) | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag()) | ((flags & MTP::f_video_processing_pending) ? Flag::EstimatedDate diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0cb02c937b..38c6c2cd79 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -187,6 +187,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ayu/ayu_settings.h" #include "ayu/utils/telegram_helpers.h" #include "ayu/features/messageshot/message_shot.h" +#include "ayu/features/forward/ayu_forward.h" #include "ayu/ui/boxes/message_shot_box.h" #include "boxes/abstract_box.h" @@ -6758,7 +6759,9 @@ void HistoryWidget::updateSendRestriction() { return; } _sendRestrictionKey = restriction.text; - if (!restriction) { + if (AyuForward::isForwarding(_peer->id)) { + _sendRestriction = AyuForwardWriteRestriction(this, _peer->id, session()); + } else if (!restriction) { _sendRestriction = nullptr; } else if (restriction.frozen) { const auto show = controller()->uiShow(); diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index a03c9ba96c..6ab10c6a2e 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -413,10 +413,6 @@ bool AddForwardMessageAction( const ContextMenuRequest &request, not_null list) { const auto item = request.item; - if (item && item->isDeleted()) { - return false; - } - if (!request.selectedItems.empty()) { return false; } else if (!item || !item->allowsForward()) { @@ -641,7 +637,7 @@ bool AddReplyToMessageAction( ? Data::CanSendAnything(topic) : Data::CanSendAnything(peer); const auto canReply = canSendReply || item->allowsForward(); - if (!canReply || item->isDeleted()) { + if (!canReply) { return false; } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index db2d5f8161..94d949ee25 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1358,7 +1358,7 @@ bool ListWidget::addToSelection( return false; } iterator->second.canDelete = item->canDelete(); - iterator->second.canForward = item->allowsForward() && !item->isDeleted(); + iterator->second.canForward = item->allowsForward(); iterator->second.canSendNow = item->allowsSendNow(); iterator->second.canReschedule = item->allowsReschedule(); return true; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 2a458c24e9..60d6cc5299 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -96,6 +96,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +// AyuGram includes +#include "ayu/features/forward/ayu_forward.h" + + namespace { void ClearBotStartToken(PeerData *peer) { @@ -563,7 +567,9 @@ bool MainWidget::setForwardDraft( .forward = &items, .ignoreSlowmodeCountdown = true, }); - if (error) { + // allow opening chat that + // already have some forward task + if (error && !AyuForward::isForwarding(history->peer->id)) { Data::ShowSendErrorToast(_controller, history->peer, error); return false; } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 232060df57..23cac3636d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -107,7 +107,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL // AyuGram includes #include "styles/style_ayu_icons.h" #include "ayu/ui/context_menu/context_menu.h" - +#include "ayu/features/forward/ayu_forward.h" namespace Window { namespace { @@ -2586,9 +2586,17 @@ QPointer ShowForwardMessagesBox( std::move(comment), options, state->box->forwardOptionsData()); - if (!state->submit && successCallback) { + + // AyuGram-changed + + // workaround for deselecting messages when using AyuForward + const auto items = history->owner().idsToItems(msgIds); + auto ayuForwarding = AyuForward::isAyuForwardNeeded(items) || AyuForward::isFullAyuForwardNeeded(items.front()); + + if (!state->submit || ayuForwarding && successCallback) { successCallback(); } + // AyuGram-changed }; const auto sendMenuType = [=] {