diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index b2f6cd3587..cc060d2a60 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -898,6 +898,8 @@ PRIVATE history/view/history_view_sticker_toast.h history/view/history_view_subsection_tabs.cpp history/view/history_view_subsection_tabs.h + history/view/history_view_suggest_options.cpp + history/view/history_view_suggest_options.h history/view/history_view_text_helper.cpp history/view/history_view_text_helper.h history/view/history_view_transcribe_button.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d72646e86b..207aecbdae 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4415,6 +4415,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to_quote" = "Reply to quote from {name}"; +"lng_suggest_bar_title" = "Suggest a Post Below"; +"lng_suggest_bar_text" = "Click to offer a price for publishing."; +"lng_suggest_bar_priced" = "{amount} for publishing anytime."; +"lng_suggest_bar_priced_dated" = "{amount} {date}"; +"lng_suggest_bar_dated" = "Publish on {date}"; +"lng_suggest_options_title" = "Suggest a Message"; +"lng_suggest_options_price" = "Enter Price in Stars"; +"lng_suggest_options_price_about" = "Choose how many Stars you want to offer {channel} to publish this message."; +"lng_suggest_options_date" = "Time"; +"lng_suggest_options_date_any" = "Anytime"; +"lng_suggest_options_date_about" = "Select the date and time you want the message to be published."; +"lng_suggest_options_offer" = "Offer {amount}"; + "lng_reply_in_another_title" = "Reply in..."; "lng_reply_in_another_chat" = "Reply in Another Chat"; "lng_reply_in_author" = "Message author"; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 5342cbd2e0..87111914ef 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -399,10 +399,12 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { } } const auto replyTo = FullReplyTo(); + const auto suggest = SuggestPostOptions(); Window::PeerMenuCreatePoll( controller, item->history()->peer, replyTo, + suggest, chosen, disabled); } break; diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index bd0e9302a4..29617a3f16 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -14,13 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Api { -MTPSuggestedPost SuggestToMTP(const std::optional &suggest) { +MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) { using Flag = MTPDsuggestedPost::Flag; - return suggest + return suggest.exists ? MTP_suggestedPost( - MTP_flags(suggest->date ? Flag::f_schedule_date : Flag()), - MTP_long(suggest->stars), - MTP_int(suggest->date)) + MTP_flags(suggest.date ? Flag::f_schedule_date : Flag()), + MTP_long(suggest.stars), + MTP_int(suggest.date)) : MTPSuggestedPost(); } diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index e648102d06..bbbff2b0cc 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -19,17 +19,7 @@ namespace Api { inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); -struct SuggestOptions { - int stars = 0; - TimeId date = 0; - - friend inline bool operator==( - const SuggestOptions &, - const SuggestOptions &) = default; -}; - -[[nodiscard]] MTPSuggestedPost SuggestToMTP( - const std::optional &suggest); +[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest); struct SendOptions { uint64 price = 0; @@ -43,7 +33,7 @@ struct SendOptions { bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; - std::optional suggest; + SuggestPostOptions suggest; friend inline bool operator==( const SendOptions &, diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 4d000104eb..1b2c0fc81f 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -239,6 +239,7 @@ void SendExistingMedia( .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, media, caption); const auto performRequest = [=](const auto &repeatRequest) -> void { @@ -426,6 +427,7 @@ bool SendDice(MessageToSend &message) { .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, TextWithEntities(), MTP_messageMediaDice( MTP_int(0), MTP_string(emoji))); @@ -652,6 +654,7 @@ void SendConfirmedFile( .postAuthor = NewMessagePostAuthor(action), .groupedId = groupId, .effectId = file->to.options.effectId, + .suggest = HistoryMessageSuggestInfo(file->to.options), }, caption, media); } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 004ae8c3b6..a1c6f380c9 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2170,7 +2170,7 @@ void ApiWrap::saveDraftsToCloud() { cloudDraft->webpage, textWithTags.text.isEmpty()), MTP_long(0), // effect - MTPSuggestedPost() // + Api::SuggestToMTP(cloudDraft->suggest) )).done([=](const MTPBool &result, const MTP::Response &response) { const auto requestId = response.requestId; history->finishSavingCloudDraft( @@ -3508,7 +3508,7 @@ void ApiWrap::forwardMessages( .shortcutId = action.options.shortcutId, .starsPaid = action.options.starsApproved, .postAuthor = NewMessagePostAuthor(action), - + .suggest = HistoryMessageSuggestInfo(action.options), // forwarded messages don't have effects //.effectId = action.options.effectId, }, item); @@ -3603,6 +3603,7 @@ void ApiWrap::sendSharedContact( .starsPaid = action.options.starsApproved, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, TextWithEntities(), MTP_messageMediaContact( MTP_string(phone), MTP_string(firstName), @@ -3986,6 +3987,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, sending, media); const auto done = [=]( const MTPUpdates &result, @@ -4036,7 +4038,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { mtpShortcut, MTP_long(action.options.effectId), MTP_long(starsPaid), - SuggestToMTP(action.options.suggest) + Api::SuggestToMTP(action.options.suggest) ), done, fail); } else { histories.sendPreparedMessage( @@ -4056,7 +4058,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { mtpShortcut, MTP_long(action.options.effectId), MTP_long(starsPaid), - SuggestToMTP(action.options.suggest) + Api::SuggestToMTP(action.options.suggest) ), done, fail); } isFirst = false; @@ -4392,7 +4394,7 @@ void ApiWrap::sendMediaWithRandomId( Data::ShortcutIdToMTP(_session, options.shortcutId), MTP_long(options.effectId), MTP_long(starsPaid), - SuggestToMTP(options.suggest) + Api::SuggestToMTP(options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4476,7 +4478,7 @@ void ApiWrap::sendMultiPaidMedia( Data::ShortcutIdToMTP(_session, options.shortcutId), MTP_long(options.effectId), MTP_long(starsPaid), - SuggestToMTP(options.suggest) + Api::SuggestToMTP(options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (const auto album = _sendingAlbums.take(groupId)) { const auto copy = (*album)->items; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 230c2ac31b..63bbaede49 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1144,6 +1144,30 @@ historyGiftToUser: IconButton(historyAttach) { icon: icon {{ "chat/input_gift", historyComposeIconFg }}; iconOver: icon {{ "chat/input_gift", historyComposeIconFgOver }}; } +historySuggestPostToggle: IconButton(historyDirectMessage) { + icon: icon{{ "menu/chat_discuss", historyComposeIconFg }}; + iconOver: icon{{ "menu/chat_discuss", historyComposeIconFgOver }}; +} +historySuggestIconPosition: point(12px, 12px); + +suggestOptionsPrice: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(2px, 20px, 2px, 0px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; + + border: 0px; + borderActive: 0px; + + heightMin: 32px; + + style: defaultTextStyle; +} historyAttachEmojiInner: IconButton(historyAttach) { icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index ee8d348f9e..95fc3c4f9c 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -21,6 +21,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" namespace Data { +namespace { + +constexpr auto kMaxSuggestStars = 1'000'000'000; + +} // namespace WebPageDraft WebPageDraft::FromItem(not_null item) { const auto previewMedia = item->media(); @@ -45,6 +50,7 @@ WebPageDraft WebPageDraft::FromItem(not_null item) { Draft::Draft( const TextWithTags &textWithTags, FullReplyTo reply, + SuggestPostOptions suggest, const MessageCursor &cursor, WebPageDraft webpage, mtpRequestId saveRequestId) @@ -58,6 +64,7 @@ Draft::Draft( Draft::Draft( not_null field, FullReplyTo reply, + SuggestPostOptions suggest, WebPageDraft webpage, mtpRequestId saveRequestId) : textWithTags(field->getTextWithTags()) @@ -106,9 +113,22 @@ void ApplyPeerCloudDraft( } }, [](const auto &) {}); } + auto suggest = SuggestPostOptions(); + if (!history->suggestDraftAllowed()) { + // Don't apply suggest options in unsupported chats. + } else if (const auto suggested = draft.vsuggested_post()) { + const auto &data = suggested->data(); + suggest.exists = 1; + suggest.date = data.vschedule_date().value_or_empty(); + suggest.stars = uint32(std::clamp( + data.vstars_amount().v, + uint64(), + uint64(kMaxSuggestStars))); + } auto cloudDraft = std::make_unique( textWithTags, replyTo, + suggest, MessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax), std::move(webpage)); cloudDraft->date = date; @@ -150,18 +170,19 @@ void SetChatLinkDraft(not_null peer, TextWithEntities draft) { const auto history = peer->owner().history(peer->id); const auto topicRootId = MsgId(); const auto monoforumPeerId = PeerId(); - history->setLocalDraft(std::make_unique( + history->setLocalDraft(std::make_unique( textWithTags, FullReplyTo{ .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), cursor, - Data::WebPageDraft())); + WebPageDraft())); history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( history, - Data::EntryUpdate::Flag::LocalDraftSet); + EntryUpdate::Flag::LocalDraftSet); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index ab19e0cb2e..5af11ee198 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -52,18 +52,21 @@ struct Draft { Draft( const TextWithTags &textWithTags, FullReplyTo reply, + SuggestPostOptions suggest, const MessageCursor &cursor, WebPageDraft webpage, mtpRequestId saveRequestId = 0); Draft( not_null field, FullReplyTo reply, + SuggestPostOptions suggest, WebPageDraft webpage, mtpRequestId saveRequestId = 0); TimeId date = 0; TextWithTags textWithTags; FullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft. + SuggestPostOptions suggest; MessageCursor cursor; WebPageDraft webpage; mtpRequestId saveRequestId = 0; @@ -240,6 +243,7 @@ using HistoryDrafts = base::flat_map>; [[nodiscard]] inline bool DraftIsNull(const Draft *draft) { return !draft || (!draft->reply.messageId + && !draft->suggest.exists && DraftStringIsEmpty(draft->textWithTags.text)); } diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 355aff7679..f093a6f2ef 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -190,6 +190,23 @@ struct FullReplyTo { friend inline bool operator==(FullReplyTo, FullReplyTo) = default; }; +struct SuggestPostOptions { + uint32 exists : 1 = 0; + uint32 stars : 31 = 0; + TimeId date = 0; + + explicit operator bool() const { + return exists != 0; + } + + friend inline auto operator<=>( + SuggestPostOptions, + SuggestPostOptions) = default; + friend inline bool operator==( + SuggestPostOptions, + SuggestPostOptions) = default; +}; + struct GlobalMsgId { FullMsgId itemId; uint64 sessionUniqueId = 0; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index bfe6db59ea..14610cf5f9 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -685,6 +685,9 @@ bool PeerData::canCreatePolls() const { } bool PeerData::canCreateTodoLists() const { + if (isMonoforum()) { + return false; + } return session().premium() && (Data::CanSend(this, ChatRestriction::SendPolls) || isUser()); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index de4f99f3ad..41c25beff6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -121,6 +121,7 @@ struct EntryState { Section section = Section::History; FilterId filterId = 0; FullReplyTo currentReplyTo; + SuggestPostOptions currentSuggest; friend inline auto operator<=>( const EntryState&, diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 5227ae17a7..431bf17de1 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -239,6 +239,9 @@ void History::createLocalDraftFromCloud( draft->reply.topicRootId = topicRootId; draft->reply.monoforumPeerId = monoforumPeerId; + if (!suggestDraftAllowed()) { + draft->suggest = SuggestPostOptions(); + } auto existing = localDraft(topicRootId, monoforumPeerId); if (Data::DraftIsNull(existing) || !existing->date @@ -247,12 +250,14 @@ void History::createLocalDraftFromCloud( setLocalDraft(std::make_unique( draft->textWithTags, draft->reply, + draft->suggest, draft->cursor, draft->webpage)); existing = localDraft(topicRootId, monoforumPeerId); } else if (existing != draft) { existing->textWithTags = draft->textWithTags; existing->reply = draft->reply; + existing->suggest = draft->suggest; existing->cursor = draft->cursor; existing->webpage = draft->webpage; } @@ -325,6 +330,7 @@ Data::Draft *History::createCloudDraft( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); cloudDraft(topicRootId, monoforumPeerId)->date = TimeId(0); @@ -337,18 +343,23 @@ Data::Draft *History::createCloudDraft( setCloudDraft(std::make_unique( fromDraft->textWithTags, reply, + fromDraft->suggest, fromDraft->cursor, fromDraft->webpage)); existing = cloudDraft(topicRootId, monoforumPeerId); } else if (existing != fromDraft) { existing->textWithTags = fromDraft->textWithTags; existing->reply = fromDraft->reply; + existing->suggest = fromDraft->suggest; existing->cursor = fromDraft->cursor; existing->webpage = fromDraft->webpage; } existing->date = base::unixtime::now(); existing->reply.topicRootId = topicRootId; existing->reply.monoforumPeerId = monoforumPeerId; + if (!suggestDraftAllowed()) { + existing->suggest = SuggestPostOptions(); + } } if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { @@ -3382,6 +3393,10 @@ bool History::amMonoforumAdmin() const { return (_flags & Flag::IsMonoforumAdmin); } +bool History::suggestDraftAllowed() const { + return peer->isMonoforum() || !peer->amMonoforumAdmin(); +} + not_null History::migrateToOrMe() const { if (const auto to = peer->migrateTo()) { return owner().history(to); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 86c14ccc37..a36d2000c6 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -79,6 +79,7 @@ public: void monoforumChanged(Data::SavedMessages *old); [[nodiscard]] bool amMonoforumAdmin() const; + [[nodiscard]] bool suggestDraftAllowed() const; [[nodiscard]] not_null migrateToOrMe() const; [[nodiscard]] History *migrateFrom() const; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 2df23d25f6..9ce8ed0b79 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -190,6 +190,7 @@ struct HistoryItem::CreateConfig { TimeId editDate = 0; HistoryMessageMarkupData markup; HistoryMessageRepliesData replies; + HistoryMessageSuggestInfo suggest; bool imported = false; // For messages created from existing messages (forwarded). @@ -3841,6 +3842,9 @@ void HistoryItem::createComponents(CreateConfig &&config) { mask |= HistoryMessageRestrictions::Bit(); } } + if (config.suggest.exists) { + mask |= HistoryMessageSuggestedPost::Bit(); + } UpdateComponents(mask); @@ -3935,6 +3939,13 @@ void HistoryItem::createComponents(CreateConfig &&config) { flagSensitiveContent(); } + if (const auto suggest = Get()) { + suggest->stars = config.suggest.stars; + suggest->date = config.suggest.date; + suggest->accepted = config.suggest.accepted; + suggest->rejected = config.suggest.rejected; + } + if (out() && isSending()) { if (const auto channel = _history->peer->asMegagroup()) { _boostsApplied = channel->mgInfo->boostsApplied; @@ -4137,6 +4148,9 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { if (fields.flags & MessageFlag::HasViews) { config.viewsCount = 1; } + if (fields.suggest.exists) { + config.suggest = fields.suggest; + } createComponents(std::move(config)); } @@ -4261,6 +4275,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { config.postAuthor = qs(data.vpost_author().value_or_empty()); config.restrictions = Data::UnavailableReason::Extract( data.vrestriction_reason()); + config.suggest = HistoryMessageSuggestInfo(data.vsuggested_post()); createComponents(std::move(config)); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index d869944333..64e6880da6 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -82,6 +82,7 @@ struct HistoryItemCommonFields { uint64 groupedId = 0; EffectId effectId = 0; HistoryMessageMarkupData markup; + HistoryMessageSuggestInfo suggest; bool ignoreForwardFrom = false; bool ignoreForwardCaptions = false; }; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 7050237255..7e6c627d27 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -614,6 +614,14 @@ struct HistoryMessageFactcheck bool requested = false; }; +struct HistoryMessageSuggestedPost +: RuntimeComponent { + int stars = 0; + TimeId date = 0; + bool accepted = false; + bool rejected = false; +}; + struct HistoryMessageRestrictions : RuntimeComponent { std::vector reasons; diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index 319a77238d..4a37ca15eb 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -312,7 +312,7 @@ HistoryMessageRepliesData::HistoryMessageRepliesData( if (!data) { return; } - const auto &fields = data->c_messageReplies(); + const auto &fields = data->data(); if (const auto list = fields.vrecent_repliers()) { recentRepliers.reserve(list->v.size()); for (const auto &id : list->v) { @@ -326,3 +326,31 @@ HistoryMessageRepliesData::HistoryMessageRepliesData( isNull = false; pts = fields.vreplies_pts().v; } + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + const MTPSuggestedPost *data) { + if (!data) { + return; + } + const auto &fields = data->data(); + stars = fields.vstars_amount().v; + date = fields.vschedule_date().value_or_empty(); + accepted = fields.is_accepted(); + rejected = fields.is_rejected(); + exists = true; +} + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + const Api::SendOptions &options) +: HistoryMessageSuggestInfo(options.suggest) { +} + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + SuggestPostOptions options) { + if (!options.exists) { + return; + } + stars = options.stars; + date = options.date; + exists = true; +} diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 77895c3aa1..8a735903fb 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flags.h" #include "data/data_chat_participant_status.h" +namespace Api { +struct SendOptions; +} // namespace Api + namespace Data { class Session; } // namespace Data @@ -136,3 +140,16 @@ struct HistoryMessageRepliesData { bool isNull = true; int pts = 0; }; + +struct HistoryMessageSuggestInfo { + HistoryMessageSuggestInfo() = default; + explicit HistoryMessageSuggestInfo(const MTPSuggestedPost *data); + explicit HistoryMessageSuggestInfo(const Api::SendOptions &options); + explicit HistoryMessageSuggestInfo(SuggestPostOptions options); + + int stars = 0; + TimeId date = 0; + bool accepted = false; + bool rejected = false; + bool exists = false; +}; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0eb51fbb53..4b968b884a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -118,6 +118,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_sticker_toast.h" #include "history/view/history_view_subsection_tabs.h" +#include "history/view/history_view_suggest_options.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" @@ -1039,6 +1040,7 @@ Dialogs::EntryState HistoryWidget::computeDialogsEntryState() const { .key = _history, .section = Dialogs::EntryState::Section::History, .currentReplyTo = replyTo(), + .currentSuggest = suggestOptions(), }; } @@ -1900,13 +1902,16 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), _preview->draft(), _saveEditMsgRequestId)); } else { - if (_replyTo || !_field->empty()) { + const auto suggest = suggestOptions(); + if (_replyTo || suggest.exists || !_field->empty()) { _history->setLocalDraft(std::make_unique( _field, _replyTo, + suggest, _preview->draft())); } else { _history->clearLocalDraft(topicRootId, monoforumPeerId); @@ -2270,6 +2275,8 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { if (_processingReplyTo) { _processingReplyItem = session().data().message( _processingReplyTo.messageId); + } else if (draft && draft->suggest) { + applySuggestOptions(draft->suggest); } processReply(); } @@ -2480,6 +2487,7 @@ void HistoryWidget::showHistory( setHistory(nullptr); _list = nullptr; _peer = nullptr; + _suggestOptions = nullptr; _sendPayment.clear(); _topicsRequested.clear(); _canSendMessages = false; @@ -2606,6 +2614,7 @@ void HistoryWidget::showHistory( } else if (_peer->isRepliesChat() || _peer->isVerifyCodes()) { updateNotifyControls(); } + refreshSuggestPostToggle(); refreshScheduledToggle(); refreshSendGiftToggle(); refreshSendAsToggle(); @@ -2917,6 +2926,7 @@ void HistoryWidget::registerDraftSource() { (editMsgId ? FullReplyTo{ FullMsgId(peerId, editMsgId) } : _replyTo), + (editMsgId ? SuggestPostOptions() : suggestOptions()), _field->getTextWithTags(), _preview->draft(), }; @@ -3154,6 +3164,41 @@ void HistoryWidget::refreshSendGiftToggle() { } } +void HistoryWidget::applySuggestOptions(SuggestPostOptions suggest) { + Expects(suggest.exists); + + using namespace HistoryView; + _suggestOptions = std::make_unique( + controller(), + _peer, + suggest); + _suggestOptions->repaints() | rpl::start_with_next([=] { + updateField(); + }, _suggestOptions->lifetime()); +} + +void HistoryWidget::refreshSuggestPostToggle() { + const auto has = _peer + && _peer->isMonoforum() + && !_peer->amMonoforumAdmin(); + if (!_toggleSuggestPost && has) { + _toggleSuggestPost.create(this, st::historySuggestPostToggle); + _toggleSuggestPost->setVisible(!_suggestOptions); + _toggleSuggestPost->addClickHandler([=] { + applySuggestOptions({ .exists = 1 }); + cancelReply(); + _processingReplyTo = FullReplyTo(); + _processingReplyItem = nullptr; + updateControlsVisibility(); + updateControlsGeometry(); + }); + orderWidgets(); + } else if (_toggleSuggestPost && !has) { + _toggleSuggestPost.destroy(); + cancelSuggestPost(); + } +} + void HistoryWidget::setupSendAsToggle() { session().sendAsPeers().updated( ) | rpl::filter([=](not_null peer) { @@ -3294,6 +3339,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->hide(); + } if (_giftToUser) { _giftToUser->hide(); } @@ -3408,6 +3456,14 @@ void HistoryWidget::updateControlsVisibility() { rightButtonsChanged = true; } } + if (_toggleSuggestPost) { + const auto was = _toggleSuggestPost->isVisible(); + const auto now = !_suggestOptions; + if (was != now) { + _toggleSuggestPost->setVisible(now); + rightButtonsChanged = true; + } + } if (_giftToUser) { const auto was = _giftToUser->isVisible(); const auto now = (!_editMsgId) && (!hideExtraButtons); @@ -3437,7 +3493,8 @@ void HistoryWidget::updateControlsVisibility() { || _replyTo || readyToForward() || _previewDrawPreview - || _kbReplyTo) { + || _kbReplyTo + || _suggestOptions) { if (_fieldBarCancel->isHidden()) { _fieldBarCancel->show(); updateControlsGeometry(); @@ -3466,6 +3523,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->hide(); + } if (_giftToUser) { _giftToUser->hide(); } @@ -4503,6 +4563,7 @@ Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); + result.options.suggest = suggestOptions(); result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -5040,7 +5101,11 @@ void HistoryWidget::updateOverStates(QPoint pos) { st::historyReplyHeight); const auto hasWebPage = !!_previewDrawPreview; const auto inDetails = detailsRect.contains(pos) - && (_editMsgId || replyTo() || isReadyToForward || hasWebPage); + && (_editMsgId + || replyTo() + || isReadyToForward + || hasWebPage + || _suggestOptions); const auto inPhotoEdit = inDetails && _photoEditMedia && QRect( @@ -5585,7 +5650,8 @@ void HistoryWidget::toggleKeyboard(bool manual) { if (!readyToForward() && !_previewDrawPreview && !_editMsgId - && !_replyTo) { + && !_replyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -5806,7 +5872,7 @@ void HistoryWidget::moveFieldControls() { } // (_botMenu.button) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel -// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send +// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_toggleSuggestPost) (_kbHide|_tabbedSelectorToggle) _send // (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages) auto buttonsBottom = bottom - _attachToggle->height(); @@ -5848,6 +5914,10 @@ void HistoryWidget::moveFieldControls() { if (kbShowShown || _cmdStartShown || _silent) { right += _botCommandStart->width(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->moveToRight(right, buttonsBottom); + right += _toggleSuggestPost->width(); + } if (_giftToUser) { _giftToUser->moveToRight(right, buttonsBottom); right += _giftToUser->width(); @@ -5912,6 +5982,9 @@ void HistoryWidget::updateFieldSize() { if (_silent && !_silent->isHidden()) { fieldWidth -= _silent->width(); } + if (_toggleSuggestPost && !_toggleSuggestPost->isHidden()) { + fieldWidth -= _toggleSuggestPost->width(); + } if (_giftToUser && !_giftToUser->isHidden()) { fieldWidth -= _giftToUser->width(); } @@ -6590,6 +6663,12 @@ FullReplyTo HistoryWidget::replyTo() const { : FullReplyTo(); } +SuggestPostOptions HistoryWidget::suggestOptions() const { + return (_history && _history->suggestDraftAllowed() && _suggestOptions) + ? _suggestOptions->values() + : SuggestPostOptions(); +} + bool HistoryWidget::hasSavedScroll() const { Expects(_history != nullptr); @@ -6797,7 +6876,8 @@ void HistoryWidget::updateHistoryGeometry( if (_editMsgId || replyTo() || readyToForward() - || _previewDrawPreview) { + || _previewDrawPreview + || _suggestOptions) { newScrollHeight -= st::historyReplyHeight; } if (_kbShown) { @@ -7143,7 +7223,8 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _kbReplyTo = nullptr; if (!readyToForward() && !_previewDrawPreview - && !_replyTo) { + && !_replyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7162,7 +7243,8 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { if (!readyToForward() && !_previewDrawPreview && !_replyTo - && !_editMsgId) { + && !_editMsgId + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7316,6 +7398,8 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { _kbReplyTo->history()->peer->id, Window::SectionShow::Way::Forward, _kbReplyTo->id); + } else if (_suggestOptions) { + _suggestOptions->edit(); } } @@ -7324,6 +7408,7 @@ void HistoryWidget::editDraftOptions() { const auto history = _history; const auto reply = _replyTo; + const auto suggest = suggestOptions(); const auto webpage = _preview->draft(); const auto forward = _forwardPanel->draft(); @@ -7348,7 +7433,7 @@ void HistoryWidget::editDraftOptions() { EditDraftOptions({ .show = controller()->uiShow(), .history = history, - .draft = Data::Draft(_field, reply, _preview->draft()), + .draft = Data::Draft(_field, reply, suggest, _preview->draft()), .usedLink = _preview->link(), .forward = _forwardPanel->draft(), .links = _preview->links(), @@ -8465,7 +8550,9 @@ void HistoryWidget::processReply() { if (!_peer || !_processingReplyTo) { return processCancel(); - } else if (!_processingReplyItem) { + } + cancelSuggestPost(); + if (!_processingReplyItem) { session().api().requestMessageData( session().data().peer(_processingReplyTo.messageId.peer), _processingReplyTo.messageId.msg, @@ -8524,16 +8611,19 @@ void HistoryWidget::setReplyFieldsFromProcessing() { if (_editMsgId) { if (const auto localDraft = _history->localDraft({}, {})) { localDraft->reply = id; + localDraft->suggest = SuggestPostOptions(); } else { _history->setLocalDraft(std::make_unique( TextWithTags(), id, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); } } else { _replyEditMsg = item; _replyTo = id; + cancelSuggestPost(); updateReplyEditText(_replyEditMsg); updateCanSendMessage(); updateBotKeyboard(); @@ -8573,10 +8663,12 @@ void HistoryWidget::editMessage( _send->clearState(); } if (!_editMsgId) { - if (_replyTo || !_field->empty()) { + const auto suggest = suggestOptions(); + if (_replyTo || suggest.exists || !_field->empty()) { _history->setLocalDraft(std::make_unique( _field, _replyTo, + suggest, _preview->draft())); } else { _history->clearLocalDraft(MsgId(), PeerId()); @@ -8593,6 +8685,7 @@ void HistoryWidget::editMessage( _history->setLocalEditDraft(std::make_unique( editData, FullReplyTo{ item->fullId() }, + SuggestPostOptions(), cursor, previewDraft)); applyDraft(); @@ -8679,7 +8772,8 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { mouseMoveEvent(0); if (!readyToForward() && !_previewDrawPreview - && !_kbReplyTo) { + && !_kbReplyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -8754,7 +8848,8 @@ void HistoryWidget::cancelEdit() { mouseMoveEvent(nullptr); if (!readyToForward() && !_previewDrawPreview - && !replyTo()) { + && !replyTo() + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -8784,9 +8879,21 @@ void HistoryWidget::cancelFieldAreaState() { _history->setForwardDraft(MsgId(), PeerId(), {}); } else if (_kbReplyTo) { toggleKeyboard(); + } else if (_suggestOptions) { + cancelSuggestPost(); } } +bool HistoryWidget::cancelSuggestPost() { + if (!_suggestOptions) { + return false; + } + _suggestOptions = nullptr; + updateControlsVisibility(); + updateControlsGeometry(); + return true; +} + void HistoryWidget::fullInfoUpdated() { auto refresh = false; if (_list) { @@ -8891,6 +8998,7 @@ bool HistoryWidget::updateCanSendMessage() { if (!_canSendMessages) { cancelReply(); } + refreshSuggestPostToggle(); refreshScheduledToggle(); refreshSendGiftToggle(); refreshSilentToggle(); @@ -9190,13 +9298,12 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { auto backh = fieldHeight() + 2 * st::historySendPadding; auto hasForward = readyToForward(); auto drawMsgText = (_editMsgId || _replyTo) ? _replyEditMsg : _kbReplyTo; - if (_editMsgId || _replyTo || (!hasForward && _kbReplyTo)) { - backy -= st::historyReplyHeight; - backh += st::historyReplyHeight; - } else if (hasForward) { - backy -= st::historyReplyHeight; - backh += st::historyReplyHeight; - } else if (_previewDrawPreview) { + if (_editMsgId + || _replyTo + || hasForward + || _kbReplyTo + || _previewDrawPreview + || _suggestOptions) { backy -= st::historyReplyHeight; backh += st::historyReplyHeight; } @@ -9361,6 +9468,8 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { - _fieldBarCancel->width() - st::msgReplyPadding.right(); _forwardPanel->paint(p, x, backy, available, width()); + } else if (_suggestOptions) { + _suggestOptions->paintBar(p, 0, backy, width()); } } @@ -9462,7 +9571,8 @@ void HistoryWidget::paintEvent(QPaintEvent *e) { if (restrictionHidden || replyTo() || readyToForward() - || _kbShown) { + || _kbShown + || _suggestOptions) { if (!isSearching()) { drawField(p, clip); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 42f2cf7f8c..f18577abe2 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -109,6 +109,7 @@ class TranslateBar; class ComposeSearch; class SubsectionTabs; struct SelectedQuote; +class SuggestOptions; } // namespace HistoryView namespace HistoryView::Controls { @@ -214,9 +215,11 @@ public: not_null peer); [[nodiscard]] FullReplyTo replyTo() const; + [[nodiscard]] SuggestPostOptions suggestOptions() const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; bool cancelReply(bool lastKeyboardUsed = false); + bool cancelSuggestPost(); void cancelEdit(); void updateForwarding(); @@ -676,6 +679,8 @@ private: void setupScheduledToggle(); void refreshScheduledToggle(); void refreshSendGiftToggle(); + void refreshSuggestPostToggle(); + void applySuggestOptions(SuggestPostOptions suggest); void setupSendAsToggle(); void refreshSendAsToggle(); void refreshAttachBotsMenu(); @@ -709,6 +714,8 @@ private: std::unique_ptr _replySpoiler; mutable base::Timer _updateEditTimeLeftDisplay; + std::unique_ptr _suggestOptions; + object_ptr _fieldBarCancel; std::unique_ptr _topBars; @@ -821,6 +828,7 @@ private: object_ptr _botKeyboardShow; object_ptr _botKeyboardHide; object_ptr _botCommandStart; + object_ptr _toggleSuggestPost = { nullptr }; object_ptr _giftToUser = { nullptr }; object_ptr _silent = { nullptr }; object_ptr _scheduled = { nullptr }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 52aecc6994..294ffd9ca5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1006,6 +1006,7 @@ void ComposeControls::setCurrentDialogsEntryState( unregisterDraftSources(); state.currentReplyTo.topicRootId = _topicRootId; state.currentReplyTo.monoforumPeerId = _monoforumPeerId; + state.currentSuggest = SuggestPostOptions(); _currentDialogsEntryState = state; updateForwarding(); registerDraftSource(); @@ -1299,7 +1300,11 @@ void ComposeControls::saveFieldToHistoryLocalDraft() { const auto key = draftKeyCurrent(); _history->setDraft( key, - std::make_unique(_field, id, _preview->draft())); + std::make_unique( + _field, + id, + SuggestPostOptions(), + _preview->draft())); } else { _history->clearDraft(draftKeyCurrent()); } @@ -1414,6 +1419,7 @@ void ComposeControls::init() { const auto topicRootId = _topicRootId; const auto monoforumPeerId = _monoforumPeerId; const auto reply = _header->replyingToMessage(); + const auto suggest = SuggestPostOptions(); const auto webpage = _preview->draft(); const auto done = [=]( @@ -1441,7 +1447,7 @@ void ComposeControls::init() { EditDraftOptions({ .show = _show, .history = history, - .draft = Data::Draft(_field, reply, _preview->draft()), + .draft = Data::Draft(_field, reply, suggest, _preview->draft()), .usedLink = _preview->link(), .forward = _header->forwardDraft(), .links = _preview->links(), @@ -1891,6 +1897,7 @@ void ComposeControls::registerDraftSource() { const auto draft = [=] { return Storage::MessageDraft{ _header->getDraftReply(), + SuggestPostOptions(), _field->getTextWithTags(), _preview->draft(), }; @@ -2980,6 +2987,7 @@ void ComposeControls::editMessage(not_null item) { .topicRootId = key.topicRootId(), .monoforumPeerId = key.monoforumPeerId(), }, + SuggestPostOptions(), cursor, Data::WebPageDraft::FromItem(item))); applyDraft(); @@ -3076,6 +3084,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { std::make_unique( TextWithTags(), id, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 1277052948..f398211636 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -1386,6 +1386,7 @@ void ShowReplyToChatBox( history->setLocalDraft(std::make_unique( textWithTags, reply, + SuggestPostOptions(), cursor, Data::WebPageDraft())); history->clearLocalEditDraft(topicRootId, monoforumPeerId); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index ff314cdf9e..de53aeb673 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -353,6 +353,7 @@ void ClearDraftReplyTo( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }; + draft.suggest = SuggestPostOptions(); if (Data::DraftIsNull(&draft)) { history->clearLocalDraft(topicRootId, monoforumPeerId); } else { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 03f871bbaf..a4d8b228ce 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1830,6 +1830,7 @@ void ChatWidget::refreshTopBarActiveChat() { ? EntryState::Section::SavedSublist : EntryState::Section::Replies, .currentReplyTo = replyTo(), + .currentSuggest = SuggestPostOptions(), }; _topBar->setActiveChat(state, _sendAction.get()); _composeControls->setCurrentDialogsEntryState(state); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index eab953b1e2..fa9ed5edd2 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_message.h" +#include "base/unixtime.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "core/ui_integration.h" #include "history/view/history_view_cursor_state.h" @@ -454,6 +455,20 @@ Message::~Message() { void Message::initPaidInformation() { const auto item = data(); if (!item->history()->peer->isUser()) { + + + if (const auto suggest = item->Get()) { + if (!suggest->stars && !suggest->date) { + setServicePreMessage({ { u"suggestion to publish for free anytime"_q } }); + } else if (!suggest->date) { + setServicePreMessage({ { u"suggestion to publish for %1 stars anytime"_q.arg(suggest->stars) }}); + } else if (!suggest->stars) { + setServicePreMessage({ { u"suggestion to publish for free %1"_q.arg(langDateTime(base::unixtime::parse(suggest->date))) }}); + } else { + setServicePreMessage({ { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } }); + } + } + return; } const auto media = this->media(); diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp new file mode 100644 index 0000000000..da6566ce0f --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -0,0 +1,237 @@ +/* +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 "history/view/history_view_suggest_options.h" + +#include "base/unixtime.h" +#include "data/data_channel.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/boxes/choose_date_time.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/widgets/buttons.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_settings.h" + +namespace HistoryView { +namespace { + +struct EditOptionsArgs { + int starsLimit = 0; + QString channelName; + SuggestPostOptions values; + Fn save; +}; + +void EditOptionsBox( + not_null box, + EditOptionsArgs &&args) { + struct State { + rpl::variable date; + }; + const auto state = box->lifetime().make_state(); + state->date = args.values.date; + + box->setTitle(tr::lng_suggest_options_title()); + + const auto container = box->verticalLayout(); + + Ui::AddSkip(container); + Ui::AddSubsectionTitle(container, tr::lng_suggest_options_price()); + + const auto wrap = box->addRow(object_ptr( + box, + st::editTagField.heightMin)); + auto owned = object_ptr( + wrap, + st::editTagField, + tr::lng_paid_cost_placeholder(), + args.values.stars ? QString::number(args.values.stars) : QString(), + args.starsLimit); + const auto field = owned.data(); + wrap->widthValue() | rpl::start_with_next([=](int width) { + field->move(0, 0); + field->resize(width, field->height()); + wrap->resize(width, field->height()); + }, wrap->lifetime()); + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); + field->selectAll(); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + Ui::AddSkip(container); + Ui::AddSkip(container); + Ui::AddDividerText( + container, + tr::lng_suggest_options_price_about( + lt_channel, + rpl::single(args.channelName))); + Ui::AddSkip(container); + + const auto time = Settings::AddButtonWithLabel( + container, + tr::lng_suggest_options_date(), + state->date.value() | rpl::map([](TimeId date) { + return date + ? langDateTime(base::unixtime::parse(date)) + : tr::lng_suggest_options_date_any(tr::now); + }), + st::settingsButtonNoIcon); + + time->setClickedCallback([=] { + box->uiShow()->show(Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ + .title = tr::lng_suggest_options_date(), + .submit = tr::lng_settings_save(), + .done = [=](TimeId result) { state->date = result; }, + .min = [] { return base::unixtime::now() + 1; }, + .time = (state->date.current() + ? state->date.current() + : (base::unixtime::now() + 86400)), + })); + }); + + Ui::AddSkip(container); + Ui::AddDividerText(container, tr::lng_suggest_options_date_about()); + AssertIsDebug()//tr::lng_suggest_options_offer + const auto save = [=] { + const auto now = uint32(field->getLastText().toULongLong()); + if (now > args.starsLimit) { + field->showError(); + return; + } + const auto weak = Ui::MakeWeak(box); + args.save({ .stars = now, .date = state->date.current()}); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + + QObject::connect(field, &Ui::NumberInput::submitted, box, save); + + box->addButton(tr::lng_settings_save(), save); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace + +SuggestOptions::SuggestOptions( + not_null controller, + not_null peer, + SuggestPostOptions values) +: _controller(controller) +, _peer(peer) +, _values(values) { + updateTexts(); +} + +SuggestOptions::~SuggestOptions() = default; + +void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) { + st::historyDirectMessage.icon.paint( + p, + QPoint(x, y) + st::historySuggestIconPosition, + outerWidth); + + x += st::historyReplySkip; + auto available = outerWidth + - x + - st::historyReplyCancel.width + - st::msgReplyPadding.right(); + p.setPen(st::windowActiveTextFg); + _title.draw(p, { + .position = QPoint(x, y + st::msgReplyPadding.top()), + .availableWidth = available, + }); + p.setPen(st::windowSubTextFg); + _text.draw(p, { + .position = QPoint( + x, + y + st::msgReplyPadding.top() + st::msgServiceNameFont->height), + .availableWidth = available, + }); +} + +void SuggestOptions::edit() { + const auto apply = [=](SuggestPostOptions values) { + _values = values; + updateTexts(); + _repaints.fire({}); + }; + const auto broadcast = _peer->monoforumBroadcast(); + const auto &appConfig = _peer->session().appConfig(); + _controller->show(Box(EditOptionsBox, EditOptionsArgs{ + .starsLimit = appConfig.suggestedPostStarsMax(), + .channelName = (broadcast ? broadcast : _peer.get())->shortName(), + .values = _values, + .save = apply, + })); +} + +void SuggestOptions::updateTexts() { + _title.setText( + st::semiboldTextStyle, + tr::lng_suggest_bar_title(tr::now)); + _text.setMarkedText(st::defaultTextStyle, composeText()); +} + +TextWithEntities SuggestOptions::composeText() const { + if (!_values.stars && !_values.date) { + return tr::lng_suggest_bar_text(tr::now, Ui::Text::WithEntities); + } else if (!_values.date) { + return tr::lng_suggest_bar_priced( + tr::now, + lt_amount, + TextWithEntities{ QString::number(_values.stars) + " stars" }, + Ui::Text::WithEntities); + } else if (!_values.stars) { + return tr::lng_suggest_bar_dated( + tr::now, + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(_values.date)), + }, + Ui::Text::WithEntities); + } + return tr::lng_suggest_bar_priced_dated( + tr::now, + lt_amount, + TextWithEntities{ QString::number(_values.stars) + " stars," }, + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(_values.date)), + }, + Ui::Text::WithEntities); +} + +SuggestPostOptions SuggestOptions::values() const { + auto result = _values; + result.exists = 1; + return result; +} + +rpl::producer<> SuggestOptions::repaints() const { + return _repaints.events(); +} + +rpl::lifetime &SuggestOptions::lifetime() { + return _lifetime; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/history_view_suggest_options.h new file mode 100644 index 0000000000..26d11c1c70 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.h @@ -0,0 +1,53 @@ +/* +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 "api/api_common.h" + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView { + +class SuggestOptions final { +public: + SuggestOptions( + not_null controller, + not_null peer, + SuggestPostOptions values); + ~SuggestOptions(); + + void paintBar(QPainter &p, int x, int y, int outerWidth); + void edit(); + + [[nodiscard]] SuggestPostOptions values() const; + + [[nodiscard]] rpl::producer<> repaints() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void updateTexts(); + + [[nodiscard]] TextWithEntities composeText() const; + + const not_null _controller; + const not_null _peer; + + Ui::Text::String _title; + Ui::Text::String _text; + + SuggestPostOptions _values; + rpl::event_stream<> _repaints; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index d6ca306167..a1d7e29ac9 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -359,6 +359,7 @@ WebViewContext ResolveContext( if (const auto thread = state.key.thread()) { context.action = Api::SendAction(thread); context.action->replyTo = state.currentReplyTo; + context.action->options.suggest = state.currentSuggest; } else { context.action = Api::SendAction(bot->owner().history(bot)); } @@ -373,6 +374,7 @@ WebViewContext ResolveContext( .key = (topic ? Key{ topic } : Key{ history }), .section = (topic ? Section::Replies : Section::History), .currentReplyTo = context.action->replyTo, + .currentSuggest = context.action->options.suggest, }; } return context; @@ -2615,11 +2617,11 @@ std::unique_ptr MakeAttachBotsMenu( ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); - const auto replyTo = action.replyTo; Window::PeerMenuCreatePoll( controller, peer, - replyTo, + action.replyTo, + action.options.suggest, flag, flag, source, @@ -2637,11 +2639,11 @@ std::unique_ptr MakeAttachBotsMenu( || action.history->peer->starsPerMessageChecked()) ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; - const auto replyTo = action.replyTo; Window::PeerMenuCreateTodoList( controller, peer, - replyTo, + action.replyTo, + action.options.suggest, source, { sendMenuType }); }, &st::menuIconCreateTodoList); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 8875884f24..1adcc5a856 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -162,6 +162,10 @@ int AppConfig::todoListItemTextLimit() const { return get(u"todo_item_length_max"_q, 64); } +int AppConfig::suggestedPostStarsMax() const { + return get(u"stars_suggested_post_amount_max"_q, 100'000); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index bf0053d79b..886e11280c 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -88,6 +88,8 @@ public: [[nodiscard]] int todoListTitleLimit() const; [[nodiscard]] int todoListItemTextLimit() const; + [[nodiscard]] int suggestedPostStarsMax() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index a26693f680..42f3494180 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -606,6 +606,7 @@ bool MainWidget::shareUrl( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), cursor, Data::WebPageDraft())); history->clearLocalEditDraft(topicRootId, monoforumPeerId); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 4773e7e068..7419a6715e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -174,7 +174,7 @@ namespace Media::Stories { Data::ShortcutIdToMTP(session, options.shortcutId), MTP_long(options.effectId), MTP_long(starsPaid), - SuggestToMTP(options.suggest) + Api::SuggestToMTP(options.suggest) ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index fa1fd40fd0..135f7df7e3 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -632,6 +632,7 @@ void ShortcutMessages::setupComposeControls() { .key = Dialogs::Key{ _history }, .section = Dialogs::EntryState::Section::ShortcutMessages, .currentReplyTo = replyTo(), + .currentSuggest = SuggestPostOptions(), }; _composeControls->setCurrentDialogsEntryState(state); diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 5d88879bfa..cbcb74e24e 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -67,6 +67,7 @@ constexpr auto kMultiDraftCursorsTagOld = quint64(0xFFFF'FFFF'FFFF'FF02ULL); constexpr auto kMultiDraftTag = quint64(0xFFFF'FFFF'FFFF'FF03ULL); constexpr auto kMultiDraftCursorsTag = quint64(0xFFFF'FFFF'FFFF'FF04ULL); constexpr auto kRichDraftsTag = quint64(0xFFFF'FFFF'FFFF'FF05ULL); +constexpr auto kDraftsTag2 = quint64(0xFFFF'FFFF'FFFF'FF06ULL); enum { // Local Storage Keys lskUserMap = 0x00, @@ -1187,6 +1188,7 @@ void EnumerateDrafts( callback( key, draft->reply, + draft->suggest, draft->textWithTags, draft->webpage, draft->cursor); @@ -1200,6 +1202,7 @@ void EnumerateDrafts( callback( key, draft.reply, + draft.suggest, draft.textWithTags, draft.webpage, cursor); @@ -1265,6 +1268,7 @@ void Account::writeDrafts(not_null history) { const auto sizeCallback = [&]( auto&&, // key const FullReplyTo &reply, + SuggestPostOptions suggest, const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor @@ -1272,6 +1276,7 @@ void Account::writeDrafts(not_null history) { + Serialize::stringSize(text.text) + TextUtilities::SerializeTagsSize(text.tags) + sizeof(qint64) + sizeof(qint64) // messageId + + sizeof(quint64) // suggest + Serialize::stringSize(webpage.url) + sizeof(qint32) // webpage.forceLargeMedia + sizeof(qint32) // webpage.forceSmallMedia @@ -1287,13 +1292,14 @@ void Account::writeDrafts(not_null history) { EncryptedDescriptor data(size); data.stream - << quint64(kRichDraftsTag) + << quint64(kDraftsTag2) << SerializePeerId(peerId) << quint32(count); const auto writeCallback = [&]( const Data::DraftKey &key, const FullReplyTo &reply, + SuggestPostOptions suggest, const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor @@ -1303,6 +1309,9 @@ void Account::writeDrafts(not_null history) { << TextUtilities::SerializeTags(text.tags) << qint64(reply.messageId.peer.value) << qint64(reply.messageId.msg.bare) + << quint64(quint64(quint32(suggest.date)) + | (quint64(suggest.stars) << 32) + | (quint64(suggest.exists) << 63)) << webpage.url << qint32(webpage.forceLargeMedia ? 1 : 0) << qint32(webpage.forceSmallMedia ? 1 : 0) @@ -1359,6 +1368,7 @@ void Account::writeDraftCursors(not_null history) { const auto writeCallback = [&]( const Data::DraftKey &key, auto&&, // reply + auto&&, // suggest auto&&, // text auto&&, // webpage const MessageCursor &cursor) { // cursor @@ -1519,12 +1529,14 @@ void Account::readDraftsWithCursors(not_null history) { } auto map = Data::HistoryDrafts(); const auto keysOld = (tag == kMultiDraftTagOld); - const auto rich = (tag == kRichDraftsTag); + const auto withSuggest = (tag == kDraftsTag2); + const auto rich = (tag == kRichDraftsTag) || withSuggest; for (auto i = 0; i != count; ++i) { TextWithTags text; QByteArray textTagsSerialized; qint64 keyValue = 0; qint64 messageIdPeer = 0, messageIdMsg = 0; + quint64 suggestSerialized = 0; qint32 keyValueOld = 0; QString webpageUrl; qint32 webpageForceLargeMedia = 0; @@ -1558,7 +1570,11 @@ void Account::readDraftsWithCursors(not_null history) { >> text.text >> textTagsSerialized >> messageIdPeer - >> messageIdMsg + >> messageIdMsg; + if (withSuggest) { + draft.stream >> suggestSerialized; + } + draft.stream >> webpageUrl >> webpageForceLargeMedia >> webpageForceSmallMedia @@ -1581,6 +1597,13 @@ void Account::readDraftsWithCursors(not_null history) { MsgId(messageIdMsg)), .topicRootId = key.topicRootId(), }, + SuggestPostOptions{ + .exists = uint32(suggestSerialized >> 63), + .stars = uint32( + (suggestSerialized & ~(1ULL << 63)) >> 32), + .date = TimeId( + uint32(suggestSerialized & 0xFFFF'FFFFULL)), + }, MessageCursor(), Data::WebPageDraft{ .url = webpageUrl, @@ -1654,6 +1677,7 @@ void Account::readDraftsWithCursorsLegacy( std::make_unique( msgData, FullReplyTo{ FullMsgId(peerId, MsgId(msgReplyTo)) }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft{ .removed = (msgPreviewCancelled == 1), @@ -1665,6 +1689,7 @@ void Account::readDraftsWithCursorsLegacy( std::make_unique( editData, FullReplyTo{ FullMsgId(peerId, editMsgId) }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft{ .removed = (editPreviewCancelled == 1), diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index c096039941..4003e10de5 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -53,6 +53,7 @@ enum class StartResult : uchar; struct MessageDraft { FullReplyTo reply; + SuggestPostOptions suggest; TextWithTags textWithTags; Data::WebPageDraft webpage; }; diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 64e9ff8152..1119d19e2c 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -166,6 +166,7 @@ Data::Draft OccupiedDraft(const QString &normalizedName) { + ";n:" + normalizedName }, FullReplyTo(), + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft() }; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 58dc24f52a..dddc7a54c7 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1177,6 +1177,7 @@ void Manager::notificationActivated( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), MessageCursor{ length, length, diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 3913b1b3c9..6c4b35abf4 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1224,11 +1224,13 @@ void Filler::addCreatePoll() { : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); const auto replyTo = _request.currentReplyTo; + const auto suggest = _request.currentSuggest; auto callback = [=] { PeerMenuCreatePoll( controller, peer, replyTo, + suggest, flag, flag, source, @@ -1263,11 +1265,13 @@ void Filler::addCreateTodoList() { ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto replyTo = _request.currentReplyTo; + const auto suggest = _request.currentSuggest; auto callback = [=] { PeerMenuCreateTodoList( controller, peer, replyTo, + suggest, source, { sendMenuType }); }; @@ -1852,6 +1856,7 @@ void PeerMenuCreatePoll( not_null controller, not_null peer, FullReplyTo replyTo, + SuggestPostOptions suggest, PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, @@ -1902,6 +1907,7 @@ void PeerMenuCreatePoll( peer->owner().history(peer), result.options); action.replyTo = replyTo; + action.options.suggest = suggest; const auto local = action.history->localDraft( replyTo.topicRootId, replyTo.monoforumPeerId); @@ -1962,6 +1968,7 @@ void PeerMenuCreateTodoList( not_null controller, not_null peer, FullReplyTo replyTo, + SuggestPostOptions suggest, Api::SendType sendType, SendMenu::Details sendMenuDetails) { if (!peer->session().premium()) { @@ -2008,6 +2015,7 @@ void PeerMenuCreateTodoList( peer->owner().history(peer), result.options); action.replyTo = replyTo; + action.options.suggest = suggest; const auto local = action.history->localDraft( replyTo.topicRootId, replyTo.monoforumPeerId); diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 1b33297498..f01801423f 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -107,6 +107,7 @@ void PeerMenuCreatePoll( not_null controller, not_null peer, FullReplyTo replyTo = FullReplyTo(), + SuggestPostOptions suggest = SuggestPostOptions(), PollData::Flags chosen = PollData::Flags(), PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, @@ -121,6 +122,7 @@ void PeerMenuCreateTodoList( not_null controller, not_null peer, FullReplyTo replyTo = FullReplyTo(), + SuggestPostOptions suggest = SuggestPostOptions(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); void PeerMenuEditTodoList( diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 736c4c2743..9ef02540bb 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2114,9 +2114,13 @@ bool SessionController::switchInlineQuery( && to.currentReplyTo.quote.empty()) { to.currentReplyTo.messageId.msg = MsgId(); } + if (!history->suggestDraftAllowed()) { + to.currentSuggest = SuggestPostOptions(); + } auto draft = std::make_unique( textWithTags, to.currentReplyTo, + to.currentSuggest, cursor, Data::WebPageDraft());