diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index 34ad40cf9..a288d10a4 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -123,10 +123,16 @@ auto InviteLinks::prepend( auto &links = i->second; const auto permanent = lookupPermanent(links); if (link.permanent) { + auto update = Update{ .peer = peer }; if (permanent) { + update.was = permanent->link; permanent->revoked = true; } editPermanentLink(peer, link.link); + if (permanent) { + update.now = *permanent; + _updates.fire(std::move(update)); + } } ++links.count; if (permanent && !link.permanent) { @@ -135,6 +141,7 @@ auto InviteLinks::prepend( links.links.insert(begin(links.links), link); } notify(peer); + _updates.fire(Update{ .peer = peer, .now = link }); return link; } @@ -205,6 +212,11 @@ void InviteLinks::performEdit( for (const auto &callback : *callbacks) { callback(link); } + _updates.fire(Update{ + .peer = peer, + .was = key.link, + .now = link + }); }); }).fail([=](const RPCError &error) { _editCallbacks.erase(key); @@ -298,6 +310,13 @@ rpl::producer InviteLinks::joinedFirstSliceValue( })); } +auto InviteLinks::updates( + not_null peer) const -> rpl::producer { + return _updates.events() | rpl::filter([=](const Update &update) { + return update.peer == peer; + }); +} + void InviteLinks::requestJoinedFirstSlice(LinkKey key) { if (_firstJoinedRequests.contains(key)) { return; @@ -422,7 +441,6 @@ auto InviteLinks::parse( .usageLimit = data.vusage_limit().value_or_empty(), .usage = data.vusage().value_or_empty(), .permanent = data.is_permanent(), - .expired = data.is_expired(), .revoked = data.is_revoked(), }; }); diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index cec3a9c03..c3bc5a97c 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -20,7 +20,6 @@ struct InviteLink { int usageLimit = 0; int usage = 0; bool permanent = false; - bool expired = false; bool revoked = false; }; @@ -39,12 +38,19 @@ struct JoinedByLinkSlice { int count = 0; }; +struct InviteLinkUpdate { + not_null peer; + QString was; + std::optional now; +}; + class InviteLinks final { public: explicit InviteLinks(not_null api); using Link = InviteLink; using Links = PeerInviteLinks; + using Update = InviteLinkUpdate; void create( not_null peer, @@ -77,6 +83,8 @@ public: not_null peer, const QString &link, int fullCount); + [[nodiscard]] rpl::producer updates( + not_null peer) const; void requestMoreLinks( not_null peer, @@ -152,6 +160,8 @@ private: std::vector>> _createCallbacks; base::flat_map>> _editCallbacks; + rpl::event_stream _updates; + }; } // namespace Api diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 10b344318..fede1dd8d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kPreloadPages = 2; +constexpr auto kFullArcLength = 360 * 16; enum class Color { Permanent, @@ -100,9 +101,11 @@ public: const InviteLinkData &data, TimeId now); - void update(const InviteLinkData &data); + void update(const InviteLinkData &data, TimeId now); + void updateExpireProgress(TimeId now); [[nodiscard]] InviteLinkData data() const; + [[nodiscard]] crl::time updateExpireIn() const; QString generateName() override; QString generateShortName() override; @@ -159,7 +162,6 @@ private: [[nodiscard]] Color ComputeColor( const InviteLinkData &link, float64 progress) { - const auto startDate = link.startDate ? link.startDate : link.date; return link.revoked ? Color::Revoked : (progress >= 1.) @@ -171,8 +173,20 @@ private: : Color::Permanent; } -[[nodiscard]] QString ComputeStatus(const InviteLinkData &link) { - return "nothing"; +[[nodiscard]] QString ComputeStatus(const InviteLinkData &link, TimeId now) { + auto result = link.usage + ? tr::lng_group_invite_joined(tr::now, lt_count_decimal, link.usage) + : tr::lng_group_invite_no_joined(tr::now); + const auto add = [&](const QString &text) { + result += QString::fromUtf8(" \xE2\xB8\xB1 ") + text; + }; + if (link.revoked) { + add(tr::lng_group_invite_link_revoked(tr::now)); + } else if ((link.usageLimit > 0 && link.usage >= link.usageLimit) + || (link.expireDate > 0 && now >= link.expireDate)) { + add(tr::lng_group_invite_link_expired(tr::now)); + } + return result; } void CopyLink(const QString &link) { @@ -338,21 +352,45 @@ Row::Row( , _data(data) , _progressTillExpire(ComputeProgress(data, now)) , _color(ComputeColor(data, _progressTillExpire)) { - setCustomStatus(ComputeStatus(data)); + setCustomStatus(ComputeStatus(data, now)); } -void Row::update(const InviteLinkData &data) { +void Row::update(const InviteLinkData &data, TimeId now) { _data = data; - _progressTillExpire = ComputeProgress(data, base::unixtime::now()); + _progressTillExpire = ComputeProgress(data, now); _color = ComputeColor(data, _progressTillExpire); - setCustomStatus(ComputeStatus(data)); + setCustomStatus(ComputeStatus(data, now)); _delegate->rowUpdateRow(this); } +void Row::updateExpireProgress(TimeId now) { + const auto updated = ComputeProgress(_data, now); + if (std::round(_progressTillExpire * 360) != std::round(updated * 360)) { + _progressTillExpire = updated; + const auto color = ComputeColor(_data, _progressTillExpire); + if (_color != color) { + _color = color; + setCustomStatus(ComputeStatus(_data, now)); + } + _delegate->rowUpdateRow(this); + } +} + InviteLinkData Row::data() const { return _data; } +crl::time Row::updateExpireIn() const { + if (_color != Color::Expiring && _color != Color::ExpireSoon) { + return 0; + } + const auto start = _data.startDate ? _data.startDate : _data.date; + if (_data.expireDate <= start) { + return 0; + } + return std::round((_data.expireDate - start) * crl::time(1000) / 720.); +} + QString Row::generateName() { auto result = _data.link; return result.replace(qstr("https://"), QString()); @@ -425,6 +463,13 @@ public: private: void appendRow(const InviteLinkData &data, TimeId now); + void prependRow(const InviteLinkData &data, TimeId now); + void updateRow(const InviteLinkData &data, TimeId now); + bool removeRow(const QString &link); + + void checkExpiringTimer(not_null row); + void expiringProgressTimer(); + [[nodiscard]] base::unique_qptr createRowContextMenu( QWidget *parent, not_null row); @@ -433,6 +478,9 @@ private: bool _revoked = false; base::unique_qptr _menu; + base::flat_set> _expiringRows; + base::Timer _updateExpiringTimer; + std::array _icons; rpl::lifetime _lifetime; @@ -440,13 +488,31 @@ private: Controller::Controller(not_null peer, bool revoked) : _peer(peer) -, _revoked(revoked) { +, _revoked(revoked) +, _updateExpiringTimer([=] { expiringProgressTimer(); }) { style::PaletteChanged( ) | rpl::start_with_next([=] { for (auto &image : _icons) { image = QImage(); } }, _lifetime); + + peer->session().api().inviteLinks().updates( + peer + ) | rpl::start_with_next([=](const Api::InviteLinkUpdate &update) { + const auto now = base::unixtime::now(); + if (!update.now + || (!update.was.isEmpty() && update.now->revoked != _revoked)) { + if (removeRow(update.was)) { + delegate()->peerListRefreshRows(); + } + } else if (update.was.isEmpty()) { + prependRow(*update.now, now); + delegate()->peerListRefreshRows(); + } else { + updateRow(*update.now, now); + } + }, _lifetime); } void Controller::prepare() { @@ -538,6 +604,60 @@ void Controller::appendRow(const InviteLinkData &data, TimeId now) { delegate()->peerListAppendRow(std::make_unique(this, data, now)); } +void Controller::prependRow(const InviteLinkData &data, TimeId now) { + delegate()->peerListPrependRow(std::make_unique(this, data, now)); +} + +void Controller::updateRow(const InviteLinkData &data, TimeId now) { + if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) { + const auto real = static_cast(row); + real->update(data, now); + checkExpiringTimer(real); + delegate()->peerListUpdateRow(row); + } +} + +bool Controller::removeRow(const QString &link) { + if (const auto row = delegate()->peerListFindRow(ComputeRowId(link))) { + delegate()->peerListRemoveRow(row); + return true; + } + return false; +} + +void Controller::checkExpiringTimer(not_null row) { + const auto updateIn = row->updateExpireIn(); + if (updateIn > 0) { + _expiringRows.emplace(row); + if (!_updateExpiringTimer.isActive() + || updateIn < _updateExpiringTimer.remainingTime()) { + _updateExpiringTimer.callOnce(updateIn); + } + } else { + _expiringRows.remove(row); + } +} + +void Controller::expiringProgressTimer() { + const auto now = base::unixtime::now(); + auto minimalIn = 0; + for (auto i = begin(_expiringRows); i != end(_expiringRows);) { + (*i)->updateExpireProgress(now); + const auto updateIn = (*i)->updateExpireIn(); + if (!updateIn) { + i = _expiringRows.erase(i); + } else { + ++i; + if (!minimalIn || minimalIn > updateIn) { + minimalIn = updateIn; + } + } + } + if (minimalIn) { + _updateExpiringTimer.callOnce(minimalIn); + } +} + void Controller::rowUpdateRow(not_null row) { delegate()->peerListUpdateRow(row); } @@ -578,7 +698,6 @@ void Controller::rowPaintIcon( } p.drawImage(x + skip, y + skip, icon); if (progress >= 0. && progress < 1.) { - const auto kFullArcLength = 360 * 16; const auto stroke = st::inviteLinkIconStroke; auto hq = PainterHighQualityEnabler(p); auto pen = QPen((*bg)->c);