Allow moderation of video stream comments.

This commit is contained in:
John Preston
2025-10-30 12:45:09 +04:00
parent 2d754c93a7
commit 89bc58ab29
9 changed files with 276 additions and 39 deletions
+3
View File
@@ -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";
@@ -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<MTPint>(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
@@ -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;
@@ -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 <QtGui/QGuiApplication>
#include <QtGui/QWindow>
@@ -154,6 +161,87 @@ void ReceiveSomeMouseEvents(
new EventFilter(scroll, std::move(handleClick)));
}
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeMessageDateAction(
not_null<Ui::PopupMenu*> 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<Ui::Menu::Action>(
menu->menu(),
st::storiesCommentSentAt,
action,
nullptr,
nullptr);
}
void ShowDeleteMessageConfirmation(
std::shared_ptr<Ui::Show> show,
MsgId id,
not_null<PeerData*> from,
bool canModerate,
Fn<void(MessageDeleteRequest)> callback) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
rpl::variable<bool> report;
rpl::variable<bool> all;
rpl::variable<bool> ban;
};
const auto state = box->lifetime().make_state<State>();
const auto confirmed = [=](Fn<void()> 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<QString> 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<Ui::Checkbox>(
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<Ui::ReactionFlyAnimation> reactionAnimation;
std::unique_ptr<Ui::RpWidget> 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<std::vector<Message>> messages,
rpl::producer<MessageIdUpdate> idUpdates,
rpl::producer<bool> canManageValue,
rpl::producer<bool> 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<Message> &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<Message> &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<QEvent*> e) {
const auto type = e->type();
if (type != QEvent::MouseButtonPress) {
return;
}
const auto m = static_cast<QMouseEvent*>(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<Ui::PopupMenu>(
_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<MessageDeleteRequest> MessagesUi::deleteRequests() const {
return _deleteRequests.events();
}
rpl::lifetime &MessagesUi::lifetime() {
return _lifetime;
}
@@ -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<std::vector<Message>> messages,
rpl::producer<MessageIdUpdate> idUpdates,
rpl::producer<bool> canManageValue,
rpl::producer<bool> shown);
~MessagesUi();
@@ -59,6 +63,7 @@ public:
void raise();
[[nodiscard]] rpl::producer<> hiddenShowRequested() const;
[[nodiscard]] rpl::producer<MessageDeleteRequest> 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<QWidget*> _parent;
const std::shared_ptr<ChatHelpers::Show> _show;
@@ -137,8 +144,11 @@ private:
Ui::RpWidget *_pinned = nullptr;
QImage _pinnedCanvas;
int _pinnedScrollSkip = 0;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::variable<bool> _canManage;
rpl::event_stream<> _hiddenShowRequested;
rpl::event_stream<MessageDeleteRequest> _deleteRequests;
std::optional<std::vector<Message>> _hidden;
std::vector<MessageView> _views;
style::complex_color _messageBg;
@@ -244,6 +244,7 @@ Panel::Panel(not_null<GroupCall*> call, ConferencePanelMigration info)
MessagesMode::GroupCall,
_call->messages()->listValue(),
_call->messages()->idUpdates(),
_call->canManageValue(),
_call->messagesEnabledValue()))
, _toasts(std::make_unique<Toasts>(this))
, _controlsBackgroundColor([] {
@@ -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;
@@ -5684,7 +5684,7 @@ void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> 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<QKeyEvent*> e) {
validatePhotoCurrentImage();
redisplayContent();
}
} else if (key == Qt::Key_V) {
} else if (key == Qt::Key_V && !_stories) {
if (_flip & Qt::Vertical) {
_flip &= ~Qt::Vertical;
} else {
@@ -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) {