Implement nice tag edit with preview.

This commit is contained in:
John Preston
2026-02-19 12:57:41 +04:00
parent 3621dc754a
commit 4d767fb279
16 changed files with 689 additions and 99 deletions
+2
View File
@@ -228,6 +228,8 @@ PRIVATE
boxes/peers/edit_members_visible.h boxes/peers/edit_members_visible.h
boxes/peers/edit_participant_box.cpp boxes/peers/edit_participant_box.cpp
boxes/peers/edit_participant_box.h boxes/peers/edit_participant_box.h
boxes/peers/edit_tag_control.cpp
boxes/peers/edit_tag_control.h
boxes/peers/edit_participants_box.cpp boxes/peers/edit_participants_box.cpp
boxes/peers/edit_participants_box.h boxes/peers/edit_participants_box.h
boxes/peers/edit_peer_color_box.cpp boxes/peers/edit_peer_color_box.cpp
+4
View File
@@ -852,6 +852,10 @@ customBadgeField: InputField(defaultInputField) {
heightMin: 32px; heightMin: 32px;
} }
tagPreviewLineHeight: 8px;
tagPreviewLineSpacing: 4px;
tagPreviewInputSkip: 16px;
pollResultsQuestion: FlatLabel(defaultFlatLabel) { pollResultsQuestion: FlatLabel(defaultFlatLabel) {
minWidth: 320px; minWidth: 320px;
textFg: windowBoldFg; textFg: windowBoldFg;
@@ -1582,6 +1582,7 @@ void AddSpecialBoxController::showRestricted(
user, user,
_additional.adminRights(user).has_value(), _additional.adminRights(user).has_value(),
currentRights, currentRights,
_additional.memberRank(user),
_additional.restrictedBy(user), _additional.restrictedBy(user),
_additional.restrictedSince(user)); _additional.restrictedSince(user));
if (_additional.canRestrictParticipant(user)) { if (_additional.canRestrictParticipant(user)) {
@@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/peers/edit_participant_box.h" #include "boxes/peers/edit_participant_box.h"
#include "boxes/peers/edit_tag_control.h"
#include "boxes/peers/edit_participants_box.h"
#include "history/view/history_view_message.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "ui/controls/userpic_button.h" #include "ui/controls/userpic_button.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
@@ -47,7 +50,64 @@ namespace {
constexpr auto kMaxRestrictDelayDays = 366; constexpr auto kMaxRestrictDelayDays = 366;
constexpr auto kSecondsInDay = 24 * 60 * 60; constexpr auto kSecondsInDay = 24 * 60 * 60;
constexpr auto kSecondsInWeek = 7 * kSecondsInDay; constexpr auto kSecondsInWeek = 7 * kSecondsInDay;
constexpr auto kAdminRoleLimit = 16;
class Cover final : public Ui::FixedHeightWidget {
public:
Cover(QWidget *parent, not_null<UserData*> user, bool hasAdminRights);
private:
void setupChildGeometry();
const style::InfoProfileCover &_st;
const not_null<UserData*> _user;
object_ptr<Ui::UserpicButton> _userpic;
object_ptr<Ui::FlatLabel> _name = { nullptr };
object_ptr<Ui::FlatLabel> _status = { nullptr };
};
Cover::Cover(
QWidget *parent,
not_null<UserData*> user,
bool hasAdminRights)
: FixedHeightWidget(parent, st::infoEditContactCover.height)
, _st(st::infoEditContactCover)
, _user(user)
, _userpic(this, _user, _st.photo) {
_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
_name = object_ptr<Ui::FlatLabel>(this, _st.name);
_name->setText(_user->name());
const auto statusText = [&] {
if (_user->isBot()) {
const auto seesAllMessages = _user->botInfo->readsAllHistory
|| hasAdminRights;
return (seesAllMessages
? tr::lng_status_bot_reads_all
: tr::lng_status_bot_not_reads_all)(tr::now);
}
return Data::OnlineText(_user->lastseen(), base::unixtime::now());
}();
_status = object_ptr<Ui::FlatLabel>(this, _st.status);
_status->setText(statusText);
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
setupChildGeometry();
}
void Cover::setupChildGeometry() {
widthValue(
) | rpl::on_next([this](int newWidth) {
_userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth);
auto nameWidth = newWidth - _st.nameLeft - _st.rightSkip;
_name->resizeToNaturalWidth(nameWidth);
_name->moveToLeft(_st.nameLeft, _st.nameTop, newWidth);
auto statusWidth = newWidth - _st.statusLeft - _st.rightSkip;
_status->resizeToNaturalWidth(statusWidth);
_status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth);
}, lifetime());
}
} // namespace } // namespace
@@ -66,6 +126,8 @@ public:
return _rows; return _rows;
} }
void setCoverMode(bool enabled);
protected: protected:
int resizeGetHeight(int newWidth) override; int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
@@ -76,6 +138,7 @@ private:
object_ptr<Ui::UserpicButton> _userPhoto; object_ptr<Ui::UserpicButton> _userPhoto;
Ui::Text::String _userName; Ui::Text::String _userName;
bool _hasAdminRights = false; bool _hasAdminRights = false;
bool _coverMode = false;
object_ptr<Ui::VerticalLayout> _rows; object_ptr<Ui::VerticalLayout> _rows;
}; };
@@ -110,7 +173,20 @@ Widget *EditParticipantBox::Inner::addControl(
return _rows->add(std::move(widget), margin); return _rows->add(std::move(widget), margin);
} }
void EditParticipantBox::Inner::setCoverMode(bool enabled) {
_coverMode = enabled;
if (enabled) {
_userPhoto->hide();
}
resizeToWidth(width());
}
int EditParticipantBox::Inner::resizeGetHeight(int newWidth) { int EditParticipantBox::Inner::resizeGetHeight(int newWidth) {
if (_coverMode) {
_rows->resizeToWidth(newWidth);
_rows->moveToLeft(0, 0, newWidth);
return _rows->heightNoMargins();
}
_userPhoto->moveToLeft( _userPhoto->moveToLeft(
st::rightsPhotoMargin.left(), st::rightsPhotoMargin.left(),
st::rightsPhotoMargin.top()); st::rightsPhotoMargin.top());
@@ -127,6 +203,10 @@ void EditParticipantBox::Inner::paintEvent(QPaintEvent *e) {
p.fillRect(e->rect(), st::boxBg); p.fillRect(e->rect(), st::boxBg);
if (_coverMode) {
return;
}
p.setPen(st::contactsNameFg); p.setPen(st::contactsNameFg);
auto namex = st::rightsPhotoMargin.left() auto namex = st::rightsPhotoMargin.left()
+ st::rightsPhotoButton.size .width() + st::rightsPhotoButton.size .width()
@@ -198,6 +278,12 @@ bool EditParticipantBox::amCreator() const {
Unexpected("Peer type in EditParticipantBox::Inner::amCreator."); Unexpected("Peer type in EditParticipantBox::Inner::amCreator.");
} }
void EditParticipantBox::setCoverMode(bool enabled) {
Expects(_inner != nullptr);
_inner->setCoverMode(enabled);
}
EditAdminBox::EditAdminBox( EditAdminBox::EditAdminBox(
QWidget*, QWidget*,
not_null<PeerData*> peer, not_null<PeerData*> peer,
@@ -255,6 +341,11 @@ void EditAdminBox::prepare() {
EditParticipantBox::prepare(); EditParticipantBox::prepare();
setCoverMode(true);
addControl(
object_ptr<Cover>(this, user(), hasAdminRights()),
style::margins());
setTitle(_addingBot setTitle(_addingBot
? (_addingBot->existing ? (_addingBot->existing
? tr::lng_rights_edit_admin() ? tr::lng_rights_edit_admin()
@@ -433,7 +524,17 @@ void EditAdminBox::prepare() {
} }
if (canSave()) { if (canSave()) {
_rank = hasRank ? addRankInput(inner).get() : nullptr; if (hasRank) {
const auto role = LookupBadgeRole(peer(), user());
_tagControl = inner->add(
object_ptr<EditTagControl>(
inner,
&peer()->session(),
user(),
_oldRank,
role),
style::margins());
}
_finishSave = [=, value = getChecked] { _finishSave = [=, value = getChecked] {
const auto newFlags = (value() | ChatAdminRight::Other) const auto newFlags = (value() | ChatAdminRight::Other)
& ((!channel || channel->amCreator()) & ((!channel || channel->amCreator())
@@ -442,7 +543,9 @@ void EditAdminBox::prepare() {
_saveCallback( _saveCallback(
_oldRights, _oldRights,
ChatAdminRightsInfo(newFlags), ChatAdminRightsInfo(newFlags),
_rank ? _rank->getLastText().trimmed() : QString()); _tagControl
? _tagControl->currentRank()
: QString());
}; };
_save = [=] { _save = [=] {
const auto show = uiShow(); const auto show = uiShow();
@@ -497,56 +600,6 @@ void EditAdminBox::refreshButtons() {
} }
} }
not_null<Ui::InputField*> EditAdminBox::addRankInput(
not_null<Ui::VerticalLayout*> container) {
// Ui::AddDivider(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_rights_edit_admin_rank_name(),
st::rightsHeaderLabel),
st::rightsHeaderMargin);
const auto isOwner = [&] {
if (user()->isSelf() && amCreator()) {
return true;
} else if (const auto chat = peer()->asChat()) {
return chat->creator == peerToUser(user()->id);
} else if (const auto channel = peer()->asChannel()) {
return channel->mgInfo && channel->mgInfo->creator == user();
}
Unexpected("Peer type in EditAdminBox::addRankInput.");
}();
const auto result = container->add(
object_ptr<Ui::InputField>(
container,
st::customBadgeField,
(isOwner ? tr::lng_owner_badge : tr::lng_admin_badge)(),
TextUtilities::RemoveEmoji(_oldRank)),
st::rightsAboutMargin);
result->setMaxLength(kAdminRoleLimit);
result->setInstantReplaces(Ui::InstantReplaces::TextOnly());
result->changes(
) | rpl::on_next([=] {
const auto text = result->getLastText();
const auto removed = TextUtilities::RemoveEmoji(text);
if (removed != text) {
result->setText(removed);
}
}, result->lifetime());
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_rights_edit_admin_rank_about(
lt_title,
(isOwner ? tr::lng_owner_badge : tr::lng_admin_badge)()));
Ui::AddSkip(container);
return result;
}
bool EditAdminBox::canTransferOwnership() const { bool EditAdminBox::canTransferOwnership() const {
if (user()->isInaccessible() || user()->isBot() || user()->isSelf()) { if (user()->isInaccessible() || user()->isBot() || user()->isSelf()) {
return false; return false;
@@ -599,10 +652,12 @@ EditRestrictedBox::EditRestrictedBox(
not_null<UserData*> user, not_null<UserData*> user,
bool hasAdminRights, bool hasAdminRights,
ChatRestrictionsInfo rights, ChatRestrictionsInfo rights,
const QString &rank,
UserData *by, UserData *by,
TimeId since) TimeId since)
: EditParticipantBox(nullptr, peer, user, hasAdminRights) : EditParticipantBox(nullptr, peer, user, hasAdminRights)
, _oldRights(rights) , _oldRights(rights)
, _oldRank(rank)
, _by(by) , _by(by)
, _since(since) { , _since(since) {
} }
@@ -613,6 +668,11 @@ void EditRestrictedBox::prepare() {
EditParticipantBox::prepare(); EditParticipantBox::prepare();
setCoverMode(true);
addControl(
object_ptr<Cover>(this, user(), hasAdminRights()),
style::margins());
setTitle(tr::lng_rights_user_restrictions()); setTitle(tr::lng_rights_user_restrictions());
Ui::AddDivider(verticalLayout()); Ui::AddDivider(verticalLayout());
@@ -661,6 +721,17 @@ void EditRestrictedBox::prepare() {
{ .isForum = peer()->isForum() }); { .isForum = peer()->isForum() });
addControl(std::move(checkboxes), QMargins()); addControl(std::move(checkboxes), QMargins());
if (canSave() && peer()->canManageRanks()) {
_tagControl = addControl(
object_ptr<EditTagControl>(
this,
&peer()->session(),
user(),
_oldRank,
HistoryView::BadgeRole::User),
style::margins());
}
_until = prepareRights.until; _until = prepareRights.until;
addControl( addControl(
object_ptr<Ui::FixedHeightWidget>(this, st::defaultVerticalListSkip)); object_ptr<Ui::FixedHeightWidget>(this, st::defaultVerticalListSkip));
@@ -710,6 +781,18 @@ void EditRestrictedBox::prepare() {
_saveCallback( _saveCallback(
_oldRights, _oldRights,
ChatRestrictionsInfo{ value(), getRealUntilValue() }); ChatRestrictionsInfo{ value(), getRealUntilValue() });
if (_tagControl) {
const auto rank = _tagControl->currentRank();
if (rank != _oldRank) {
SaveMemberRank(
uiShow(),
peer(),
user(),
rank,
nullptr,
nullptr);
}
}
}; };
addButton(tr::lng_settings_save(), save); addButton(tr::lng_settings_save(), save);
addButton(tr::lng_cancel(), [=] { closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); });
@@ -23,6 +23,7 @@ class SlideWrap;
} // namespace Ui } // namespace Ui
class ChannelOwnershipTransfer; class ChannelOwnershipTransfer;
class EditTagControl;
class EditParticipantBox : public Ui::BoxContent { class EditParticipantBox : public Ui::BoxContent {
public: public:
@@ -48,6 +49,8 @@ protected:
template <typename Widget> template <typename Widget>
Widget *addControl(object_ptr<Widget> widget, QMargins margin = {}); Widget *addControl(object_ptr<Widget> widget, QMargins margin = {});
void setCoverMode(bool enabled);
bool hasAdminRights() const { bool hasAdminRights() const {
return _hasAdminRights; return _hasAdminRights;
} }
@@ -93,8 +96,6 @@ protected:
private: private:
[[nodiscard]] ChatAdminRightsInfo defaultRights() const; [[nodiscard]] ChatAdminRightsInfo defaultRights() const;
not_null<Ui::InputField*> addRankInput(
not_null<Ui::VerticalLayout*> container);
void transferOwnership(); void transferOwnership();
bool canSave() const { bool canSave() const {
return _saveCallback != nullptr; return _saveCallback != nullptr;
@@ -116,7 +117,7 @@ private:
base::weak_qptr<Ui::BoxContent> _confirmBox; base::weak_qptr<Ui::BoxContent> _confirmBox;
Ui::Checkbox *_addAsAdmin = nullptr; Ui::Checkbox *_addAsAdmin = nullptr;
Ui::SlideWrap<Ui::VerticalLayout> *_adminControlsWrap = nullptr; Ui::SlideWrap<Ui::VerticalLayout> *_adminControlsWrap = nullptr;
Ui::InputField *_rank = nullptr; EditTagControl *_tagControl = nullptr;
Fn<void()> _save, _finishSave; Fn<void()> _save, _finishSave;
@@ -139,6 +140,7 @@ public:
not_null<UserData*> user, not_null<UserData*> user,
bool hasAdminRights, bool hasAdminRights,
ChatRestrictionsInfo rights, ChatRestrictionsInfo rights,
const QString &rank,
UserData *by, UserData *by,
TimeId since); TimeId since);
@@ -164,6 +166,8 @@ private:
TimeId getRealUntilValue() const; TimeId getRealUntilValue() const;
const ChatRestrictionsInfo _oldRights; const ChatRestrictionsInfo _oldRights;
const QString _oldRank;
EditTagControl *_tagControl = nullptr;
UserData *_by = nullptr; UserData *_by = nullptr;
TimeId _since = 0; TimeId _since = 0;
TimeId _until = 0; TimeId _until = 0;
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_participants.h" #include "api/api_chat_participants.h"
#include "boxes/peers/edit_participant_box.h" #include "boxes/peers/edit_participant_box.h"
#include "boxes/peers/edit_tag_control.h"
#include "boxes/peers/add_participants_box.h" #include "boxes/peers/add_participants_box.h"
#include "boxes/peers/prepare_short_info_box.h" // PrepareShortInfoBox #include "boxes/peers/prepare_short_info_box.h" // PrepareShortInfoBox
#include "boxes/peers/edit_members_visible.h" #include "boxes/peers/edit_members_visible.h"
@@ -39,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_values.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "history/history.h" #include "history/history.h"
#include "history/view/history_view_message.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
@@ -172,6 +174,29 @@ void SaveChannelAdmin(
}).send(); }).send();
} }
void SaveChatParticipantKick(
not_null<ChatData*> chat,
not_null<UserData*> user,
Fn<void()> onDone,
Fn<void()> onFail) {
chat->session().api().request(MTPmessages_DeleteChatUser(
MTP_flags(0),
chat->inputChat(),
user->inputUser()
)).done([=](const MTPUpdates &result) {
chat->session().api().applyUpdates(result);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
} // namespace
void SaveMemberRank( void SaveMemberRank(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer, not_null<PeerData*> peer,
@@ -222,29 +247,6 @@ void SaveMemberRank(
}).send(); }).send();
} }
void SaveChatParticipantKick(
not_null<ChatData*> chat,
not_null<UserData*> user,
Fn<void()> onDone,
Fn<void()> onFail) {
chat->session().api().request(MTPmessages_DeleteChatUser(
MTP_flags(0),
chat->inputChat(),
user->inputUser()
)).done([=](const MTPUpdates &result) {
chat->session().api().applyUpdates(result);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
} // namespace
Fn<void( Fn<void(
ChatAdminRightsInfo oldRights, ChatAdminRightsInfo oldRights,
ChatAdminRightsInfo newRights, ChatAdminRightsInfo newRights,
@@ -1993,6 +1995,7 @@ void ParticipantsBoxController::showRestricted(not_null<UserData*> user) {
user, user,
hasAdminRights, hasAdminRights,
currentRights, currentRights,
_additional.memberRank(user),
_additional.restrictedBy(user), _additional.restrictedBy(user),
_additional.restrictedSince(user)); _additional.restrictedSince(user));
if (_additional.canRestrictParticipant(user)) { if (_additional.canRestrictParticipant(user)) {
@@ -2662,7 +2665,6 @@ void EditCustomRankBox(
const QString &currentRank, const QString &currentRank,
bool isSelf, bool isSelf,
Fn<void(QString rank)> onSaved) { Fn<void(QString rank)> onSaved) {
constexpr auto kRankLimit = 16;
struct State { struct State {
bool saving = false; bool saving = false;
}; };
@@ -2677,21 +2679,16 @@ void EditCustomRankBox(
? tr::lng_context_edit_member_tag() ? tr::lng_context_edit_member_tag()
: tr::lng_context_add_member_tag())); : tr::lng_context_add_member_tag()));
const auto field = box->addRow(object_ptr<Ui::InputField>( const auto role = LookupBadgeRole(peer, user);
box, const auto control = box->addRow(
st::customBadgeField, object_ptr<EditTagControl>(
tr::lng_rights_edit_admin_rank_name(), box,
TextUtilities::RemoveEmoji(currentRank))); &peer->session(),
field->setMaxLength(kRankLimit); user,
field->setInstantReplaces(Ui::InstantReplaces::TextOnly()); currentRank,
field->changes( role),
) | rpl::on_next([=] { style::margins());
const auto text = field->getLastText(); const auto field = control->field();
const auto removed = TextUtilities::RemoveEmoji(text);
if (removed != text) {
field->setText(removed);
}
}, field->lifetime());
box->setFocusCallback([=] { field->setFocusFast(); }); box->setFocusCallback([=] { field->setFocusFast(); });
@@ -2701,8 +2698,7 @@ void EditCustomRankBox(
return; return;
} }
state->saving = true; state->saving = true;
const auto rank = TextUtilities::RemoveEmoji( const auto rank = control->currentRank();
TextUtilities::SingleLine(field->getLastText().trimmed()));
SaveMemberRank( SaveMemberRank(
show, show,
peer, peer,
@@ -58,6 +58,14 @@ void EditCustomRankBox(
bool isSelf, bool isSelf,
Fn<void(QString rank)> onSaved); Fn<void(QString rank)> onSaved);
void SaveMemberRank(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<UserData*> user,
const QString &rank,
Fn<void()> onDone,
Fn<void()> onFail);
void SubscribeToMigration( void SubscribeToMigration(
not_null<PeerData*> peer, not_null<PeerData*> peer,
rpl::lifetime &lifetime, rpl::lifetime &lifetime,
@@ -0,0 +1,401 @@
/*
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 "boxes/peers/edit_tag_control.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/admin_log/history_admin_log_item.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_message.h"
#include "history/view/media/history_view_media_generic.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "window/section_widget.h"
#include "window/themes/window_theme.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace {
constexpr auto kRankLimit = 16;
constexpr auto kTextLinesAlpha = 0.1;
using namespace HistoryView;
class TextLinesPart final : public MediaGenericPart {
public:
TextLinesPart(QMargins margins);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
QMargins _margins;
};
TextLinesPart::TextLinesPart(QMargins margins)
: _margins(margins) {
}
QSize TextLinesPart::countOptimalSize() {
const auto h = _margins.top()
+ 4 * st::tagPreviewLineHeight
+ 3 * st::tagPreviewLineSpacing
+ _margins.bottom();
return { st::msgMinWidth, h };
}
QSize TextLinesPart::countCurrentSize(int newWidth) {
return { newWidth, minHeight() };
}
void TextLinesPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto &stm = context.messageStyle();
auto color = stm->historyTextFg->c;
color.setAlphaF(color.alphaF() * kTextLinesAlpha);
p.setPen(Qt::NoPen);
p.setBrush(color);
auto hq = PainterHighQualityEnabler(p);
const auto available = outerWidth - _margins.left() - _margins.right();
const auto lineHeight = st::tagPreviewLineHeight;
const auto radius = lineHeight / 2.0;
const auto fractions = { 1.0, 0.85, 0.65, 0.4 };
auto y = double(_margins.top());
for (const auto fraction : fractions) {
const auto w = available * fraction;
p.drawRoundedRect(
QRectF(_margins.left(), y, w, lineHeight),
radius,
radius);
y += lineHeight + st::tagPreviewLineSpacing;
}
}
class TagPreviewDelegate final : public DefaultElementDelegate {
public:
TagPreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
void setTagText(const QString &text);
[[nodiscard]] const QString &tagText() const;
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
QString elementAuthorRank(not_null<const Element*> view) override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
QString _tagText;
};
TagPreviewDelegate::TagPreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, std::move(update))) {
}
void TagPreviewDelegate::setTagText(const QString &text) {
_tagText = text;
}
const QString &TagPreviewDelegate::tagText() const {
return _tagText;
}
bool TagPreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto TagPreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context TagPreviewDelegate::elementContext() {
return Context::AdminLog;
}
QString TagPreviewDelegate::elementAuthorRank(
not_null<const Element*> view) {
return _tagText;
}
} // namespace
HistoryView::BadgeRole LookupBadgeRole(
not_null<PeerData*> peer,
not_null<UserData*> user) {
if (const auto channel = peer->asMegagroup()) {
const auto info = channel->mgInfo.get();
if (info && info->creator == user) {
return HistoryView::BadgeRole::Creator;
}
const auto userId = peerToUser(user->id);
if (info && info->admins.contains(userId)) {
return HistoryView::BadgeRole::Admin;
}
} else if (const auto chat = peer->asChat()) {
if (peerToUser(user->id) == chat->creator) {
return HistoryView::BadgeRole::Creator;
} else if (chat->admins.contains(user)) {
return HistoryView::BadgeRole::Admin;
}
}
return HistoryView::BadgeRole::User;
}
class EditTagControl::PreviewWidget final : public Ui::RpWidget {
public:
PreviewWidget(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &initialText,
HistoryView::BadgeRole role);
~PreviewWidget();
void setTagText(const QString &text);
private:
void paintEvent(QPaintEvent *e) override;
void createItem();
void applyBadge(const QString &text);
const not_null<History*> _history;
const not_null<UserData*> _user;
const HistoryView::BadgeRole _role;
const std::unique_ptr<Ui::ChatTheme> _theme;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<TagPreviewDelegate> _delegate;
AdminLog::OwnedItem _item;
QPoint _position;
Ui::PeerUserpicView _userpic;
};
EditTagControl::PreviewWidget::PreviewWidget(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &initialText,
HistoryView::BadgeRole role)
: RpWidget(parent)
, _history(session->data().history(PeerData::kServiceNotificationsId))
, _user(user)
, _role(role)
, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
, _style(std::make_unique<Ui::ChatStyle>(
session->colorIndicesValue()))
, _delegate(std::make_unique<TagPreviewDelegate>(
this,
_style.get(),
[=] { update(); }))
, _position(0, st::msgMargin.bottom()) {
_style->apply(_theme.get());
_delegate->setTagText(initialText);
_history->owner().viewRepaintRequest(
) | rpl::on_next([=](Data::RequestViewRepaint data) {
if (data.view == _item.get()) {
update();
}
}, lifetime());
createItem();
widthValue(
) | rpl::filter([=](int w) {
return w >= st::msgMinWidth;
}) | rpl::on_next([=](int w) {
const auto h = _position.y()
+ _item->resizeGetHeight(w)
+ _position.y();
resize(w, h);
}, lifetime());
_history->owner().itemResizeRequest(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
if (_item && item == _item->data() && width() >= st::msgMinWidth) {
const auto h = _position.y()
+ _item->resizeGetHeight(width())
+ _position.y();
resize(width(), h);
}
}, lifetime());
}
EditTagControl::PreviewWidget::~PreviewWidget() {
_item = {};
}
void EditTagControl::PreviewWidget::createItem() {
const auto item = _history->addNewLocalMessage({
.id = _history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeHistoryItem
| MessageFlag::HasFromId),
.from = _user->id,
.date = base::unixtime::now(),
}, TextWithEntities(), MTP_messageMediaEmpty());
auto owned = AdminLog::OwnedItem(_delegate.get(), item);
owned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>(
owned.get(),
[](not_null<HistoryView::MediaGeneric*>,
Fn<void(std::unique_ptr<HistoryView::MediaGenericPart>)> push) {
push(std::make_unique<TextLinesPart>(st::msgPadding));
}));
_item = std::move(owned);
applyBadge(_delegate->tagText());
if (width() >= st::msgMinWidth) {
const auto h = _position.y()
+ _item->resizeGetHeight(width())
+ _position.y();
resize(width(), h);
}
}
void EditTagControl::PreviewWidget::applyBadge(const QString &text) {
if (_item) {
_item->overrideRightBadge(text, _role);
}
}
void EditTagControl::PreviewWidget::setTagText(const QString &text) {
_delegate->setTagText(text);
applyBadge(text);
update();
}
void EditTagControl::PreviewWidget::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e->rect();
if (!clip.isEmpty()) {
p.setClipRect(clip);
Window::SectionWidget::PaintBackground(
p,
_theme.get(),
QSize(width(), window()->height()),
clip);
}
auto context = _theme->preparePaintContext(
_style.get(),
rect(),
rect(),
e->rect(),
!window()->isActiveWindow());
p.translate(_position);
_item->draw(p, context);
if (_item->displayFromPhoto()) {
auto userpicBottom = height()
- _item->marginBottom()
- _item->marginTop();
const auto userpicTop = userpicBottom - st::msgPhotoSize;
_user->paintUserpicLeft(
p,
_userpic,
st::historyPhotoLeft,
userpicTop,
width(),
st::msgPhotoSize);
}
}
EditTagControl::EditTagControl(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &currentRank,
HistoryView::BadgeRole role)
: RpWidget(parent)
, _preview(Ui::CreateChild<PreviewWidget>(
this,
session,
user,
TextUtilities::RemoveEmoji(currentRank),
role))
, _field(Ui::CreateChild<Ui::InputField>(
this,
st::customBadgeField,
tr::lng_rights_edit_admin_rank_name(),
TextUtilities::RemoveEmoji(currentRank))) {
_field->setMaxLength(kRankLimit);
_field->setInstantReplaces(Ui::InstantReplaces::TextOnly());
_field->changes(
) | rpl::on_next([=] {
const auto text = _field->getLastText();
const auto removed = TextUtilities::RemoveEmoji(text);
if (removed != text) {
_field->setText(removed);
}
_preview->setTagText(_field->getLastText());
}, _field->lifetime());
widthValue(
) | rpl::on_next([=](int w) {
_preview->resizeToWidth(w);
const auto inputMargins = st::boxRowPadding;
_field->resizeToWidth(w - inputMargins.left() - inputMargins.right());
_field->moveToLeft(
inputMargins.left(),
_preview->height() + st::tagPreviewInputSkip);
resize(w, _field->y() + _field->height());
}, lifetime());
_preview->heightValue(
) | rpl::skip(1) | rpl::on_next([=] {
_field->moveToLeft(
st::boxRowPadding.left(),
_preview->height() + st::tagPreviewInputSkip);
resize(width(), _field->y() + _field->height());
}, lifetime());
}
EditTagControl::~EditTagControl() = default;
QString EditTagControl::currentRank() const {
return TextUtilities::RemoveEmoji(
TextUtilities::SingleLine(_field->getLastText().trimmed()));
}
not_null<Ui::InputField*> EditTagControl::field() const {
return _field;
}
@@ -0,0 +1,50 @@
/*
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 "ui/rp_widget.h"
namespace Ui {
class InputField;
} // namespace Ui
namespace Main {
class Session;
} // namespace Main
class PeerData;
class UserData;
namespace HistoryView {
enum class BadgeRole : uchar;
} // namespace HistoryView
[[nodiscard]] HistoryView::BadgeRole LookupBadgeRole(
not_null<PeerData*> peer,
not_null<UserData*> user);
class EditTagControl final : public Ui::RpWidget {
public:
EditTagControl(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &currentRank,
HistoryView::BadgeRole role);
~EditTagControl();
[[nodiscard]] QString currentRank() const;
[[nodiscard]] not_null<Ui::InputField*> field() const;
private:
class PreviewWidget;
not_null<PreviewWidget*> _preview;
not_null<Ui::InputField*> _field;
};
@@ -1579,6 +1579,7 @@ void InnerWidget::suggestRestrictParticipant(
user, user,
hasAdminRights, hasAdminRights,
currentRights, currentRights,
QString(),
by, by,
since); since);
box->setSaveCallback([=]( box->setSaveCallback([=](
@@ -1422,6 +1422,40 @@ void Element::overrideMedia(std::unique_ptr<Media> media) {
_flags |= Flag::MediaOverriden; _flags |= Flag::MediaOverriden;
} }
void Element::overrideRightBadge(const QString &text, BadgeRole role) {
if (text.isEmpty()) {
if (Has<RightBadge>()) {
Get<RightBadge>()->overridden = false;
RemoveComponents(RightBadge::Bit());
}
return;
}
if (!Has<RightBadge>()) {
AddComponents(RightBadge::Bit());
}
const auto badge = Get<RightBadge>();
badge->overridden = true;
badge->role = role;
badge->tag.setMarkedText(
st::defaultTextStyle,
{ text },
Ui::NameTextOptions());
badge->boosts.clear();
if (role == BadgeRole::User) {
badge->width = badge->tag.maxWidth();
} else {
const auto &padding = st::msgTagBadgePadding;
const auto tagTextWidth = badge->tag.maxWidth();
const auto contentWidth = padding.left()
+ tagTextWidth
+ padding.right();
const auto pillHeight = padding.top()
+ st::msgFont->height
+ padding.bottom();
badge->width = std::max(contentWidth, pillHeight);
}
}
not_null<PurchasedTag*> Element::enforcePurchasedTag() { not_null<PurchasedTag*> Element::enforcePurchasedTag() {
if (const auto purchased = Get<PurchasedTag>()) { if (const auto purchased = Get<PurchasedTag>()) {
return purchased; return purchased;
@@ -49,6 +49,7 @@ class InlineList;
namespace HistoryView { namespace HistoryView {
using PaintContext = Ui::ChatPaintContext; using PaintContext = Ui::ChatPaintContext;
enum class BadgeRole : uchar;
enum class PointState : char; enum class PointState : char;
enum class InfoDisplayType : char; enum class InfoDisplayType : char;
struct StateRequest; struct StateRequest;
@@ -661,6 +662,7 @@ public:
-> std::unique_ptr<Ui::ReactionFlyAnimation>; -> std::unique_ptr<Ui::ReactionFlyAnimation>;
void overrideMedia(std::unique_ptr<Media> media); void overrideMedia(std::unique_ptr<Media> media);
void overrideRightBadge(const QString &text, BadgeRole role);
[[nodiscard]] not_null<PurchasedTag*> enforcePurchasedTag(); [[nodiscard]] not_null<PurchasedTag*> enforcePurchasedTag();
@@ -257,6 +257,9 @@ void Message::initPaidInformation() {
} }
void Message::refreshRightBadge() { void Message::refreshRightBadge() {
if (const auto badge = Get<RightBadge>(); badge && badge->overridden) {
return;
}
const auto item = data(); const auto item = data();
const auto [text, role] = [&]() -> std::pair<QString, BadgeRole> { const auto [text, role] = [&]() -> std::pair<QString, BadgeRole> {
if (item->isDiscussionPost()) { if (item->isDiscussionPost()) {
@@ -70,6 +70,7 @@ struct RightBadge : RuntimeComponent<RightBadge, Element> {
Ui::Text::String boosts; Ui::Text::String boosts;
int width = 0; int width = 0;
BadgeRole role = BadgeRole::User; BadgeRole role = BadgeRole::User;
bool overridden = false;
}; };
struct BottomRippleMask { struct BottomRippleMask {
+1 -1
View File
@@ -1459,4 +1459,4 @@ earnTonIconMargin: margins(0px, 2px, 0px, 0px);
infoMusicButtonRipple: universalRippleAnimation; infoMusicButtonRipple: universalRippleAnimation;
infoMusicButtonPadding: margins(16px, 6px, 13px, 6px); infoMusicButtonPadding: margins(16px, 6px, 13px, 6px);
memberTagPillPadding: margins(5px, 1px, 5px, 1px); memberTagPillPadding: margins(5px, -1px, 5px, 0px);
+1 -1
View File
@@ -23,7 +23,7 @@ msgMinWidth: 160px;
msgPhotoSize: 33px; msgPhotoSize: 33px;
msgPhotoSkip: 40px; msgPhotoSkip: 40px;
msgPadding: margins(11px, 8px, 11px, 8px); msgPadding: margins(11px, 8px, 11px, 8px);
msgTagBadgePadding: margins(5px, 0px, 5px, 0px); msgTagBadgePadding: margins(5px, -1px, 5px, 0px);
msgTagBadgeBoostSkip: 2px; msgTagBadgeBoostSkip: 2px;
msgMargin: margins(16px, 6px, 56px, 2px); msgMargin: margins(16px, 6px, 56px, 2px);
msgMarginTopAttached: 0px; msgMarginTopAttached: 0px;