diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6010bf8332..76e06c8e1b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -454,6 +454,8 @@ PRIVATE data/business/data_business_common.h data/business/data_business_info.cpp data/business/data_business_info.h + data/business/data_shortcut_messages.cpp + data/business/data_shortcut_messages.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp @@ -1287,6 +1289,8 @@ PRIVATE profile/profile_cover_drop_area.h settings/business/settings_away_message.cpp settings/business/settings_away_message.h + settings/business/settings_shortcut_messages.cpp + settings/business/settings_shortcut_messages.h settings/business/settings_chatbots.cpp settings/business/settings_chatbots.h settings/business/settings_greeting.cpp diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 155666a5d4..fe0d489b01 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -22,6 +22,7 @@ inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); struct SendOptions { PeerData *sendAs = nullptr; TimeId scheduled = 0; + BusinessShortcutId shortcutId = 0; bool silent = false; bool handleSupportSwitch = false; bool hideViaBot = false; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index f465d7f379..3532e2c79d 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "apiwrap.h" #include "base/random.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" #include "data/data_poll.h" @@ -64,6 +65,9 @@ void Polls::create( if (action.options.scheduled) { sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto sendAs = action.options.sendAs; if (sendAs) { sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; @@ -85,7 +89,7 @@ void Polls::create( MTPVector(), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index babce93291..96bdf4a4a1 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "base/random.h" #include "base/unixtime.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_document.h" #include "data/data_photo.h" #include "data/data_channel.h" // ChannelData::addsSignature. @@ -128,6 +129,9 @@ void SendExistingMedia( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (message.action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } session->data().registerMessageRandomId(randomId, newId); @@ -146,10 +150,12 @@ void SendExistingMedia( const auto performRequest = [=](const auto &repeatRequest) -> void { auto &histories = history->owner().histories(); + const auto session = &history->session(); + const auto &action = message.action; const auto usedFileReference = media->fileReference(); histories.sendPreparedMessage( history, - message.action.replyTo, + action.replyTo, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), @@ -160,9 +166,9 @@ void SendExistingMedia( MTP_long(randomId), MTPReplyMarkup(), sentEntities, - MTP_int(message.action.options.scheduled), + MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -260,7 +266,10 @@ bool SendDice(MessageToSend &message) { message.textWithTags = TextWithTags(); message.action.clearDraft = false; message.action.generateLocal = true; - api->sendAction(message.action); + + + const auto &action = message.action; + api->sendAction(action); const auto newId = FullMsgId( peer->id, @@ -270,17 +279,17 @@ bool SendDice(MessageToSend &message) { auto &histories = history->owner().histories(); auto flags = NewMessageFlags(peer); auto sendFlags = MTPmessages_SendMedia::Flags(0); - if (message.action.replyTo) { + if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); - const auto silentPost = ShouldSendSilent(peer, message.action.options); - InnerFillMessagePostFlags(message.action.options, peer, flags); + const auto silentPost = ShouldSendSilent(peer, action.options); + InnerFillMessagePostFlags(action.options, peer, flags); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - const auto sendAs = message.action.options.sendAs; + const auto sendAs = action.options.sendAs; const auto messageFromId = sendAs ? sendAs->id : anonymousPost @@ -293,10 +302,13 @@ bool SendDice(MessageToSend &message) { ? session->user()->name() : QString(); - if (message.action.options.scheduled) { + if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } session->data().registerMessageRandomId(randomId, newId); @@ -305,8 +317,8 @@ bool SendDice(MessageToSend &message) { newId.msg, flags, viaBotId, - message.action.replyTo, - HistoryItem::NewMessageDate(message.action.options.scheduled), + action.replyTo, + HistoryItem::NewMessageDate(action.options.scheduled), messageFromId, messagePostAuthor, TextWithEntities(), @@ -314,7 +326,7 @@ bool SendDice(MessageToSend &message) { HistoryMessageMarkupData()); histories.sendPreparedMessage( history, - message.action.replyTo, + action.replyTo, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), @@ -325,14 +337,14 @@ bool SendDice(MessageToSend &message) { MTP_long(randomId), MTPReplyMarkup(), MTP_vector(), - MTP_int(message.action.options.scheduled), + MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); }); - api->finishForwarding(message.action); + api->finishForwarding(action); return true; } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 0d603a0448..bca32c3dac 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "api/api_user_names.h" #include "api/api_websites.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" #include "data/data_web_page.h" @@ -3140,7 +3141,9 @@ void ApiWrap::sharedMediaDone( } void ApiWrap::sendAction(const SendAction &action) { - if (!action.options.scheduled && !action.replaceMediaOf) { + if (!action.options.scheduled + && !action.options.shortcutId + && !action.replaceMediaOf) { const auto topicRootId = action.replyTo.topicRootId; const auto topic = topicRootId ? action.history->peer->forumTopicFor(topicRootId) @@ -3175,11 +3178,13 @@ void ApiWrap::finishForwarding(const SendAction &action) { } _session->data().sendHistoryChangeNotifications(); - _session->changes().historyUpdated( - history, - (action.options.scheduled - ? Data::HistoryUpdate::Flag::ScheduledSent - : Data::HistoryUpdate::Flag::MessageSent)); + if (!action.options.shortcutId) { + _session->changes().historyUpdated( + history, + (action.options.scheduled + ? Data::HistoryUpdate::Flag::ScheduledSent + : Data::HistoryUpdate::Flag::MessageSent)); + } } void ApiWrap::forwardMessages( @@ -3208,7 +3213,7 @@ void ApiWrap::forwardMessages( const auto history = action.history; const auto peer = history->peer; - if (!action.options.scheduled) { + if (!action.options.scheduled && !action.options.shortcutId) { histories.readInbox(history); } const auto anonymousPost = peer->amAnonymous(); @@ -3226,6 +3231,9 @@ void ApiWrap::forwardMessages( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= SendFlag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= SendFlag::f_quick_reply_shortcut; + } if (draft.options != Data::ForwardOptions::PreserveInfo) { sendFlags |= SendFlag::f_drop_author; } @@ -3265,7 +3273,7 @@ void ApiWrap::forwardMessages( MTP_int(topMsgId), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, action.options.shortcutId) )).done([=](const MTPUpdates &result) { applyUpdates(result); if (shared && !--shared->requestsLeft) { @@ -3728,6 +3736,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date; mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut; + mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto viaBotId = UserId(); lastMessage = history->addNewLocalMessage( newId.msg, @@ -3763,6 +3775,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { UnixtimeFromMsgId(response.outerMsgId)); } }; + const auto mtpShortcut = Data::ShortcutIdToMTP( + _session, + action.options.shortcutId); if (exactWebPage && !ignoreWebPage && (manualWebPage || sending.empty())) { @@ -3779,9 +3794,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTP_long(randomId), MTPReplyMarkup(), sentEntities, - MTP_int(message.action.options.scheduled), + MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + mtpShortcut ), done, fail); } else { histories.sendPreparedMessage( @@ -3798,7 +3813,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sentEntities, MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + mtpShortcut ), done, fail); } isFirst = false; @@ -3887,6 +3902,9 @@ void ApiWrap::sendInlineResult( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= SendFlag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= SendFlag::f_quick_reply_shortcut; + } if (action.options.hideViaBot) { sendFlags |= SendFlag::f_hide_via; } @@ -3932,7 +3950,7 @@ void ApiWrap::sendInlineResult( MTP_string(data->getId()), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, @@ -4061,7 +4079,8 @@ void ApiWrap::sendMediaWithRandomId( : Flag(0)) | (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0)) | (options.scheduled ? Flag::f_schedule_date : Flag(0)) - | (options.sendAs ? Flag::f_send_as : Flag(0)); + | (options.sendAs ? Flag::f_send_as : Flag(0)) + | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; @@ -4081,7 +4100,7 @@ void ApiWrap::sendMediaWithRandomId( sentEntities, MTP_int(options.scheduled), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4166,7 +4185,10 @@ void ApiWrap::sendAlbumIfReady(not_null album) { ? Flag::f_silent : Flag(0)) | (album->options.scheduled ? Flag::f_schedule_date : Flag(0)) - | (sendAs ? Flag::f_send_as : Flag(0)); + | (sendAs ? Flag::f_send_as : Flag(0)) + | (album->options.shortcutId + ? Flag::f_quick_reply_shortcut + : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; histories.sendPreparedMessage( @@ -4180,7 +4202,7 @@ void ApiWrap::sendAlbumIfReady(not_null album) { MTP_vector(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(_session, album->options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); }, [=](const MTP::Error &error, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 8577aad628..4581fac40d 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/share_message_phrase_factory.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_channel.h" #include "data/data_game.h" #include "data/data_histories.h" @@ -1543,11 +1544,15 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( const auto threadHistory = thread->owningHistory(); histories.sendRequest(threadHistory, requestType, [=]( Fn finish) { - auto &api = threadHistory->session().api(); + const auto session = &threadHistory->session(); + auto &api = session->api(); const auto sendFlags = commonSendFlags | (topMsgId ? Flag::f_top_msg_id : Flag(0)) | (ShouldSendSilent(peer, options) ? Flag::f_silent + : Flag(0)) + | (options.shortcutId + ? Flag::f_quick_reply_shortcut : Flag(0)); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( @@ -1559,7 +1564,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(topMsgId), MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, options.shortcutId) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 914d625031..6f3b716dc1 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -191,7 +191,7 @@ struct AwaySchedule { struct AwaySettings { BusinessRecipients recipients; AwaySchedule schedule; - int shortcutId = 0; + BusinessShortcutId shortcutId = 0; explicit operator bool() const { return schedule.type != AwayScheduleType::Never; @@ -205,7 +205,7 @@ struct AwaySettings { struct GreetingSettings { BusinessRecipients recipients; int noActivityDays = 0; - int shortcutId = 0; + BusinessShortcutId shortcutId = 0; explicit operator bool() const { return noActivityDays > 0; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp new file mode 100644 index 0000000000..1013c05bcd --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -0,0 +1,539 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/business/data_shortcut_messages.h" + +#include "api/api_hash.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "api/api_text_entities.h" +#include "main/main_session.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "history/history_item_helpers.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRequestTimeLimit = 60 * crl::time(1000); + +[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) { + Expects(IsServerMsgId(id)); + + return ServerMaxMsgId + id + 1; +} + +[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) { + Expects(IsShortcutMsgId(id)); + + return (id - ServerMaxMsgId - 1); +} + +[[nodiscard]] bool TooEarlyForRequest(crl::time received) { + return (received > 0) && (received + kRequestTimeLimit > crl::now()); +} + +[[nodiscard]] MTPMessage PrepareMessage( + BusinessShortcutId shortcutId, + const MTPMessage &message) { + return message.match([&](const MTPDmessageEmpty &data) { + return MTP_messageEmpty( + data.vflags(), + data.vid(), + data.vpeer_id() ? *data.vpeer_id() : MTPPeer()); + }, [&](const MTPDmessageService &data) { + return MTP_messageService( + data.vflags(), + data.vid(), + data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), + data.vpeer_id(), + data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), + data.vdate(), + data.vaction(), + MTP_int(data.vttl_period().value_or_empty())); + }, [&](const MTPDmessage &data) { + return MTP_message( + MTP_flags(data.vflags().v | MTPDmessage::Flag::f_quick_reply_shortcut_id), + data.vid(), + data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), + MTPint(), // from_boosts_applied + data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), + data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), + MTP_long(data.vvia_bot_id().value_or_empty()), + data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), + data.vdate(), + data.vmessage(), + data.vmedia() ? *data.vmedia() : MTPMessageMedia(), + data.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(), + (data.ventities() + ? *data.ventities() + : MTPVector()), + MTP_int(data.vviews().value_or_empty()), + MTP_int(data.vforwards().value_or_empty()), + data.vreplies() ? *data.vreplies() : MTPMessageReplies(), + MTP_int(data.vedit_date().value_or_empty()), + MTP_bytes(data.vpost_author().value_or_empty()), + MTP_long(data.vgrouped_id().value_or_empty()), + MTPMessageReactions(), + MTPVector(), + MTP_int(data.vttl_period().value_or_empty()), + MTP_int(shortcutId)); + }); +} + +} // namespace + +bool IsShortcutMsgId(MsgId id) { + return (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId); +} + +ShortcutMessages::ShortcutMessages(not_null owner) +: _session(&owner->session()) +, _history(owner->history(_session->userPeerId())) +, _clearTimer([=] { clearOldRequests(); }) { + owner->itemRemoved( + ) | rpl::filter([](not_null item) { + return item->isBusinessShortcut(); + }) | rpl::start_with_next([=](not_null item) { + remove(item); + }, _lifetime); +} + +ShortcutMessages::~ShortcutMessages() { + for (const auto &request : _requests) { + _session->api().request(request.second.requestId).cancel(); + } +} + +void ShortcutMessages::clearOldRequests() { + const auto now = crl::now(); + while (true) { + const auto i = ranges::find_if(_requests, [&](const auto &value) { + const auto &request = value.second; + return !request.requestId + && (request.lastReceived + kRequestTimeLimit <= now); + }); + if (i == end(_requests)) { + break; + } + _requests.erase(i); + } +} + +MsgId ShortcutMessages::localMessageId(MsgId remoteId) const { + return RemoteToLocalMsgId(remoteId); +} + +MsgId ShortcutMessages::lookupId(not_null item) const { + Expects(item->isBusinessShortcut()); + Expects(!item->isSending()); + Expects(!item->hasFailed()); + + return LocalToRemoteMsgId(item->id); +} + +int ShortcutMessages::count(BusinessShortcutId shortcutId) const { + const auto i = _data.find(shortcutId); + return (i != end(_data)) ? i->second.items.size() : 0; +} + +void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { + const auto &message = update.vmessage(); + const auto shortcutId = BusinessShortcutIdFromMessage(message); + if (!shortcutId) { + return; + } + auto &list = _data[shortcutId]; + append(shortcutId, list, message); + sort(list); + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::apply( + const MTPDupdateDeleteQuickReplyMessages &update) { + const auto shortcutId = update.vshortcut_id().v; + if (!shortcutId) { + return; + } + auto i = _data.find(shortcutId); + if (i == end(_data)) { + return; + } + for (const auto &id : update.vmessages().v) { + const auto &list = i->second; + const auto j = list.itemById.find(id.v); + if (j != end(list.itemById)) { + j->second->destroy(); + i = _data.find(shortcutId); + if (i == end(_data)) { + break; + } + } + } + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { + const auto shortcutId = update.vshortcut_id().v; + if (!shortcutId) { + return; + } + auto i = _data.find(shortcutId); + while (i != end(_data)) { + Assert(!i->second.itemById.empty()); + i->second.itemById.back().second->destroy(); + i = _data.find(shortcutId); + } + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::apply( + const MTPDupdateMessageID &update, + not_null local) { + const auto id = update.vid().v; + const auto i = _data.find(local->shortcutId()); + Assert(i != end(_data)); + auto &list = i->second; + const auto j = list.itemById.find(id); + if (j != end(list.itemById) || !IsServerMsgId(id)) { + local->destroy(); + } else { + Assert(!list.itemById.contains(local->id)); + local->setRealId(localMessageId(id)); + list.itemById.emplace(id, local); + } +} + +void ShortcutMessages::appendSending(not_null item) { + Expects(item->isSending()); + Expects(item->isBusinessShortcut()); + + const auto shortcutId = item->shortcutId(); + auto &list = _data[shortcutId]; + list.items.emplace_back(item); + sort(list); + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::removeSending(not_null item) { + Expects(item->isSending() || item->hasFailed()); + Expects(item->isBusinessShortcut()); + + item->destroy(); +} + +rpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) { + request(shortcutId); + + return _updates.events( + ) | rpl::filter([=](BusinessShortcutId value) { + return (value == shortcutId); + }) | rpl::to_empty; +} + +Data::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) { + auto result = Data::MessagesSlice(); + const auto i = _data.find(shortcutId); + if (i == end(_data)) { + const auto i = _requests.find(shortcutId); + if (i == end(_requests)) { + return result; + } + result.fullCount = result.skippedAfter = result.skippedBefore = 0; + return result; + } + const auto &list = i->second.items; + result.skippedAfter = result.skippedBefore = 0; + result.fullCount = int(list.size()); + result.ids = ranges::views::all( + list + ) | ranges::views::transform( + &HistoryItem::fullId + ) | ranges::to_vector; + return result; +} + +void ShortcutMessages::preloadShortcuts() { + if (_shortcutsLoaded || _shortcutsRequestId) { + return; + } + const auto owner = &_session->data(); + _shortcutsRequestId = owner->session().api().request( + MTPmessages_GetQuickReplies(MTP_long(_shortcutsHash)) + ).done([=](const MTPmessages_QuickReplies &result) { + result.match([&](const MTPDmessages_quickReplies &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + owner->processMessages( + data.vmessages(), + NewMessageType::Existing); + auto shortcuts = Shortcuts(); + const auto messages = &owner->shortcutMessages(); + for (const auto &reply : data.vquick_replies().v) { + const auto &data = reply.data(); + const auto id = BusinessShortcutId(data.vshortcut_id().v); + shortcuts.list.emplace(id, Shortcut{ + .name = qs(data.vshortcut()), + .topMessageId = messages->localMessageId( + data.vtop_message().v), + .count = data.vcount().v, + }); + } + for (auto &[id, shortcut] : _shortcuts.list) { + if (id < 0) { + shortcuts.list.emplace(id, shortcut); + } + } + const auto changed = !_shortcutsLoaded + || (shortcuts != _shortcuts); + if (changed) { + _shortcuts = std::move(shortcuts); + _shortcutsLoaded = true; + _shortcutsChanged.fire({}); + } + }, [&](const MTPDmessages_quickRepliesNotModified &) { + if (!_shortcutsLoaded) { + _shortcutsLoaded = true; + _shortcutsChanged.fire({}); + } + }); + }).send(); +} + +const Shortcuts &ShortcutMessages::shortcuts() const { + return _shortcuts; +} + +bool ShortcutMessages::shortcutsLoaded() const { + return _shortcutsLoaded; +} + +rpl::producer<> ShortcutMessages::shortcutsChanged() const { + return _shortcutsChanged.events(); +} + +BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { + Expects(_shortcutsLoaded); + + for (auto &[id, shortcut] : _shortcuts.list) { + if (shortcut.name == name) { + return id; + } + } + const auto result = --_localShortcutId; + _shortcuts.list.emplace(result, Shortcut{ name }); + return result; +} + +Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { + const auto i = _shortcuts.list.find(id); + + Ensures(i != end(_shortcuts.list)); + return i->second; +} + +void ShortcutMessages::request(BusinessShortcutId shortcutId) { + auto &request = _requests[shortcutId]; + if (request.requestId || TooEarlyForRequest(request.lastReceived)) { + return; + } + const auto i = _data.find(shortcutId); + const auto hash = (i != end(_data)) + ? countListHash(i->second) + : uint64(0); + request.requestId = _session->api().request( + MTPmessages_GetQuickReplyMessages( + MTP_flags(0), + MTP_int(shortcutId), + MTPVector(), + MTP_long(hash)) + ).done([=](const MTPmessages_Messages &result) { + parse(shortcutId, result); + }).fail([=] { + _requests.remove(shortcutId); + }).send(); +} + +void ShortcutMessages::parse( + BusinessShortcutId shortcutId, + const MTPmessages_Messages &list) { + auto &request = _requests[shortcutId]; + request.lastReceived = crl::now(); + request.requestId = 0; + if (!_clearTimer.isActive()) { + _clearTimer.callOnce(kRequestTimeLimit * 2); + } + + list.match([&](const MTPDmessages_messagesNotModified &data) { + }, [&](const auto &data) { + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + + const auto &messages = data.vmessages().v; + if (messages.isEmpty()) { + clearNotSending(shortcutId); + return; + } + auto received = base::flat_set>(); + auto clear = base::flat_set>(); + auto &list = _data.emplace(shortcutId, List()).first->second; + for (const auto &message : messages) { + if (const auto item = append(shortcutId, list, message)) { + received.emplace(item); + } + } + for (const auto &owned : list.items) { + const auto item = owned.get(); + if (!item->isSending() && !received.contains(item)) { + clear.emplace(item); + } + } + updated(shortcutId, received, clear); + }); +} + +HistoryItem *ShortcutMessages::append( + BusinessShortcutId shortcutId, + List &list, + const MTPMessage &message) { + const auto id = message.match([&](const auto &data) { + return data.vid().v; + }); + const auto i = list.itemById.find(id); + if (i != end(list.itemById)) { + const auto existing = i->second; + message.match([&](const MTPDmessage &data) { + if (data.is_edit_hide()) { + existing->applyEdition(HistoryMessageEdition(_session, data)); + } else { + existing->updateSentContent({ + qs(data.vmessage()), + Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()) + }, data.vmedia()); + existing->updateReplyMarkup( + HistoryMessageMarkupData(data.vreply_markup())); + existing->updateForwardedInfo(data.vfwd_from()); + } + existing->updateDate(data.vdate().v); + _history->owner().requestItemTextRefresh(existing); + }, [&](const auto &data) {}); + return existing; + } + + if (!IsServerMsgId(id)) { + LOG(("API Error: Bad id in quick reply messages: %1.").arg(id)); + return nullptr; + } + const auto item = _session->data().addNewMessage( + localMessageId(id), + PrepareMessage(shortcutId, message), + MessageFlags(), // localFlags + NewMessageType::Existing); + if (!item + || item->history() != _history + || item->shortcutId() != shortcutId) { + LOG(("API Error: Bad data received in quick reply messages.")); + return nullptr; + } + list.items.emplace_back(item); + list.itemById.emplace(id, item); + return item; +} + +void ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) { + const auto i = _data.find(shortcutId); + if (i == end(_data)) { + return; + } + auto clear = base::flat_set>(); + for (const auto &owned : i->second.items) { + if (!owned->isSending() && !owned->hasFailed()) { + clear.emplace(owned.get()); + } + } + updated(shortcutId, {}, clear); +} + +void ShortcutMessages::updated( + BusinessShortcutId shortcutId, + const base::flat_set> &added, + const base::flat_set> &clear) { + if (!clear.empty()) { + for (const auto &item : clear) { + item->destroy(); + } + } + const auto i = _data.find(shortcutId); + if (i != end(_data)) { + sort(i->second); + } + if (!added.empty() || !clear.empty()) { + _updates.fire_copy(shortcutId); + } +} + +void ShortcutMessages::sort(List &list) { + ranges::sort(list.items, ranges::less(), &HistoryItem::position); +} + +void ShortcutMessages::remove(not_null item) { + const auto shortcutId = item->shortcutId(); + const auto i = _data.find(shortcutId); + Assert(i != end(_data)); + auto &list = i->second; + + if (!item->isSending() && !item->hasFailed()) { + list.itemById.remove(lookupId(item)); + } + const auto k = ranges::find(list.items, item, &OwnedItem::get); + Assert(k != list.items.end()); + k->release(); + list.items.erase(k); + + if (list.items.empty()) { + _data.erase(i); + } + _updates.fire_copy(shortcutId); +} + +uint64 ShortcutMessages::countListHash(const List &list) const { + using namespace Api; + + auto hash = HashInit(); + auto &&serverside = ranges::views::all( + list.items + ) | ranges::views::filter([](const OwnedItem &item) { + return !item->isSending() && !item->hasFailed(); + }) | ranges::views::reverse; + for (const auto &item : serverside) { + HashUpdate(hash, lookupId(item.get()).bare); + if (const auto edited = item->Get()) { + HashUpdate(hash, edited->date); + } else { + HashUpdate(hash, TimeId(0)); + } + } + return HashFinalize(hash); +} + +MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null session, + BusinessShortcutId id) { + if (id >= 0) { + return MTP_inputQuickReplyShortcutId(MTP_int(id)); + } + return MTP_inputQuickReplyShortcut(MTP_string( + session->data().shortcutMessages().lookupShortcut(id).name)); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h new file mode 100644 index 0000000000..2028f2ece2 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -0,0 +1,125 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/history_item.h" +#include "base/timer.h" + +class History; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; +struct MessagesSlice; + +struct Shortcut { + QString name; + MsgId topMessageId = 0; + int count = 0; + + friend inline bool operator==( + const Shortcut &a, + const Shortcut &b) = default; +}; + +struct Shortcuts { + base::flat_map list; + + friend inline bool operator==( + const Shortcuts &a, + const Shortcuts &b) = default; +}; + +[[nodiscard]] bool IsShortcutMsgId(MsgId id); + +class ShortcutMessages final { +public: + explicit ShortcutMessages(not_null owner); + ~ShortcutMessages(); + + [[nodiscard]] MsgId lookupId(not_null item) const; + [[nodiscard]] int count(BusinessShortcutId shortcutId) const; + [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; + + void apply(const MTPDupdateQuickReplyMessage &update); + void apply(const MTPDupdateDeleteQuickReplyMessages &update); + void apply(const MTPDupdateDeleteQuickReply &update); + void apply( + const MTPDupdateMessageID &update, + not_null local); + + void appendSending(not_null item); + void removeSending(not_null item); + + [[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId); + [[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId); + + void preloadShortcuts(); + [[nodiscard]] const Shortcuts &shortcuts() const; + [[nodiscard]] bool shortcutsLoaded() const; + [[nodiscard]] rpl::producer<> shortcutsChanged() const; + [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); + [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; + +private: + using OwnedItem = std::unique_ptr; + struct List { + std::vector items; + base::flat_map> itemById; + }; + struct Request { + mtpRequestId requestId = 0; + crl::time lastReceived = 0; + }; + + void request(BusinessShortcutId shortcutId); + void parse( + BusinessShortcutId shortcutId, + const MTPmessages_Messages &list); + HistoryItem *append( + BusinessShortcutId shortcutId, + List &list, + const MTPMessage &message); + void clearNotSending(BusinessShortcutId shortcutId); + void updated( + BusinessShortcutId shortcutId, + const base::flat_set> &added, + const base::flat_set> &clear); + void sort(List &list); + void remove(not_null item); + [[nodiscard]] uint64 countListHash(const List &list) const; + void clearOldRequests(); + + const not_null _session; + const not_null _history; + + base::Timer _clearTimer; + base::flat_map _data; + base::flat_map _requests; + rpl::event_stream _updates; + + Shortcuts _shortcuts; + rpl::event_stream<> _shortcutsChanged; + BusinessShortcutId _localShortcutId = 0; + uint64 _shortcutsHash = 0; + mtpRequestId _shortcutsRequestId = 0; + bool _shortcutsLoaded = false; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null session, + BusinessShortcutId id); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index d2790a2881..b3ecb27b0b 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -54,6 +54,7 @@ Q_DECLARE_METATYPE(MsgId); } using StoryId = int32; +using BusinessShortcutId = int32; struct FullStoryId { PeerId peer = 0; @@ -77,7 +78,8 @@ constexpr auto ServerMaxStoryId = StoryId(1 << 30); constexpr auto StoryMsgIds = int64(ServerMaxStoryId); constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); -constexpr auto ScheduledMsgIdsRange = (1LL << 32); +constexpr auto ScheduledMaxMsgId = MsgId(ServerMaxMsgId + (1LL << 32)); +constexpr auto ShortcutMaxMsgId = MsgId(ScheduledMaxMsgId + (1LL << 32)); constexpr auto ShowAtUnreadMsgId = MsgId(0); constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 41f3cd22f9..3fe58f5305 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -96,8 +96,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); } // namespace bool IsScheduledMsgId(MsgId id) { - return (id > ServerMaxMsgId) - && (id < ServerMaxMsgId + ScheduledMsgIdsRange); + return (id > ServerMaxMsgId) && (id < ScheduledMaxMsgId); } ScheduledMessages::ScheduledMessages(not_null owner) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 37fb68e86b..24ea2cd468 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name #include "data/business/data_business_chatbots.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" @@ -256,14 +257,12 @@ Session::Session(not_null session) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _groups(this) , _chatsFilters(std::make_unique(this)) -, _scheduledMessages(std::make_unique(this)) , _cloudThemes(std::make_unique(session)) , _sendActionManager(std::make_unique()) , _streaming(std::make_unique(this)) , _mediaRotation(std::make_unique()) , _histories(std::make_unique(this)) , _stickers(std::make_unique(this)) -, _sponsoredMessages(std::make_unique(this)) , _reactions(std::make_unique(this)) , _emojiStatuses(std::make_unique(this)) , _forumIcons(std::make_unique(this)) @@ -272,7 +271,10 @@ Session::Session(not_null session) , _stories(std::make_unique(this)) , _savedMessages(std::make_unique(this)) , _chatbots(std::make_unique(this)) -, _businessInfo(std::make_unique(this)) { +, _businessInfo(std::make_unique(this)) +, _scheduledMessages(std::make_unique(this)) +, _shortcutMessages(std::make_unique(this)) +, _sponsoredMessages(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -396,6 +398,7 @@ void Session::clear() { _histories->unloadAll(); _scheduledMessages = nullptr; + _shortcutMessages = nullptr; _sponsoredMessages = nullptr; _dependentMessages.clear(); base::take(_messages); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index aab939cb4e..74204af26e 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -44,6 +44,7 @@ class Folder; class LocationPoint; class WallPaper; class ScheduledMessages; +class ShortcutMessages; class SendActionManager; class SponsoredMessages; class Reactions; @@ -102,6 +103,9 @@ public: [[nodiscard]] ScheduledMessages &scheduledMessages() const { return *_scheduledMessages; } + [[nodiscard]] ShortcutMessages &shortcutMessages() const { + return *_shortcutMessages; + } [[nodiscard]] SendActionManager &sendActionManager() const { return *_sendActionManager; } @@ -1058,14 +1062,12 @@ private: Groups _groups; const std::unique_ptr _chatsFilters; - std::unique_ptr _scheduledMessages; const std::unique_ptr _cloudThemes; const std::unique_ptr _sendActionManager; const std::unique_ptr _streaming; const std::unique_ptr _mediaRotation; const std::unique_ptr _histories; const std::unique_ptr _stickers; - std::unique_ptr _sponsoredMessages; const std::unique_ptr _reactions; const std::unique_ptr _emojiStatuses; const std::unique_ptr _forumIcons; @@ -1075,8 +1077,11 @@ private: const std::unique_ptr _savedMessages; const std::unique_ptr _chatbots; const std::unique_ptr _businessInfo; + std::unique_ptr _scheduledMessages; + std::unique_ptr _shortcutMessages; + std::unique_ptr _sponsoredMessages; - MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; + MsgId _nonHistoryEntryId = ShortcutMaxMsgId; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index 8d5489d70f..6b762e2dbe 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -27,8 +27,7 @@ using SparseUnsortedIdsSlice = AbstractSparseIds>; class SparseIdsMergedSlice { public: using UniversalMsgId = MsgId; - static constexpr MsgId kScheduledTopicId - = ServerMaxMsgId + ScheduledMsgIdsRange; + static constexpr MsgId kScheduledTopicId = ScheduledMaxMsgId; struct Key { Key( diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 997d4282eb..ab792cd305 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -145,6 +145,15 @@ TimeId DateFromMessage(const MTPmessage &message) { }); } +BusinessShortcutId BusinessShortcutIdFromMessage( + const MTPmessage &message) { + return message.match([](const MTPDmessage &data) { + return data.vquick_reply_shortcut_id().value_or_empty(); + }, [](const auto &) { + return BusinessShortcutId(); + }); +} + bool GoodStickerDimensions(int width, int height) { // Show all .webp (except very large ones) as stickers, // allow to open them in media viewer to see details. diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 3b511cba01..176a13f743 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -109,10 +109,13 @@ using FilterId = int32; using MessageIdsList = std::vector; -PeerId PeerFromMessage(const MTPmessage &message); -MTPDmessage::Flags FlagsFromMessage(const MTPmessage &message); -MsgId IdFromMessage(const MTPmessage &message); -TimeId DateFromMessage(const MTPmessage &message); +[[nodiscard]] PeerId PeerFromMessage(const MTPmessage &message); +[[nodiscard]] MTPDmessage::Flags FlagsFromMessage( + const MTPmessage &message); +[[nodiscard]] MsgId IdFromMessage(const MTPmessage &message); +[[nodiscard]] TimeId DateFromMessage(const MTPmessage &message); +[[nodiscard]] BusinessShortcutId BusinessShortcutIdFromMessage( + const MTPmessage &message); [[nodiscard]] inline MTPint MTP_int(MsgId id) noexcept { return MTP_int(id.bare); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 0525e5c122..febc8d67c2 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -210,6 +210,12 @@ public: [[nodiscard]] bool isSponsored() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; + [[nodiscard]] BusinessShortcutId shortcutId() const { + return _shortcutId; + } + [[nodiscard]] bool isBusinessShortcut() const { + return _shortcutId != 0; + } void addLogEntryOriginal( WebPageId localId, @@ -662,6 +668,7 @@ private: TimeId _date = 0; TimeId _ttlDestroyAt = 0; int _boostsApplied = 0; + BusinessShortcutId _shortcutId = 0; HistoryView::Element *_mainView = nullptr; MessageGroupId _groupId = MessageGroupId(); diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 8cf7a38d55..ab5a0ef5e2 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -203,6 +203,13 @@ void ContentWidget::applyAdditionalScroll(int additionalScroll) { } } +void ContentWidget::applyMaxVisibleHeight(int maxVisibleHeight) { + if (_maxVisibleHeight != maxVisibleHeight) { + _maxVisibleHeight = maxVisibleHeight; + update(); + } +} + rpl::producer ContentWidget::desiredHeightValue() const { using namespace rpl::mappers; return rpl::combine( @@ -328,6 +335,10 @@ rpl::producer ContentWidget::desiredBottomShadowVisibility() const { }); } +not_null ContentWidget::scroll() const { + return _scroll.data(); +} + Key ContentMemento::key() const { if (const auto topic = this->topic()) { return Key(topic); diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index a7782d4f9a..b8d1ebe4b7 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -81,6 +81,7 @@ public: const QRect &newGeometry, int topDelta); void applyAdditionalScroll(int additionalScroll); + void applyMaxVisibleHeight(int maxVisibleHeight); int scrollTillBottom(int forHeight) const; [[nodiscard]] rpl::producer scrollTillBottomChanges() const; [[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const { @@ -115,9 +116,13 @@ protected: doSetInnerWidget(std::move(inner))); } - not_null controller() const { + [[nodiscard]] not_null controller() const { return _controller; } + [[nodiscard]] not_null scroll() const; + [[nodiscard]] int maxVisibleHeight() const { + return _maxVisibleHeight; + } void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; @@ -151,6 +156,7 @@ private: base::unique_qptr _searchWrap = nullptr; QPointer _searchField; int _innerDesiredHeight = 0; + int _maxVisibleHeight = 0; bool _isStackBottom = false; // Saving here topDelta in setGeometryWithTopMoved() to get it passed to resizeEvent(). diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index cbf5117c33..b4fdd2414a 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -291,9 +291,10 @@ QRect LayerWidget::countGeometry(int newWidth) { const auto newBottom = newTop; const auto bottomRadius = st::boxRadius; + const auto maxVisibleHeight = windowHeight - newTop; // Top rounding is included in _contentHeight. auto desiredHeight = _contentHeight + bottomRadius; - accumulate_min(desiredHeight, windowHeight - newTop - newBottom); + accumulate_min(desiredHeight, maxVisibleHeight - newBottom); // First resize content to new width and get the new desired height. const auto contentLeft = 0; @@ -308,7 +309,7 @@ QRect LayerWidget::countGeometry(int newWidth) { desiredHeight += additionalScroll; contentHeight += additionalScroll; - _tillBottom = (newTop + desiredHeight >= windowHeight); + _tillBottom = (desiredHeight >= maxVisibleHeight); if (_tillBottom) { additionalScroll += contentBottom; } @@ -321,7 +322,7 @@ QRect LayerWidget::countGeometry(int newWidth) { contentTop, contentWidth, contentHeight, - }, expanding, additionalScroll); + }, expanding, additionalScroll, maxVisibleHeight); return QRect(newLeft, newTop, newWidth, desiredHeight); } diff --git a/Telegram/SourceFiles/info/info_section_widget.cpp b/Telegram/SourceFiles/info/info_section_widget.cpp index 084b2f223b..a701314284 100644 --- a/Telegram/SourceFiles/info/info_section_widget.cpp +++ b/Telegram/SourceFiles/info/info_section_widget.cpp @@ -54,7 +54,11 @@ void SectionWidget::init() { const auto full = !_content->scrollBottomSkip(); const auto height = size.height() - (full ? 0 : st::boxRadius); const auto wrapGeometry = QRect{ 0, 0, size.width(), height }; - _content->updateGeometry(wrapGeometry, expanding, additionalScroll); + _content->updateGeometry( + wrapGeometry, + expanding, + additionalScroll, + size.height()); }, lifetime()); _connecting = std::make_unique( diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 32bd69c7a5..bf03f71317 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -934,13 +934,17 @@ object_ptr WrapWidget::createTopBarSurrogate( void WrapWidget::updateGeometry( QRect newGeometry, bool expanding, - int additionalScroll) { + int additionalScroll, + int maxVisibleHeight) { auto scrollChanged = (_additionalScroll != additionalScroll); auto geometryChanged = (geometry() != newGeometry); auto shrinkingContent = (additionalScroll < _additionalScroll); _additionalScroll = additionalScroll; + _maxVisibleHeight = maxVisibleHeight; _expanding = expanding; + _content->applyMaxVisibleHeight(maxVisibleHeight); + if (geometryChanged) { if (shrinkingContent) { setGeometry(newGeometry); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index d16108114d..f102cc8349 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -124,7 +124,8 @@ public: void updateGeometry( QRect newGeometry, bool expanding, - int additionalScroll); + int additionalScroll, + int maxVisibleHeight); [[nodiscard]] int scrollBottomSkip() const; [[nodiscard]] int scrollTillBottom(int forHeight) const; [[nodiscard]] rpl::producer scrollTillBottomChanges() const; @@ -207,6 +208,7 @@ private: std::unique_ptr _controller; object_ptr _content = { nullptr }; int _additionalScroll = 0; + int _maxVisibleHeight = 0; bool _expanding = false; rpl::variable _grabbingForExpanding = false; object_ptr _topBar = { nullptr }; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index cd6d6bd032..2cddcf63e7 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -44,7 +44,10 @@ Widget::Widget( , _self(controller->key().settingsSelf()) , _type(controller->section().settingsType()) , _inner([&] { - auto inner = _type->create(this, controller->parentController()); + auto inner = _type->create( + this, + controller->parentController(), + scroll()); if (inner->hasFlexibleTopBar()) { auto filler = setInnerWidget(object_ptr(this)); filler->resize(1, 1); @@ -229,6 +232,12 @@ rpl::producer Widget::title() { return _inner->title(); } +void Widget::paintEvent(QPaintEvent *e) { + if (!_inner->paintOuter(this, maxVisibleHeight(), e->rect())) { + ContentWidget::paintEvent(e); + } +} + std::shared_ptr Widget::doCreateMemento() { auto result = std::make_shared(self(), _type); saveState(result.get()); diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index e6715d68ce..d2eb636152 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -84,6 +84,8 @@ private: void saveState(not_null memento); void restoreState(not_null memento); + void paintEvent(QPaintEvent *e) override; + std::shared_ptr doCreateMemento() override; not_null _self; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 9a3722eae8..2ba11542e0 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -79,7 +79,8 @@ Session::Session( not_null account, const MTPUser &user, std::unique_ptr settings) -: _account(account) +: _userId(user.c_user().vid()) +, _account(account) , _settings(std::move(settings)) , _changes(std::make_unique(this)) , _api(std::make_unique(this)) @@ -89,7 +90,6 @@ Session::Session( , _uploader(std::make_unique(_api.get())) , _storage(std::make_unique()) , _data(std::make_unique(this)) -, _userId(user.c_user().vid()) , _user(_data->processUser(user)) , _emojiStickersPack(std::make_unique(this)) , _diceStickersPacks(std::make_unique(this)) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index cca061d087..635f453d5e 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -199,6 +199,7 @@ private: void parseColorIndices(const MTPDhelp_peerColors &data); + const UserId _userId; const not_null _account; const std::unique_ptr _settings; @@ -212,7 +213,6 @@ private: // _data depends on _downloader / _uploader. const std::unique_ptr _data; - const UserId _userId; const not_null _user; // _emojiStickersPack depends on _data. diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 05fceb77b0..76e353c006 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "boxes/share_box.h" #include "chat_helpers/compose/compose_show.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_chat_participant_status.h" #include "data/data_forum_topic.h" #include "data/data_histories.h" @@ -119,6 +120,7 @@ namespace Media::Stories { message.action.clearDraft = false; api->sendMessage(std::move(message)); } + const auto session = &thread->session(); const auto threadPeer = thread->peer(); const auto threadHistory = thread->owningHistory(); const auto randomId = base::RandomValue(); @@ -132,6 +134,12 @@ namespace Media::Stories { if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } + if (options.scheduled) { + sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + } + if (options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto done = [=] { if (!--state->requests) { if (show->valid()) { @@ -155,7 +163,7 @@ namespace Media::Stories { MTPVector(), MTP_int(action.options.scheduled), MTP_inputPeerEmpty(), - MTPInputQuickReplyShortcut() + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index b38e4e1846..6c820afa9d 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "core/application.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "settings/business/settings_shortcut_messages.h" #include "ui/boxes/choose_date_time.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -37,10 +39,13 @@ public: [[nodiscard]] rpl::producer title() override; + [[nodiscard]] rpl::producer sectionShowOther() override; + private: void setupContent(not_null controller); void save(); + rpl::event_stream _showOther; rpl::variable _recipients; rpl::variable _schedule; rpl::variable _enabled; @@ -197,6 +202,10 @@ rpl::producer AwayMessage::title() { return tr::lng_away_title(); } +rpl::producer AwayMessage::sectionShowOther() { + return _showOther.events(); +} + void AwayMessage::setupContent( not_null controller) { using namespace Data; @@ -258,7 +267,9 @@ void AwayMessage::setupContent( st::settingsButtonLightNoIcon )); create->setClickedCallback([=] { - + const auto owner = &controller->session().data(); + const auto id = owner->shortcutMessages().emplaceShortcut("away"); + _showOther.fire(ShortcutMessagesId(id)); }); Ui::AddSkip(createInner); Ui::AddDivider(createInner); diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 5500ca539f..18d9c9545f 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -6,7 +6,7 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_chatbots.h" -// + #include "core/application.h" #include "data/business/data_business_chatbots.h" #include "data/data_session.h" diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 39520846fe..7e5ad6b1e2 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -10,9 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "core/application.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/business/settings_shortcut_messages.h" #include "settings/business/settings_recipients_helper.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" @@ -50,6 +52,7 @@ private: Ui::RoundRect _bottomSkipRounding; + rpl::event_stream _showOther; rpl::variable _recipients; rpl::variable _noActivityDays; rpl::variable _enabled; @@ -229,6 +232,28 @@ void Greeting::setupContent( object_ptr(content))); const auto inner = wrap->entity(); + const auto createWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto createInner = createWrap->entity(); + Ui::AddSkip(createInner); + const auto create = createInner->add(object_ptr( + createInner, + tr::lng_greeting_create(), + st::settingsButtonLightNoIcon + )); + create->setClickedCallback([=] { + const auto owner = &controller->session().data(); + const auto id = owner->shortcutMessages().emplaceShortcut("hello"); + _showOther.fire(ShortcutMessagesId(id)); + }); + Ui::AddSkip(createInner); + Ui::AddDivider(createInner); + + createWrap->toggleOn(rpl::single(true)); + + Ui::AddSkip(inner); AddBusinessRecipientsSelector(inner, { .controller = controller, .title = tr::lng_greeting_recipients(), diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp new file mode 100644 index 0000000000..290aa222c0 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -0,0 +1,1183 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "settings/business/settings_shortcut_messages.h" + +#include "api/api_editing.h" +#include "api/api_sending.h" +#include "apiwrap.h" +#include "base/call_delayed.h" +#include "boxes/delete_messages_box.h" +#include "boxes/premium_limits_box.h" +#include "boxes/send_files_box.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" +#include "data/data_message_reaction_id.h" +#include "data/data_premium_limits.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/view/controls/compose_controls_common.h" +#include "history/view/controls/history_view_compose_controls.h" +#include "history/view/history_view_corner_buttons.h" +#include "history/view/history_view_empty_list_bubble.h" +#include "history/view/history_view_list_widget.h" +#include "history/view/history_view_sticker_toast.h" +#include "history/history.h" +#include "history/history_item.h" +#include "inline_bots/inline_bot_result.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "menu/menu_send.h" +#include "settings/business/settings_recipients_helper.h" +#include "storage/localimageloader.h" +#include "storage/storage_account.h" +#include "storage/storage_media_prepare.h" +#include "ui/chat/attach/attach_send_files_way.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/controls/jump_down_button.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/scroll_area.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_chat.h" + +namespace Settings { +namespace { + +using namespace HistoryView; + +class ShortcutMessages + : public AbstractSection + , private ListDelegate + , private CornerButtonsDelegate { +public: + ShortcutMessages( + QWidget *parent, + not_null controller, + not_null scroll, + BusinessShortcutId shortcutId); + ~ShortcutMessages(); + + [[nodiscard]] static Type Id(BusinessShortcutId shortcutId); + + [[nodiscard]] Type id() const final override { + return Id(_shortcutId); + } + + [[nodiscard]] rpl::producer title() override; + + bool paintOuter( + not_null outer, + int maxVisibleHeight, + QRect clip) override; + +private: + // ListDelegate interface. + Context listContext() override; + bool listScrollTo(int top, bool syntetic = true) override; + void listCancelRequest() override; + void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; + rpl::producer listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) override; + bool listAllowsMultiSelect() override; + bool listIsItemGoodForSelection(not_null item) override; + bool listIsLessInOrder( + not_null first, + not_null second) override; + void listSelectionChanged(SelectedItems &&items) override; + void listMarkReadTill(not_null item) override; + void listMarkContentsRead( + const base::flat_set> &items) override; + MessagesBarData listMessagesBar( + const std::vector> &elements) override; + void listContentRefreshed() override; + void listUpdateDateLink( + ClickHandlerPtr &link, + not_null view) override; + bool listElementHideReply(not_null view) override; + bool listElementShownUnread(not_null view) override; + bool listIsGoodForAroundPosition( + not_null view) override; + void listSendBotCommand( + const QString &command, + const FullMsgId &context) override; + void listSearch( + const QString &query, + const FullMsgId &context) override; + void listHandleViaClick(not_null bot) override; + not_null listChatTheme() override; + CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; + CopyRestrictionType listCopyMediaRestrictionType( + not_null item) override; + CopyRestrictionType listSelectRestrictionType() override; + auto listAllowedReactionsValue() + -> rpl::producer override; + void listShowPremiumToast(not_null document) override; + void listOpenPhoto( + not_null photo, + FullMsgId context) override; + void listOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) override; + void listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) override; + QString listElementAuthorRank(not_null view) override; + History *listTranslateHistory() override; + void listAddTranslatedItems( + not_null tracker) override; + + // CornerButtonsDelegate delegate. + void cornerButtonsShowAtPosition( + Data::MessagePosition position) override; + Data::Thread *cornerButtonsThread() override; + FullMsgId cornerButtonsCurrentId() override; + bool cornerButtonsIgnoreVisibility() override; + std::optional cornerButtonsDownShown() override; + bool cornerButtonsUnreadMayBeShown() override; + bool cornerButtonsHas(CornerButtonType type) override; + + QPointer createPinnedToBottom( + not_null parent) override; + void setupComposeControls(); + + + void uploadFile(const QByteArray &fileContent, SendMediaType type); + bool confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional overrideSendImagesAsPhotos = std::nullopt, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + const QStringList &files, + const QString &insertTextOnCancel); + bool confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + not_null data, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel = QString()); + bool showSendingFilesError(const Ui::PreparedList &list) const; + bool showSendingFilesError( + const Ui::PreparedList &list, + std::optional compress) const; + void sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter); + + void sendExistingDocument(not_null document); + bool sendExistingDocument( + not_null document, + Api::SendOptions options, + std::optional localId); + void sendExistingPhoto(not_null photo); + bool sendExistingPhoto( + not_null photo, + Api::SendOptions options); + void sendInlineResult( + not_null result, + not_null bot); + void sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options, + std::optional localMessageId); + + [[nodiscard]] Api::SendAction prepareSendAction( + Api::SendOptions options) const; + void send(); + void send(Api::SendOptions options); + void sendVoice(Controls::VoiceToSend &&data); + void edit( + not_null item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId); + void chooseAttach(std::optional overrideSendImagesAsPhotos); + [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] FullReplyTo replyTo() const; + void doSetInnerFocus(); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId = {}); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId, + const Window::SectionShow ¶ms); + void showAtEnd(); + void finishSending(); + + const not_null _controller; + const not_null _session; + const not_null _scroll; + const BusinessShortcutId _shortcutId; + const not_null _history; + std::shared_ptr _style; + std::shared_ptr _theme; + QPointer _inner; + std::unique_ptr _controlsWrap; + std::unique_ptr _composeControls; + bool _skipScrollEvent = false; + + std::unique_ptr _stickerToast; + + FullMsgId _lastShownAt; + CornerButtons _cornerButtons; + + Data::MessagesSlice _lastSlice; + bool _choosingAttach = false; + +}; + +struct Factory : AbstractSectionFactory { + explicit Factory(BusinessShortcutId shortcutId) + : shortcutId(shortcutId) { + } + + object_ptr create( + not_null parent, + not_null controller, + not_null scroll + ) const final override { + return object_ptr( + parent, + controller, + scroll, + shortcutId); + } + + const BusinessShortcutId shortcutId = {}; +}; + +ShortcutMessages::ShortcutMessages( + QWidget *parent, + not_null controller, + not_null scroll, + BusinessShortcutId shortcutId) +: AbstractSection(parent) +, _controller(controller) +, _session(&controller->session()) +, _scroll(scroll) +, _shortcutId(shortcutId) +, _history(_session->data().history(_session->user()->id)) +, _cornerButtons( + _scroll, + controller->chatStyle(), + static_cast(this)) { + controller->chatStyle()->paletteChanged( + ) | rpl::start_with_next([=] { + _scroll->updateBars(); + }, _scroll->lifetime()); + + _style = std::make_shared(_session->colorIndicesValue()); + _theme = std::shared_ptr( + Window::Theme::DefaultChatThemeOn(lifetime())); + + _inner = Ui::CreateChild( + this, + controller, + static_cast(this)); + //_scroll->scrolls( + //) | rpl::start_with_next([=] { + // onScroll(); + //}, lifetime()); + + _inner->editMessageRequested( + ) | rpl::start_with_next([=](auto fullId) { + if (const auto item = _session->data().message(fullId)) { + const auto media = item->media(); + if (!media || media->webpage() || media->allowsEditCaption()) { + _composeControls->editMessage(fullId); + } + } + }, _inner->lifetime()); + + { + auto emptyInfo = base::make_unique_q( + _inner, + controller->chatStyle(), + st::msgServicePadding); + const auto emptyText = Ui::Text::Semibold( + tr::lng_scheduled_messages_empty(tr::now)); + emptyInfo->setText(emptyText); + _inner->setEmptyInfoWidget(std::move(emptyInfo)); + } + + widthValue() | rpl::start_with_next([=](int width) { + resize(width, width); + }, lifetime()); +} + +ShortcutMessages::~ShortcutMessages() = default; + +Type ShortcutMessages::Id(BusinessShortcutId shortcutId) { + return std::make_shared(shortcutId); +} + +rpl::producer ShortcutMessages::title() { + return rpl::single(u"Editing messages list"_q); +} + +bool ShortcutMessages::paintOuter( + not_null outer, + int maxVisibleHeight, + QRect clip) { + const auto window = outer->window()->height(); + Window::SectionWidget::PaintBackground( + _theme.get(), + outer, + std::max(outer->height(), maxVisibleHeight), + 0, + clip); + return true; +} + +void ShortcutMessages::setupComposeControls() { + _composeControls->setHistory({ + .history = _history.get(), + .writeRestriction = rpl::single(Controls::WriteRestriction()), + }); + + _composeControls->height( + ) | rpl::start_with_next([=](int height) { + const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop()); + _controlsWrap->resize(width(), height); + if (wasMax) { + listScrollTo(_scroll->scrollTopMax()); + } + }, lifetime()); + + _composeControls->cancelRequests( + ) | rpl::start_with_next([=] { + listCancelRequest(); + }, lifetime()); + + _composeControls->sendRequests( + ) | rpl::start_with_next([=] { + send(); + }, lifetime()); + + _composeControls->sendVoiceRequests( + ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { + sendVoice(std::move(data)); + }, lifetime()); + + _composeControls->sendCommandRequests( + ) | rpl::start_with_next([=](const QString &command) { + listSendBotCommand(command, FullMsgId()); + }, lifetime()); + + const auto saveEditMsgRequestId = lifetime().make_state(0); + _composeControls->editRequests( + ) | rpl::start_with_next([=](auto data) { + if (const auto item = _session->data().message(data.fullId)) { + if (item->isBusinessShortcut()) { + edit(item, data.options, saveEditMsgRequestId); + } + } + }, lifetime()); + + _composeControls->attachRequests( + ) | rpl::filter([=] { + return !_choosingAttach; + }) | rpl::start_with_next([=](std::optional overrideCompress) { + _choosingAttach = true; + base::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] { + _choosingAttach = false; + chooseAttach(overrideCompress); + }); + }, lifetime()); + + _composeControls->fileChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + _controller->hideLayer(anim::type::normal); + sendExistingDocument(data.document); + }, lifetime()); + + _composeControls->photoChosen( + ) | rpl::start_with_next([=](ChatHelpers::PhotoChosen chosen) { + sendExistingPhoto(chosen.photo); + }, lifetime()); + + _composeControls->inlineResultChosen( + ) | rpl::start_with_next([=](ChatHelpers::InlineChosen chosen) { + sendInlineResult(chosen.result, chosen.bot); + }, lifetime()); + + _composeControls->jumpToItemRequests( + ) | rpl::start_with_next([=](FullReplyTo to) { + if (const auto item = _session->data().message(to.messageId)) { + showAtPosition(item->position()); + } + }, lifetime()); + + _composeControls->scrollKeyEvents( + ) | rpl::start_with_next([=](not_null e) { + _scroll->keyPressEvent(e); + }, lifetime()); + + _composeControls->editLastMessageRequests( + ) | rpl::start_with_next([=](not_null e) { + if (!_inner->lastMessageEditRequestNotify()) { + _scroll->keyPressEvent(e); + } + }, lifetime()); + + _composeControls->setMimeDataHook([=]( + not_null data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return Core::CanSendFiles(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + //return confirmSendingFiles( + // data, + // std::nullopt, + // Core::ReadMimeText(data));#TODO + } + Unexpected("action in MimeData hook."); + }); + + _composeControls->lockShowStarts( + ) | rpl::start_with_next([=] { + _cornerButtons.updateJumpDownVisibility(); + _cornerButtons.updateUnreadThingsVisibility(); + }, lifetime()); + + _composeControls->viewportEvents( + ) | rpl::start_with_next([=](not_null e) { + _scroll->viewportEvent(e); + }, lifetime()); + + _controlsWrap->widthValue() | rpl::start_with_next([=](int width) { + _composeControls->resizeToWidth(width); + }, _controlsWrap->lifetime()); + _composeControls->height() | rpl::start_with_next([=](int height) { + _controlsWrap->resize(_controlsWrap->width(), height); + }, _controlsWrap->lifetime()); +} + +QPointer ShortcutMessages::createPinnedToBottom( + not_null parent) { + _controlsWrap = std::make_unique(parent); + _composeControls = std::make_unique( + _controlsWrap.get(), + _controller, + [=](not_null emoji) { listShowPremiumToast(emoji); }, + ComposeControls::Mode::Scheduled, + SendMenu::Type::Disabled); + + setupComposeControls(); + + return _controlsWrap.get(); +} + +Context ShortcutMessages::listContext() { + return Context::History; +} + +bool ShortcutMessages::listScrollTo(int top, bool syntetic) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { + //updateInnerVisibleArea(); + return false; + } + _scroll->scrollToY(top); + return true; +} + +void ShortcutMessages::listCancelRequest() { + if (_inner && !_inner->getSelectedItems().empty()) { + //clearSelected(); + return; + } else if (_composeControls->handleCancelRequest()) { + return; + } + _controller->showBackFromStack(); +} + +void ShortcutMessages::listDeleteRequest() { + //confirmDeleteSelected(); +} + +void ShortcutMessages::listTryProcessKeyInput(not_null e) { + _composeControls->tryProcessKeyInput(e); +} + +rpl::producer ShortcutMessages::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto data = &_controller->session().data(); + //return rpl::single(rpl::empty) | rpl::then( + // data->scheduledMessages().updates(_history) + //) | rpl::map([=] { + // return data->scheduledMessages().list(_history); + //}) | rpl::after_next([=](const Data::MessagesSlice &slice) { + // highlightSingleNewMessage(slice); + //}); + return rpl::never(); +} + +bool ShortcutMessages::listAllowsMultiSelect() { + return true; +} + +bool ShortcutMessages::listIsItemGoodForSelection( + not_null item) { + return !item->isSending() && !item->hasFailed(); +} + +bool ShortcutMessages::listIsLessInOrder( + not_null first, + not_null second) { + return first->position() < second->position(); +} + +void ShortcutMessages::listSelectionChanged(SelectedItems &&items) { +} + +void ShortcutMessages::listMarkReadTill(not_null item) { +} + +void ShortcutMessages::listMarkContentsRead( + const base::flat_set> &items) { +} + +MessagesBarData ShortcutMessages::listMessagesBar( + const std::vector> &elements) { + return {}; +} + +void ShortcutMessages::listContentRefreshed() { +} + +void ShortcutMessages::listUpdateDateLink( + ClickHandlerPtr &link, + not_null view) { +} + +bool ShortcutMessages::listElementHideReply(not_null view) { + return false; +} + +bool ShortcutMessages::listElementShownUnread(not_null view) { + return true; +} + +bool ShortcutMessages::listIsGoodForAroundPosition( + not_null view) { + return true; +} + +void ShortcutMessages::listSendBotCommand( + const QString &command, + const FullMsgId &context) { +} + +void ShortcutMessages::listSearch( + const QString &query, + const FullMsgId &context) { + const auto inChat = _history->peer->isUser() + ? Dialogs::Key() + : Dialogs::Key(_history); + _controller->searchMessages(query, inChat); +} + +void ShortcutMessages::listHandleViaClick(not_null bot) { + _composeControls->setText({ '@' + bot->username() + ' ' }); +} + +not_null ShortcutMessages::listChatTheme() { + return _theme.get(); +} + +CopyRestrictionType ShortcutMessages::listCopyRestrictionType( + HistoryItem *item) { + return CopyRestrictionType::None; +} + +CopyRestrictionType ShortcutMessages::listCopyMediaRestrictionType( + not_null item) { + if (const auto media = item->media()) { + if (const auto invoice = media->invoice()) { + if (invoice->extendedMedia) { + return CopyMediaRestrictionTypeFor(_history->peer, item); + } + } + } + return CopyRestrictionType::None; +} + +CopyRestrictionType ShortcutMessages::listSelectRestrictionType() { + return CopyRestrictionType::None; +} + +auto ShortcutMessages::listAllowedReactionsValue() +-> rpl::producer { + return rpl::single(Data::AllowedReactions()); +} + +void ShortcutMessages::listShowPremiumToast( + not_null document) { + if (!_stickerToast) { + _stickerToast = std::make_unique( + _controller, + this, + [=] { _stickerToast = nullptr; }); + } + _stickerToast->showFor(document); +} + +void ShortcutMessages::listOpenPhoto( + not_null photo, + FullMsgId context) { + _controller->openPhoto(photo, { context }); +} + +void ShortcutMessages::listOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { + _controller->openDocument(document, showInMediaView, { context }); +} + +void ShortcutMessages::listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) { +} + +QString ShortcutMessages::listElementAuthorRank( + not_null view) { + return {}; +} + +History *ShortcutMessages::listTranslateHistory() { + return nullptr; +} + +void ShortcutMessages::listAddTranslatedItems( + not_null tracker) { +} + +void ShortcutMessages::cornerButtonsShowAtPosition( + Data::MessagePosition position) { + //showAtPosition(position); +} + +Data::Thread *ShortcutMessages::cornerButtonsThread() { + return _history; +} + +FullMsgId ShortcutMessages::cornerButtonsCurrentId() { + return _lastShownAt; +} + +bool ShortcutMessages::cornerButtonsIgnoreVisibility() { + return false;// animatingShow(); +} + +std::optional ShortcutMessages::cornerButtonsDownShown() { + if (_composeControls->isLockPresent() + || _composeControls->isTTLButtonShown()) { + return false; + } + //const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; + //if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { + // return true; + //} else if (_inner->loadedAtBottomKnown()) { + // return !_inner->loadedAtBottom(); + //} + return std::nullopt; +} + +bool ShortcutMessages::cornerButtonsUnreadMayBeShown() { + return _inner->loadedAtBottomKnown() + && !_composeControls->isLockPresent() + && !_composeControls->isTTLButtonShown(); +} + +bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { + return (type == CornerButtonType::Down); +} + +void ShortcutMessages::uploadFile( + const QByteArray &fileContent, + SendMediaType type) { + // #TODO replies schedule + _session->api().sendFile(fileContent, type, prepareSendAction({})); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedList &list) const { + return showSendingFilesError(list, std::nullopt); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedList &list, + std::optional compress) const { + const auto text = [&] { + using Error = Ui::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return u"(toolarge)"_q; + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (text.isEmpty()) { + return false; + } else if (text == u"(toolarge)"_q) { + const auto fileSize = list.files.back().size; + _controller->show( + Box(FileSizeLimitBox, _session, fileSize, nullptr)); + return true; + } + + _controller->showToast(text); + return true; +} + +Api::SendAction ShortcutMessages::prepareSendAction( + Api::SendOptions options) const { + auto result = Api::SendAction(_history, options); + result.replyTo = replyTo(); + result.options.shortcutId = _shortcutId; + result.options.sendAs = _composeControls->sendAsPeer(); + return result; +} + +void ShortcutMessages::send() { + if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) { + return; + } + send({}); + // #TODO replies schedule + //const auto callback = [=](Api::SendOptions options) { send(options); }; + //Ui::show( + // PrepareScheduleBox(this, sendMenuType(), callback), + // Ui::LayerOption::KeepOther); +} + +void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { + auto action = prepareSendAction(data.options); + _session->api().sendVoiceMessage( + data.bytes, + data.waveform, + data.duration, + std::move(action)); + + _composeControls->cancelReplyMessage(); + _composeControls->clearListenState(); + finishSending(); +} + +void ShortcutMessages::send(Api::SendOptions options) { + _cornerButtons.clearReplyReturns(); + + auto message = Api::MessageToSend(prepareSendAction(options)); + message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); + message.webPage = _composeControls->webPageDraft(); + + _session->api().sendMessage(std::move(message)); + + _composeControls->clear(); + + finishSending(); +} + +void ShortcutMessages::edit( + not_null item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId) { + if (*saveEditMsgRequestId) { + return; + } + const auto webpage = _composeControls->webPageDraft(); + auto sending = TextWithEntities(); + auto left = _composeControls->prepareTextForEditMsg(); + + const auto originalLeftSize = left.text.size(); + const auto hasMediaWithCaption = item + && item->media() + && item->media()->allowsEditCaption(); + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(_session).captionLengthCurrent(); + if (!TextUtilities::CutPart(sending, left, maxCaptionSize) + && !hasMediaWithCaption) { + if (item) { + _controller->show(Box(item, false)); + } else { + doSetInnerFocus(); + } + return; + } else if (!left.text.isEmpty()) { + const auto remove = originalLeftSize - maxCaptionSize; + _controller->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } + + lifetime().add([=] { + if (!*saveEditMsgRequestId) { + return; + } + _session->api().request(base::take(*saveEditMsgRequestId)).cancel(); + }); + + const auto done = [=](mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + _composeControls->cancelEditMessage(); + } + }; + + const auto fail = [=](const QString &error, mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + } + + if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) { + _controller->showToast(tr::lng_edit_error(tr::now)); + } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { + _composeControls->cancelEditMessage(); + } else if (error == u"MESSAGE_EMPTY"_q) { + doSetInnerFocus(); + } else { + _controller->showToast(tr::lng_edit_error(tr::now)); + } + update(); + return true; + }; + + *saveEditMsgRequestId = Api::EditTextMessage( + item, + sending, + webpage, + options, + crl::guard(this, done), + crl::guard(this, fail)); + + _composeControls->hidePanelsAnimated(); + doSetInnerFocus(); +} + +bool ShortcutMessages::confirmSendingFiles( + not_null data, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + const auto hasImage = data->hasImage(); + const auto premium = _controller->session().user()->isPremium(); + + if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { + auto list = Storage::PrepareMediaList( + urls, + st::sendMediaPreviewSize, + premium); + if (list.error != Ui::PreparedList::Error::NonLocalUrl) { + if (list.error == Ui::PreparedList::Error::None + || !hasImage) { + const auto emptyTextOnCancel = QString(); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list), emptyTextOnCancel); + return true; + } + } + } + + if (auto read = Core::ReadMimeImage(data)) { + confirmSendingFiles( + std::move(read.image), + std::move(read.content), + overrideSendImagesAsPhotos, + insertTextOnCancel); + return true; + } + return false; +} + +bool ShortcutMessages::confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel) { + if (_composeControls->confirmMediaEdit(list)) { + return true; + } else if (showSendingFilesError(list)) { + return false; + } + + auto box = Box( + _controller, + std::move(list), + _composeControls->getTextWithAppliedMarkdown(), + _history->peer, + Api::SendType::Normal, + SendMenu::Type::SilentOnly); // #TODO replies schedule + + box->setConfirmedCallback(crl::guard(this, [=]( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + sendingFilesConfirmed( + std::move(list), + way, + std::move(caption), + options, + ctrlShiftEnter); + })); + box->setCancelledCallback(_composeControls->restoreTextCallback( + insertTextOnCancel)); + + //ActivateWindow(_controller); + _controller->show(std::move(box)); + + return true; +} + +bool ShortcutMessages::confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + if (image.isNull()) { + return false; + } + + auto list = Storage::PrepareMediaFromImage( + std::move(image), + std::move(content), + st::sendMediaPreviewSize); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + return confirmSendingFiles(std::move(list), insertTextOnCancel); +} + +void ShortcutMessages::sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + Expects(list.filesToProcess.empty()); + + if (showSendingFilesError(list, way.sendImagesAsPhotos())) { + return; + } + auto groups = DivideByGroups( + std::move(list), + way, + _history->peer->slowmodeApplied()); + const auto type = way.sendImagesAsPhotos() + ? SendMediaType::Photo + : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + if ((groups.size() != 1 || !groups.front().sentWithCaption()) + && !caption.text.isEmpty()) { + auto message = Api::MessageToSend(action); + message.textWithTags = base::take(caption); + _session->api().sendMessage(std::move(message)); + } + for (auto &group : groups) { + const auto album = (group.type != Ui::AlbumType::None) + ? std::make_shared() + : nullptr; + _session->api().sendFiles( + std::move(group.list), + type, + base::take(caption), + album, + action); + } + if (_composeControls->replyingToMessage() == action.replyTo) { + _composeControls->cancelReplyMessage(); + } + finishSending(); +} + +void ShortcutMessages::chooseAttach( + std::optional overrideSendImagesAsPhotos) { + _choosingAttach = false; + + const auto filter = (overrideSendImagesAsPhotos == true) + ? FileDialog::ImagesOrAllFilter() + : FileDialog::AllOrImagesFilter(); + FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( + FileDialog::OpenResult &&result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!result.remoteContent.isEmpty()) { + auto read = Images::Read({ + .content = result.remoteContent, + }); + if (!read.image.isNull() && !read.animated) { + confirmSendingFiles( + std::move(read.image), + std::move(result.remoteContent), + overrideSendImagesAsPhotos); + } else { + uploadFile(result.remoteContent, SendMediaType::File); + } + } else { + const auto premium = _controller->session().user()->isPremium(); + auto list = Storage::PrepareMediaList( + result.paths, + st::sendMediaPreviewSize, + premium); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list)); + } + }), nullptr); +} + +void ShortcutMessages::finishSending() { + _composeControls->hidePanelsAnimated(); + //if (_previewData && _previewData->pendingTill) previewCancel(); + doSetInnerFocus(); + showAtEnd(); +} + +void ShortcutMessages::showAtEnd() { + showAtPosition(Data::MaxMessagePosition); +} + +void ShortcutMessages::doSetInnerFocus() { + if (!_inner->getSelectedText().rich.text.isEmpty() + || !_inner->getSelectedItems().empty() + || !_composeControls->focus()) { + _inner->setFocus(); + } +} + +void ShortcutMessages::sendExistingDocument( + not_null document) { + sendExistingDocument(document, {}, std::nullopt); +} + +bool ShortcutMessages::sendExistingDocument( + not_null document, + Api::SendOptions options, + std::optional localId) { + Api::SendExistingDocument( + Api::MessageToSend(prepareSendAction(options)), + document, + localId); + + _composeControls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ShortcutMessages::sendExistingPhoto(not_null photo) { + sendExistingPhoto(photo, {}); +} + +bool ShortcutMessages::sendExistingPhoto( + not_null photo, + Api::SendOptions options) { + Api::SendExistingPhoto( + Api::MessageToSend(prepareSendAction(options)), + photo); + + _composeControls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ShortcutMessages::sendInlineResult( + not_null result, + not_null bot) { + const auto errorText = result->getErrorOnSend(_history); + if (!errorText.isEmpty()) { + _controller->showToast(errorText); + return; + } + sendInlineResult(result, bot, {}, std::nullopt); + //const auto callback = [=](Api::SendOptions options) { + // sendInlineResult(result, bot, options); + //}; + //Ui::show( + // PrepareScheduleBox(this, sendMenuType(), callback), + // Ui::LayerOption::KeepOther); +} + +void ShortcutMessages::sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options, + std::optional localMessageId) { + auto action = prepareSendAction(options); + action.generateLocal = true; + _session->api().sendInlineResult(bot, result, action, localMessageId); + + _composeControls->clear(); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + + auto &bots = cRefRecentInlineBots(); + const auto index = bots.indexOf(bot); + if (index) { + if (index > 0) { + bots.removeAt(index); + } else if (bots.size() >= RecentInlineBotsLimit) { + bots.resize(RecentInlineBotsLimit - 1); + } + bots.push_front(bot); + bot->session().local().writeRecentHashtagsAndBots(); + } + finishSending(); +} + +void ShortcutMessages::showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId) { + showAtPosition(position, originItemId, {}); +} + +void ShortcutMessages::showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId, + const Window::SectionShow ¶ms) { + _lastShownAt = position.fullId; + _inner->showAtPosition( + position, + params, + _cornerButtons.doneJumpFrom(position.fullId, originItemId, true)); +} + +FullReplyTo ShortcutMessages::replyTo() const { + return _composeControls->replyingToMessage(); +} + +} // namespace + +Type ShortcutMessagesId(int shortcutId) { + return ShortcutMessages::Id(shortcutId); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h new file mode 100644 index 0000000000..325b126025 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h @@ -0,0 +1,16 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type ShortcutMessagesId(int shortcutId); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index aae5868df7..e3aa431290 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" // AmPremiumValue. #include "data/data_session.h" #include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "info/info_wrap_widget.h" // Info::Wrap. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "lang/lang_keys.h" @@ -359,6 +360,7 @@ void Business::setupContent() { const auto content = Ui::CreateChild(this); _controller->session().data().businessInfo().preloadTimezones(); + _controller->session().data().shortcutMessages().preloadShortcuts(); Ui::AddSkip(content, st::settingsFromFileTop); @@ -566,7 +568,8 @@ template <> struct SectionFactory : AbstractSectionFactory { object_ptr create( not_null parent, - not_null controller + not_null controller, + not_null scroll ) const final override { return object_ptr(parent, controller); } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index ea80b45735..02beb10033 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -90,6 +90,13 @@ public: } virtual void setStepDataReference(std::any &data) { } + + virtual bool paintOuter( + not_null outer, + int maxVisibleHeight, + QRect clip) { + return false; + } }; enum class IconType { diff --git a/Telegram/SourceFiles/settings/settings_common_session.h b/Telegram/SourceFiles/settings/settings_common_session.h index 911e22897b..16a03b9e46 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.h +++ b/Telegram/SourceFiles/settings/settings_common_session.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "settings/settings_type.h" +namespace Ui { +class ScrollArea; +} // namespace Ui + namespace Ui::Menu { struct MenuCallback; } // namespace Ui::Menu @@ -27,7 +31,8 @@ class AbstractSection; struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr create( not_null parent, - not_null controller) const = 0; + not_null controller, + not_null scroll) const = 0; [[nodiscard]] virtual bool hasCustomTopBar() const { return false; } @@ -39,7 +44,8 @@ template struct SectionFactory : AbstractSectionFactory { object_ptr create( not_null parent, - not_null controller + not_null controller, + not_null scroll ) const final override { return object_ptr(parent, controller); } diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index 52114830fd..717627573e 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -43,7 +43,8 @@ struct Factory : AbstractSectionFactory { object_ptr create( not_null parent, - not_null controller + not_null controller, + not_null scroll ) const final override { return object_ptr(parent, controller, type); } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index e56122c942..ec17c8aaca 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1267,7 +1267,8 @@ template <> struct SectionFactory : AbstractSectionFactory { object_ptr create( not_null parent, - not_null controller + not_null controller, + not_null scroll ) const final override { return object_ptr(parent, controller); }