Show approve/decline service messages.

This commit is contained in:
John Preston 2025-06-19 20:53:15 +04:00
parent cb987c1baf
commit bf9492e083
17 changed files with 576 additions and 67 deletions

View file

@ -818,6 +818,8 @@ PRIVATE
history/view/media/history_view_sticker_player_abstract.h
history/view/media/history_view_story_mention.cpp
history/view/media/history_view_story_mention.h
history/view/media/history_view_suggest_decision.cpp
history/view/media/history_view_suggest_decision.h
history/view/media/history_view_theme_document.cpp
history/view/media/history_view_theme_document.h
history/view/media/history_view_todo_list.cpp

View file

@ -4421,12 +4421,45 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_suggest_bar_priced_dated" = "{amount} {date}";
"lng_suggest_bar_dated" = "Publish on {date}";
"lng_suggest_options_title" = "Suggest a Message";
"lng_suggest_options_change" = "Suggest Changes";
"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_price_about" = "Choose how many Stars to pay 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_suggest_options_update" = "Update Terms";
"lng_suggest_action_decline" = "Decline";
"lng_suggest_action_accept" = "Accept";
"lng_suggest_action_change" = "Suggest Changes";
"lng_suggest_action_your" = "You suggest to post this message.";
"lng_suggest_action_his" = "{from} suggests to post this message.";
"lng_suggest_action_price_label" = "Price";
"lng_suggest_action_time_label" = "Time";
"lng_suggest_action_agreement" = "Agreement reached!";
"lng_suggest_action_agree_date" = "The post will be automatically published on {channel} {date}.";
"lng_suggest_action_your_charged" = "You have been charged {amount}.";
"lng_suggest_action_his_charged" = "{from} have been charged {amount}.";
"lng_suggest_action_agree_receive" = "{channel} will receive the Stars once the post has been live for 24 hours.";
"lng_suggest_action_agree_removed" = "If {channel} removes the post before it has been live for 24 hours, the Stars will be refunded.";
"lng_suggest_action_your_not_enough" = "**Transaction failed** because you didn't have enough Stars.";
"lng_suggest_action_his_not_enough" = "**Transaction failed** because the user didn't have enough Stars.";
"lng_suggest_action_declined" = "{from} rejected the message.";
"lng_suggest_action_declined_reason" = "{from} rejected the message with the comment.";
"lng_suggest_change_price" = "{from} suggests a new price for the message.";
"lng_suggest_change_time" = "{from} suggests a new time for the message.";
"lng_suggest_change_price_time" = "{from} suggests a new price and time for the message.";
"lng_suggest_change_content" = "{from} suggests changes for the message.";
"lng_suggest_change_price_label" = "New Price";
"lng_suggest_change_time_label" = "New Time";
"lng_suggest_change_text_label" = "Check the suggested message below";
"lng_suggest_menu_edit_message" = "Edit Message";
"lng_suggest_menu_edit_price" = "Edit Price";
"lng_suggest_menu_edit_time" = "Edit Time";
"lng_suggest_decline_title" = "Decline";
"lng_suggest_decline_text" = "Do you want to decline publishing this post from {from}?";
"lng_suggest_decline_reason" = "Add a reason (optional)";
"lng_reply_in_another_title" = "Reply in...";
"lng_reply_in_another_chat" = "Reply in Another Chat";

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_cloud_password.h"
#include "api/api_send_progress.h"
#include "api/api_suggest_post.h"
#include "boxes/share_box.h"
#include "boxes/passcode_box.h"
#include "boxes/url_auth_box.h"
@ -521,6 +522,27 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
controller->showToast(tr::lng_text_copied(tr::now));
}
} break;
case ButtonType::SuggestAccept: {
Api::AcceptClickHandler(item)->onClick(ClickContext{
Qt::LeftButton,
QVariant::fromValue(context),
});
} break;
case ButtonType::SuggestDecline: {
Api::DeclineClickHandler(item)->onClick(ClickContext{
Qt::LeftButton,
QVariant::fromValue(context),
});
} break;
case ButtonType::SuggestChange: {
Api::SuggestChangesClickHandler(item)->onClick(ClickContext{
Qt::LeftButton,
QVariant::fromValue(context),
});
} break;
}
}

View file

@ -17,7 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/layers/generic_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace Api {
namespace {
@ -128,6 +134,49 @@ void RequestApprovalDate(
controller->uiShow()->show(std::move(dateBox));
}
void RequestDeclineComment(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto id = item->fullId();
controller->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto callback = std::make_shared<Fn<void()>>();
Ui::ConfirmBox(box, {
.text = tr::lng_suggest_decline_text(
lt_from,
rpl::single(Ui::Text::Bold(item->from()->shortName())),
Ui::Text::WithEntities),
.confirmed = [=](Fn<void()> close) { (*callback)(); close(); },
.confirmText = tr::lng_suggest_action_decline(),
.confirmStyle = &st::attentionBoxButton,
.title = tr::lng_suggest_decline_title(),
});
const auto reason = box->addRow(object_ptr<Ui::InputField>(
box,
st::factcheckField,
Ui::InputField::Mode::NoNewlines,
tr::lng_suggest_decline_reason()));
box->setFocusCallback([=] {
reason->setFocusFast();
});
*callback = [=, weak = Ui::MakeWeak(box)] {
const auto item = controller->session().data().message(id);
if (!item) {
return;
}
SendDecline(controller, item, reason->getLastText().trimmed());
if (const auto strong = weak.data()) {
strong->closeBox();
}
};
reason->submits(
) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) {
if (!(modifiers & Qt::ShiftModifier)) {
(*callback)();
}
}, box->lifetime());
}));
}
} // namespace
std::shared_ptr<ClickHandler> AcceptClickHandler(
@ -157,24 +206,14 @@ std::shared_ptr<ClickHandler> AcceptClickHandler(
std::shared_ptr<ClickHandler> DeclineClickHandler(
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
const auto id = item->fullId();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
if (!controller || &controller->session() != session) {
if (!controller) {
return;
}
const auto item = session->data().message(id);
if (!item) {
return;
}
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
if (!suggestion) {
return;
} else {
SendDecline(controller, item, "sorry, bro..");
}
RequestDeclineComment(controller, item);
});
}

View file

@ -828,6 +828,8 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() {
return done;
} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {
return append;
} else if (const auto decision = Get<HistoryServiceSuggestDecision>()) {
return decision;
}
return nullptr;
}
@ -1068,8 +1070,58 @@ bool HistoryItem::checkDiscussionLink(ChannelId id) const {
return false;
}
void HistoryItem::setReplyMarkup(HistoryMessageMarkupData &&markup) {
SuggestionActions HistoryItem::computeSuggestionActions() const {
return computeSuggestionActions(Get<HistoryMessageSuggestedPost>());
}
SuggestionActions HistoryItem::computeSuggestionActions(
const HistoryMessageSuggestedPost *suggest) const {
return suggest
? computeSuggestionActions(suggest->accepted, suggest->rejected)
: SuggestionActions::None;
}
SuggestionActions HistoryItem::computeSuggestionActions(
bool accepted,
bool rejected) const {
const auto channelIsAuthor = from()->isChannel();
const auto amMonoforumAdmin = history()->peer->amMonoforumAdmin();
const auto broadcast = history()->peer->monoforumBroadcast();
const auto canDecline = isRegular()
&& !(accepted || rejected)
&& (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin);
const auto canAccept = canDecline
&& (channelIsAuthor
? !amMonoforumAdmin
: (amMonoforumAdmin
&& broadcast
&& broadcast->canPostMessages()));
return canAccept
? SuggestionActions::AcceptAndDecline
: canDecline
? SuggestionActions::Decline
: SuggestionActions::None;
}
void HistoryItem::updateSuggestControls(
const HistoryMessageSuggestedPost *suggest) {
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
markup->updateSuggestControls(computeSuggestionActions(suggest));
}
}
void HistoryItem::setReplyMarkup(
HistoryMessageMarkupData &&markup,
bool ignoreSuggestButtons) {
const auto requestUpdate = [&] {
const auto actions = computeSuggestionActions();
if (actions != SuggestionActions::None
&& !Has<HistoryMessageReplyMarkup>()) {
AddComponents(HistoryMessageReplyMarkup::Bit());
}
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
markup->updateSuggestControls(actions);
}
history()->owner().requestItemResize(this);
history()->session().changes().messageUpdated(
this,
@ -1901,8 +1953,10 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) {
suggest->date = edition.suggest.date;
suggest->accepted = edition.suggest.accepted;
suggest->rejected = edition.suggest.rejected;
updateSuggestControls(suggest);
} else {
RemoveComponents(HistoryMessageSuggestedPost::Bit());
updateSuggestControls(nullptr);
}
}
@ -1942,7 +1996,7 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) {
const auto wasSublist = savedSublist();
if (message.vaction().type() == mtpc_messageActionHistoryClear) {
const auto wasGrouped = history()->owner().groups().isGrouped(this);
setReplyMarkup({});
setReplyMarkup({}, true);
removeFromSharedMediaIndex();
refreshMedia(nullptr);
setTextValue({});
@ -2144,8 +2198,10 @@ void HistoryItem::applyEditionToHistoryCleared() {
).c_messageService());
}
void HistoryItem::updateReplyMarkup(HistoryMessageMarkupData &&markup) {
setReplyMarkup(std::move(markup));
void HistoryItem::updateReplyMarkup(
HistoryMessageMarkupData &&markup,
bool ignoreSuggestButtons) {
setReplyMarkup(std::move(markup), ignoreSuggestButtons);
}
void HistoryItem::contributeToSlowmode(TimeId realDate) {
@ -3865,6 +3921,12 @@ void HistoryItem::createComponents(CreateConfig &&config) {
}
if (config.suggest.exists) {
mask |= HistoryMessageSuggestedPost::Bit();
if (computeSuggestionActions(
config.suggest.accepted,
config.suggest.rejected
) != SuggestionActions::None) {
mask |= HistoryMessageReplyMarkup::Bit();
}
}
UpdateComponents(mask);
@ -3965,6 +4027,7 @@ void HistoryItem::createComponents(CreateConfig &&config) {
suggest->date = config.suggest.date;
suggest->accepted = config.suggest.accepted;
suggest->rejected = config.suggest.rejected;
updateSuggestControls(suggest);
}
if (out() && isSending()) {
@ -4601,6 +4664,15 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
) | ranges::views::transform([&](const MTPTodoItem &item) {
return TodoListItemFromMTP(session, item);
}) | ranges::to_vector;
} else if (type == mtpc_messageActionSuggestedPostApproval) {
const auto &data = action.c_messageActionSuggestedPostApproval();
UpdateComponents(HistoryServiceSuggestDecision::Bit());
const auto decision = Get<HistoryServiceSuggestDecision>();
decision->stars = data.vstars_amount().value_or_empty();
decision->balanceTooLow = data.is_balance_too_low();
decision->rejected = data.is_rejected();
decision->rejectComment = qs(data.vreject_comment().value_or_empty());
decision->date = data.vschedule_date().value_or_empty();
}
if (const auto replyTo = message.vreply_to()) {
replyTo->match([&](const MTPDmessageReplyHeader &data) {
@ -4629,7 +4701,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
: PeerId();
const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin();
if (savedSublistPeer || requiresMonoforumPeer) {
UpdateComponents(HistoryMessageSaved::Bit());
AddComponents(HistoryMessageSaved::Bit());
const auto saved = Get<HistoryMessageSaved>();
saved->sublistPeerId = savedSublistPeer
? savedSublistPeer
@ -5950,14 +6022,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
};
auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &data) {
if (data.is_balance_too_low()) {
return PreparedServiceText{ { u"balance too low :( need %1 stars"_q.arg(data.vstars_amount().value_or_empty()) } };
} else if (data.is_rejected()) {
return PreparedServiceText{ { u"rejected :( comment: %1"_q.arg(qs(data.vreject_comment().value_or_empty())) } };
} else if (const auto date = data.vschedule_date().value_or_empty()) {
return PreparedServiceText{ { u"approved!! for date: %1"_q.arg(langDateTime(base::unixtime::parse(date))) } };
}
return PreparedServiceText{ { "approved!!" } }; AssertIsDebug();
return PreparedServiceText{ { u"hello"_q } };
};
auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText {

View file

@ -22,6 +22,7 @@ struct HistoryMessageMarkupData;
struct HistoryMessageReplyMarkup;
struct HistoryMessageTranslation;
struct HistoryMessageForwarded;
struct HistoryMessageSuggestedPost;
struct HistoryServiceDependentData;
struct HistoryServiceTodoCompletions;
enum class HistorySelfDestructType;
@ -29,6 +30,7 @@ struct PreparedServiceText;
struct MessageFactcheck;
class ReplyKeyboard;
struct LanguageId;
enum class SuggestionActions : uchar;
namespace base {
template <typename Enum>
@ -353,7 +355,9 @@ public:
void overrideMedia(std::unique_ptr<Data::Media> media);
void applyEditionToHistoryCleared();
void updateReplyMarkup(HistoryMessageMarkupData &&markup);
void updateReplyMarkup(
HistoryMessageMarkupData &&markup,
bool ignoreSuggestButtons = false);
void contributeToSlowmode(TimeId realDate = 0);
void clearMediaAsExpired();
@ -575,7 +579,16 @@ private:
[[nodiscard]] bool checkDiscussionLink(ChannelId id) const;
void setReplyMarkup(HistoryMessageMarkupData &&markup);
void setReplyMarkup(
HistoryMessageMarkupData &&markup,
bool ignoreSuggestButtons = false);
[[nodiscard]] SuggestionActions computeSuggestionActions() const;
[[nodiscard]] SuggestionActions computeSuggestionActions(
const HistoryMessageSuggestedPost *suggest) const;
[[nodiscard]] SuggestionActions computeSuggestionActions(
bool accepted,
bool rejected) const;
void updateSuggestControls(const HistoryMessageSuggestedPost *suggest);
void changeReplyToTopCounter(
not_null<HistoryMessageReply*> reply,

View file

@ -1214,6 +1214,95 @@ bool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const {
return false;
}
void HistoryMessageReplyMarkup::updateSuggestControls(
SuggestionActions actions) {
if (actions == SuggestionActions::AcceptAndDecline) {
data.flags |= ReplyMarkupFlag::SuggestionAccept;
} else {
data.flags &= ~ReplyMarkupFlag::SuggestionAccept;
}
if (actions == SuggestionActions::None) {
data.flags &= ~ReplyMarkupFlag::SuggestionDecline;
} else {
data.flags |= ReplyMarkupFlag::Inline
| ReplyMarkupFlag::SuggestionDecline;
}
using Type = HistoryMessageMarkupButton::Type;
const auto has = [&](Type type) {
return !data.rows.empty()
&& ranges::contains(
data.rows.back(),
type,
&HistoryMessageMarkupButton::type);
};
if (actions == SuggestionActions::AcceptAndDecline) {
// ... rows ...
// [decline] | [accept]
// [suggestchanges]
if (has(Type::SuggestChange)) {
// Nothing changed.
} else {
if (has(Type::SuggestDecline)) {
data.rows.pop_back();
}
data.rows.push_back({
{
Type::SuggestDecline,
tr::lng_suggest_action_decline(tr::now),
},
{
Type::SuggestAccept,
tr::lng_suggest_action_accept(tr::now),
},
});
data.rows.push_back({ {
Type::SuggestChange,
tr::lng_suggest_action_change(tr::now),
} });
data.flags |= ReplyMarkupFlag::SuggestionAccept
| ReplyMarkupFlag::SuggestionDecline;
}
if (data.rows.size() > 2) {
data.flags |= ReplyMarkupFlag::SuggestionSeparator;
} else {
data.flags &= ~ReplyMarkupFlag::SuggestionSeparator;
}
} else {
while (!data.rows.empty()) {
if (has(Type::SuggestChange) || has(Type::SuggestAccept)) {
data.rows.pop_back();
} else if (has(Type::SuggestDecline)
&& actions == SuggestionActions::None) {
data.rows.pop_back();
} else {
break;
}
}
data.flags &= ~ReplyMarkupFlag::SuggestionAccept;
if (actions == SuggestionActions::None) {
data.flags &= ReplyMarkupFlag::SuggestionDecline;
data.flags &= ~ReplyMarkupFlag::SuggestionSeparator;
} else {
if (!has(Type::SuggestDecline)) {
// ... rows ...
// [decline]
data.rows.push_back({ {
Type::SuggestDecline,
tr::lng_suggest_action_decline(tr::now),
} });
data.flags |= ReplyMarkupFlag::SuggestionDecline;
}
if (data.rows.size() > 1) {
data.flags |= ReplyMarkupFlag::SuggestionSeparator;
} else {
data.flags &= ~ReplyMarkupFlag::SuggestionSeparator;
}
}
}
inlineKeyboard = nullptr;
}
HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() = default;
HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal(

View file

@ -58,6 +58,12 @@ struct BotKeyboardButton;
extern const char kOptionFastButtonsMode[];
[[nodiscard]] bool FastButtonsMode();
enum class SuggestionActions : uchar {
None,
Decline,
AcceptAndDecline,
};
struct HistoryMessageVia : RuntimeComponent<HistoryMessageVia, HistoryItem> {
void create(not_null<Data::Session*> owner, UserId userId);
void resize(int32 availw) const;
@ -383,6 +389,7 @@ struct HistoryMessageReplyMarkup
void createForwarded(const HistoryMessageReplyMarkup &original);
void updateData(HistoryMessageMarkupData &&markup);
void updateSuggestControls(SuggestionActions actions);
[[nodiscard]] bool hiddenBy(Data::Media *media) const;
@ -691,6 +698,16 @@ struct HistoryServiceTodoAppendTasks
[[nodiscard]] TextWithEntities ComposeTodoTasksList(
not_null<HistoryServiceTodoAppendTasks*> append);
struct HistoryServiceSuggestDecision
: RuntimeComponent<HistoryServiceSuggestDecision, HistoryItem>
, HistoryServiceDependentData {
int stars = 0;
TimeId date = 0;
QString rejectComment;
bool rejected = false;
bool balanceTooLow = false;
};
struct HistoryServiceGameScore
: RuntimeComponent<HistoryServiceGameScore, HistoryItem>
, HistoryServiceDependentData {

View file

@ -37,6 +37,9 @@ enum class ReplyMarkupFlag : uint32 {
IsNull = (1U << 7),
OnlyBuyButton = (1U << 8),
Persistent = (1U << 9),
SuggestionDecline = (1U << 10),
SuggestionAccept = (1U << 11),
SuggestionSeparator = (1U << 12),
};
inline constexpr bool is_flag_type(ReplyMarkupFlag) { return true; }
using ReplyMarkupFlags = base::flags<ReplyMarkupFlag>;
@ -85,6 +88,10 @@ struct HistoryMessageMarkupButton {
WebView,
SimpleWebView,
CopyText,
SuggestDecline,
SuggestAccept,
SuggestChange,
};
HistoryMessageMarkupButton(

View file

@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_service_message.h"
#include "history/view/history_view_message.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_media_grouped.h"
#include "history/view/media/history_view_similar_channels.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_large_emoji.h"
#include "history/view/media/history_view_custom_emoji.h"
#include "history/view/media/history_view_suggest_decision.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/reactions/history_view_reactions.h"
#include "history/view/history_view_cursor_state.h"
@ -1072,6 +1074,16 @@ void Element::refreshMedia(Element *replacing) {
this,
std::make_unique<LargeEmoji>(this, emoji));
}
} else if (const auto decision = item->Get<HistoryServiceSuggestDecision>()) {
_media = std::make_unique<MediaGeneric>(
this,
GenerateSuggestDecisionMedia(this, decision),
MediaGenericDescriptor{
.maxWidth = st::chatSuggestInfoWidth,
.serviceLink = decision->lnk,
.service = true,
.hideServiceText = true,
});
} else {
_media = nullptr;
}

View file

@ -468,33 +468,6 @@ void Message::initPaidInformation() {
} else {
text = { { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } };
}
const auto channelIsAuthor = item->from()->isChannel();
const auto amMonoforumAdmin = item->history()->peer->amMonoforumAdmin();
const auto broadcast = item->history()->peer->monoforumBroadcast();
const auto canDecline = item->isRegular()
&& !(suggest->accepted || suggest->rejected)
&& (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin);
const auto canAccept = canDecline
&& (channelIsAuthor
? !amMonoforumAdmin
: (amMonoforumAdmin
&& broadcast
&& broadcast->canPostMessages()));
if (canDecline) {
text.links.push_back(Api::DeclineClickHandler(item));
text.text.append(", ").append(Ui::Text::Link("[Decline]", text.links.size()));
if (canAccept) {
text.links.push_back(Api::AcceptClickHandler(item));
text.text.append(", ").append(Ui::Text::Link("[Accept]", text.links.size()));
text.links.push_back(Api::SuggestChangesClickHandler(item));
text.text.append(", ").append(Ui::Text::Link("[SuggestChanges]", text.links.size()));
}
} else if (suggest->accepted) {
text.text.append(", accepted!");
} else if (suggest->rejected) {
text.text.append(", rejected :(");
}
setServicePreMessage(std::move(text));
}

View file

@ -78,9 +78,7 @@ void EditOptionsBox(
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_suggest_options_price_about(
lt_channel,
rpl::single(args.channelName)));
tr::lng_suggest_options_price_about());
Ui::AddSkip(container);
const auto time = Settings::AddButtonWithLabel(

View file

@ -236,9 +236,11 @@ MediaGenericTextPart::MediaGenericTextPart(
QMargins margins,
const style::TextStyle &st,
const base::flat_map<uint16, ClickHandlerPtr> &links,
const Ui::Text::MarkedContext &context)
const Ui::Text::MarkedContext &context,
style::align align)
: _text(st::msgMinWidth)
, _margins(margins) {
, _margins(margins)
, _align(align) {
_text.setMarkedText(
st,
text,
@ -254,12 +256,18 @@ void MediaGenericTextPart::draw(
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto use = (width() - _margins.left() - _margins.right());
setupPen(p, owner, context);
_text.draw(p, {
.position = { (outerWidth - width()) / 2, _margins.top() },
.position = {
((_align == style::al_top)
? ((outerWidth - use) / 2)
: _margins.left()),
_margins.top(),
},
.outerWidth = outerWidth,
.availableWidth = width(),
.align = style::al_top,
.availableWidth = use,
.align = _align,
.palette = &(owner->service()
? context.st->serviceTextPalette()
: context.messageStyle()->textPalette),
@ -284,11 +292,17 @@ TextState MediaGenericTextPart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
point -= QPoint{ (outerWidth - width()) / 2, _margins.top() };
const auto use = (width() - _margins.left() - _margins.right());
point -= QPoint{
((_align == style::al_top)
? ((outerWidth - use) / 2)
: _margins.left()),
_margins.top(),
};
auto result = TextState();
auto forText = request.forText();
forText.align = style::al_top;
result.link = _text.getState(point, width(), forText).link;
forText.align = _align;
result.link = _text.getState(point, use, forText).link;
return result;
}

View file

@ -141,7 +141,8 @@ public:
QMargins margins,
const style::TextStyle &st = st::defaultTextStyle,
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const Ui::Text::MarkedContext &context = {});
const Ui::Text::MarkedContext &context = {},
style::align align = style::al_top);
void draw(
Painter &p,
@ -165,6 +166,7 @@ protected:
private:
Ui::Text::String _text;
QMargins _margins;
style::align _align = {};
};

View file

@ -0,0 +1,192 @@
/*
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/media/history_view_suggest_decision.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "ui/text/text_utilities.h"
#include "ui/text/format_values.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
namespace HistoryView {
namespace {
enum EmojiType {
kAgreement,
kCalendar,
kMoney,
kHourglass,
kReload,
kDecline,
kDiscard,
kWarning,
};
[[nodiscard]] const char *Raw(EmojiType type) {
switch (type) {
case EmojiType::kAgreement: return "\xf0\x9f\xa4\x9d";
case EmojiType::kCalendar: return "\xf0\x9f\x93\x86";
case EmojiType::kMoney: return "\xf0\x9f\x92\xb0";
case EmojiType::kHourglass: return "\xe2\x8c\x9b\xef\xb8\x8f";
case EmojiType::kReload: return "\xf0\x9f\x94\x84";
case EmojiType::kDecline: return "\xe2\x9d\x8c";
case EmojiType::kDiscard: return "\xf0\x9f\x9a\xab";
case EmojiType::kWarning: return "\xe2\x9a\xa0\xef\xb8\x8f";
}
Unexpected("EmojiType in Raw.");
}
[[nodiscard]] QString Emoji(EmojiType type) {
return QString::fromUtf8(Raw(type));
}
} // namespace
auto GenerateSuggestDecisionMedia(
not_null<Element*> parent,
not_null<const HistoryServiceSuggestDecision*> decision)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto peer = parent->history()->peer;
const auto broadcast = peer->monoforumBroadcast();
if (!broadcast) {
return;
}
const auto sublistPeerId = parent->data()->sublistPeerId();
const auto sublistPeer = peer->owner().peer(sublistPeerId);
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
style::align align = style::al_left,
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links,
Ui::Text::MarkedContext(),
align));
};
if (decision->balanceTooLow) {
pushText(
TextWithEntities(
).append(Emoji(kWarning)).append(' ').append(
(sublistPeer->isSelf()
? tr::lng_suggest_action_your_not_enough
: tr::lng_suggest_action_his_not_enough)(
tr::now,
Ui::Text::RichLangValue)),
st::chatSuggestInfoFullMargin,
style::al_top);
} else if (decision->rejected) {
const auto withComment = !decision->rejectComment.isEmpty();
pushText(
TextWithEntities(
).append(Emoji(kDecline)).append(' ').append(
(withComment
? tr::lng_suggest_action_declined_reason
: tr::lng_suggest_action_declined)(
tr::now,
lt_from,
Ui::Text::Bold(broadcast->name()),
Ui::Text::WithEntities)),
(withComment
? st::chatSuggestInfoTitleMargin
: st::chatSuggestInfoFullMargin));
if (withComment) {
pushText(
TextWithEntities().append('"').append(
decision->rejectComment
).append('"'),
st::chatSuggestInfoLastMargin,
style::al_top);
}
} else {
const auto stars = decision->stars;
pushText(
TextWithEntities(
).append(Emoji(kAgreement)).append(' ').append(
Ui::Text::Bold(tr::lng_suggest_action_agreement(tr::now))
),
st::chatSuggestInfoTitleMargin,
style::al_top);
pushText(
TextWithEntities(
).append(Emoji(kCalendar)).append(' ').append(
tr::lng_suggest_action_agree_date(
tr::now,
lt_channel,
Ui::Text::Bold(broadcast->name()),
lt_date,
Ui::Text::Bold(Ui::FormatDateTime(
base::unixtime::parse(decision->date))),
Ui::Text::WithEntities)),
(stars
? st::chatSuggestInfoMiddleMargin
: st::chatSuggestInfoLastMargin));
if (stars) {
const auto amount = Ui::Text::Bold(
tr::lng_prize_credits_amount(tr::now, lt_count, stars));
pushText(
TextWithEntities(
).append(Emoji(kMoney)).append(' ').append(
(sublistPeer->isSelf()
? tr::lng_suggest_action_your_charged(
tr::now,
lt_amount,
amount,
Ui::Text::WithEntities)
: tr::lng_suggest_action_his_charged(
tr::now,
lt_from,
Ui::Text::Bold(sublistPeer->shortName()),
lt_amount,
amount,
Ui::Text::WithEntities))),
st::chatSuggestInfoMiddleMargin);
pushText(
TextWithEntities(
).append(Emoji(kHourglass)).append(' ').append(
tr::lng_suggest_action_agree_receive(
tr::now,
lt_channel,
Ui::Text::Bold(broadcast->name()),
Ui::Text::WithEntities)),
st::chatSuggestInfoMiddleMargin);
pushText(
TextWithEntities(
).append(Emoji(kReload)).append(' ').append(
tr::lng_suggest_action_agree_removed(
tr::now,
lt_channel,
Ui::Text::Bold(broadcast->name()),
Ui::Text::WithEntities)),
st::chatSuggestInfoLastMargin);
}
}
};
}
} // namespace HistoryView

View file

@ -0,0 +1,25 @@
/*
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
struct HistoryServiceSuggestDecision;
namespace HistoryView {
class Element;
class MediaGeneric;
class MediaGenericPart;
auto GenerateSuggestDecisionMedia(
not_null<Element*> parent,
not_null<const HistoryServiceSuggestDecision*> decision
) -> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
} // namespace HistoryView

View file

@ -1052,6 +1052,12 @@ chatSimilarName: TextStyle(defaultTextStyle) {
chatSimilarWidthMax: 424px;
chatSimilarSkip: 12px;
chatSuggestInfoWidth: 272px;
chatSuggestInfoTitleMargin: margins(16px, 16px, 16px, 6px);
chatSuggestInfoMiddleMargin: margins(16px, 4px, 16px, 4px);
chatSuggestInfoLastMargin: margins(16px, 4px, 16px, 16px);
chatSuggestInfoFullMargin: margins(16px, 16px, 16px, 16px);
premiumRequiredWidth: 186px;
premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }};
premiumRequiredCircle: 60px;