From 89bc58ab296975d9d328d3c1369be7eb5efb8f63 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 30 Oct 2025 12:45:09 +0400 Subject: [PATCH] Allow moderation of video stream comments. --- Telegram/Resources/langs/lang.strings | 3 + .../calls/group/calls_group_messages.cpp | 47 +++- .../calls/group/calls_group_messages.h | 10 + .../calls/group/calls_group_messages_ui.cpp | 214 +++++++++++++++--- .../calls/group/calls_group_messages_ui.h | 10 + .../calls/group/calls_group_panel.cpp | 1 + .../SourceFiles/media/view/media_view.style | 19 +- .../media/view/media_view_overlay_widget.cpp | 4 +- .../media/view/media_view_video_stream.cpp | 7 + 9 files changed, 276 insertions(+), 39 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 925832be7f..47b8fb1143 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4573,6 +4573,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_seen_reacted_none" = "Nobody Reacted"; "lng_context_seen_reacted_all" = "Show All Reactions"; "lng_context_sent_by" = "Sent by {user}"; +"lng_context_sent_today" = "Sent today at {time}"; +"lng_context_sent_yesterday" = "Sent yesterday at {time}"; +"lng_context_sent_date" = "Sent {date} at {time}"; "lng_context_set_as_quick" = "Set As Quick"; "lng_context_filter_by_tag" = "Filter by Tag"; "lng_context_tag_add_name" = "Add Name"; diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages.cpp b/Telegram/SourceFiles/calls/group/calls_group_messages.cpp index 5d748a05a5..a028c93402 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_messages.cpp @@ -8,12 +8,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_group_messages.h" #include "apiwrap.h" +#include "api/api_blocked_peers.h" +#include "api/api_chat_participants.h" #include "api/api_text_entities.h" #include "base/random.h" #include "base/unixtime.h" #include "calls/group/ui/calls_group_stars_coloring.h" #include "calls/group/calls_group_call.h" #include "calls/group/calls_group_message_encryption.h" +#include "data/data_channel.h" #include "data/data_group_call.h" #include "data/data_message_reactions.h" #include "data/data_peer.h" @@ -150,6 +153,7 @@ void Messages::send(TextWithTags text, int stars) { .peer = from, .text = std::move(prepared), .stars = stars, + .mine = true, }); if (!_call->conference()) { @@ -324,15 +328,18 @@ void Messages::received( if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) { allowedEntityTypes.pop_back(); } + const auto author = peer->owner().peer(peerFromMTP(from)); _messages.push_back({ .id = id, .date = date, .pinFinishDate = PinFinishDate(date, stars), - .peer = peer->owner().peer(peerFromMTP(from)), + .peer = author, .text = Ui::Text::Filtered( Api::ParseTextWithEntities(&peer->session(), message), allowedEntityTypes), .stars = stars, + .mine = (author->isSelf() + || (author->isChannel() && author->asChannel()->amCreator())) }); ranges::sort(_messages, ranges::less(), &Message::id); checkDestroying(true); @@ -570,6 +577,7 @@ void Messages::reactionsPaidSend() { .id = localId, .peer = from, .stars = int(send.count), + .mine = true, }); using Flag = MTPphone_SendGroupCallMessage::Flag; @@ -606,4 +614,41 @@ Messages::PaidLocalState Messages::starsLocalState() const { return { .total = total, .my = my }; } +void Messages::deleteConfirmed(MessageDeleteRequest request) { + const auto eraseFrom = [&](auto iterator) { + if (iterator != end(_messages)) { + _messages.erase(iterator, end(_messages)); + pushChanges(); + } + }; + const auto peer = _call->peer(); + if (const auto from = request.deleteAllFrom) { + using Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag; + _api->request(MTPphone_DeleteGroupCallParticipantMessages( + MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()), + _call->inputCall(), + from->input + )).send(); + eraseFrom(ranges::remove(_messages, not_null(from), &Message::peer)); + } else { + using Flag = MTPphone_DeleteGroupCallMessages::Flag; + _api->request(MTPphone_DeleteGroupCallMessages( + MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()), + _call->inputCall(), + MTP_vector(1, MTP_int(request.id.bare)) + )).send(); + eraseFrom(ranges::remove(_messages, request.id, &Message::id)); + } + if (const auto ban = request.ban) { + if (const auto channel = peer->asChannel()) { + ban->session().api().chatParticipants().kick( + channel, + ban, + ChatRestrictionsInfo()); + } else { + ban->session().api().blockedPeers().block(ban); + } + } +} + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages.h b/Telegram/SourceFiles/calls/group/calls_group_messages.h index 55293bf35b..c6a87023d0 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages.h +++ b/Telegram/SourceFiles/calls/group/calls_group_messages.h @@ -38,6 +38,7 @@ struct Message { TextWithEntities text; int stars = 0; bool failed = false; + bool mine = false; }; struct MessageIdUpdate { @@ -45,6 +46,13 @@ struct MessageIdUpdate { MsgId realId = 0; }; +struct MessageDeleteRequest { + MsgId id = 0; + PeerData *deleteAllFrom = nullptr; + PeerData *ban = nullptr; + bool reportSpam = false; +}; + struct StarsTopDonor { PeerData *peer = nullptr; int stars = 0; @@ -105,6 +113,8 @@ public: return _hiddenShowRequests.events(); } + void deleteConfirmed(MessageDeleteRequest request); + private: struct Pending { TextWithTags text; diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp index c71c7e195b..e280301301 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp @@ -27,14 +27,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/boxes/confirm_box.h" #include "ui/controls/emoji_button.h" #include "ui/controls/send_button.h" #include "ui/effects/animations.h" #include "ui/effects/radial_animation.h" #include "ui/effects/reaction_fly_animation.h" +#include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" +#include "ui/widgets/menu/menu_item_base.h" +#include "ui/widgets/menu/menu_action.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/elastic_scroll.h" +#include "ui/widgets/popup_menu.h" #include "ui/color_int_conversion.h" #include "ui/painter.h" #include "ui/ui_utility.h" @@ -44,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat.h" #include "styles/style_credits.h" #include "styles/style_media_view.h" +#include "styles/style_menu_icons.h" #include #include @@ -154,6 +161,87 @@ void ReceiveSomeMouseEvents( new EventFilter(scroll, std::move(handleClick))); } +[[nodiscard]] base::unique_qptr MakeMessageDateAction( + not_null menu, + TimeId value) { + const auto parent = menu->menu(); + + const auto parsed = base::unixtime::parse(value); + const auto date = parsed.date(); + const auto time = QLocale().toString( + parsed.time(), + QLocale::ShortFormat); + const auto today = QDateTime::currentDateTime().date(); + const auto text = (date == today) + ? tr::lng_context_sent_today(tr::now, lt_time, time) + : (date.addDays(1) == today) + ? tr::lng_context_sent_yesterday(tr::now, lt_time, time) + : tr::lng_context_sent_date( + tr::now, + lt_date, + langDayOfMonthFull(date), + lt_time, + time); + const auto action = Ui::Menu::CreateAction(parent, text, [] {}); + action->setDisabled(true); + return base::make_unique_q( + menu->menu(), + st::storiesCommentSentAt, + action, + nullptr, + nullptr); +} + +void ShowDeleteMessageConfirmation( + std::shared_ptr show, + MsgId id, + not_null from, + bool canModerate, + Fn callback) { + show->show(Box([=](not_null box) { + struct State { + rpl::variable report; + rpl::variable all; + rpl::variable ban; + }; + const auto state = box->lifetime().make_state(); + const auto confirmed = [=](Fn close) { + callback(MessageDeleteRequest{ + .id = id, + .deleteAllFrom = state->all.current() ? from.get() : nullptr, + .ban = state->ban.current() ? from.get() : nullptr, + .reportSpam = state->report.current(), + }); + close(); + }; + Ui::ConfirmBox(box, { + .text = tr::lng_selected_delete_sure_this(), + .confirmed = confirmed, + .confirmText = tr::lng_box_delete(), + .labelStyle = &st::groupCallBoxLabel, + }); + if (canModerate) { + const auto check = [&](rpl::producer text) { + const auto add = st::groupCallCheckbox.margin; + const auto added = QMargins(0, add.top(), 0, add.bottom()); + const auto margin = st::boxRowPadding + added; + + return box->addRow(object_ptr( + box, + std::move(text), + false, + st::groupCallCheckbox + ), margin)->checkedValue(); + }; + state->report = check(tr::lng_report_spam()); + state->all = check(tr::lng_delete_all_from_user( + lt_user, + rpl::single(from->shortName()))), + state->ban = check(tr::lng_ban_user()); + } + })); +} + } // namespace struct MessagesUi::MessageView { @@ -168,9 +256,11 @@ struct MessagesUi::MessageView { std::unique_ptr reactionAnimation; std::unique_ptr reactionWidget; QPoint reactionShift; + TextWithEntities original; Ui::PeerUserpicView view; Ui::Text::String text; Ui::Text::String price; + TimeId date = 0; int stars = 0; int top = 0; int width = 0; @@ -180,6 +270,7 @@ struct MessagesUi::MessageView { bool removed = false; bool sending = false; bool failed = false; + bool mine = false; }; struct MessagesUi::PinnedView { @@ -215,10 +306,12 @@ MessagesUi::MessagesUi( MessagesMode mode, rpl::producer> messages, rpl::producer idUpdates, + rpl::producer canManageValue, rpl::producer shown) : _parent(parent) , _show(std::move(show)) , _mode(mode) +, _canManage(std::move(canManageValue)) , _messageBg([] { auto result = st::groupCallBg->c; result.setAlphaF(kMessageBgOpacity); @@ -274,6 +367,7 @@ void MessagesUi::showList(const std::vector &list) { } else if (entry.sending != (i->date == 0)) { animateMessageSent(entry); } + entry.date = i->date; if (i == from) { appendPinned(*i, now); ++from; @@ -293,6 +387,7 @@ void MessagesUi::showList(const std::vector &list) { } else if (j->sending != (i->date == 0)) { animateMessageSent(*j); } + j->date = i->date; toggleMessage(*j, true); } else { if (i + 1 == till && !i->date) { @@ -562,9 +657,12 @@ void MessagesUi::appendMessage(const Message &data) { auto &entry = _views.emplace_back(MessageView{ .id = id, .from = peer, + .original = data.text, + .date = data.date, .stars = data.stars, .top = top, .sending = !data.date, + .mine = data.mine, }); const auto repaint = [=] { repaintMessage(id); @@ -1092,40 +1190,100 @@ void MessagesUi::receiveSomeMouseEvents() { for (const auto &entry : _views) { if (entry.failed || entry.top + entry.height <= point.y()) { continue; - } else if (entry.top >= point.y() - || entry.left >= point.x() - || entry.left + entry.width <= point.x()) { - return false; + } else if (entry.top < point.y() + && entry.left < point.x() + && entry.left + entry.width > point.x()) { + handleClick(entry, point); + return true; } - - const auto padding = st::groupCallMessagePadding; - const auto userpicSize = st::groupCallUserpic; - const auto userpicPadding = st::groupCallUserpicPadding; - const auto leftSkip = userpicPadding.left() - + userpicSize - + userpicPadding.right(); - const auto userpic = QRect( - entry.left + userpicPadding.left(), - entry.top + userpicPadding.top(), - userpicSize, - userpicSize); - const auto link = userpic.contains(point) - ? entry.fromLink - : entry.text.getState(point - QPoint( - entry.left + leftSkip, - entry.top + padding.top() - ), entry.width - leftSkip - padding.right()).link; - if (link) { - ActivateClickHandler(_messages, link, Qt::LeftButton); - } - return true; + break; } return false; }); } void MessagesUi::receiveAllMouseEvents() { + _messages->events() | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type != QEvent::MouseButtonPress) { + return; + } + const auto m = static_cast(e.get()); + const auto point = m->pos(); + for (const auto &entry : _views) { + if (entry.failed || entry.top + entry.height <= point.y()) { + continue; + } else if (entry.top < point.y() + && entry.left < point.x() + && entry.left + entry.width > point.x()) { + if (m->button() == Qt::LeftButton) { + handleClick(entry, point); + } else { + showContextMenu(entry, m->globalPos()); + } + } + break; + } + }, _messages->lifetime()); +} +void MessagesUi::handleClick(const MessageView &entry, QPoint point) { + const auto padding = st::groupCallMessagePadding; + const auto userpicSize = st::groupCallUserpic; + const auto userpicPadding = st::groupCallUserpicPadding; + const auto leftSkip = userpicPadding.left() + + userpicSize + + userpicPadding.right(); + const auto userpic = QRect( + entry.left + userpicPadding.left(), + entry.top + userpicPadding.top(), + userpicSize, + userpicSize); + const auto link = userpic.contains(point) + ? entry.fromLink + : entry.text.getState(point - QPoint( + entry.left + leftSkip, + entry.top + padding.top() + ), entry.width - leftSkip - padding.right()).link; + if (link) { + ActivateClickHandler(_messages, link, Qt::LeftButton); + } +} + +void MessagesUi::showContextMenu( + const MessageView &entry, + QPoint globalPoint) { + if (_menu || !entry.date || entry.failed) { + return; + } + _menu = base::make_unique_q( + _parent, + st::groupCallPopupMenuWithIcons); + _menu->addAction(MakeMessageDateAction(_menu.get(), entry.date)); + const auto &original = entry.original; + const auto canCopy = !original.empty(); + const auto canDelete = entry.mine || _canManage.current(); + if (canCopy || canDelete) { + _menu->addSeparator(&st::mediaviewWideMenuSeparator); + } + if (canCopy) { + _menu->addAction(tr::lng_context_copy_text(tr::now), [=] { + TextUtilities::SetClipboardText( + TextForMimeData::WithExpandedLinks(original)); + }, &st::mediaMenuIconCopy); + } + if (canDelete) { + const auto id = entry.id; + const auto from = entry.from; + const auto canModerate = _canManage.current() && !entry.mine; + _menu->addAction(tr::lng_context_delete_msg(tr::now), [=] { + ShowDeleteMessageConfirmation(_show, id, from, canModerate, [=]( + MessageDeleteRequest request) { + _deleteRequests.fire_copy(request); + }); + }, &st::mediaMenuIconDelete); + } + _menu->popup(globalPoint); } void MessagesUi::setupPinnedWidget() { @@ -1511,6 +1669,10 @@ rpl::producer<> MessagesUi::hiddenShowRequested() const { return _hiddenShowRequested.events(); } +rpl::producer MessagesUi::deleteRequests() const { + return _deleteRequests.events(); +} + rpl::lifetime &MessagesUi::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h index 8e06e24fd1..75d21e9526 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h +++ b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/unique_qptr.h" #include "ui/effects/animations.h" #include "ui/round_rect.h" @@ -26,6 +27,7 @@ class ElasticScroll; class EmojiButton; class InputField; class SendButton; +class PopupMenu; class RpWidget; } // namespace Ui @@ -38,6 +40,7 @@ namespace Calls::Group { struct Message; struct MessageIdUpdate; +struct MessageDeleteRequest; enum class MessagesMode { GroupCall, @@ -52,6 +55,7 @@ public: MessagesMode mode, rpl::producer> messages, rpl::producer idUpdates, + rpl::producer canManageValue, rpl::producer shown); ~MessagesUi(); @@ -59,6 +63,7 @@ public: void raise(); [[nodiscard]] rpl::producer<> hiddenShowRequested() const; + [[nodiscard]] rpl::producer deleteRequests() const; [[nodiscard]] rpl::lifetime &lifetime(); @@ -124,6 +129,8 @@ private: void receiveSomeMouseEvents(); void receiveAllMouseEvents(); + void handleClick(const MessageView &entry, QPoint point); + void showContextMenu(const MessageView &entry, QPoint globalPoint); const not_null _parent; const std::shared_ptr _show; @@ -137,8 +144,11 @@ private: Ui::RpWidget *_pinned = nullptr; QImage _pinnedCanvas; int _pinnedScrollSkip = 0; + base::unique_qptr _menu; + rpl::variable _canManage; rpl::event_stream<> _hiddenShowRequested; + rpl::event_stream _deleteRequests; std::optional> _hidden; std::vector _views; style::complex_color _messageBg; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index b01d5df5f7..d269cde8e2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -244,6 +244,7 @@ Panel::Panel(not_null call, ConferencePanelMigration info) MessagesMode::GroupCall, _call->messages()->listValue(), _call->messages()->idUpdates(), + _call->canManageValue(), _call->messagesEnabledValue())) , _toasts(std::make_unique(this)) , _controlsBackgroundColor([] { diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c72a02309a..ac8f432904 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -310,13 +310,12 @@ mediaviewTitleMaximizeMacPadding: margins(0px, 4px, 8px, 4px); mediaviewShadowTop: icon{{ "mediaview/shadow_top", windowShadowFg }}; mediaviewShadowBottom: icon{{ "mediaview/shadow_bottom", windowShadowFg }}; - +mediaviewWideMenuSeparator: MenuSeparator(mediaviewMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; +} mediaviewSpeedMenuInner: Menu(mediaviewMenu) { - separator: MenuSeparator(mediaviewMenuSeparator) { - fg: groupCallMenuBgOver; - padding: margins(0px, 4px, 0px, 4px); - width: 6px; - } + separator: mediaviewWideMenuSeparator; itemPadding: margins(54px, 7px, 54px, 9px); itemFgDisabled: mediaviewTextLinkFg; } @@ -609,10 +608,7 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) { fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBg }}; fadeRight: icon {{ "fade_horizontal", storiesComposeBg }}; menu: storiesPopupMenuWithIcons; - expandedSeparator: MenuSeparator(storiesMenuSeparator) { - padding: margins(0px, 4px, 0px, 4px); - width: 6px; - } + expandedSeparator: mediaviewWideMenuSeparator; tabs: SettingsSlider(emojiTabs) { barFgActive: storiesComposeBlue; labelFg: storiesComposeGrayText; @@ -700,6 +696,9 @@ storiesChooseSendAs: ChooseSendAs(defaultChooseSendAs) { } list: groupCallJoinAsList; } +storiesCommentSentAt: Menu(storiesMenu, whoSentItem) { + itemFgDisabled: groupCallMembersFg; +} storiesComposeControls: ComposeControls(defaultComposeControls) { bg: storiesComposeBg; radius: 21px; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 0635bd307b..0b9612f064 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -5684,7 +5684,7 @@ void OverlayWidget::handleKeyPress(not_null e) { activateControls(); } moveToNext(-1); - } else if (key == Qt::Key_H) { + } else if (key == Qt::Key_H && !_stories) { if (_flip & Qt::Horizontal) { _flip &= ~Qt::Horizontal; } else { @@ -5694,7 +5694,7 @@ void OverlayWidget::handleKeyPress(not_null e) { validatePhotoCurrentImage(); redisplayContent(); } - } else if (key == Qt::Key_V) { + } else if (key == Qt::Key_V && !_stories) { if (_flip & Qt::Vertical) { _flip &= ~Qt::Vertical; } else { diff --git a/Telegram/SourceFiles/media/view/media_view_video_stream.cpp b/Telegram/SourceFiles/media/view/media_view_video_stream.cpp index b52d552b7c..b551b32586 100644 --- a/Telegram/SourceFiles/media/view/media_view_video_stream.cpp +++ b/Telegram/SourceFiles/media/view/media_view_video_stream.cpp @@ -105,6 +105,7 @@ VideoStream::VideoStream( Calls::Group::MessagesMode::VideoStream, _call->messages()->listValue(), _call->messages()->idUpdates(), + _call->canManageValue(), rpl::combine( _call->messagesEnabledValue(), _commentsShown.value(), @@ -212,6 +213,12 @@ void VideoStream::setupMessages() { _messages->hiddenShowRequested() | rpl::start_with_next([=] { _call->messages()->requestHiddenShow(); }, _messages->lifetime()); + + + _messages->deleteRequests( + ) | rpl::start_with_next([=](Calls::Group::MessageDeleteRequest event) { + _call->messages()->deleteConfirmed(event); + }, _messages->lifetime()); } void VideoStream::setVolume(float64 volume) {