Improve folder link chats list edit design.

This commit is contained in:
John Preston 2023-03-30 17:51:52 +04:00
parent 0faadc8fa0
commit 7a9961b0e9
7 changed files with 195 additions and 35 deletions

View file

@ -3566,9 +3566,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_filters_link_chats#one" = "{count} chat selected"; "lng_filters_link_chats#one" = "{count} chat selected";
"lng_filters_link_chats#other" = "{count} chats selected"; "lng_filters_link_chats#other" = "{count} chats selected";
"lng_filters_link_bot_status" = "you can't share chats with bots"; "lng_filters_link_bot_status" = "you can't share chats with bots";
"lng_filters_link_bot_error" = "Chat's with bots can't be shared You can't share chats with bots"; "lng_filters_link_bot_error" = "Chats with bots can't be shared.";
"lng_filters_link_private_status" = "you can't share private chats"; "lng_filters_link_private_status" = "you can't share private chats";
"lng_filters_link_private_error" = "You can't share private chats"; "lng_filters_link_private_error" = "Private chats can't be shared.";
"lng_filters_link_noadmin_status" = "you can't invite others here"; "lng_filters_link_noadmin_status" = "you can't invite others here";
"lng_filters_link_noadmin_group_error" = "You don't have the admin rights to share invite links to this group chat."; "lng_filters_link_noadmin_group_error" = "You don't have the admin rights to share invite links to this group chat.";
"lng_filters_link_noadmin_channel_error" = "You don't have the admin rights to share invite links to this channel."; "lng_filters_link_noadmin_channel_error" = "You don't have the admin rights to share invite links to this channel.";

View file

@ -751,13 +751,20 @@ void EditFilterBox(
box->setTitle(tr::lng_filters_edit()); box->setTitle(tr::lng_filters_edit());
nameEditing->custom = true; nameEditing->custom = true;
*data = updated; // Comparison of ChatFilter-s don't take id into account!
data->force_assign(updated);
const auto id = updated.id(); const auto id = updated.id();
state->links = owner->chatsFilters().communityLinks(id); state->links = owner->chatsFilters().communityLinks(id);
ExportFilterLink(id, shared, [=](Data::ChatFilterLink link) { ExportFilterLink(id, shared, [=](Data::ChatFilterLink link) {
Expects(link.id == id); Expects(link.id == id);
window->show(ShowLinkBox(window, updated, link)); window->show(ShowLinkBox(window, updated, link));
}, [=](QString error) {
if (error == "COMMUNITIES_TOO_MUCH") {
// #TODO filters
} else {
window->show(ShowLinkBox(window, updated, { .id = id }));
}
}); });
})); }));
}, createLink->lifetime()); }, createLink->lifetime());

View file

@ -43,7 +43,7 @@ namespace {
constexpr auto kMaxLinkTitleLength = 32; constexpr auto kMaxLinkTitleLength = 32;
using InviteLinkData = Data::ChatFilterLink; using InviteLinkData = Data::ChatFilterLink;
class Row; class LinkRow;
enum class Color { enum class Color {
Permanent, Permanent,
@ -89,7 +89,7 @@ struct Errors {
} }
return std::nullopt; return std::nullopt;
} else if (const auto channel = history->peer->asChannel()) { } else if (const auto channel = history->peer->asChannel()) {
if (!channel->canHaveInviteLink()) { if (!channel->canHaveInviteLink() && !channel->hasUsername()) {
return result( return result(
tr::lng_filters_link_noadmin_status(tr::now), tr::lng_filters_link_noadmin_status(tr::now),
(channel->isMegagroup() (channel->isMegagroup()
@ -180,9 +180,9 @@ void ChatFilterLinkBox(
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
} }
class RowDelegate { class LinkRowDelegate {
public: public:
virtual void rowUpdateRow(not_null<Row*> row) = 0; virtual void rowUpdateRow(not_null<LinkRow*> row) = 0;
virtual void rowPaintIcon( virtual void rowPaintIcon(
QPainter &p, QPainter &p,
int x, int x,
@ -191,9 +191,9 @@ public:
Color color) = 0; Color color) = 0;
}; };
class Row final : public PeerListRow { class LinkRow final : public PeerListRow {
public: public:
Row(not_null<RowDelegate*> delegate, const InviteLinkData &data); LinkRow(not_null<LinkRowDelegate*> delegate, const InviteLinkData &data);
void update(const InviteLinkData &data); void update(const InviteLinkData &data);
@ -215,13 +215,28 @@ public:
bool actionSelected) override; bool actionSelected) override;
private: private:
const not_null<RowDelegate*> _delegate; const not_null<LinkRowDelegate*> _delegate;
InviteLinkData _data; InviteLinkData _data;
QString _status; QString _status;
Color _color = Color::Permanent; Color _color = Color::Permanent;
}; };
class ChatRow final : public PeerListRow {
public:
ChatRow(not_null<PeerData*> peer, const QString &status, bool disabled);
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
private:
const bool _disabled = false;
QImage _disabledFrame;
InMemoryKey _userpicKey;
int _paletteVersion = 0;
};
[[nodiscard]] uint64 ComputeRowId(const QString &link) { [[nodiscard]] uint64 ComputeRowId(const QString &link) {
return XXH64(link.data(), link.size() * sizeof(ushort), 0); return XXH64(link.data(), link.size() * sizeof(ushort), 0);
} }
@ -238,7 +253,9 @@ private:
return tr::lng_filters_chats_count(tr::now, lt_count, link.chats.size()); return tr::lng_filters_chats_count(tr::now, lt_count, link.chats.size());
} }
Row::Row(not_null<RowDelegate*> delegate, const InviteLinkData &data) LinkRow::LinkRow(
not_null<LinkRowDelegate*> delegate,
const InviteLinkData &data)
: PeerListRow(ComputeRowId(data)) : PeerListRow(ComputeRowId(data))
, _delegate(delegate) , _delegate(delegate)
, _data(data) , _data(data)
@ -246,7 +263,7 @@ Row::Row(not_null<RowDelegate*> delegate, const InviteLinkData &data)
setCustomStatus(ComputeStatus(data)); setCustomStatus(ComputeStatus(data));
} }
void Row::update(const InviteLinkData &data) { void LinkRow::update(const InviteLinkData &data) {
_data = data; _data = data;
_color = ComputeColor(data); _color = ComputeColor(data);
setCustomStatus(ComputeStatus(data)); setCustomStatus(ComputeStatus(data));
@ -254,11 +271,11 @@ void Row::update(const InviteLinkData &data) {
_delegate->rowUpdateRow(this); _delegate->rowUpdateRow(this);
} }
InviteLinkData Row::data() const { InviteLinkData LinkRow::data() const {
return _data; return _data;
} }
QString Row::generateName() { QString LinkRow::generateName() {
if (!_data.title.isEmpty()) { if (!_data.title.isEmpty()) {
return _data.title; return _data.title;
} }
@ -275,11 +292,12 @@ QString Row::generateName() {
); );
} }
QString Row::generateShortName() { QString LinkRow::generateShortName() {
return generateName(); return generateName();
} }
PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) { PaintRoundImageCallback LinkRow::generatePaintUserpicCallback(
bool forceRound) {
return [=]( return [=](
QPainter &p, QPainter &p,
int x, int x,
@ -290,13 +308,13 @@ PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
}; };
} }
QSize Row::rightActionSize() const { QSize LinkRow::rightActionSize() const {
return QSize( return QSize(
st::inviteLinkThreeDotsIcon.width(), st::inviteLinkThreeDotsIcon.width(),
st::inviteLinkThreeDotsIcon.height()); st::inviteLinkThreeDotsIcon.height());
} }
QMargins Row::rightActionMargins() const { QMargins LinkRow::rightActionMargins() const {
return QMargins( return QMargins(
0, 0,
(st::inviteLinkList.item.height - rightActionSize().height()) / 2, (st::inviteLinkList.item.height - rightActionSize().height()) / 2,
@ -304,7 +322,7 @@ QMargins Row::rightActionMargins() const {
0); 0);
} }
void Row::rightActionPaint( void LinkRow::rightActionPaint(
Painter &p, Painter &p,
int x, int x,
int y, int y,
@ -316,9 +334,106 @@ void Row::rightActionPaint(
: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
} }
ChatRow::ChatRow(
not_null<PeerData*> peer,
const QString &status,
bool disabled)
: PeerListRow(peer)
, _disabled(disabled) {
if (!status.isEmpty()) {
setCustomStatus(status);
}
}
PaintRoundImageCallback ChatRow::generatePaintUserpicCallback(
bool forceRound) {
const auto peer = this->peer();
const auto saved = peer->isSelf();
const auto replies = peer->isRepliesChat();
auto userpic = (saved || replies)
? Ui::PeerUserpicView()
: ensureUserpicView();
auto paint = [=](
Painter &p,
int x,
int y,
int outerWidth,
int size) mutable {
if (forceRound && peer->isForum()) {
ForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size);
} else if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
}
};
return [=](
Painter &p,
int x,
int y,
int outerWidth,
int size) mutable {
if (!_disabled) {
paint(p, x, y, outerWidth, size);
return;
}
const auto wide = size + style::ConvertScale(3);
const auto full = QSize(wide, wide) * style::DevicePixelRatio();
auto repaint = false;
if (_disabledFrame.size() != full) {
repaint = true;
_disabledFrame = QImage(
full,
QImage::Format_ARGB32_Premultiplied);
_disabledFrame.setDevicePixelRatio(style::DevicePixelRatio());
} else {
repaint = (_paletteVersion != style::PaletteVersion())
|| (!saved
&& !replies
&& (_userpicKey != peer->userpicUniqueKey(userpic)));
}
if (repaint) {
_paletteVersion = style::PaletteVersion();
_userpicKey = peer->userpicUniqueKey(userpic);
_disabledFrame.fill(Qt::transparent);
auto p = Painter(&_disabledFrame);
paint(p, 0, 0, wide, size);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::boxBg);
p.setPen(Qt::NoPen);
const auto two = style::ConvertScaleExact(2.5);
const auto half = size / 2.;
const auto rect = QRectF(half, half, half, half).translated(
{ two, two });
p.drawEllipse(rect);
auto pen = st::windowSubTextFg->p;
const auto width = style::ConvertScaleExact(1.5);
const auto dash = 0.55;
const auto dashWithCaps = dash + 1.;
pen.setWidthF(width);
// 11 parts = M_PI * half / ((dashWithCaps + space) * width)
// 11 = M_PI * half / ((dashWithCaps + space) * width)
// space = (M_PI * half / (11 * width)) - dashWithCaps
const auto space = M_PI * half / (11 * width) - dashWithCaps;
pen.setDashPattern(QVector<qreal>{ dash, space });
pen.setDashOffset(1.);
pen.setCapStyle(Qt::RoundCap);
p.setBrush(Qt::NoBrush);
p.setPen(pen);
p.drawEllipse(rect.marginsRemoved({ two, two, two, two }));
}
p.drawImage(x, y, _disabledFrame);
};
}
class LinksController final class LinksController final
: public PeerListController : public PeerListController
, public RowDelegate , public LinkRowDelegate
, public base::has_weak_ptr { , public base::has_weak_ptr {
public: public:
LinksController( LinksController(
@ -334,7 +449,7 @@ public:
not_null<PeerListRow*> row) override; not_null<PeerListRow*> row) override;
Main::Session &session() const override; Main::Session &session() const override;
void rowUpdateRow(not_null<Row*> row) override; void rowUpdateRow(not_null<LinkRow*> row) override;
void rowPaintIcon( void rowPaintIcon(
QPainter &p, QPainter &p,
int x, int x,
@ -454,7 +569,7 @@ void LinkController::addHeader(not_null<Ui::VerticalLayout*> container) {
rpl::single(Ui::Text::Bold(_filterTitle)), rpl::single(Ui::Text::Bold(_filterTitle)),
Ui::Text::WithEntities)), Ui::Text::WithEntities)),
st::settingsFilterDividerLabel)), st::settingsFilterDividerLabel)),
st::settingsFilterDividerLabelPadding); st::filterLinkDividerLabelPadding);
verticalLayout->geometryValue( verticalLayout->geometryValue(
) | rpl::start_with_next([=](const QRect &r) { ) | rpl::start_with_next([=](const QRect &r) {
@ -564,9 +679,26 @@ void LinkController::prepare() {
setupAboveWidget(); setupAboveWidget();
setupBelowWidget(); setupBelowWidget();
const auto countStatus = [&](not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
if (const auto count = chat->count; count > 0) {
return tr::lng_chat_status_members(tr::now, lt_count, count);
}
} else if (const auto channel = peer->asChannel()) {
if (channel->membersCountKnown()) {
return (channel->isBroadcast()
? tr::lng_chat_status_subscribers
: tr::lng_chat_status_members)(
tr::now,
lt_count,
channel->membersCount());
}
}
return QString();
};
for (const auto &history : _data.chats) { for (const auto &history : _data.chats) {
const auto peer = history->peer; const auto peer = history->peer;
auto row = std::make_unique<PeerListRow>(peer); auto row = std::make_unique<ChatRow>(peer, countStatus(peer), false);
const auto raw = row.get(); const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row)); delegate()->peerListAppendRow(std::move(row));
delegate()->peerListSetRowChecked(raw, true); delegate()->peerListSetRowChecked(raw, true);
@ -577,11 +709,14 @@ void LinkController::prepare() {
continue; continue;
} }
const auto peer = history->peer; const auto peer = history->peer;
auto row = std::make_unique<PeerListRow>(peer); const auto error = ErrorForSharing(history);
auto row = std::make_unique<ChatRow>(
peer,
error ? error->status : countStatus(peer),
error.has_value());
const auto raw = row.get(); const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row)); delegate()->peerListAppendRow(std::move(row));
if (const auto error = ErrorForSharing(history)) { if (error) {
raw->setCustomStatus(error->status);
_denied.emplace(peer, error->toast); _denied.emplace(peer, error->toast);
} else if (_data.url.isEmpty()) { } else if (_data.url.isEmpty()) {
_denied.emplace(peer); _denied.emplace(peer);
@ -646,6 +781,11 @@ void LinkController::setupAboveWidget() {
std::move(subtitle), std::move(subtitle),
st::filterLinkSubsectionTitlePadding); st::filterLinkSubsectionTitlePadding);
// Fix label cutting on text change from smaller to longer.
_selected.changes() | rpl::start_with_next([=] {
container->resizeToWidth(container->widthNoMargins());
}, container->lifetime());
delegate()->peerListSetAboveWidget(std::move(wrap)); delegate()->peerListSetAboveWidget(std::move(wrap));
} }
@ -702,7 +842,7 @@ void LinksController::rebuild(const std::vector<InviteLinkData> &rows) {
while (i < rows.size()) { while (i < rows.size()) {
if (i < count) { if (i < count) {
const auto row = delegate()->peerListRowAt(i); const auto row = delegate()->peerListRowAt(i);
static_cast<Row*>(row.get())->update(rows[i]); static_cast<LinkRow*>(row.get())->update(rows[i]);
} else { } else {
appendRow(rows[i]); appendRow(rows[i]);
} }
@ -716,7 +856,7 @@ void LinksController::rebuild(const std::vector<InviteLinkData> &rows) {
} }
void LinksController::rowClicked(not_null<PeerListRow*> row) { void LinksController::rowClicked(not_null<PeerListRow*> row) {
const auto link = static_cast<Row*>(row.get())->data(); const auto link = static_cast<LinkRow*>(row.get())->data();
delegate()->peerListShowBox( delegate()->peerListShowBox(
ShowLinkBox(_window, _currentFilter(), link), ShowLinkBox(_window, _currentFilter(), link),
Ui::LayerOption::KeepOther); Ui::LayerOption::KeepOther);
@ -746,7 +886,7 @@ base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu( base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
QWidget *parent, QWidget *parent,
not_null<PeerListRow*> row) { not_null<PeerListRow*> row) {
const auto real = static_cast<Row*>(row.get()); const auto real = static_cast<LinkRow*>(row.get());
const auto data = real->data(); const auto data = real->data();
const auto link = data.url; const auto link = data.url;
const auto copyLink = [=] { const auto copyLink = [=] {
@ -803,7 +943,7 @@ Main::Session &LinksController::session() const {
} }
void LinksController::appendRow(const InviteLinkData &data) { void LinksController::appendRow(const InviteLinkData &data) {
delegate()->peerListAppendRow(std::make_unique<Row>(this, data)); delegate()->peerListAppendRow(std::make_unique<LinkRow>(this, data));
} }
bool LinksController::removeRow(const QString &link) { bool LinksController::removeRow(const QString &link) {
@ -814,7 +954,7 @@ bool LinksController::removeRow(const QString &link) {
return false; return false;
} }
void LinksController::rowUpdateRow(not_null<Row*> row) { void LinksController::rowUpdateRow(not_null<LinkRow*> row) {
delegate()->peerListUpdateRow(row); delegate()->peerListUpdateRow(row);
} }
@ -927,7 +1067,8 @@ bool GoodForExportFilterLink(
void ExportFilterLink( void ExportFilterLink(
FilterId id, FilterId id,
const std::vector<not_null<PeerData*>> &peers, const std::vector<not_null<PeerData*>> &peers,
Fn<void(Data::ChatFilterLink)> done) { Fn<void(Data::ChatFilterLink)> done,
Fn<void(QString)> fail) {
Expects(!peers.empty()); Expects(!peers.empty());
const auto front = peers.front(); const auto front = peers.front();
@ -950,7 +1091,7 @@ void ExportFilterLink(
data.vinvite()); data.vinvite());
done(link); done(link);
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
done({ .id = id }); fail(error.type());
}).send(); }).send();
} }
@ -987,6 +1128,7 @@ object_ptr<Ui::BoxContent> ShowLinkBox(
const Data::ChatFilter &filter, const Data::ChatFilter &filter,
const Data::ChatFilterLink &link) { const Data::ChatFilterLink &link) {
auto controller = std::make_unique<LinkController>(window, filter, link); auto controller = std::make_unique<LinkController>(window, filter, link);
controller->setStyleOverrides(&st::inviteLinkChatList);
const auto raw = controller.get(); const auto raw = controller.get();
auto initBox = [=](not_null<Ui::BoxContent*> box) { auto initBox = [=](not_null<Ui::BoxContent*> box) {
box->setTitle(!link.title.isEmpty() box->setTitle(!link.title.isEmpty()
@ -995,6 +1137,8 @@ object_ptr<Ui::BoxContent> ShowLinkBox(
raw->hasChangesValue( raw->hasChangesValue(
) | rpl::start_with_next([=](bool has) { ) | rpl::start_with_next([=](bool has) {
box->setCloseByOutsideClick(!has);
box->setCloseByEscape(!has);
box->clearButtons(); box->clearButtons();
if (has) { if (has) {
box->addButton(tr::lng_settings_save(), [=] { box->addButton(tr::lng_settings_save(), [=] {

View file

@ -33,7 +33,8 @@ class SessionController;
void ExportFilterLink( void ExportFilterLink(
FilterId id, FilterId id,
const std::vector<not_null<PeerData*>> &peers, const std::vector<not_null<PeerData*>> &peers,
Fn<void(Data::ChatFilterLink)> done); Fn<void(Data::ChatFilterLink)> done,
Fn<void(QString)> fail);
object_ptr<Ui::BoxContent> ShowLinkBox( object_ptr<Ui::BoxContent> ShowLinkBox(
not_null<Window::SessionController*> window, not_null<Window::SessionController*> window,

View file

@ -800,6 +800,9 @@ inviteLinkList: PeerList(defaultPeerList) {
item: inviteLinkListItem; item: inviteLinkListItem;
padding: margins(0px, 4px, 0px, 0px); padding: margins(0px, 4px, 0px, 0px);
} }
inviteLinkChatList: PeerList(peerListBox) {
padding: margins(0px, 4px, 0px, 6px);
}
inviteLinkAdminsList: PeerList(inviteLinkList) { inviteLinkAdminsList: PeerList(inviteLinkList) {
item: PeerListItem(inviteLinkListItem) { item: PeerListItem(inviteLinkListItem) {
photoPosition: point(16px, 9px); photoPosition: point(16px, 9px);

View file

@ -560,6 +560,7 @@ filterInviteButtonBadgeStyle: TextStyle(defaultTextStyle) {
} }
filterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px); filterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px);
filterInviteButtonBadgeSkip: 5px; filterInviteButtonBadgeSkip: 5px;
filterLinkDividerLabelPadding: margins(0px, 10px, 0px, 17px);
filterLinkTitlePadding: margins(0px, 15px, 0px, 17px); filterLinkTitlePadding: margins(0px, 15px, 0px, 17px);
filterLinkAboutTextStyle: TextStyle(defaultTextStyle) { filterLinkAboutTextStyle: TextStyle(defaultTextStyle) {
font: font(12px); font: font(12px);

View file

@ -150,9 +150,12 @@ struct FilterRow {
const Data::ChatFilter &filter, const Data::ChatFilter &filter,
bool check = false) { bool check = false) {
const auto count = ComputeCount(session, filter, check); const auto count = ComputeCount(session, filter, check);
return count const auto result = count
? tr::lng_filters_chats_count(tr::now, lt_count_short, count) ? tr::lng_filters_chats_count(tr::now, lt_count_short, count)
: tr::lng_filters_no_chats(tr::now); : tr::lng_filters_no_chats(tr::now);
return filter.community()
? result + QString::fromUtf8(" \xE2\x80\xA2 shareable folder")
: result;
} }
FilterRowButton::FilterRowButton( FilterRowButton::FilterRowButton(
@ -327,6 +330,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) {
not_null<Ui::VerticalLayout*> container) { not_null<Ui::VerticalLayout*> container) {
auto &lifetime = container->lifetime(); auto &lifetime = container->lifetime();
const auto weak = Ui::MakeWeak(container);
const auto session = &controller->session(); const auto session = &controller->session();
const auto limit = [=] { const auto limit = [=] {
return Data::PremiumLimits(session).dialogFiltersCurrent(); return Data::PremiumLimits(session).dialogFiltersCurrent();