init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,424 @@
/*
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/add_bot_to_chat_box.h"
#include "lang/lang_keys.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_histories.h"
#include "history/history.h"
#include "main/main_session.h"
#include "boxes/peers/edit_participant_box.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/ui_utility.h"
#include "base/random.h"
#include "base/weak_ptr.h"
#include "api/api_chat_participants.h"
#include "window/window_session_controller.h"
#include "apiwrap.h"
#include "styles/style_boxes.h"
namespace {
class Controller final
: public PeerListController
, public base::has_weak_ptr {
public:
Controller(
not_null<Main::Session*> session,
rpl::producer<not_null<PeerData*>> add,
Fn<void(not_null<PeerData*> chat)> callback);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
private:
void addRow(not_null<PeerData*> peer);
const not_null<Main::Session*> _session;
Fn<void(not_null<PeerData*> chat)> _callback;
std::vector<not_null<PeerData*>> _list;
bool _prepared = false;
bool _refreshing = false;
rpl::lifetime _lifetime;
};
Controller::Controller(
not_null<Main::Session*> session,
rpl::producer<not_null<PeerData*>> add,
Fn<void(not_null<PeerData*> chat)> callback)
: _session(session)
, _callback(std::move(callback)) {
std::move(
add
) | rpl::on_next([=](not_null<PeerData*> peer) {
if (_prepared) {
addRow(peer);
} else {
_list.push_back(peer);
}
}, _lifetime);
}
Main::Session &Controller::session() const {
return *_session;
}
void Controller::prepare() {
_prepared = true;
for (const auto &peer : _list) {
addRow(peer);
}
}
void Controller::rowClicked(not_null<PeerListRow*> row) {
_callback(row->peer());
}
void Controller::addRow(not_null<PeerData*> peer) {
if (delegate()->peerListFindRow(peer->id.value)) {
return;
}
delegate()->peerListAppendRow(std::make_unique<PeerListRow>(peer));
if (!_refreshing) {
_refreshing = true;
Ui::PostponeCall(this, [=] {
_refreshing = false;
delegate()->peerListRefreshRows();
});
}
}
} // namespace
void AddBotToGroupBoxController::Start(
not_null<Window::SessionController*> controller,
not_null<UserData*> bot,
Scope scope,
const QString &token,
ChatAdminRights requestedRights) {
if (controller->showFrozenError()) {
return;
}
auto initBox = [=](not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [box] { box->closeBox(); });
};
controller->show(Box<PeerListBox>(
std::make_unique<AddBotToGroupBoxController>(
controller,
bot,
scope,
token,
requestedRights),
std::move(initBox)));
}
AddBotToGroupBoxController::AddBotToGroupBoxController(
not_null<Window::SessionController*> controller,
not_null<UserData*> bot,
Scope scope,
const QString &token,
ChatAdminRights requestedRights)
: ChatsListBoxController(std::unique_ptr<PeerListSearchController>())
, _controller(controller)
, _bot(bot)
, _scope(scope)
, _token(token)
, _requestedRights(requestedRights)
, _adminToGroup((scope == Scope::GroupAdmin)
|| (scope == Scope::All && _bot->botInfo->groupAdminRights != 0))
, _adminToChannel((scope == Scope::ChannelAdmin)
|| (scope == Scope::All && _bot->botInfo->channelAdminRights != 0))
, _memberToGroup(scope == Scope::All) {
}
Main::Session &AddBotToGroupBoxController::session() const {
return _bot->session();
}
void AddBotToGroupBoxController::rowClicked(not_null<PeerListRow*> row) {
addBotToGroup(row->peer());
}
void AddBotToGroupBoxController::requestExistingRights(
not_null<ChannelData*> channel) {
if (_existingRightsChannel == channel) {
return;
}
_existingRightsChannel = channel;
_bot->session().api().request(_existingRightsRequestId).cancel();
_existingRightsRequestId = _bot->session().api().request(
MTPchannels_GetParticipant(
_existingRightsChannel->inputChannel(),
_bot->input())
).done([=](const MTPchannels_ChannelParticipant &result) {
result.match([&](const MTPDchannels_channelParticipant &data) {
channel->owner().processUsers(data.vusers());
const auto participant = Api::ChatParticipant(
data.vparticipant(),
channel);
_existingRights = participant.rights().flags;
_existingRank = participant.rank();
_promotedSince = participant.promotedSince();
_promotedBy = participant.by();
addBotToGroup(_existingRightsChannel);
});
}).fail([=] {
_existingRights = ChatAdminRights();
_existingRank = QString();
_promotedSince = 0;
_promotedBy = 0;
addBotToGroup(_existingRightsChannel);
}).send();
}
void AddBotToGroupBoxController::addBotToGroup(not_null<PeerData*> chat) {
if (const auto megagroup = chat->asMegagroup()) {
if (!megagroup->canAddMembers()) {
_controller->show(
Ui::MakeInformBox(tr::lng_error_cant_add_member()));
return;
}
}
if (_existingRightsChannel != chat) {
_existingRights = {};
_existingRank = QString();
_existingRightsChannel = nullptr;
_promotedSince = 0;
_promotedBy = 0;
_bot->session().api().request(_existingRightsRequestId).cancel();
}
const auto requestedAddAdmin = (_scope == Scope::GroupAdmin)
|| (_scope == Scope::ChannelAdmin);
if (chat->isChannel()
&& requestedAddAdmin
&& !_existingRights.has_value()) {
requestExistingRights(chat->asChannel());
return;
}
const auto bot = _bot;
const auto controller = _controller;
const auto close = [=](auto&&...) {
using Way = Window::SectionShow::Way;
controller->hideLayer();
controller->showPeerHistory(chat, Way::ClearStack, ShowAtUnreadMsgId);
};
const auto rights = requestedAddAdmin
? _requestedRights
: (chat->isBroadcast()
&& chat->asBroadcast()->canAddAdmins())
? bot->botInfo->channelAdminRights
: ((chat->isMegagroup() && chat->asMegagroup()->canAddAdmins())
|| (chat->isChat() && chat->asChat()->canAddAdmins()))
? bot->botInfo->groupAdminRights
: ChatAdminRights();
const auto addingAdmin = requestedAddAdmin || (rights != 0);
const auto show = controller->uiShow();
if (addingAdmin) {
const auto scope = _scope;
const auto token = _token;
const auto done = [=](
ChatAdminRightsInfo newRights,
const QString &rank) {
if (scope == Scope::GroupAdmin) {
chat->session().api().sendBotStart(show, bot, chat, token);
}
close();
};
const auto saveCallback = SaveAdminCallback(
show,
chat,
bot,
done,
close);
auto box = Box<EditAdminBox>(
chat,
bot,
ChatAdminRightsInfo(rights),
_existingRank,
_promotedSince,
_promotedBy ? chat->owner().user(_promotedBy).get() : nullptr,
EditAdminBotFields{
_token,
_existingRights.value_or(ChatAdminRights()),
});
box->setSaveCallback(saveCallback);
controller->show(std::move(box));
} else {
auto callback = crl::guard(this, [=] {
AddBotToGroup(show, bot, chat, _token);
controller->hideLayer();
});
controller->show(Ui::MakeConfirmBox({
tr::lng_bot_sure_invite(tr::now, lt_group, chat->name()),
std::move(callback),
}));
}
}
auto AddBotToGroupBoxController::createRow(not_null<History*> history)
-> std::unique_ptr<ChatsListBoxController::Row> {
if (!needToCreateRow(history->peer)) {
return nullptr;
}
return std::make_unique<Row>(history);
}
bool AddBotToGroupBoxController::needToCreateRow(
not_null<PeerData*> peer) const {
if (const auto chat = peer->asChat()) {
if (onlyAdminToGroup()) {
return chat->canAddAdmins();
} else if (_adminToGroup && chat->canAddAdmins()) {
_groups.fire_copy(peer);
} else if (!onlyAdminToChannel()) {
return chat->canAddMembers();
}
} else if (const auto group = peer->asMegagroup()) {
if (onlyAdminToGroup()) {
return group->canAddAdmins();
} else if (_adminToGroup && group->canAddAdmins()) {
_groups.fire_copy(peer);
} else if (!onlyAdminToChannel()) {
return group->canAddMembers();
}
} else if (const auto channel = peer->asBroadcast()) {
if (onlyAdminToChannel()) {
return channel->canAddAdmins();
} else if (_adminToChannel && channel->canAddAdmins()) {
_channels.fire_copy(peer);
}
}
return false;
}
QString AddBotToGroupBoxController::emptyBoxText() const {
return !session().data().chatsListLoaded()
? tr::lng_contacts_loading(tr::now)
: _adminToChannel
? tr::lng_bot_no_chats(tr::now)
: tr::lng_bot_no_groups(tr::now);
}
QString AddBotToGroupBoxController::noResultsText() const {
return !session().data().chatsListLoaded()
? tr::lng_contacts_loading(tr::now)
: _adminToChannel
? tr::lng_bot_chats_not_found(tr::now)
: tr::lng_bot_groups_not_found(tr::now);
}
void AddBotToGroupBoxController::updateLabels() {
setSearchNoResultsText(noResultsText());
}
object_ptr<Ui::RpWidget> AddBotToGroupBoxController::prepareAdminnedChats() {
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
const auto callback = [=](not_null<PeerData*> chat) {
addBotToGroup(chat);
};
const auto addList = [&](
tr::phrase<> subtitle,
rpl::event_stream<not_null<PeerData*>> &items) {
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
wrap->hide(anim::type::instant);
const auto inner = wrap->entity();
inner->add(CreatePeerListSectionSubtitle(inner, subtitle()));
const auto delegate = inner->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = inner->lifetime().make_state<Controller>(
&session(),
items.events(),
callback);
const auto content = inner->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
items.events() | rpl::take(1) | rpl::on_next([=] {
wrap->show(anim::type::instant);
}, inner->lifetime());
};
if (_adminToChannel) {
addList(tr::lng_bot_channels_manage, _channels);
}
if (_adminToGroup) {
addList(tr::lng_bot_groups_manage, _groups);
}
rpl::merge(
_groups.events(),
_channels.events()
) | rpl::take(1) | rpl::on_next([=] {
container->add(CreatePeerListSectionSubtitle(
container,
tr::lng_bot_groups()));
}, container->lifetime());
return result;
}
bool AddBotToGroupBoxController::onlyAdminToGroup() const {
return _adminToGroup && !_memberToGroup && !_adminToChannel;
}
bool AddBotToGroupBoxController::onlyAdminToChannel() const {
return _adminToChannel && !_memberToGroup && !_adminToGroup;
}
void AddBotToGroupBoxController::prepareViewHook() {
delegate()->peerListSetTitle(_adminToChannel
? tr::lng_bot_choose_chat()
: tr::lng_bot_choose_group());
if ((_adminToGroup && !onlyAdminToGroup())
|| (_adminToChannel && !onlyAdminToChannel())) {
delegate()->peerListSetAboveWidget(prepareAdminnedChats());
}
updateLabels();
session().data().chatsListLoadedEvents(
) | rpl::filter([=](Data::Folder *folder) {
return !folder;
}) | rpl::on_next([=] {
updateLabels();
}, lifetime());
}
void AddBotToGroup(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
not_null<PeerData*> chat,
const QString &startToken) {
if (!startToken.isEmpty()) {
chat->session().api().sendBotStart(show, bot, chat, startToken);
} else {
chat->session().api().chatParticipants().add(show, chat, { 1, bot });
}
if (const auto window = chat->session().tryResolveWindow()) {
window->showPeerHistory(chat);
}
}

View File

@@ -0,0 +1,84 @@
/*
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 "boxes/peer_list_controllers.h"
#include "data/data_chat_participant_status.h"
class AddBotToGroupBoxController
: public ChatsListBoxController
, public base::has_weak_ptr {
public:
enum class Scope {
None,
GroupAdmin,
ChannelAdmin,
All,
};
static void Start(
not_null<Window::SessionController*> controller,
not_null<UserData*> bot,
Scope scope = Scope::All,
const QString &token = QString(),
ChatAdminRights requestedRights = {});
AddBotToGroupBoxController(
not_null<Window::SessionController*> controller,
not_null<UserData*> bot,
Scope scope,
const QString &token,
ChatAdminRights requestedRights);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
protected:
std::unique_ptr<Row> createRow(not_null<History*> history) override;
void prepareViewHook() override;
QString emptyBoxText() const override;
private:
[[nodiscard]] object_ptr<Ui::RpWidget> prepareAdminnedChats();
[[nodiscard]] bool onlyAdminToGroup() const;
[[nodiscard]] bool onlyAdminToChannel() const;
bool needToCreateRow(not_null<PeerData*> peer) const;
QString noResultsText() const;
void updateLabels();
void addBotToGroup(not_null<PeerData*> chat);
void requestExistingRights(not_null<ChannelData*> channel);
const not_null<Window::SessionController*> _controller;
const not_null<UserData*> _bot;
const Scope _scope = Scope::None;
const QString _token;
const ChatAdminRights _requestedRights;
ChannelData *_existingRightsChannel = nullptr;
mtpRequestId _existingRightsRequestId = 0;
std::optional<ChatAdminRights> _existingRights;
QString _existingRank;
TimeId _promotedSince = 0;
UserId _promotedBy = 0;
rpl::event_stream<not_null<PeerData*>> _groups;
rpl::event_stream<not_null<PeerData*>> _channels;
bool _adminToGroup = false;
bool _adminToChannel = false;
bool _memberToGroup = false;
};
void AddBotToGroup(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
not_null<PeerData*> chat,
const QString &startToken);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,227 @@
/*
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 "boxes/peer_list_controllers.h"
#include "boxes/peers/edit_participants_box.h"
struct ChatAdminRightsInfo;
struct ChatRestrictionsInfo;
namespace Window {
class SessionNavigation;
} // namespace Window
class AddParticipantsBoxController : public ContactsBoxController {
public:
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<ChatData*> chat);
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel);
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
base::flat_set<not_null<UserData*>> &&alreadyIn);
explicit AddParticipantsBoxController(not_null<Main::Session*> session);
explicit AddParticipantsBoxController(not_null<PeerData*> peer);
AddParticipantsBoxController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> &&alreadyIn);
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
void rowClicked(not_null<PeerListRow*> row) override;
void itemDeselectedHook(not_null<PeerData*> peer) override;
protected:
void prepareViewHook() override;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
virtual bool needsInviteLinkButton();
private:
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
base::flat_set<not_null<UserData*>> &&alreadyIn,
bool justCreated);
base::weak_qptr<Ui::BoxContent> showBox(object_ptr<Ui::BoxContent> box) const;
void addInviteLinkButton();
void inviteSelectedUsers(
not_null<PeerListBox*> box,
Fn<void()> done) const;
void subscribeToMigration();
int alreadyInCount() const;
bool isAlreadyIn(not_null<UserData*> user) const;
int fullCount() const;
void updateTitle();
PeerData *_peer = nullptr;
base::flat_set<not_null<UserData*>> _alreadyIn;
};
struct ForbiddenInvites {
std::vector<not_null<UserData*>> users;
std::vector<not_null<UserData*>> premiumAllowsInvite;
std::vector<not_null<UserData*>> premiumAllowsWrite;
[[nodiscard]] bool empty() const {
return users.empty();
}
};
[[nodiscard]] ForbiddenInvites CollectForbiddenUsers(
not_null<Main::Session*> session,
const MTPmessages_InvitedUsers &result);
bool ChatInviteForbidden(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
ForbiddenInvites forbidden);
// Adding an admin, banned or restricted user from channel members
// with search + contacts search + global search.
class AddSpecialBoxController
: public PeerListController
, public base::has_weak_ptr {
public:
using Role = ParticipantsBoxController::Role;
using AdminDoneCallback = Fn<void(
not_null<UserData*> user,
ChatAdminRightsInfo adminRights,
const QString &rank)>;
using BannedDoneCallback = Fn<void(
not_null<PeerData*> participant,
ChatRestrictionsInfo bannedRights)>;
AddSpecialBoxController(
not_null<PeerData*> peer,
Role role,
AdminDoneCallback adminDoneCallback,
BannedDoneCallback bannedDoneCallback);
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
[[nodiscard]] Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
[[nodiscard]] std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
private:
template <typename Callback>
bool checkInfoLoaded(not_null<PeerData*> participant, Callback callback);
void prepareChatRows(not_null<ChatData*> chat);
void rebuildChatRows(not_null<ChatData*> chat);
void showAdmin(not_null<UserData*> user, bool sure = false);
void editAdminDone(
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank);
void showRestricted(not_null<UserData*> user, bool sure = false);
void editRestrictedDone(
not_null<PeerData*> participant,
ChatRestrictionsInfo rights);
void kickUser(not_null<PeerData*> participant, bool sure = false);
bool appendRow(not_null<PeerData*> participant);
bool prependRow(not_null<UserData*> user);
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const;
void subscribeToMigration();
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);
base::weak_qptr<Ui::BoxContent> showBox(object_ptr<Ui::BoxContent> box) const;
not_null<PeerData*> _peer;
MTP::Sender _api;
Role _role = Role::Admins;
int _offset = 0;
mtpRequestId _loadRequestId = 0;
bool _allLoaded = false;
ParticipantsAdditionalData _additional;
std::unique_ptr<ParticipantsOnlineSorter> _onlineSorter;
Ui::BoxPointer _editBox;
base::weak_qptr<Ui::BoxContent> _editParticipantBox;
AdminDoneCallback _adminDoneCallback;
BannedDoneCallback _bannedDoneCallback;
protected:
bool _excludeSelf = true;
};
// Finds chat/channel members, then contacts, then global search results.
class AddSpecialBoxSearchController : public PeerListSearchController {
public:
using Role = ParticipantsBoxController::Role;
AddSpecialBoxSearchController(
not_null<PeerData*> peer,
not_null<ParticipantsAdditionalData*> additional);
void searchQuery(const QString &query) override;
bool isLoading() override;
bool loadMoreRows() override;
private:
struct CacheEntry {
MTPchannels_ChannelParticipants result;
int requestedCount = 0;
};
struct Query {
QString text;
int offset = 0;
};
void searchOnServer();
bool searchParticipantsInCache();
void searchParticipantsDone(
mtpRequestId requestId,
const MTPchannels_ChannelParticipants &result,
int requestedCount);
bool searchGlobalInCache();
void searchGlobalDone(
mtpRequestId requestId,
const MTPcontacts_Found &result);
void requestParticipants();
void addChatMembers(not_null<ChatData*> chat);
void addChatsContacts();
void requestGlobal();
void subscribeToMigration();
not_null<PeerData*> _peer;
not_null<ParticipantsAdditionalData*> _additional;
MTP::Sender _api;
base::Timer _timer;
QString _query;
mtpRequestId _requestId = 0;
int _offset = 0;
bool _participantsLoaded = false;
bool _chatsContactsAdded = false;
bool _chatMembersAdded = false;
bool _globalLoaded = false;
std::map<QString, CacheEntry> _participantsCache;
std::map<mtpRequestId, Query> _participantsQueries;
std::map<QString, MTPcontacts_Found> _globalCache;
std::map<mtpRequestId, QString> _globalQueries;
};

View File

@@ -0,0 +1,546 @@
/*
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/choose_peer_box.h"
#include "apiwrap.h" // ApiWrap::botCommonGroups / requestBotCommonGroups.
#include "boxes/add_contact_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_limits_box.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item_reply_markup.h"
#include "info/profile/info_profile_icon.h"
#include "lang/lang_keys.h"
#include "main/main_session.h" // Session::api().
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
namespace {
class ChoosePeerBoxController final
: public ChatsListBoxController
, public base::has_weak_ptr {
public:
ChoosePeerBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot,
RequestPeerQuery query,
Fn<void(std::vector<not_null<PeerData*>>)> callback);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
[[nodiscard]] rpl::producer<int> selectedCountValue() const;
void submit();
QString savedMessagesChatStatus() const override {
return {};
}
private:
void prepareViewHook() override;
std::unique_ptr<Row> createRow(not_null<History*> history) override;
QString emptyBoxText() const override;
void prepareRestrictions();
const not_null<Window::SessionNavigation*> _navigation;
not_null<UserData*> _bot;
RequestPeerQuery _query;
base::flat_set<not_null<PeerData*>> _commonGroups;
base::flat_set<not_null<PeerData*>> _selected;
rpl::variable<int> _selectedCount;
Fn<void(std::vector<not_null<PeerData*>>)> _callback;
};
using RightsMap = std::vector<std::pair<ChatAdminRight, tr::phrase<>>>;
[[nodiscard]] RightsMap GroupRights() {
using Flag = ChatAdminRight;
return {
{ Flag::ChangeInfo, tr::lng_request_group_change_info },
{ Flag::DeleteMessages, tr::lng_request_group_delete_messages },
{ Flag::BanUsers, tr::lng_request_group_ban_users },
{ Flag::InviteByLinkOrAdd, tr::lng_request_group_invite },
{ Flag::PinMessages, tr::lng_request_group_pin_messages },
{ Flag::ManageTopics, tr::lng_request_group_manage_topics },
{ Flag::ManageCall, tr::lng_request_group_manage_video_chats },
{ Flag::Anonymous, tr::lng_request_group_anonymous },
{ Flag::AddAdmins, tr::lng_request_group_add_admins },
};
}
[[nodiscard]] RightsMap BroadcastRights() {
using Flag = ChatAdminRight;
return {
{ Flag::ChangeInfo, tr::lng_request_channel_change_info },
{ Flag::PostMessages, tr::lng_request_channel_post_messages },
{ Flag::EditMessages, tr::lng_request_channel_edit_messages },
{ Flag::DeleteMessages, tr::lng_request_channel_delete_messages },
{ Flag::InviteByLinkOrAdd, tr::lng_request_channel_add_subscribers },
{ Flag::ManageCall, tr::lng_request_channel_manage_livestreams },
{ Flag::ManageDirect, tr::lng_request_channel_manage_direct },
{ Flag::AddAdmins, tr::lng_request_channel_add_admins },
{ Flag::BanUsers, tr::lng_request_group_ban_users },
};
}
[[nodiscard]] QString RightsText(
ChatAdminRights rights,
const RightsMap &phrases) {
auto list = QStringList();
for (const auto &[flag, phrase] : phrases) {
if (rights & flag) {
list.push_back(phrase(tr::now));
}
}
const auto count = list.size();
if (!count) {
return QString();
}
const auto last = list.back();
return (count > 1)
? tr::lng_request_peer_rights_and(
tr::now,
lt_rights,
list.mid(0, count - 1).join(", "),
lt_last,
last)
: last;
}
[[nodiscard]] QString GroupRightsText(ChatAdminRights rights) {
return RightsText(rights, GroupRights());
}
[[nodiscard]] QString BroadcastRightsText(ChatAdminRights rights) {
return RightsText(rights, BroadcastRights());
}
[[nodiscard]] QStringList RestrictionsList(RequestPeerQuery query) {
using Type = RequestPeerQuery::Type;
using Restriction = RequestPeerQuery::Restriction;
auto result = QStringList();
const auto addRestriction = [&](
Restriction value,
tr::phrase<> yes,
tr::phrase<> no) {
if (value == Restriction::Yes) {
result.push_back(yes(tr::now));
} else if (value == Restriction::No) {
result.push_back(no(tr::now));
}
};
const auto addRights = [&](const QString &rights) {
if (!rights.isEmpty()) {
result.push_back(
tr::lng_request_peer_rights(tr::now, lt_rights, rights));
}
};
switch (query.type) {
case Type::User:
if (query.userIsBot != Restriction::Yes) {
addRestriction(
query.userIsPremium,
tr::lng_request_user_premium_yes,
tr::lng_request_user_premium_no);
}
break;
case Type::Group:
addRestriction(
query.hasUsername,
tr::lng_request_group_public_yes,
tr::lng_request_group_public_no);
addRestriction(
query.groupIsForum,
tr::lng_request_group_topics_yes,
tr::lng_request_group_topics_no);
if (query.amCreator) {
result.push_back(tr::lng_request_group_am_owner(tr::now));
} else {
addRights(GroupRightsText(query.myRights));
}
break;
case Type::Broadcast:
addRestriction(
query.hasUsername,
tr::lng_request_channel_public_yes,
tr::lng_request_channel_public_no);
if (query.amCreator) {
result.push_back(tr::lng_request_channel_am_owner(tr::now));
} else {
addRights(BroadcastRightsText(query.myRights));
}
break;
}
return result;
}
object_ptr<Ui::BoxContent> MakeConfirmBox(
not_null<UserData*> bot,
not_null<PeerData*> peer,
RequestPeerQuery query,
Fn<void()> confirmed) {
const auto name = peer->name();
const auto botName = bot->name();
auto text = tr::lng_request_peer_confirm(
tr::now,
lt_chat,
tr::bold(name),
lt_bot,
tr::bold(botName),
tr::marked);
if (!peer->isUser()) {
const auto rights = peer->isBroadcast()
? BroadcastRightsText(query.botRights)
: GroupRightsText(query.botRights);
if (!rights.isEmpty()) {
text.append('\n').append('\n').append(
tr::lng_request_peer_confirm_rights(
tr::now,
lt_bot,
tr::bold(botName),
lt_chat,
tr::bold(name),
lt_rights,
TextWithEntities{ rights },
tr::marked));
} else if (!peer->isBroadcast() && query.isBotParticipant) {
const auto common = bot->session().api().botCommonGroups(bot);
if (!common || !ranges::contains(*common, peer)) {
text.append('\n').append('\n').append(
tr::lng_request_peer_confirm_add(
tr::now,
lt_bot,
tr::bold(botName),
lt_chat,
tr::bold(name),
tr::marked));
}
}
}
return Ui::MakeConfirmBox({
.text = std::move(text),
.confirmed = [=](Fn<void()> close) { confirmed(); close(); },
.confirmText = tr::lng_request_peer_confirm_send(tr::now),
});
}
object_ptr<Ui::BoxContent> CreatePeerByQueryBox(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot,
RequestPeerQuery query,
Fn<void(std::vector<not_null<PeerData*>>)> done) {
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
auto callback = [=](not_null<PeerData*> peer) {
done({ peer });
if (const auto strong = weak->get()) {
strong->closeBox();
}
};
auto result = Box<GroupInfoBox>(
navigation,
bot,
query,
std::move(callback));
*weak = result.data();
return result;
}
[[nodiscard]] bool FilterPeerByQuery(
not_null<PeerData*> peer,
RequestPeerQuery query,
const base::flat_set<not_null<PeerData*>> &commonGroups) {
using Type = RequestPeerQuery::Type;
using Restriction = RequestPeerQuery::Restriction;
const auto checkRestriction = [](Restriction restriction, bool value) {
return (restriction == Restriction::Any)
|| ((restriction == Restriction::Yes) == value);
};
const auto checkRights = [](
ChatAdminRights wanted,
bool creator,
ChatAdminRights rights) {
return creator || ((rights & wanted) == wanted);
};
switch (query.type) {
case Type::User: {
const auto user = peer->asUser();
return user
&& !user->isInaccessible()
&& !user->isNotificationsUser()
&& checkRestriction(query.userIsBot, user->isBot())
&& checkRestriction(query.userIsPremium, user->isPremium());
}
case Type::Group: {
const auto chat = peer->asChat();
const auto megagroup = peer->asMegagroup();
return (chat || megagroup)
&& (!query.amCreator
|| (chat ? chat->amCreator() : megagroup->amCreator()))
&& checkRestriction(query.groupIsForum, peer->isForum())
&& checkRestriction(
query.hasUsername,
megagroup && megagroup->hasUsername())
&& checkRights(
query.myRights,
chat ? chat->amCreator() : megagroup->amCreator(),
chat ? chat->adminRights() : megagroup->adminRights())
&& (!query.isBotParticipant
|| query.myRights
|| commonGroups.contains(peer)
|| (chat
? chat->canAddMembers()
: megagroup->canAddMembers()));
}
case Type::Broadcast: {
const auto broadcast = peer->asBroadcast();
return broadcast
&& (!query.amCreator || broadcast->amCreator())
&& checkRestriction(query.hasUsername, broadcast->hasUsername())
&& checkRights(
query.myRights,
broadcast->amCreator(),
broadcast->adminRights());
}
}
Unexpected("Type in FilterPeerByQuery.");
}
ChoosePeerBoxController::ChoosePeerBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot,
RequestPeerQuery query,
Fn<void(std::vector<not_null<PeerData*>>)> callback)
: ChatsListBoxController(&navigation->session())
, _navigation(navigation)
, _bot(bot)
, _query(query)
, _callback(std::move(callback)) {
if (const auto list = _bot->session().api().botCommonGroups(_bot)) {
_commonGroups = { begin(*list), end(*list) };
}
}
Main::Session &ChoosePeerBoxController::session() const {
return _navigation->session();
}
void ChoosePeerBoxController::prepareRestrictions() {
auto above = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto raw = above.data();
auto rows = RestrictionsList(_query);
if (!rows.empty()) {
Ui::AddSubsectionTitle(
raw,
tr::lng_request_peer_requirements(),
{ 0, st::membersMarginTop, 0, 0 });
const auto skip = st::defaultSubsectionTitlePadding.left();
auto separator = QString::fromUtf8("\n\xE2\x80\xA2 ");
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
separator + rows.join(separator),
st::requestPeerRestriction),
{ skip, 0, skip, st::membersMarginTop });
Ui::AddDivider(raw);
}
const auto make = [&](tr::phrase<> text, const style::icon &st) {
auto button = raw->add(
object_ptr<Ui::SettingsButton>(
raw,
text(),
st::inviteViaLinkButton),
{ 0, st::membersMarginTop, 0, 0 });
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
button,
st,
QPoint());
button->heightValue(
) | rpl::on_next([=](int height) {
icon->moveToLeft(
st::choosePeerCreateIconLeft,
(height - st::inviteViaLinkIcon.height()) / 2);
}, icon->lifetime());
button->setClickedCallback([=] {
_navigation->parentController()->show(
CreatePeerByQueryBox(_navigation, _bot, _query, _callback));
});
button->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter);
}) | rpl::on_next([=] {
delegate()->peerListMouseLeftGeometry();
}, button->lifetime());
return button;
};
if (_query.type == RequestPeerQuery::Type::Group) {
make(tr::lng_request_group_create, st::choosePeerGroupIcon);
} else if (_query.type == RequestPeerQuery::Type::Broadcast) {
make(tr::lng_request_channel_create, st::choosePeerChannelIcon);
}
if (raw->count() > 0) {
delegate()->peerListSetAboveWidget(std::move(above));
}
}
void ChoosePeerBoxController::prepareViewHook() {
delegate()->peerListSetTitle([&] {
using Type = RequestPeerQuery::Type;
using Restriction = RequestPeerQuery::Restriction;
switch (_query.type) {
case Type::User: return (_query.userIsBot == Restriction::Yes)
? tr::lng_request_bot_title()
: (_query.maxQuantity > 1)
? tr::lng_request_users_title()
: tr::lng_request_user_title();
case Type::Group: return tr::lng_request_group_title();
case Type::Broadcast: return tr::lng_request_channel_title();
}
Unexpected("Type in RequestPeerQuery.");
}());
prepareRestrictions();
}
void ChoosePeerBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto limit = _query.maxQuantity;
const auto multiselect = (limit > 1);
const auto peer = row->peer();
if (multiselect) {
if (_selected.contains(peer) || _selected.size() < limit) {
delegate()->peerListSetRowChecked(row, !row->checked());
if (row->checked()) {
_selected.emplace(peer);
} else {
_selected.remove(peer);
}
_selectedCount = int(_selected.size());
}
return;
}
const auto done = [callback = _callback, peer] {
const auto onstack = callback;
onstack({ peer });
};
if (peer->isUser()) {
done();
} else {
delegate()->peerListUiShow()->showBox(
MakeConfirmBox(_bot, peer, _query, done));
}
}
rpl::producer<int> ChoosePeerBoxController::selectedCountValue() const {
return _selectedCount.value();
}
void ChoosePeerBoxController::submit() {
const auto onstack = _callback;
onstack(ranges::to_vector(_selected));
}
auto ChoosePeerBoxController::createRow(not_null<History*> history)
-> std::unique_ptr<Row> {
return FilterPeerByQuery(history->peer, _query, _commonGroups)
? std::make_unique<Row>(history)
: nullptr;
}
QString ChoosePeerBoxController::emptyBoxText() const {
using Type = RequestPeerQuery::Type;
using Restriction = RequestPeerQuery::Restriction;
const auto result = [](tr::phrase<> title, tr::phrase<> text) {
return title(tr::now) + "\n\n" + text(tr::now);
};
switch (_query.type) {
case Type::User: return (_query.userIsBot == Restriction::Yes)
? result(tr::lng_request_bot_no, tr::lng_request_bot_no_about)
: result(tr::lng_request_user_no, tr::lng_request_user_no_about);
case Type::Group:
return result(
tr::lng_request_group_no,
tr::lng_request_group_no_about);
case Type::Broadcast:
return result(
tr::lng_request_channel_no,
tr::lng_request_channel_no_about);
}
Unexpected("Type in ChoosePeerBoxController::emptyBoxText.");
}
} // namespace
void ShowChoosePeerBox(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot,
RequestPeerQuery query,
Fn<void(std::vector<not_null<PeerData*>>)> chosen) {
const auto needCommonGroups = query.isBotParticipant
&& (query.type == RequestPeerQuery::Type::Group)
&& !query.myRights;
if (needCommonGroups && !bot->session().api().botCommonGroups(bot)) {
const auto weak = base::make_weak(navigation);
bot->session().api().requestBotCommonGroups(bot, [=] {
if (const auto strong = weak.get()) {
ShowChoosePeerBox(strong, bot, query, chosen);
}
});
return;
}
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
auto callback = [=, done = std::move(chosen)](
std::vector<not_null<PeerData*>> peers) {
done(std::move(peers));
if (const auto strong = weak->get()) {
strong->closeBox();
}
};
const auto limit = query.maxQuantity;
auto controller = std::make_unique<ChoosePeerBoxController>(
navigation,
bot,
query,
std::move(callback));
auto initBox = [=, ptr = controller.get()](not_null<PeerListBox*> box) {
ptr->selectedCountValue() | rpl::on_next([=](int count) {
box->clearButtons();
if (limit > 1) {
box->setAdditionalTitle(rpl::single(u"%1 / %2"_q.arg(count).arg(limit)));
}
if (count > 0) {
box->addButton(tr::lng_intro_submit(), [=] {
ptr->submit();
if (*weak) {
(*weak)->closeBox();
}
});
}
box->addButton(tr::lng_cancel(), [box] {
box->closeBox();
});
}, box->lifetime());
};
*weak = navigation->parentController()->show(Box<PeerListBox>(
std::move(controller),
std::move(initBox)));
}

View File

@@ -0,0 +1,24 @@
/*
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
struct RequestPeerQuery;
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
void ShowChoosePeerBox(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot,
RequestPeerQuery query,
Fn<void(std::vector<not_null<PeerData*>>)> chosen);

View File

@@ -0,0 +1,899 @@
/*
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_contact_box.h"
#include "api/api_peer_photo.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/call_delayed.h"
#include "boxes/peers/edit_peer_common.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/ui_integration.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_premium_limits.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_layer_widget.h"
#include "history/view/controls/history_view_characters_limit.h"
#include "info/profile/info_profile_cover.h"
#include "info/userpic/info_userpic_emoji_builder_common.h"
#include "info/userpic/info_userpic_emoji_builder_menu_item.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_common.h"
#include "lottie/lottie_frame_generator.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "ui/animated_icon.h"
#include "ui/controls/emoji_button_factory.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/userpic_button.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_entity.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "styles/style_widgets.h"
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
namespace {
constexpr auto kAnimationStartFrame = 0;
constexpr auto kAnimationEndFrame = 21;
QString UserPhone(not_null<UserData*> user) {
const auto phone = user->phone();
return phone.isEmpty()
? user->owner().findContactPhone(peerToUser(user->id))
: phone;
}
void SendRequest(
base::weak_qptr<Ui::GenericBox> box,
not_null<UserData*> user,
bool sharePhone,
const QString &first,
const QString &last,
const QString &phone,
const TextWithEntities &note,
Fn<void()> done) {
const auto wasContact = user->isContact();
using Flag = MTPcontacts_AddContact::Flag;
user->session().api().request(MTPcontacts_AddContact(
MTP_flags(Flag::f_note
| (sharePhone ? Flag::f_add_phone_privacy_exception : Flag(0))),
user->inputUser(),
MTP_string(first),
MTP_string(last),
MTP_string(phone),
note.text.isEmpty()
? MTPTextWithEntities()
: MTP_textWithEntities(
MTP_string(note.text),
Api::EntitiesToMTP(&user->session(), note.entities))
)).done([=](const MTPUpdates &result) {
user->setName(
first,
last,
user->nameOrPhone,
user->username());
user->session().api().applyUpdates(result);
if (const auto settings = user->barSettings()) {
const auto flags = PeerBarSetting::AddContact
| PeerBarSetting::BlockContact
| PeerBarSetting::ReportSpam;
user->setBarSettings(*settings & ~flags);
}
if (box) {
if (!wasContact) {
box->showToast(
tr::lng_new_contact_add_done(tr::now, lt_user, first));
}
box->closeBox();
}
done();
}).send();
}
class Controller {
public:
Controller(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<UserData*> user,
bool focusOnNotes = false);
void prepare();
private:
void setupContent();
void setupCover();
void setupNameFields();
void setupNotesField();
void setupPhotoButtons();
void setupDeleteContactButton();
void setupWarning();
void setupSharePhoneNumber();
void initNameFields(
not_null<Ui::InputField*> first,
not_null<Ui::InputField*> last,
bool inverted);
void showPhotoMenu(bool suggest);
void choosePhotoFile(bool suggest);
void processChosenPhoto(QImage &&image, bool suggest);
void processChosenPhotoWithMarkup(
UserpicBuilder::Result &&data,
bool suggest);
void executeWithDelay(
Fn<void()> callback,
bool suggest,
bool startAnimation = true);
void finishIconAnimation(bool suggest);
not_null<Ui::GenericBox*> _box;
not_null<Window::SessionController*> _window;
not_null<UserData*> _user;
bool _focusOnNotes = false;
Ui::Checkbox *_sharePhone = nullptr;
Ui::InputField *_notesField = nullptr;
Ui::InputField *_firstNameField = nullptr;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
base::unique_qptr<Ui::PopupMenu> _photoMenu;
std::unique_ptr<Ui::AnimatedIcon> _suggestIcon;
std::unique_ptr<Ui::AnimatedIcon> _cameraIcon;
Ui::RpWidget *_suggestIconWidget = nullptr;
Ui::RpWidget *_cameraIconWidget = nullptr;
QString _phone;
Fn<void()> _focus;
Fn<void()> _save;
Fn<std::optional<QImage>()> _updatedPersonalPhoto;
};
Controller::Controller(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<UserData*> user,
bool focusOnNotes)
: _box(box)
, _window(window)
, _user(user)
, _focusOnNotes(focusOnNotes)
, _phone(UserPhone(user)) {
}
void Controller::prepare() {
setupContent();
_box->setTitle(_user->isContact()
? tr::lng_edit_contact_title()
: tr::lng_enter_contact_data());
_box->addButton(tr::lng_box_done(), _save);
_box->addButton(tr::lng_cancel(), [=] { _box->closeBox(); });
_box->setFocusCallback(_focus);
}
void Controller::setupContent() {
setupCover();
setupNameFields();
setupNotesField();
setupPhotoButtons();
setupDeleteContactButton();
setupWarning();
setupSharePhoneNumber();
}
void Controller::setupCover() {
const auto cover = _box->addRow(
object_ptr<Info::Profile::Cover>(
_box,
_window,
_user,
Info::Profile::Cover::Role::EditContact,
(_phone.isEmpty()
? tr::lng_contact_mobile_hidden()
: rpl::single(Ui::FormatPhone(_phone)))),
style::margins());
_updatedPersonalPhoto = [=] { return cover->updatedPersonalPhoto(); };
}
void Controller::setupNameFields() {
const auto inverted = langFirstNameGoesSecond();
_firstNameField = _box->addRow(
object_ptr<Ui::InputField>(
_box,
st::defaultInputField,
tr::lng_signup_firstname(),
_user->firstName),
st::addContactFieldMargin);
const auto first = _firstNameField;
auto preparedLast = object_ptr<Ui::InputField>(
_box,
st::defaultInputField,
tr::lng_signup_lastname(),
_user->lastName);
const auto last = inverted
? _box->insertRow(
_box->rowsCount() - 1,
std::move(preparedLast),
st::addContactFieldMargin)
: _box->addRow(std::move(preparedLast), st::addContactFieldMargin);
initNameFields(first, last, inverted);
}
void Controller::initNameFields(
not_null<Ui::InputField*> first,
not_null<Ui::InputField*> last,
bool inverted) {
const auto getValue = [](not_null<Ui::InputField*> field) {
return TextUtilities::SingleLine(field->getLastText()).trimmed();
};
if (inverted) {
_box->setTabOrder(last, first);
}
_focus = [=] {
if (_focusOnNotes && _notesField) {
_notesField->setFocusFast();
_notesField->setCursorPosition(_notesField->getLastText().size());
return;
}
const auto firstValue = getValue(first);
const auto lastValue = getValue(last);
const auto empty = firstValue.isEmpty() && lastValue.isEmpty();
const auto focusFirst = (inverted != empty);
(focusFirst ? first : last)->setFocusFast();
};
_save = [=] {
const auto firstValue = getValue(first);
const auto lastValue = getValue(last);
const auto empty = firstValue.isEmpty() && lastValue.isEmpty();
if (empty) {
_focus();
(inverted ? last : first)->showError();
return;
}
if (_notesField) {
const auto limit = Data::PremiumLimits(
&_user->session()).contactNoteLengthCurrent();
const auto remove = Ui::ComputeFieldCharacterCount(_notesField)
- limit;
if (remove > 0) {
_box->showToast(tr::lng_contact_notes_limit_reached(
tr::now,
lt_count,
remove));
_notesField->setFocus();
return;
}
}
const auto user = _user;
const auto personal = _updatedPersonalPhoto
? _updatedPersonalPhoto()
: std::nullopt;
const auto done = [=] {
if (personal) {
if (personal->isNull()) {
user->session().api().peerPhoto().clearPersonal(user);
} else {
user->session().api().peerPhoto().upload(
user,
{ base::duplicate(*personal) });
}
}
};
const auto noteValue = _notesField
? [&] {
auto textWithTags = _notesField->getTextWithAppliedMarkdown();
return TextWithEntities{
base::take(textWithTags.text),
TextUtilities::ConvertTextTagsToEntities(
base::take(textWithTags.tags)),
};
}()
: TextWithEntities();
SendRequest(
base::make_weak(_box),
user,
_sharePhone && _sharePhone->checked(),
firstValue,
lastValue,
_phone,
noteValue,
done);
};
const auto submit = [=] {
const auto firstValue = first->getLastText().trimmed();
const auto lastValue = last->getLastText().trimmed();
const auto empty = firstValue.isEmpty() && lastValue.isEmpty();
if (inverted ? last->hasFocus() : empty) {
first->setFocus();
} else if (inverted ? empty : first->hasFocus()) {
last->setFocus();
} else {
_save();
}
};
first->submits() | rpl::on_next(submit, first->lifetime());
last->submits() | rpl::on_next(submit, last->lifetime());
first->setMaxLength(Ui::EditPeer::kMaxUserFirstLastName);
first->setMaxLength(Ui::EditPeer::kMaxUserFirstLastName);
}
void Controller::setupWarning() {
if (_user->isContact() || !_phone.isEmpty()) {
return;
}
_box->addRow(
object_ptr<Ui::FlatLabel>(
_box,
tr::lng_contact_phone_after(tr::now, lt_user, _user->shortName()),
st::changePhoneLabel),
st::addContactWarningMargin);
}
void Controller::setupNotesField() {
Ui::AddSkip(_box->verticalLayout());
Ui::AddDivider(_box->verticalLayout());
Ui::AddSkip(_box->verticalLayout());
_notesField = _box->addRow(
object_ptr<Ui::InputField>(
_box,
st::notesFieldWithEmoji,
Ui::InputField::Mode::MultiLine,
tr::lng_contact_add_notes(),
QString()),
st::addContactFieldMargin);
_notesField->setMarkdownSet(Ui::MarkdownSet::Notes);
_notesField->setCustomTextContext(Core::TextContext({
.session = &_user->session()
}));
_notesField->setTextWithTags({
_user->note().text,
TextUtilities::ConvertEntitiesToTextTags(_user->note().entities)
});
_notesField->setMarkdownReplacesEnabled(rpl::single(
Ui::MarkdownEnabledState{
Ui::MarkdownEnabled{
{
Ui::InputField::kTagBold,
Ui::InputField::kTagItalic,
Ui::InputField::kTagUnderline,
Ui::InputField::kTagStrikeOut,
Ui::InputField::kTagSpoiler
}
}
}
));
const auto container = _box->getDelegate()->outerContainer();
using Selector = ChatHelpers::TabbedSelector;
_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
container,
_window,
object_ptr<Selector>(
nullptr,
_window->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::EmojiOnly));
_emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_emojiPanel->hide();
_emojiPanel->selector()->setCurrentPeer(_window->session().user());
_emojiPanel->selector()->emojiChosen(
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
Ui::InsertEmojiAtCursor(_notesField->textCursor(), data.emoji);
}, _notesField->lifetime());
_emojiPanel->selector()->customEmojiChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
const auto info = data.document->sticker();
if (info
&& info->setType == Data::StickersType::Emoji
&& !_window->session().premium()) {
ShowPremiumPreviewBox(
_window,
PremiumFeature::AnimatedEmoji);
} else {
Data::InsertCustomEmoji(_notesField, data.document);
}
}, _notesField->lifetime());
const auto emojiButton = Ui::AddEmojiToggleToField(
_notesField,
_box,
_window,
_emojiPanel.get(),
st::sendGifWithCaptionEmojiPosition);
emojiButton->show();
using Limit = HistoryView::Controls::CharactersLimitLabel;
struct LimitState {
base::unique_qptr<Limit> charsLimitation;
};
const auto limitState = _notesField->lifetime().make_state<LimitState>();
const auto checkCharsLimitation = [=, w = _notesField->window()] {
const auto limit = Data::PremiumLimits(
&_user->session()).contactNoteLengthCurrent();
const auto remove = Ui::ComputeFieldCharacterCount(_notesField)
- limit;
if (!limitState->charsLimitation) {
const auto border = _notesField->st().borderActive;
limitState->charsLimitation = base::make_unique_q<Limit>(
_box->verticalLayout(),
emojiButton,
style::al_top,
QMargins{ 0, -border - _notesField->st().border, 0, 0 });
rpl::combine(
limitState->charsLimitation->geometryValue(),
_notesField->geometryValue()
) | rpl::on_next([=](QRect limit, QRect field) {
limitState->charsLimitation->setVisible(
(w->mapToGlobal(limit.bottomLeft()).y() - border)
< w->mapToGlobal(field.bottomLeft()).y());
limitState->charsLimitation->raise();
}, limitState->charsLimitation->lifetime());
}
limitState->charsLimitation->setLeft(remove);
};
_notesField->changes() | rpl::on_next([=] {
checkCharsLimitation();
}, _notesField->lifetime());
Ui::AddDividerText(
_box->verticalLayout(),
tr::lng_contact_add_notes_about());
}
void Controller::setupPhotoButtons() {
if (!_user->isContact()) {
return;
}
const auto iconPlaceholder = st::restoreUserpicIcon.size * 2;
auto nameValue = _firstNameField
? rpl::merge(
rpl::single(_firstNameField->getLastText().trimmed()),
_firstNameField->changes() | rpl::map([=] {
return _firstNameField->getLastText().trimmed();
})) | rpl::map([=](const QString &text) {
return text.isEmpty() ? Ui::kQEllipsis : text;
})
: rpl::single(_user->shortName()) | rpl::type_erased;
const auto inner = _box->verticalLayout();
Ui::AddSkip(inner);
const auto suggestBirthdayWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto suggestBirthdayButton = Settings::AddButtonWithIcon(
suggestBirthdayWrap->entity(),
tr::lng_suggest_birthday(),
st::settingsButtonLight,
{ &st::editContactSuggestBirthday });
suggestBirthdayButton->setClickedCallback([=] {
Core::App().openInternalUrl(
u"internal:edit_birthday:suggest:%1"_q.arg(
peerToUser(_user->id).bare),
QVariant::fromValue(ClickHandlerContext{
.sessionWindow = base::make_weak(_window),
}));
});
suggestBirthdayWrap->toggleOn(rpl::single(!_user->birthday().valid()
&& !_user->starsPerMessageChecked()));
_suggestIcon = Ui::MakeAnimatedIcon({
.generator = [] {
return std::make_unique<Lottie::FrameGenerator>(
Lottie::ReadContent(
QByteArray(),
u":/animations/photo_suggest_icon.tgs"_q));
},
.sizeOverride = iconPlaceholder,
.colorized = true,
});
_cameraIcon = Ui::MakeAnimatedIcon({
.generator = [] {
return std::make_unique<Lottie::FrameGenerator>(
Lottie::ReadContent(
QByteArray(),
u":/animations/camera_outline.tgs"_q));
},
.sizeOverride = iconPlaceholder,
.colorized = true,
});
const auto suggestButtonWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
suggestButtonWrap->toggleOn(
rpl::single(!_user->starsPerMessageChecked()));
const auto suggestButton = Settings::AddButtonWithIcon(
suggestButtonWrap->entity(),
tr::lng_suggest_photo_for(lt_user, rpl::duplicate(nameValue)),
st::settingsButtonLight,
{ nullptr });
_suggestIconWidget = Ui::CreateChild<Ui::RpWidget>(suggestButton);
_suggestIconWidget->resize(iconPlaceholder);
_suggestIconWidget->paintRequest() | rpl::on_next([=] {
if (_suggestIcon && _suggestIcon->valid()) {
auto p = QPainter(_suggestIconWidget);
const auto frame = _suggestIcon->frame(st::lightButtonFg->c);
p.drawImage(_suggestIconWidget->rect(), frame);
}
}, _suggestIconWidget->lifetime());
suggestButton->sizeValue() | rpl::on_next([=](QSize size) {
_suggestIconWidget->move(
st::settingsButtonLight.iconLeft - iconPlaceholder.width() / 4,
(size.height() - _suggestIconWidget->height()) / 2);
}, _suggestIconWidget->lifetime());
suggestButton->setClickedCallback([=] {
if (_suggestIcon && _suggestIcon->valid()) {
_suggestIcon->setCustomStartFrame(kAnimationStartFrame);
_suggestIcon->setCustomEndFrame(kAnimationEndFrame);
_suggestIcon->jumpToStart([=] { _suggestIconWidget->update(); });
_suggestIcon->animate([=] { _suggestIconWidget->update(); });
}
showPhotoMenu(true);
});
const auto setButton = Settings::AddButtonWithIcon(
inner,
tr::lng_set_photo_for_user(lt_user, rpl::duplicate(nameValue)),
st::settingsButtonLight,
{ nullptr });
_cameraIconWidget = Ui::CreateChild<Ui::RpWidget>(setButton);
_cameraIconWidget->resize(iconPlaceholder);
_cameraIconWidget->paintRequest() | rpl::on_next([=] {
if (_cameraIcon && _cameraIcon->valid()) {
auto p = QPainter(_cameraIconWidget);
const auto frame = _cameraIcon->frame(st::lightButtonFg->c);
p.drawImage(_cameraIconWidget->rect(), frame);
}
}, _cameraIconWidget->lifetime());
setButton->sizeValue() | rpl::on_next([=](QSize size) {
_cameraIconWidget->move(
st::settingsButtonLight.iconLeft - iconPlaceholder.width() / 4,
(size.height() - _cameraIconWidget->height()) / 2);
}, _cameraIconWidget->lifetime());
setButton->setClickedCallback([=] {
if (_cameraIcon && _cameraIcon->valid()) {
_cameraIcon->setCustomStartFrame(kAnimationStartFrame);
_cameraIcon->setCustomEndFrame(kAnimationEndFrame);
_cameraIcon->jumpToStart([=] { _cameraIconWidget->update(); });
_cameraIcon->animate([=] { _cameraIconWidget->update(); });
}
showPhotoMenu(false);
});
const auto resetButtonWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto resetButton = Settings::AddButtonWithIcon(
resetButtonWrap->entity(),
tr::lng_profile_photo_reset(),
st::settingsButtonLight,
{ nullptr });
const auto userpicButton = Ui::CreateChild<Ui::UserpicButton>(
resetButton,
_window,
_user,
Ui::UserpicButton::Role::Custom,
Ui::UserpicButton::Source::NonPersonalIfHasPersonal,
st::restoreUserpicIcon);
userpicButton->setAttribute(Qt::WA_TransparentForMouseEvents);
resetButton->sizeValue(
) | rpl::on_next([=](QSize size) {
userpicButton->move(
st::settingsButtonLight.iconLeft,
(size.height() - userpicButton->height()) / 2);
}, userpicButton->lifetime());
resetButtonWrap->toggleOn(
_user->session().changes().peerFlagsValue(
_user,
Data::PeerUpdate::Flag::FullInfo | Data::PeerUpdate::Flag::Photo
) | rpl::map([=] {
return _user->hasPersonalPhoto();
}) | rpl::distinct_until_changed());
resetButton->setClickedCallback([=] {
_window->show(Ui::MakeConfirmBox({
.text = tr::lng_profile_photo_reset_sure(
tr::now,
lt_user,
_user->shortName()),
.confirmed = [=](Fn<void()> close) {
_window->session().api().peerPhoto().clearPersonal(_user);
close();
},
.confirmText = tr::lng_profile_photo_reset_button(tr::now),
}));
});
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
tr::lng_contact_photo_replace_info(lt_user, std::move(nameValue)));
Ui::AddSkip(inner);
}
void Controller::setupDeleteContactButton() {
if (!_user->isContact()) {
return;
}
const auto inner = _box->verticalLayout();
const auto deleteButton = Settings::AddButtonWithIcon(
inner,
tr::lng_info_delete_contact(),
st::settingsAttentionButton,
{ nullptr });
deleteButton->setClickedCallback([=] {
const auto text = tr::lng_sure_delete_contact(
tr::now,
lt_contact,
_user->name());
const auto deleteSure = [=](Fn<void()> &&close) {
close();
_user->session().api().request(MTPcontacts_DeleteContacts(
MTP_vector<MTPInputUser>(1, _user->inputUser())
)).done([=](const MTPUpdates &result) {
_user->session().api().applyUpdates(result);
_box->closeBox();
}).send();
};
_window->show(Ui::MakeConfirmBox({
.text = text,
.confirmed = deleteSure,
.confirmText = tr::lng_box_delete(),
.confirmStyle = &st::attentionBoxButton,
}));
});
Ui::AddSkip(inner);
}
void Controller::setupSharePhoneNumber() {
const auto settings = _user->barSettings();
if (!settings
|| !((*settings) & PeerBarSetting::NeedContactsException)) {
return;
}
_sharePhone = _box->addRow(
object_ptr<Ui::Checkbox>(
_box,
tr::lng_contact_share_phone(tr::now),
true,
st::defaultBoxCheckbox),
st::addContactWarningMargin);
_box->addRow(
object_ptr<Ui::FlatLabel>(
_box,
tr::lng_contact_phone_will_be_shared(tr::now, lt_user, _user->shortName()),
st::changePhoneLabel),
st::addContactWarningMargin);
}
void Controller::showPhotoMenu(bool suggest) {
_photoMenu = base::make_unique_q<Ui::PopupMenu>(
_box,
st::popupMenuWithIcons);
QObject::connect(_photoMenu.get(), &QObject::destroyed, [=] {
finishIconAnimation(suggest);
});
_photoMenu->addAction(
tr::lng_attach_photo(tr::now),
[=] { executeWithDelay([=] { choosePhotoFile(suggest); }, suggest); },
&st::menuIconPhoto);
if (const auto data = QGuiApplication::clipboard()->mimeData()) {
if (data->hasImage()) {
auto callback = [=] {
Editor::PrepareProfilePhoto(
_box,
&_window->window(),
Editor::EditorData{
.about = (suggest
? tr::lng_profile_suggest_sure(
tr::now,
lt_user,
tr::bold(_user->shortName()),
tr::marked)
: tr::lng_profile_set_personal_sure(
tr::now,
lt_user,
tr::bold(_user->shortName()),
tr::marked)),
.confirm = (suggest
? tr::lng_profile_suggest_button(tr::now)
: tr::lng_profile_set_photo_button(tr::now)),
.cropType = Editor::EditorData::CropType::Ellipse,
.keepAspectRatio = true,
},
[=](QImage &&editedImage) {
processChosenPhoto(std::move(editedImage), suggest);
},
qvariant_cast<QImage>(data->imageData()));
};
_photoMenu->addAction(
tr::lng_profile_photo_from_clipboard(tr::now),
[=] { executeWithDelay(callback, suggest); },
&st::menuIconPhoto);
}
}
UserpicBuilder::AddEmojiBuilderAction(
_window,
_photoMenu.get(),
_window->session().api().peerPhoto().emojiListValue(
Api::PeerPhoto::EmojiListType::Profile),
[=](UserpicBuilder::Result data) {
processChosenPhotoWithMarkup(std::move(data), suggest);
},
false);
_photoMenu->popup(QCursor::pos());
}
void Controller::choosePhotoFile(bool suggest) {
Editor::PrepareProfilePhotoFromFile(
_box,
&_window->window(),
Editor::EditorData{
.about = (suggest
? tr::lng_profile_suggest_sure(
tr::now,
lt_user,
tr::bold(_user->shortName()),
tr::marked)
: tr::lng_profile_set_personal_sure(
tr::now,
lt_user,
tr::bold(_user->shortName()),
tr::marked)),
.confirm = (suggest
? tr::lng_profile_suggest_button(tr::now)
: tr::lng_profile_set_photo_button(tr::now)),
.cropType = Editor::EditorData::CropType::Ellipse,
.keepAspectRatio = true,
},
[=](QImage &&image) {
processChosenPhoto(std::move(image), suggest);
});
}
void Controller::processChosenPhoto(QImage &&image, bool suggest) {
Api::PeerPhoto::UserPhoto photo{
.image = base::duplicate(image),
};
if (suggest) {
_window->session().api().peerPhoto().suggest(_user, std::move(photo));
_window->showPeerHistory(_user->id);
} else {
_window->session().api().peerPhoto().upload(_user, std::move(photo));
}
}
void Controller::processChosenPhotoWithMarkup(
UserpicBuilder::Result &&data,
bool suggest) {
Api::PeerPhoto::UserPhoto photo{
.image = std::move(data.image),
.markupDocumentId = data.id,
.markupColors = std::move(data.colors),
};
if (suggest) {
_window->session().api().peerPhoto().suggest(_user, std::move(photo));
_window->showPeerHistory(_user->id);
} else {
_window->session().api().peerPhoto().upload(_user, std::move(photo));
}
}
void Controller::finishIconAnimation(bool suggest) {
const auto icon = suggest ? _suggestIcon.get() : _cameraIcon.get();
const auto widget = suggest ? _suggestIconWidget : _cameraIconWidget;
if (icon && icon->valid()) {
icon->setCustomStartFrame(icon->frameIndex());
icon->setCustomEndFrame(-1);
icon->animate([=] { widget->update(); });
}
}
void Controller::executeWithDelay(
Fn<void()> callback,
bool suggest,
bool startAnimation) {
const auto icon = suggest ? _suggestIcon.get() : _cameraIcon.get();
const auto widget = suggest ? _suggestIconWidget : _cameraIconWidget;
if (startAnimation && icon && icon->valid()) {
icon->setCustomStartFrame(icon->frameIndex());
icon->setCustomEndFrame(-1);
icon->animate([=] { widget->update(); });
}
if (icon && icon->valid() && icon->animating()) {
base::call_delayed(50, [=] {
executeWithDelay(callback, suggest, false);
});
} else {
callback();
}
}
} // namespace
void EditContactBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<UserData*> user) {
box->setWidth(st::boxWideWidth);
box->lifetime().make_state<Controller>(box, window, user)->prepare();
}
void EditContactNoteBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<UserData*> user) {
box->setWidth(st::boxWideWidth);
box->lifetime().make_state<Controller>(
box,
window,
user,
true)->prepare();
}

View File

@@ -0,0 +1,26 @@
/*
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/layers/generic_box.h"
class UserData;
namespace Window {
class SessionController;
} // namespace Window
void EditContactBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<UserData*> user);
void EditContactNoteBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<UserData*> user);

View File

@@ -0,0 +1,365 @@
/*
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_discussion_link_box.h"
#include "lang/lang_keys.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "settings/settings_common.h" // AddButton.
#include "data/data_changes.h"
#include "ui/widgets/labels.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/text/text_utilities.h" // tr::rich
#include "boxes/peer_list_box.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/add_contact_box.h"
#include "apiwrap.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include "styles/style_settings.h"
namespace {
constexpr auto kEnableSearchRowsCount = 10;
class Controller : public PeerListController, public base::has_weak_ptr {
public:
Controller(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
ChannelData *chat,
const std::vector<not_null<PeerData*>> &chats,
Fn<void(ChannelData*)> callback,
Fn<void(not_null<PeerData*>)> showHistoryCallback);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
int contentWidth() const override;
private:
void choose(not_null<ChannelData*> chat);
void choose(not_null<ChatData*> chat);
not_null<Window::SessionNavigation*> _navigation;
not_null<ChannelData*> _channel;
ChannelData *_chat = nullptr;
std::vector<not_null<PeerData*>> _chats;
Fn<void(ChannelData*)> _callback;
Fn<void(not_null<PeerData*>)> _showHistoryCallback;
ChannelData *_waitForFull = nullptr;
rpl::event_stream<not_null<PeerData*>> _showHistoryRequest;
};
Controller::Controller(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
ChannelData *chat,
const std::vector<not_null<PeerData*>> &chats,
Fn<void(ChannelData*)> callback,
Fn<void(not_null<PeerData*>)> showHistoryCallback)
: _navigation(navigation)
, _channel(channel)
, _chat(chat)
, _chats(std::move(chats))
, _callback(std::move(callback))
, _showHistoryCallback(std::move(showHistoryCallback)) {
channel->session().changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer == _waitForFull);
}) | rpl::on_next([=](const Data::PeerUpdate &update) {
choose(std::exchange(_waitForFull, nullptr));
}, lifetime());
}
Main::Session &Controller::session() const {
return _channel->session();
}
int Controller::contentWidth() const {
return st::boxWidth;
}
void Controller::prepare() {
const auto appendRow = [&](not_null<PeerData*> chat) {
if (delegate()->peerListFindRow(chat->id.value)) {
return;
}
auto row = std::make_unique<PeerListRow>(chat);
const auto username = chat->username();
row->setCustomStatus(!username.isEmpty()
? ('@' + username)
: (chat->isChannel() && !chat->isMegagroup())
? tr::lng_manage_linked_channel_private_status(tr::now)
: tr::lng_manage_discussion_group_private_status(tr::now));
delegate()->peerListAppendRow(std::move(row));
};
if (_chat) {
appendRow(_chat);
} else {
for (const auto chat : _chats) {
appendRow(chat);
}
if (_chats.size() >= kEnableSearchRowsCount) {
delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
}
}
}
void Controller::rowClicked(not_null<PeerListRow*> row) {
if (_chat != nullptr) {
_showHistoryCallback(_chat);
return;
}
const auto peer = row->peer();
if (const auto channel = peer->asChannel()) {
if (channel->wasFullUpdated()) {
choose(channel);
return;
}
_waitForFull = channel;
channel->updateFull();
} else if (const auto chat = peer->asChat()) {
choose(chat);
}
}
void Controller::choose(not_null<ChannelData*> chat) {
if (chat->isForum()) {
ShowForumForDiscussionError(_navigation);
return;
}
auto text = tr::lng_manage_discussion_group_sure(
tr::now,
lt_group,
tr::bold(chat->name()),
lt_channel,
tr::bold(_channel->name()),
tr::marked);
if (!_channel->isPublic()) {
text.append(
"\n\n" + tr::lng_manage_linked_channel_private(tr::now));
}
if (!chat->isPublic()) {
text.append(
"\n\n" + tr::lng_manage_discussion_group_private(tr::now));
if (chat->hiddenPreHistory()) {
text.append("\n\n");
text.append(tr::lng_manage_discussion_group_warning(
tr::now,
tr::rich));
}
}
const auto sure = [=](Fn<void()> &&close) {
close();
const auto onstack = _callback;
onstack(chat);
};
delegate()->peerListUiShow()->showBox(Ui::MakeConfirmBox({
.text = text,
.confirmed = sure,
.confirmText = tr::lng_manage_discussion_group_link(tr::now),
}));
}
void Controller::choose(not_null<ChatData*> chat) {
auto text = tr::lng_manage_discussion_group_sure(
tr::now,
lt_group,
tr::bold(chat->name()),
lt_channel,
tr::bold(_channel->name()),
tr::marked);
if (!_channel->isPublic()) {
text.append("\n\n" + tr::lng_manage_linked_channel_private(tr::now));
}
text.append("\n\n" + tr::lng_manage_discussion_group_private(tr::now));
text.append("\n\n");
text.append(tr::lng_manage_discussion_group_warning(
tr::now,
tr::rich));
const auto sure = [=](Fn<void()> &&close) {
close();
const auto done = [=](not_null<ChannelData*> chat) {
const auto onstack = _callback;
onstack(chat);
};
chat->session().api().migrateChat(chat, crl::guard(this, done));
};
delegate()->peerListUiShow()->showBox(Ui::MakeConfirmBox({
.text = text,
.confirmed = sure,
.confirmText = tr::lng_manage_discussion_group_link(tr::now),
}));
}
[[nodiscard]] rpl::producer<TextWithEntities> About(
not_null<ChannelData*> channel,
ChannelData *chat) {
if (!channel->isBroadcast()) {
return tr::lng_manage_linked_channel_about(
lt_channel,
rpl::single(tr::bold(chat->name())),
tr::marked);
} else if (chat != nullptr) {
return tr::lng_manage_discussion_group_about_chosen(
lt_group,
rpl::single(tr::bold(chat->name())),
tr::marked);
}
return tr::lng_manage_discussion_group_about(tr::marked);
}
[[nodiscard]] object_ptr<Ui::BoxContent> EditDiscussionLinkBox(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
ChannelData *chat,
std::vector<not_null<PeerData*>> &&chats,
bool canEdit,
Fn<void(ChannelData*)> callback) {
Expects((channel->isBroadcast() && canEdit) || (chat != nullptr));
class ListBox final : public PeerListBox {
public:
ListBox(
QWidget *parent,
std::unique_ptr<PeerListController> controller,
Fn<void(not_null<ListBox*>)> init)
: PeerListBox(
parent,
std::move(controller),
[=](not_null<PeerListBox*>) { init(this); }) {
}
void showFinished() override {
_showFinished.fire({});
}
rpl::producer<> showFinishes() const {
return _showFinished.events();
}
private:
rpl::event_stream<> _showFinished;
};
const auto init = [=](not_null<ListBox*> box) {
auto above = object_ptr<Ui::VerticalLayout>(box);
Settings::AddDividerTextWithLottie(above, {
.lottie = u"discussion"_q,
.showFinished = box->showFinishes(),
.about = About(channel, chat),
});
if (!chat) {
Assert(channel->isBroadcast());
Ui::AddSkip(above);
Settings::AddButtonWithIcon(
above,
tr::lng_manage_discussion_group_create(),
st::infoCreateDiscussionLinkButton,
{ &st::menuBlueIconGroupCreate }
)->addClickHandler([=, parent = above.data()] {
const auto guarded = crl::guard(parent, callback);
navigation->uiShow()->showBox(Box<GroupInfoBox>(
navigation,
GroupInfoBox::Type::Megagroup,
channel->name() + " Chat",
guarded));
});
}
box->peerListSetAboveWidget(std::move(above));
auto below = object_ptr<Ui::VerticalLayout>(box);
if (chat && canEdit) {
Settings::AddButtonWithIcon(
below,
(channel->isBroadcast()
? tr::lng_manage_discussion_group_unlink
: tr::lng_manage_linked_channel_unlink)(),
st::infoUnlinkDiscussionLinkButton,
{ &st::menuIconRemoveAttention }
)->addClickHandler([=] { callback(nullptr); });
}
Ui::AddSkip(below);
Ui::AddDividerText(
below,
(channel->isBroadcast()
? tr::lng_manage_discussion_group_posted
: tr::lng_manage_linked_channel_posted)());
box->peerListSetBelowWidget(std::move(below));
box->setTitle(channel->isBroadcast()
? tr::lng_manage_discussion_group()
: tr::lng_manage_linked_channel());
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
};
auto showHistoryCallback = [=](not_null<PeerData*> peer) {
navigation->showPeerHistory(
peer,
Window::SectionShow::Way::ClearStack,
ShowAtUnreadMsgId);
};
auto controller = std::make_unique<Controller>(
navigation,
channel,
chat,
std::move(chats),
std::move(callback),
std::move(showHistoryCallback));
return Box<ListBox>(std::move(controller), init);
}
} // namespace
object_ptr<Ui::BoxContent> EditDiscussionLinkBox(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
std::vector<not_null<PeerData*>> &&chats,
Fn<void(ChannelData*)> callback) {
return EditDiscussionLinkBox(
navigation,
channel,
nullptr,
std::move(chats),
true,
callback);
}
object_ptr<Ui::BoxContent> EditDiscussionLinkBox(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
not_null<ChannelData*> chat,
bool canEdit,
Fn<void(ChannelData*)> callback) {
return EditDiscussionLinkBox(
navigation,
channel,
chat,
{},
canEdit,
callback);
}
void ShowForumForDiscussionError(
not_null<Window::SessionNavigation*> navigation) {
navigation->showToast(
tr::lng_forum_topics_no_discussion(
tr::now,
tr::rich));
}

View File

@@ -0,0 +1,34 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
[[nodiscard]] object_ptr<Ui::BoxContent> EditDiscussionLinkBox(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
not_null<ChannelData*> chat,
bool canEdit,
Fn<void(ChannelData*)> callback);
[[nodiscard]] object_ptr<Ui::BoxContent> EditDiscussionLinkBox(
not_null<Window::SessionNavigation*> navigation,
not_null<ChannelData*> channel,
std::vector<not_null<PeerData*>> &&chats,
Fn<void(ChannelData*)> callback);
void ShowForumForDiscussionError(
not_null<Window::SessionNavigation*> navigation);

View File

@@ -0,0 +1,609 @@
/*
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_forum_topic_box.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/shadow.h"
#include "ui/effects/emoji_fly_animation.h"
#include "ui/abstract_button.h"
#include "ui/vertical_list.h"
#include "data/data_document.h"
#include "data/data_forum.h"
#include "data/data_forum_icons.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "base/event_filter.h"
#include "base/random.h"
#include "base/qt_signal_producer.h"
#include "chat_helpers/emoji_list_widget.h"
#include "chat_helpers/stickers_list_footer.h"
#include "boxes/premium_preview_box.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/view/history_view_chat_section.h"
#include "history/view/history_view_sticker_toast.h"
#include "lang/lang_keys.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "apiwrap.h"
#include "mainwindow.h"
#include "styles/style_layers.h"
#include "styles/style_dialogs.h"
#include "styles/style_chat_helpers.h"
namespace {
constexpr auto kDefaultIconId = DocumentId(0x7FFF'FFFF'FFFF'FFFFULL);
using DefaultIcon = Data::TopicIconDescriptor;
class DefaultIconEmoji final : public Ui::Text::CustomEmoji {
public:
DefaultIconEmoji(
rpl::producer<DefaultIcon> value,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag);
int width() override;
QString entityData() override;
void paint(QPainter &p, const Context &context) override;
void unload() override;
bool ready() override;
bool readyInDefaultState() override;
private:
DefaultIcon _icon = {};
QImage _image;
Data::CustomEmojiSizeTag _tag = {};
rpl::lifetime _lifetime;
};
DefaultIconEmoji::DefaultIconEmoji(
rpl::producer<DefaultIcon> value,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag)
: _tag(tag) {
std::move(value) | rpl::on_next([=](DefaultIcon value) {
_icon = value;
_image = QImage();
if (repaint) {
repaint();
}
}, _lifetime);
}
int DefaultIconEmoji::width() {
return st::emojiSize + 2 * st::emojiPadding;
}
QString DefaultIconEmoji::entityData() {
return u"topic_icon:%1"_q.arg(_icon.colorId);
}
void DefaultIconEmoji::paint(QPainter &p, const Context &context) {
const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)
? st::normalForumTopicIcon
: st::defaultForumTopicIcon;
const auto general = Data::IsForumGeneralIconTitle(_icon.title);
if (_image.isNull()) {
_image = general
? Data::ForumTopicGeneralIconFrame(
st.size,
QColor(255, 255, 255))
: Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);
}
const auto full = (_tag == Data::CustomEmojiSizeTag::Normal)
? Ui::Emoji::GetSizeNormal()
: Ui::Emoji::GetSizeLarge();
const auto esize = full / style::DevicePixelRatio();
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto skip = (customSize - st.size) / 2;
p.drawImage(context.position + QPoint(skip, skip), general
? style::colorizeImage(_image, context.textColor)
: _image);
}
void DefaultIconEmoji::unload() {
_image = QImage();
}
bool DefaultIconEmoji::ready() {
return true;
}
bool DefaultIconEmoji::readyInDefaultState() {
return true;
}
[[nodiscard]] int EditIconSize() {
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
return Data::FrameSizeFromTag(tag) / style::DevicePixelRatio();
}
[[nodiscard]] int32 ChooseNextColorId(
int32 currentId,
std::vector<int32> &otherIds) {
if (otherIds.size() == 1 && otherIds.front() == currentId) {
otherIds = Data::ForumTopicColorIds();
}
const auto i = ranges::find(otherIds, currentId);
if (i != end(otherIds)) {
otherIds.erase(i);
}
return otherIds.empty()
? currentId
: otherIds[base::RandomIndex(otherIds.size())];
}
[[nodiscard]] not_null<Ui::AbstractButton*> EditIconButton(
not_null<QWidget*> parent,
not_null<Window::SessionController*> controller,
rpl::producer<DefaultIcon> defaultIcon,
rpl::producer<DocumentId> iconId,
Fn<bool(not_null<Ui::RpWidget*>)> paintIconFrame) {
using namespace Info::Profile;
struct State {
std::unique_ptr<Ui::Text::CustomEmoji> icon;
QImage defaultIcon;
};
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
const auto size = EditIconSize();
const auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());
result->show();
const auto state = result->lifetime().make_state<State>();
std::move(
iconId
) | rpl::on_next([=](DocumentId id) {
const auto owner = &controller->session().data();
state->icon = id
? owner->customEmojiManager().create(
id,
[=] { result->update(); },
tag)
: nullptr;
result->update();
}, result->lifetime());
std::move(
defaultIcon
) | rpl::on_next([=](DefaultIcon icon) {
state->defaultIcon = Data::ForumTopicIconFrame(
icon.colorId,
icon.title,
st::largeForumTopicIcon);
result->update();
}, result->lifetime());
result->resize(size, size);
result->paintRequest(
) | rpl::filter([=] {
return !paintIconFrame(result);
}) | rpl::on_next([=](QRect clip) {
auto args = Ui::Text::CustomEmoji::Context{
.textColor = st::windowFg->c,
.now = crl::now(),
.paused = controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer),
};
auto p = QPainter(result);
if (state->icon) {
state->icon->paint(p, args);
} else {
const auto skip = (size - st::largeForumTopicIcon.size) / 2;
p.drawImage(skip, skip, state->defaultIcon);
}
}, result->lifetime());
return result;
}
[[nodiscard]] not_null<Ui::AbstractButton*> GeneralIconPreview(
not_null<QWidget*> parent) {
using namespace Info::Profile;
struct State {
QImage frame;
};
const auto size = EditIconSize();
const auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());
result->show();
result->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto state = result->lifetime().make_state<State>();
rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
state->frame = Data::ForumTopicGeneralIconFrame(
st::largeForumTopicIcon.size,
st::windowSubTextFg->c);
result->update();
}, result->lifetime());
result->resize(size, size);
result->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(result);
const auto skip = (size - st::largeForumTopicIcon.size) / 2;
p.drawImage(skip, skip, state->frame);
}, result->lifetime());
return result;
}
struct IconSelector {
Fn<bool(not_null<Ui::RpWidget*>)> paintIconFrame;
rpl::producer<DocumentId> iconIdValue;
};
[[nodiscard]] IconSelector AddIconSelector(
not_null<Ui::GenericBox*> box,
not_null<Ui::RpWidget*> button,
not_null<Window::SessionController*> controller,
rpl::producer<DefaultIcon> defaultIcon,
rpl::producer<int> coverHeight,
DocumentId iconId,
Fn<void(object_ptr<Ui::RpWidget>)> placeFooter) {
using namespace ChatHelpers;
struct State {
std::unique_ptr<Ui::EmojiFlyAnimation> animation;
std::unique_ptr<HistoryView::StickerToast> toast;
rpl::variable<DocumentId> iconId;
QPointer<QWidget> button;
};
const auto state = box->lifetime().make_state<State>(State{
.iconId = iconId,
.button = button.get(),
});
const auto manager = &controller->session().data().customEmojiManager();
auto factory = [=](DocumentId id, Fn<void()> repaint)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
if (id == kDefaultIconId) {
return std::make_unique<DefaultIconEmoji>(
rpl::duplicate(defaultIcon),
std::move(repaint),
tag);
}
return manager->create(id, std::move(repaint), tag);
};
const auto icons = &controller->session().data().forumIcons();
const auto body = box->verticalLayout();
const auto recent = [=] {
auto list = icons->list();
list.insert(begin(list), kDefaultIconId);
return list;
};
const auto selector = body->add(
object_ptr<EmojiListWidget>(body, EmojiListDescriptor{
.show = controller->uiShow(),
.mode = EmojiListWidget::Mode::TopicIcon,
.paused = Window::PausedIn(controller, PauseReason::Layer),
.customRecentList = DocumentListToRecent(recent()),
.customRecentFactory = std::move(factory),
.st = &st::reactPanelEmojiPan,
}),
st::reactPanelEmojiPan.padding);
icons->requestDefaultIfUnknown();
icons->defaultUpdates(
) | rpl::on_next([=] {
selector->provideRecent(DocumentListToRecent(recent()));
}, selector->lifetime());
placeFooter(selector->createFooter());
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(box.get());
shadow->show();
rpl::combine(
rpl::duplicate(coverHeight),
selector->widthValue()
) | rpl::on_next([=](int top, int width) {
shadow->setGeometry(0, top, width, st::lineWidth);
}, shadow->lifetime());
selector->refreshEmoji();
selector->scrollToRequests(
) | rpl::on_next([=](int y) {
box->scrollToY(y);
shadow->update();
}, selector->lifetime());
rpl::combine(
box->heightValue(),
std::move(coverHeight),
rpl::mappers::_1 - rpl::mappers::_2
) | rpl::on_next([=](int height) {
selector->setMinimalHeight(selector->width(), height);
}, body->lifetime());
const auto showToast = [=](not_null<DocumentData*> document) {
if (!state->toast) {
state->toast = std::make_unique<HistoryView::StickerToast>(
controller,
controller->widget()->bodyWidget(),
[=] { state->toast = nullptr; });
}
state->toast->showFor(
document,
HistoryView::StickerToast::Section::TopicIcon);
};
selector->customChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
const auto owner = &controller->session().data();
const auto document = data.document;
const auto id = document->id;
const auto custom = (id != kDefaultIconId);
const auto premium = custom
&& !ranges::contains(document->owner().forumIcons().list(), id);
if (premium && !controller->session().premium()) {
showToast(document);
return;
}
const auto body = controller->window().widget()->bodyWidget();
if (state->button && custom) {
const auto &from = data.messageSendingFrom;
auto args = Ui::ReactionFlyAnimationArgs{
.id = { { id } },
.flyIcon = from.frame,
.flyFrom = body->mapFromGlobal(from.globalStartGeometry),
};
state->animation = std::make_unique<Ui::EmojiFlyAnimation>(
body,
&owner->reactions(),
std::move(args),
[=] { state->animation->repaint(); },
[] { return st::windowFg->c; },
Data::CustomEmojiSizeTag::Large);
}
state->iconId = id;
}, selector->lifetime());
auto paintIconFrame = [=](not_null<Ui::RpWidget*> button) {
if (!state->animation) {
return false;
} else if (state->animation->paintBadgeFrame(button)) {
return true;
}
InvokeQueued(state->animation->layer(), [=] {
state->animation = nullptr;
});
return false;
};
return {
.paintIconFrame = std::move(paintIconFrame),
.iconIdValue = state->iconId.value(),
};
}
} // namespace
void NewForumTopicBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
not_null<History*> forum) {
EditForumTopicBox(box, controller, forum, MsgId(0));
}
void EditForumTopicBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
not_null<History*> forum,
MsgId rootId) {
const auto creating = !rootId;
const auto topic = (!creating && forum->peer->forum())
? forum->peer->forum()->topicFor(rootId)
: nullptr;
const auto bot = forum->peer->isBot();
const auto created = topic && !topic->creating();
box->setTitle(creating
? tr::lng_forum_topic_new()
: bot
? tr::lng_bot_thread_edit()
: tr::lng_forum_topic_edit());
box->setMaxHeight(st::editTopicMaxHeight);
struct State {
rpl::variable<DefaultIcon> defaultIcon;
rpl::variable<DocumentId> iconId = 0;
std::vector<int32> otherColorIds;
mtpRequestId requestId = 0;
Fn<bool(not_null<Ui::RpWidget*>)> paintIconFrame;
};
const auto state = box->lifetime().make_state<State>();
const auto &colors = Data::ForumTopicColorIds();
state->iconId = topic ? topic->iconId() : 0;
state->otherColorIds = colors;
state->defaultIcon = DefaultIcon{
topic ? topic->title() : QString(),
topic ? topic->colorId() : ChooseNextColorId(0, state->otherColorIds)
};
const auto top = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box));
const auto title = top->add(
object_ptr<Ui::InputField>(
box,
st::defaultInputField,
(bot
? tr::lng_bot_thread_title()
: tr::lng_forum_topic_title()),
topic ? topic->title() : QString()),
st::editTopicTitleMargin);
box->setFocusCallback([=] {
title->setFocusFast();
});
const auto paintIconFrame = [=](not_null<Ui::RpWidget*> widget) {
return state->paintIconFrame && state->paintIconFrame(widget);
};
const auto icon = (topic && topic->isGeneral())
? GeneralIconPreview(title->parentWidget())
: EditIconButton(
title->parentWidget(),
controller,
state->defaultIcon.value(),
state->iconId.value(),
paintIconFrame);
title->geometryValue(
) | rpl::on_next([=](QRect geometry) {
icon->move(
st::editTopicIconPosition.x(),
st::editTopicIconPosition.y());
}, icon->lifetime());
state->iconId.value(
) | rpl::on_next([=](DocumentId iconId) {
icon->setAttribute(
Qt::WA_TransparentForMouseEvents,
created || (iconId != 0));
}, box->lifetime());
icon->setClickedCallback([=] {
const auto current = state->defaultIcon.current();
state->defaultIcon = DefaultIcon{
current.title,
ChooseNextColorId(current.colorId, state->otherColorIds),
};
});
title->changes(
) | rpl::on_next([=] {
state->defaultIcon = DefaultIcon{
title->getLastText().trimmed(),
state->defaultIcon.current().colorId,
};
}, title->lifetime());
title->submits() | rpl::on_next([box] {
box->triggerButton(0);
}, title->lifetime());
if (!topic || !topic->isGeneral()) {
Ui::AddDividerText(top, bot
? tr::lng_bot_thread_choose_title_and_icon()
: tr::lng_forum_choose_title_and_icon());
box->setScrollStyle(st::reactPanelScroll);
auto selector = AddIconSelector(
box,
icon,
controller,
state->defaultIcon.value(),
top->heightValue(),
state->iconId.current(),
[&](object_ptr<Ui::RpWidget> footer) {
top->add(std::move(footer)); });
state->paintIconFrame = std::move(selector.paintIconFrame);
std::move(
selector.iconIdValue
) | rpl::on_next([=](DocumentId iconId) {
state->iconId = (iconId != kDefaultIconId) ? iconId : 0;
}, box->lifetime());
}
const auto create = [=] {
if (!forum->peer->isForum()) {
box->closeBox();
return;
} else if (title->getLastText().trimmed().isEmpty()) {
title->showError();
return;
}
using namespace HistoryView;
controller->showSection(
std::make_shared<ChatMemento>(ChatViewId{
.history = forum,
.repliesRootId = forum->peer->forum()->reserveCreatingId(
title->getLastText().trimmed(),
state->defaultIcon.current().colorId,
state->iconId.current()),
}),
Window::SectionShow::Way::ClearStack);
};
const auto save = [=] {
const auto parent = forum->peer->forum();
const auto topic = parent
? parent->topicFor(rootId)
: nullptr;
if (!topic) {
box->closeBox();
return;
} else if (state->requestId > 0) {
return;
} else if (title->getLastText().trimmed().isEmpty()) {
title->showError();
return;
} else if (parent->creating(rootId)) {
topic->applyTitle(title->getLastText().trimmed());
topic->applyColorId(state->defaultIcon.current().colorId);
topic->applyIconId(state->iconId.current());
box->closeBox();
} else {
using Flag = MTPmessages_EditForumTopic::Flag;
const auto api = &forum->session().api();
const auto weak = base::make_weak(box);
state->requestId = api->request(MTPmessages_EditForumTopic(
MTP_flags(Flag::f_title
| (topic->isGeneral() ? Flag() : Flag::f_icon_emoji_id)),
topic->peer()->input(),
MTP_int(rootId),
MTP_string(title->getLastText().trimmed()),
MTP_long(state->iconId.current()),
MTPBool(), // closed
MTPBool() // hidden
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
if (const auto strong = weak.get()) {
strong->closeBox();
}
}).fail([=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
if (error.type() == u"TOPIC_NOT_MODIFIED") {
strong->closeBox();
} else {
state->requestId = -1;
}
}
}).send();
}
};
if (creating) {
box->addButton(tr::lng_create_group_create(), create);
} else {
box->addButton(tr::lng_settings_save(), save);
}
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
Data::TopicIconDescriptor descriptor,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag) {
return std::make_unique<DefaultIconEmoji>(
rpl::single(descriptor),
std::move(repaint),
tag);
}

View File

@@ -0,0 +1,41 @@
/*
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/layers/generic_box.h"
class History;
namespace Data {
struct TopicIconDescriptor;
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Window {
class SessionController;
} // namespace Window
void NewForumTopicBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
not_null<History*> forum);
void EditForumTopicBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
not_null<History*> forum,
MsgId rootId);
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
Data::TopicIconDescriptor descriptor,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag);

View File

@@ -0,0 +1,77 @@
/*
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_members_visible.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "data/data_channel.h"
#include "ui/rp_widget.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/widgets/buttons.h"
#include "ui/vertical_list.h"
#include "settings/settings_common.h" // IconDescriptor.
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"
namespace {
[[nodiscard]] int EnableHideMembersMin(not_null<ChannelData*> channel) {
return channel->session().appConfig().get<int>(
u"hidden_members_group_size_min"_q,
100);
}
} // namespace
[[nodiscard]] object_ptr<Ui::RpWidget> CreateMembersVisibleButton(
not_null<ChannelData*> megagroup) {
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
const auto min = EnableHideMembersMin(megagroup);
if (!megagroup->canBanMembers() || megagroup->membersCount() < min) {
return { nullptr };
}
struct State {
rpl::event_stream<bool> toggled;
};
Ui::AddSkip(container);
const auto state = container->lifetime().make_state<State>();
const auto button = container->add(
EditPeerInfoBox::CreateButton(
container,
tr::lng_profile_hide_participants(),
rpl::single(QString()),
[] {},
st::manageGroupNoIconButton,
{}
))->toggleOn(rpl::single(
(megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0
) | rpl::then(state->toggled.events()));
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_profile_hide_participants_about());
button->toggledValue(
) | rpl::on_next([=](bool toggled) {
megagroup->session().api().request(
MTPchannels_ToggleParticipantsHidden(
megagroup->inputChannel(),
MTP_bool(toggled)
)
).done([=](const MTPUpdates &result) {
megagroup->session().api().applyUpdates(result);
}).send();
}, button->lifetime());
return result;
}

View File

@@ -0,0 +1,19 @@
/*
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 "base/object_ptr.h"
class ChannelData;
namespace Ui {
class RpWidget;
} // namespace Ui
[[nodiscard]] object_ptr<Ui::RpWidget> CreateMembersVisibleButton(
not_null<ChannelData*> megagroup);

View File

@@ -0,0 +1,982 @@
/*
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_participant_box.h"
#include "lang/lang_keys.h"
#include "ui/controls/userpic_button.h"
#include "ui/vertical_list.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/layers/generic_box.h"
#include "ui/toast/toast.h"
#include "ui/text/text_utilities.h"
#include "ui/text/text_options.h"
#include "ui/painter.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "settings/settings_privacy_security.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/passcode_box.h"
#include "boxes/peers/add_bot_to_chat_box.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "data/data_peer_values.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "core/core_cloud_password.h"
#include "base/unixtime.h"
#include "apiwrap.h"
#include "api/api_cloud_password.h"
#include "main/main_session.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace {
constexpr auto kMaxRestrictDelayDays = 366;
constexpr auto kSecondsInDay = 24 * 60 * 60;
constexpr auto kSecondsInWeek = 7 * kSecondsInDay;
constexpr auto kAdminRoleLimit = 16;
} // namespace
class EditParticipantBox::Inner : public Ui::RpWidget {
public:
Inner(
QWidget *parent,
not_null<PeerData*> peer,
not_null<UserData*> user,
bool hasAdminRights);
template <typename Widget>
Widget *addControl(object_ptr<Widget> widget, QMargins margin);
[[nodiscard]] not_null<Ui::VerticalLayout*> verticalLayout() const {
return _rows;
}
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
not_null<PeerData*> _peer;
not_null<UserData*> _user;
object_ptr<Ui::UserpicButton> _userPhoto;
Ui::Text::String _userName;
bool _hasAdminRights = false;
object_ptr<Ui::VerticalLayout> _rows;
};
EditParticipantBox::Inner::Inner(
QWidget *parent,
not_null<PeerData*> peer,
not_null<UserData*> user,
bool hasAdminRights)
: RpWidget(parent)
, _peer(peer)
, _user(user)
, _userPhoto(this, _user, st::rightsPhotoButton)
, _hasAdminRights(hasAdminRights)
, _rows(this) {
_rows->heightValue(
) | rpl::on_next([=] {
resizeToWidth(width());
}, lifetime());
_userPhoto->setAttribute(Qt::WA_TransparentForMouseEvents);
_userName.setText(
st::rightsNameStyle,
_user->name(),
Ui::NameTextOptions());
}
template <typename Widget>
Widget *EditParticipantBox::Inner::addControl(
object_ptr<Widget> widget,
QMargins margin) {
return _rows->add(std::move(widget), margin);
}
int EditParticipantBox::Inner::resizeGetHeight(int newWidth) {
_userPhoto->moveToLeft(
st::rightsPhotoMargin.left(),
st::rightsPhotoMargin.top());
const auto rowsTop = st::rightsPhotoMargin.top()
+ st::rightsPhotoButton.size.height()
+ st::rightsPhotoMargin.bottom();
_rows->resizeToWidth(newWidth);
_rows->moveToLeft(0, rowsTop, newWidth);
return rowsTop + _rows->heightNoMargins();
}
void EditParticipantBox::Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
p.fillRect(e->rect(), st::boxBg);
p.setPen(st::contactsNameFg);
auto namex = st::rightsPhotoMargin.left()
+ st::rightsPhotoButton.size .width()
+ st::rightsPhotoMargin.right();
auto namew = width() - namex - st::rightsPhotoMargin.right();
_userName.drawLeftElided(
p,
namex,
st::rightsPhotoMargin.top() + st::rightsNameTop,
namew,
width());
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());
}();
p.setFont(st::contactsStatusFont);
p.setPen(st::contactsStatusFg);
p.drawTextLeft(
namex,
st::rightsPhotoMargin.top() + st::rightsStatusTop,
width(),
statusText);
}
EditParticipantBox::EditParticipantBox(
QWidget*,
not_null<PeerData*> peer,
not_null<UserData*> user,
bool hasAdminRights)
: _peer(peer)
, _user(user)
, _hasAdminRights(hasAdminRights) {
}
not_null<Ui::VerticalLayout*> EditParticipantBox::verticalLayout() const {
return _inner->verticalLayout();
}
void EditParticipantBox::prepare() {
_inner = setInnerWidget(object_ptr<Inner>(
this,
_peer,
_user,
hasAdminRights()));
setDimensionsToContent(st::boxWideWidth, _inner);
}
template <typename Widget>
Widget *EditParticipantBox::addControl(
object_ptr<Widget> widget,
QMargins margin) {
Expects(_inner != nullptr);
return _inner->addControl(std::move(widget), margin);
}
bool EditParticipantBox::amCreator() const {
if (const auto chat = _peer->asChat()) {
return chat->amCreator();
} else if (const auto channel = _peer->asChannel()) {
return channel->amCreator();
}
Unexpected("Peer type in EditParticipantBox::Inner::amCreator.");
}
EditAdminBox::EditAdminBox(
QWidget*,
not_null<PeerData*> peer,
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank,
TimeId promotedSince,
UserData *by,
std::optional<EditAdminBotFields> addingBot)
: EditParticipantBox(
nullptr,
peer,
user,
(rights.flags != 0))
, _oldRights(rights)
, _oldRank(rank)
, _promotedSince(promotedSince)
, _by(by)
, _addingBot(std::move(addingBot)) {
}
ChatAdminRightsInfo EditAdminBox::defaultRights() const {
using Flag = ChatAdminRight;
return peer()->isChat()
? peer()->asChat()->defaultAdminRights(user())
: peer()->isMegagroup()
? ChatAdminRightsInfo{ (Flag::ChangeInfo
| Flag::DeleteMessages
| Flag::PostStories
| Flag::EditStories
| Flag::DeleteStories
| Flag::BanUsers
| Flag::InviteByLinkOrAdd
| Flag::ManageTopics
| Flag::PinMessages
| Flag::ManageCall) }
: ChatAdminRightsInfo{ (Flag::ChangeInfo
| Flag::PostMessages
| Flag::EditMessages
| Flag::DeleteMessages
| Flag::PostStories
| Flag::EditStories
| Flag::DeleteStories
| Flag::InviteByLinkOrAdd
| Flag::ManageCall
| Flag::ManageDirect
| Flag::BanUsers) };
}
void EditAdminBox::prepare() {
using namespace rpl::mappers;
using Flag = ChatAdminRight;
using Flags = ChatAdminRights;
EditParticipantBox::prepare();
setTitle(_addingBot
? (_addingBot->existing
? tr::lng_rights_edit_admin()
: tr::lng_bot_add_title())
: _oldRights.flags
? tr::lng_rights_edit_admin()
: tr::lng_channel_add_admin());
if (_addingBot
&& !_addingBot->existing
&& !peer()->isBroadcast()
&& _saveCallback) {
addControl(
object_ptr<Ui::BoxContentDivider>(this),
st::rightsDividerMargin / 2);
_addAsAdmin = addControl(
object_ptr<Ui::Checkbox>(
this,
tr::lng_bot_as_admin_check(tr::now),
st::rightsCheckbox,
std::make_unique<Ui::ToggleView>(
st::rightsToggle,
true)),
st::rightsToggleMargin + (st::rightsDividerMargin / 2));
_addAsAdmin->checkedChanges(
) | rpl::on_next([=](bool checked) {
_adminControlsWrap->toggle(checked, anim::type::normal);
refreshButtons();
}, _addAsAdmin->lifetime());
}
_adminControlsWrap = addControl(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
this,
object_ptr<Ui::VerticalLayout>(this)));
const auto inner = _adminControlsWrap->entity();
if (_promotedSince) {
const auto parsed = base::unixtime::parse(_promotedSince);
const auto label = Ui::AddDividerText(
inner,
tr::lng_rights_about_by(
lt_user,
rpl::single(_by
? tr::link(_by->name(), 1)
: TextWithEntities{ QString::fromUtf8("\U0001F47B") }),
lt_date,
rpl::single(TextWithEntities{ langDateTimeFull(parsed) }),
tr::marked));
if (_by) {
label->setLink(1, _by->createOpenLink());
}
Ui::AddSkip(inner);
} else {
Ui::AddDivider(inner);
Ui::AddSkip(inner);
}
const auto chat = peer()->asChat();
const auto channel = peer()->asChannel();
const auto prepareRights = _addingBot
? ChatAdminRightsInfo(_oldRights.flags | _addingBot->existing)
: _oldRights.flags
? _oldRights
: defaultRights();
const auto disabledByDefaults = (channel && !channel->isMegagroup())
? ChatAdminRights()
: DisabledByDefaultRestrictions(peer());
const auto filterByMyRights = canSave()
&& !_oldRights.flags
&& channel
&& !channel->amCreator();
const auto prepareFlags = disabledByDefaults
| (prepareRights.flags
& (filterByMyRights ? channel->adminRights() : ~Flag(0)));
const auto disabledMessages = [&] {
auto result = base::flat_map<Flags, QString>();
if (!canSave()) {
result.emplace(
~Flags(0),
tr::lng_rights_about_admin_cant_edit(tr::now));
} else {
result.emplace(
disabledByDefaults,
tr::lng_rights_permission_for_all(tr::now));
if (amCreator() && user()->isSelf()) {
result.emplace(
~Flag::Anonymous,
tr::lng_rights_permission_cant_edit(tr::now));
} else if (const auto channel = peer()->asChannel()) {
if (!channel->amCreator()) {
result.emplace(
~channel->adminRights(),
tr::lng_rights_permission_cant_edit(tr::now));
}
}
}
return result;
}();
const auto isGroup = chat || channel->isMegagroup();
const auto anyoneCanAddMembers = chat
? chat->anyoneCanAddMembers()
: channel->anyoneCanAddMembers();
const auto options = Data::AdminRightsSetOptions{
.isGroup = isGroup,
.isForum = peer()->isForum(),
.anyoneCanAddMembers = anyoneCanAddMembers,
};
Ui::AddSubsectionTitle(inner, tr::lng_rights_edit_admin_header());
auto [checkboxes, getChecked, changes] = CreateEditAdminRights(
inner,
prepareFlags,
disabledMessages,
options);
inner->add(std::move(checkboxes), QMargins());
auto selectedFlags = rpl::single(
getChecked()
) | rpl::then(std::move(
changes
));
const auto hasRank = canSave() && (chat || channel->isMegagroup());
{
const auto aboutAddAdminsInner = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto emptyAboutAddAdminsInner = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
aboutAddAdminsInner->toggle(false, anim::type::instant);
emptyAboutAddAdminsInner->toggle(false, anim::type::instant);
Ui::AddSkip(emptyAboutAddAdminsInner->entity());
if (hasRank) {
Ui::AddDivider(emptyAboutAddAdminsInner->entity());
Ui::AddSkip(emptyAboutAddAdminsInner->entity());
}
Ui::AddSkip(aboutAddAdminsInner->entity());
Ui::AddDividerText(
aboutAddAdminsInner->entity(),
rpl::duplicate(
selectedFlags
) | rpl::map(
(_1 & Flag::AddAdmins) != 0
) | rpl::distinct_until_changed(
) | rpl::map([=](bool canAddAdmins) -> rpl::producer<QString> {
const auto empty = (amCreator() && user()->isSelf());
aboutAddAdminsInner->toggle(!empty, anim::type::instant);
emptyAboutAddAdminsInner->toggle(empty, anim::type::instant);
if (empty) {
return rpl::single(QString());
} else if (!canSave()) {
return tr::lng_rights_about_admin_cant_edit();
} else if (canAddAdmins) {
return tr::lng_rights_about_add_admins_yes();
}
return tr::lng_rights_about_add_admins_no();
}) | rpl::flatten_latest());
}
if (canTransferOwnership()) {
const auto allFlags = AdminRightsForOwnershipTransfer(options);
setupTransferButton(
inner,
isGroup
)->toggleOn(rpl::duplicate(
selectedFlags
) | rpl::map(
((_1 & allFlags) == allFlags)
))->setDuration(0);
}
if (canSave()) {
_rank = hasRank ? addRankInput(inner).get() : nullptr;
_finishSave = [=, value = getChecked] {
const auto newFlags = (value() | ChatAdminRight::Other)
& ((!channel || channel->amCreator())
? ~Flags(0)
: channel->adminRights());
_saveCallback(
_oldRights,
ChatAdminRightsInfo(newFlags),
_rank ? _rank->getLastText().trimmed() : QString());
};
_save = [=] {
const auto show = uiShow();
if (!_saveCallback) {
return;
} else if (_addAsAdmin && !_addAsAdmin->checked()) {
const auto weak = base::make_weak(this);
AddBotToGroup(show, user(), peer(), _addingBot->token);
if (const auto strong = weak.get()) {
strong->closeBox();
}
return;
} else if (_addingBot && !_addingBot->existing) {
const auto phrase = peer()->isBroadcast()
? tr::lng_bot_sure_add_text_channel
: tr::lng_bot_sure_add_text_group;
_confirmBox = getDelegate()->show(Ui::MakeConfirmBox({
phrase(
tr::now,
lt_group,
tr::bold(peer()->name()),
tr::marked),
crl::guard(this, [=] { finishAddAdmin(); })
}));
} else {
_finishSave();
}
};
}
refreshButtons();
}
void EditAdminBox::finishAddAdmin() {
_finishSave();
if (_confirmBox) {
_confirmBox->closeBox();
}
}
void EditAdminBox::refreshButtons() {
clearButtons();
if (canSave()) {
addButton((!_addingBot || _addingBot->existing)
? tr::lng_settings_save()
: _adminControlsWrap->toggled()
? tr::lng_bot_add_as_admin()
: tr::lng_bot_add_as_member(), _save);
addButton(tr::lng_cancel(), [=] { closeBox(); });
} else {
addButton(tr::lng_box_ok(), [=] { closeBox(); });
}
}
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 {
if (user()->isInaccessible() || user()->isBot() || user()->isSelf()) {
return false;
} else if (const auto chat = peer()->asChat()) {
return chat->amCreator();
} else if (const auto channel = peer()->asChannel()) {
return channel->amCreator();
}
Unexpected("Chat type in EditAdminBox::canTransferOwnership.");
}
not_null<Ui::SlideWrap<Ui::RpWidget>*> EditAdminBox::setupTransferButton(
not_null<Ui::VerticalLayout*> container,
bool isGroup) {
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto inner = wrap->entity();
inner->add(
object_ptr<Ui::BoxContentDivider>(inner),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
inner->add(EditPeerInfoBox::CreateButton(
inner,
(isGroup
? tr::lng_rights_transfer_group
: tr::lng_rights_transfer_channel)(),
rpl::single(QString()),
[=] { transferOwnership(); },
st::peerPermissionsButton,
{}));
return wrap;
}
void EditAdminBox::transferOwnership() {
if (_checkTransferRequestId) {
return;
}
const auto channel = peer()->isChannel()
? peer()->asChannel()->inputChannel()
: MTP_inputChannelEmpty();
const auto api = &peer()->session().api();
api->cloudPassword().reload();
_checkTransferRequestId = api->request(MTPchannels_EditCreator(
channel,
MTP_inputUserEmpty(),
MTP_inputCheckPasswordEmpty()
)).fail([=](const MTP::Error &error) {
_checkTransferRequestId = 0;
if (!handleTransferPasswordError(error.type())) {
const auto callback = crl::guard(this, [=](Fn<void()> &&close) {
transferOwnershipChecked();
close();
});
getDelegate()->show(Ui::MakeConfirmBox({
.text = tr::lng_rights_transfer_about(
tr::now,
lt_group,
tr::bold(peer()->name()),
lt_user,
tr::bold(user()->shortName()),
tr::rich),
.confirmed = callback,
.confirmText = tr::lng_rights_transfer_sure(),
}));
}
}).send();
}
bool EditAdminBox::handleTransferPasswordError(const QString &error) {
const auto session = &user()->session();
auto about = tr::lng_rights_transfer_check_about(
tr::now,
lt_user,
tr::bold(user()->shortName()),
tr::marked);
if (auto box = PrePasswordErrorBox(error, session, std::move(about))) {
getDelegate()->show(std::move(box));
return true;
}
return false;
}
void EditAdminBox::transferOwnershipChecked() {
if (const auto chat = peer()->asChatNotMigrated()) {
peer()->session().api().migrateChat(chat, crl::guard(this, [=](
not_null<ChannelData*> channel) {
requestTransferPassword(channel);
}));
} else if (const auto channel = peer()->asChannelOrMigrated()) {
requestTransferPassword(channel);
} else {
Unexpected("Peer in SaveAdminCallback.");
}
}
void EditAdminBox::requestTransferPassword(not_null<ChannelData*> channel) {
peer()->session().api().cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
auto fields = PasscodeBox::CloudFields::From(state);
fields.customTitle = tr::lng_rights_transfer_password_title();
fields.customDescription
= tr::lng_rights_transfer_password_description(tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = crl::guard(this, [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> box) {
sendTransferRequestFrom(box, channel, result);
});
getDelegate()->show(Box<PasscodeBox>(&channel->session(), fields));
}, lifetime());
}
void EditAdminBox::sendTransferRequestFrom(
base::weak_qptr<PasscodeBox> box,
not_null<ChannelData*> channel,
const Core::CloudPasswordResult &result) {
if (_transferRequestId) {
return;
}
const auto weak = base::make_weak(this);
const auto user = this->user();
const auto api = &channel->session().api();
_transferRequestId = api->request(MTPchannels_EditCreator(
channel->inputChannel(),
user->inputUser(),
result.result
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
if (!box && !weak) {
return;
}
const auto show = box ? box->uiShow() : weak->uiShow();
show->showToast(
(channel->isBroadcast()
? tr::lng_rights_transfer_done_channel
: tr::lng_rights_transfer_done_group)(
tr::now,
lt_user,
user->shortName()));
show->hideLayer();
}).fail(crl::guard(this, [=](const MTP::Error &error) {
if (weak) {
_transferRequestId = 0;
}
if (box && box->handleCustomCheckError(error)) {
return;
}
const auto &type = error.type();
const auto problem = [&] {
if (type == u"CHANNELS_ADMIN_PUBLIC_TOO_MUCH"_q) {
return tr::lng_channels_too_much_public_other(tr::now);
} else if (type == u"CHANNELS_ADMIN_LOCATED_TOO_MUCH"_q) {
return tr::lng_channels_too_much_located_other(tr::now);
} else if (type == u"ADMINS_TOO_MUCH"_q) {
return (channel->isBroadcast()
? tr::lng_error_admin_limit_channel
: tr::lng_error_admin_limit)(tr::now);
} else if (type == u"CHANNEL_INVALID"_q) {
return (channel->isBroadcast()
? tr::lng_channel_not_accessible
: tr::lng_group_not_accessible)(tr::now);
}
return Lang::Hard::ServerError();
}();
const auto recoverable = [&] {
return (type == u"PASSWORD_MISSING"_q)
|| type.startsWith(u"PASSWORD_TOO_FRESH_"_q)
|| type.startsWith(u"SESSION_TOO_FRESH_"_q);
}();
const auto weak = base::make_weak(this);
getDelegate()->show(Ui::MakeInformBox(problem));
if (box) {
box->closeBox();
}
if (weak && !recoverable) {
closeBox();
}
})).handleFloodErrors().send();
}
EditRestrictedBox::EditRestrictedBox(
QWidget*,
not_null<PeerData*> peer,
not_null<UserData*> user,
bool hasAdminRights,
ChatRestrictionsInfo rights,
UserData *by,
TimeId since)
: EditParticipantBox(nullptr, peer, user, hasAdminRights)
, _oldRights(rights)
, _by(by)
, _since(since) {
}
void EditRestrictedBox::prepare() {
using Flag = ChatRestriction;
using Flags = ChatRestrictions;
EditParticipantBox::prepare();
setTitle(tr::lng_rights_user_restrictions());
Ui::AddDivider(verticalLayout());
Ui::AddSkip(verticalLayout());
const auto chat = peer()->asChat();
const auto channel = peer()->asChannel();
const auto defaultRestrictions = chat
? chat->defaultRestrictions()
: channel->defaultRestrictions();
const auto prepareRights = _oldRights.flags
? _oldRights
: defaultRights();
const auto prepareFlags = FixDependentRestrictions(
prepareRights.flags
| defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
const auto disabledMessages = [&] {
auto result = base::flat_map<Flags, QString>();
if (!canSave()) {
result.emplace(
~Flags(0),
tr::lng_rights_about_restriction_cant_edit(tr::now));
} else {
const auto disabled = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
result.emplace(
disabled,
tr::lng_rights_restriction_for_all(tr::now));
}
return result;
}();
Ui::AddSubsectionTitle(
verticalLayout(),
tr::lng_rights_user_restrictions_header());
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
this,
prepareFlags,
disabledMessages,
{ .isForum = peer()->isForum() });
addControl(std::move(checkboxes), QMargins());
_until = prepareRights.until;
addControl(
object_ptr<Ui::FixedHeightWidget>(this, st::defaultVerticalListSkip));
Ui::AddDivider(verticalLayout());
addControl(
object_ptr<Ui::FlatLabel>(
this,
tr::lng_rights_chat_banned_until_header(tr::now),
st::rightsHeaderLabel),
st::rightsHeaderMargin);
setRestrictUntil(_until);
//addControl(
// object_ptr<Ui::LinkButton>(
// this,
// tr::lng_rights_chat_banned_block(tr::now),
// st::boxLinkButton));
if (_since) {
const auto parsed = base::unixtime::parse(_since);
const auto inner = addControl(object_ptr<Ui::VerticalLayout>(this));
const auto isBanned = (_oldRights.flags
& ChatRestriction::ViewMessages);
Ui::AddSkip(inner);
const auto label = Ui::AddDividerText(
inner,
(isBanned
? tr::lng_rights_chat_banned_by
: tr::lng_rights_chat_restricted_by)(
lt_user,
rpl::single(_by
? tr::link(_by->name(), 1)
: TextWithEntities{ QString::fromUtf8("\U0001F47B") }),
lt_date,
rpl::single(TextWithEntities{ langDateTimeFull(parsed) }),
tr::marked));
if (_by) {
label->setLink(1, _by->createOpenLink());
}
}
if (canSave()) {
const auto save = [=, value = getRestrictions] {
if (!_saveCallback) {
return;
}
_saveCallback(
_oldRights,
ChatRestrictionsInfo{ value(), getRealUntilValue() });
};
addButton(tr::lng_settings_save(), save);
addButton(tr::lng_cancel(), [=] { closeBox(); });
} else {
addButton(tr::lng_box_ok(), [=] { closeBox(); });
}
}
ChatRestrictionsInfo EditRestrictedBox::defaultRights() const {
return ChatRestrictionsInfo();
}
void EditRestrictedBox::showRestrictUntil() {
uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto save = [=](TimeId result) {
if (!result) {
return;
}
setRestrictUntil(result);
box->closeBox();
};
const auto now = base::unixtime::now();
const auto time = isUntilForever()
? (now + kSecondsInDay)
: getRealUntilValue();
ChooseDateTimeBox(box, {
.title = tr::lng_rights_chat_banned_until_header(),
.submit = tr::lng_settings_save(),
.done = save,
.min = [=] { return now; },
.time = time,
.max = [=] {
return now + kSecondsInDay * kMaxRestrictDelayDays;
},
});
}));
}
void EditRestrictedBox::setRestrictUntil(TimeId until) {
_until = until;
_untilVariants.clear();
createUntilGroup();
createUntilVariants();
}
bool EditRestrictedBox::isUntilForever() const {
return ChannelData::IsRestrictedForever(_until);
}
void EditRestrictedBox::createUntilGroup() {
_untilGroup = std::make_shared<Ui::RadiobuttonGroup>(
isUntilForever() ? 0 : _until);
_untilGroup->setChangedCallback([this](int value) {
if (value == kUntilCustom) {
_untilGroup->setValue(_until);
showRestrictUntil();
} else if (_until != value) {
_until = value;
}
});
}
void EditRestrictedBox::createUntilVariants() {
auto addVariant = [&](int value, const QString &text) {
if (!canSave() && _untilGroup->current() != value) {
return;
}
_untilVariants.emplace_back(
addControl(
object_ptr<Ui::Radiobutton>(
this,
_untilGroup,
value,
text,
st::defaultCheckbox),
st::rightsToggleMargin));
if (!canSave()) {
_untilVariants.back()->setDisabled(true);
}
};
auto addCustomVariant = [&](TimeId until, TimeId from, TimeId to) {
if (!ChannelData::IsRestrictedForever(until)
&& until > from
&& until <= to) {
addVariant(
until,
tr::lng_rights_chat_banned_custom_date(
tr::now,
lt_date,
langDateTime(base::unixtime::parse(until))));
}
};
auto addCurrentVariant = [&](TimeId from, TimeId to) {
auto oldUntil = _oldRights.until;
if (oldUntil < _until) {
addCustomVariant(oldUntil, from, to);
}
addCustomVariant(_until, from, to);
if (oldUntil > _until) {
addCustomVariant(oldUntil, from, to);
}
};
addVariant(0, tr::lng_rights_chat_banned_forever(tr::now));
auto now = base::unixtime::now();
auto nextDay = now + kSecondsInDay;
auto nextWeek = now + kSecondsInWeek;
addCurrentVariant(0, nextDay);
addVariant(kUntilOneDay, tr::lng_rights_chat_banned_day(tr::now, lt_count, 1));
addCurrentVariant(nextDay, nextWeek);
addVariant(kUntilOneWeek, tr::lng_rights_chat_banned_week(tr::now, lt_count, 1));
addCurrentVariant(nextWeek, INT_MAX);
addVariant(kUntilCustom, tr::lng_rights_chat_banned_custom(tr::now));
}
TimeId EditRestrictedBox::getRealUntilValue() const {
Expects(_until != kUntilCustom);
if (_until == kUntilOneDay) {
return base::unixtime::now() + kSecondsInDay;
} else if (_until == kUntilOneWeek) {
return base::unixtime::now() + kSecondsInWeek;
}
Assert(_until >= 0);
return _until;
}

View File

@@ -0,0 +1,189 @@
/*
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/layers/box_content.h"
#include "base/unique_qptr.h"
#include "data/data_chat_participant_status.h"
namespace Ui {
class FlatLabel;
class LinkButton;
class Checkbox;
class Radiobutton;
class RadiobuttonGroup;
class VerticalLayout;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Core {
struct CloudPasswordResult;
} // namespace Core
class PasscodeBox;
class EditParticipantBox : public Ui::BoxContent {
public:
EditParticipantBox(
QWidget*,
not_null<PeerData*> peer,
not_null<UserData*> user,
bool hasAdminRights);
[[nodiscard]] not_null<Ui::VerticalLayout*> verticalLayout() const;
protected:
void prepare() override;
[[nodiscard]] not_null<UserData*> user() const {
return _user;
}
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
[[nodiscard]] bool amCreator() const;
template <typename Widget>
Widget *addControl(object_ptr<Widget> widget, QMargins margin = {});
bool hasAdminRights() const {
return _hasAdminRights;
}
private:
not_null<PeerData*> _peer;
not_null<UserData*> _user;
bool _hasAdminRights = false;
class Inner;
QPointer<Inner> _inner;
};
struct EditAdminBotFields {
QString token;
ChatAdminRights existing;
};
class EditAdminBox : public EditParticipantBox {
public:
EditAdminBox(
QWidget*,
not_null<PeerData*> peer,
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank,
TimeId promotedSince,
UserData *by,
std::optional<EditAdminBotFields> addingBot = {});
void setSaveCallback(
Fn<void(
ChatAdminRightsInfo,
ChatAdminRightsInfo,
const QString &rank)> callback) {
_saveCallback = std::move(callback);
}
protected:
void prepare() override;
private:
[[nodiscard]] ChatAdminRightsInfo defaultRights() const;
not_null<Ui::InputField*> addRankInput(
not_null<Ui::VerticalLayout*> container);
void transferOwnership();
void transferOwnershipChecked();
bool handleTransferPasswordError(const QString &error);
void requestTransferPassword(not_null<ChannelData*> channel);
void sendTransferRequestFrom(
base::weak_qptr<PasscodeBox> box,
not_null<ChannelData*> channel,
const Core::CloudPasswordResult &result);
bool canSave() const {
return _saveCallback != nullptr;
}
void finishAddAdmin();
void refreshButtons();
bool canTransferOwnership() const;
not_null<Ui::SlideWrap<Ui::RpWidget>*> setupTransferButton(
not_null<Ui::VerticalLayout*> container,
bool isGroup);
const ChatAdminRightsInfo _oldRights;
const QString _oldRank;
Fn<void(
ChatAdminRightsInfo,
ChatAdminRightsInfo,
const QString &rank)> _saveCallback;
base::weak_qptr<Ui::BoxContent> _confirmBox;
Ui::Checkbox *_addAsAdmin = nullptr;
Ui::SlideWrap<Ui::VerticalLayout> *_adminControlsWrap = nullptr;
Ui::InputField *_rank = nullptr;
mtpRequestId _checkTransferRequestId = 0;
mtpRequestId _transferRequestId = 0;
Fn<void()> _save, _finishSave;
TimeId _promotedSince = 0;
UserData *_by = nullptr;
std::optional<EditAdminBotFields> _addingBot;
};
// Restricted box works with flags in the opposite way.
// If some flag is set in the rights then the checkbox is unchecked.
class EditRestrictedBox : public EditParticipantBox {
public:
EditRestrictedBox(
QWidget*,
not_null<PeerData*> peer,
not_null<UserData*> user,
bool hasAdminRights,
ChatRestrictionsInfo rights,
UserData *by,
TimeId since);
void setSaveCallback(
Fn<void(ChatRestrictionsInfo, ChatRestrictionsInfo)> callback) {
_saveCallback = std::move(callback);
}
protected:
void prepare() override;
private:
[[nodiscard]] ChatRestrictionsInfo defaultRights() const;
bool canSave() const {
return !!_saveCallback;
}
void showRestrictUntil();
void setRestrictUntil(TimeId until);
bool isUntilForever() const;
void createUntilGroup();
void createUntilVariants();
TimeId getRealUntilValue() const;
const ChatRestrictionsInfo _oldRights;
UserData *_by = nullptr;
TimeId _since = 0;
TimeId _until = 0;
Fn<void(ChatRestrictionsInfo, ChatRestrictionsInfo)> _saveCallback;
std::shared_ptr<Ui::RadiobuttonGroup> _untilGroup;
std::vector<base::unique_qptr<Ui::Radiobutton>> _untilVariants;
static constexpr auto kUntilOneDay = -1;
static constexpr auto kUntilOneWeek = -2;
static constexpr auto kUntilCustom = -3;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
/*
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 <rpl/variable.h>
#include "mtproto/sender.h"
#include "base/timer.h"
#include "base/weak_ptr.h"
#include "info/profile/info_profile_members_controllers.h"
class PeerListStories;
struct ChatAdminRightsInfo;
struct ChatRestrictionsInfo;
namespace Ui {
class Show;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Api {
class ChatParticipant;
} // namespace Api
Fn<void(
ChatAdminRightsInfo oldRights,
ChatAdminRightsInfo newRights,
const QString &rank)> SaveAdminCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<UserData*> user,
Fn<void(
ChatAdminRightsInfo newRights,
const QString &rank)> onDone,
Fn<void()> onFail);
Fn<void(
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights)> SaveRestrictedCallback(
not_null<PeerData*> peer,
not_null<PeerData*> participant,
Fn<void(ChatRestrictionsInfo newRights)> onDone,
Fn<void()> onFail);
void SubscribeToMigration(
not_null<PeerData*> peer,
rpl::lifetime &lifetime,
Fn<void(not_null<ChannelData*>)> migrate);
enum class ParticipantsRole {
Profile,
Members,
Admins,
Restricted,
Kicked,
};
class ParticipantsOnlineSorter {
public:
ParticipantsOnlineSorter(
not_null<PeerData*> peer,
not_null<PeerListDelegate*> delegate);
void sort();
rpl::producer<int> onlineCountValue() const;
private:
void sortDelayed();
void refreshOnlineCount();
const not_null<PeerData*> _peer;
const not_null<PeerListDelegate*> _delegate;
base::Timer _sortByOnlineTimer;
rpl::variable<int> _onlineCount = 0;
rpl::lifetime _lifetime;
};
class ParticipantsAdditionalData {
public:
using Role = ParticipantsRole;
ParticipantsAdditionalData(not_null<PeerData*> peer, Role role);
PeerData *applyParticipant(const Api::ChatParticipant &data);
PeerData *applyParticipant(
const Api::ChatParticipant &data,
Role overrideRole);
void setExternal(not_null<PeerData*> participant);
void checkForLoaded(not_null<PeerData*> participant);
void fillFromPeer();
[[nodiscard]] bool infoLoaded(not_null<PeerData*> participant) const;
[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canAddOrEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictParticipant(
not_null<PeerData*> participant) const;
[[nodiscard]] bool canRemoveParticipant(
not_null<PeerData*> participant) const;
[[nodiscard]] std::optional<ChatAdminRightsInfo> adminRights(
not_null<UserData*> user) const;
[[nodiscard]] QString adminRank(not_null<UserData*> user) const;
[[nodiscard]] std::optional<ChatRestrictionsInfo> restrictedRights(
not_null<PeerData*> participant) const;
[[nodiscard]] bool isCreator(not_null<UserData*> user) const;
[[nodiscard]] bool isExternal(not_null<PeerData*> participant) const;
[[nodiscard]] bool isKicked(not_null<PeerData*> participant) const;
[[nodiscard]] UserData *adminPromotedBy(not_null<UserData*> user) const;
[[nodiscard]] UserData *restrictedBy(
not_null<PeerData*> participant) const;
[[nodiscard]] TimeId adminPromotedSince(not_null<UserData*>) const;
[[nodiscard]] TimeId restrictedSince(not_null<PeerData*>) const;
[[nodiscard]] TimeId memberSince(not_null<UserData*>) const;
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);
void applyAdminLocally(
UserData *user,
ChatAdminRightsInfo rights,
const QString &rank);
void applyBannedLocally(
not_null<PeerData*> participant,
ChatRestrictionsInfo rights);
private:
UserData *applyCreator(const Api::ChatParticipant &data);
UserData *applyAdmin(const Api::ChatParticipant &data);
UserData *applyRegular(UserId userId);
PeerData *applyBanned(const Api::ChatParticipant &data);
void fillFromChat(not_null<ChatData*> chat);
void fillFromChannel(not_null<ChannelData*> channel);
not_null<PeerData*> _peer;
Role _role = Role::Members;
UserData *_creator = nullptr;
// Data for chats.
base::flat_set<not_null<UserData*>> _members;
base::flat_set<not_null<UserData*>> _admins;
// Data for channels.
base::flat_map<not_null<UserData*>, ChatAdminRightsInfo> _adminRights;
base::flat_map<not_null<UserData*>, QString> _adminRanks;
base::flat_map<not_null<UserData*>, TimeId> _adminPromotedSince;
base::flat_map<not_null<PeerData*>, TimeId> _restrictedSince;
base::flat_map<not_null<UserData*>, TimeId> _memberSince;
base::flat_set<not_null<UserData*>> _adminCanEdit;
base::flat_map<not_null<UserData*>, not_null<UserData*>> _adminPromotedBy;
std::map<not_null<PeerData*>, ChatRestrictionsInfo> _restrictedRights;
std::set<not_null<PeerData*>> _kicked;
std::map<not_null<PeerData*>, not_null<UserData*>> _restrictedBy;
std::set<not_null<PeerData*>> _external;
std::set<not_null<PeerData*>> _infoNotLoaded;
};
// Viewing admins, banned or restricted users list with search.
class ParticipantsBoxController
: public PeerListController
, public base::has_weak_ptr {
public:
using Role = ParticipantsRole;
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Role role);
ParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Role role);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
void loadMoreRows() override;
bool trackSelectedList() override {
return !_stories;
}
void peerListSearchAddRow(not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
[[nodiscard]] rpl::producer<int> onlineCountValue() const;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
void setStoriesShown(bool shown);
protected:
// Allow child controllers not providing navigation.
// This is their responsibility to override all methods that use it.
struct CreateTag {
};
ParticipantsBoxController(
CreateTag,
Window::SessionNavigation *navigation,
not_null<PeerData*> peer,
Role role);
virtual std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const;
private:
using Row = Info::Profile::MemberListRow;
using Type = Row::Type;
using Rights = Row::Rights;
struct SavedState : SavedStateBase {
explicit SavedState(const ParticipantsAdditionalData &additional);
using SearchStateBase = PeerListSearchController::SavedStateBase;
std::unique_ptr<SearchStateBase> searchState;
int offset = 0;
bool allLoaded = false;
bool wasLoading = false;
ParticipantsAdditionalData additional;
rpl::lifetime lifetime;
};
static std::unique_ptr<PeerListSearchController> CreateSearchController(
not_null<PeerData*> peer,
Role role,
not_null<ParticipantsAdditionalData*> additional);
base::weak_qptr<Ui::BoxContent> showBox(object_ptr<Ui::BoxContent> box) const;
void prepareChatRows(not_null<ChatData*> chat);
void rebuildChatRows(not_null<ChatData*> chat);
void rebuildChatParticipants(not_null<ChatData*> chat);
void rebuildChatAdmins(not_null<ChatData*> chat);
void chatListReady();
void rebuildRowTypes();
void rebuild();
void unload();
void addNewItem();
void addNewParticipants();
void refreshDescription();
void setupListChangeViewers();
void showAdmin(not_null<UserData*> user);
void editAdminDone(
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank);
void showRestricted(not_null<UserData*> user);
void editRestrictedDone(
not_null<PeerData*> participant,
ChatRestrictionsInfo rights);
void removeKicked(
not_null<PeerListRow*> row,
not_null<PeerData*> participant);
void removeKickedWithRow(not_null<PeerData*> participant);
void removeKicked(not_null<PeerData*> participant);
void kickParticipant(not_null<PeerData*> participant);
void kickParticipantSure(not_null<PeerData*> participant);
void unkickParticipant(not_null<UserData*> user);
void removeAdmin(not_null<UserData*> user);
void removeAdminSure(not_null<UserData*> user);
bool appendRow(not_null<PeerData*> participant);
bool prependRow(not_null<PeerData*> participant);
bool removeRow(not_null<PeerData*> participant);
void refreshCustomStatus(not_null<PeerListRow*> row) const;
bool feedMegagroupLastParticipants();
Type computeType(not_null<PeerData*> participant) const;
void recomputeTypeFor(not_null<PeerData*> participant);
void subscribeToMigration();
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);
void subscribeToCreatorChange(not_null<ChannelData*> channel);
void fullListRefresh();
void refreshRows();
// It may be nullptr in subclasses of this controller.
Window::SessionNavigation *_navigation = nullptr;
not_null<PeerData*> _peer;
MTP::Sender _api;
Role _role = Role::Admins;
int _offset = 0;
mtpRequestId _loadRequestId = 0;
bool _allLoaded = false;
ParticipantsAdditionalData _additional;
std::unique_ptr<ParticipantsOnlineSorter> _onlineSorter;
rpl::variable<int> _onlineCountValue;
rpl::variable<int> _fullCountValue;
Ui::BoxPointer _editBox;
Ui::BoxPointer _addBox;
base::weak_qptr<Ui::BoxContent> _editParticipantBox;
std::unique_ptr<PeerListStories> _stories;
};
// Members, banned and restricted users server side search.
class ParticipantsBoxSearchController : public PeerListSearchController {
public:
using Role = ParticipantsBoxController::Role;
ParticipantsBoxSearchController(
not_null<ChannelData*> channel,
Role role,
not_null<ParticipantsAdditionalData*> additional);
void searchQuery(const QString &query) override;
bool isLoading() override;
bool loadMoreRows() override;
std::unique_ptr<SavedStateBase> saveState() const override;
void restoreState(std::unique_ptr<SavedStateBase> state) override;
private:
struct SavedState : SavedStateBase {
QString query;
int offset = 0;
bool allLoaded = false;
bool wasLoading = false;
};
struct CacheEntry {
MTPchannels_ChannelParticipants result;
int requestedCount = 0;
};
struct Query {
QString text;
int offset = 0;
};
void searchOnServer();
bool searchInCache();
void searchDone(
mtpRequestId requestId,
const MTPchannels_ChannelParticipants &result,
int requestedCount);
not_null<ChannelData*> _channel;
Role _role = Role::Restricted;
not_null<ParticipantsAdditionalData*> _additional;
MTP::Sender _api;
base::Timer _timer;
QString _query;
mtpRequestId _requestId = 0;
int _offset = 0;
bool _allLoaded = false;
std::map<QString, CacheEntry> _cache;
std::map<mtpRequestId, Query> _queries;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
/*
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
namespace style {
struct SettingsButton;
} // namespace style
namespace st {
extern const style::SettingsButton &peerAppearanceButton;
} // namespace st
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui {
class RpWidget;
class GenericBox;
class ChatStyle;
class ChatTheme;
class VerticalLayout;
struct AskBoostReason;
class RpWidget;
class SettingsButton;
} // namespace Ui
void AddLevelBadge(
int level,
not_null<Ui::SettingsButton*> button,
Ui::RpWidget *right,
not_null<ChannelData*> channel,
const QMargins &padding,
rpl::producer<QString> text);
void EditPeerColorBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
std::shared_ptr<Ui::ChatStyle> style = nullptr,
std::shared_ptr<Ui::ChatTheme> theme = nullptr);
void AddPeerColorButton(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const style::SettingsButton &st);
void CheckBoostLevel(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
Fn<std::optional<Ui::AskBoostReason>(int level)> askMore,
Fn<void()> cancel);
struct ButtonWithEmoji {
not_null<const style::SettingsButton*> st;
int emojiWidth = 0;
int noneWidth = 0;
int added = 0;
};
[[nodiscard]] ButtonWithEmoji ButtonStyleWithRightEmoji(
not_null<Ui::RpWidget*> parent,
const QString &noneString,
const style::SettingsButton &parentSt = st::peerAppearanceButton);

View File

@@ -0,0 +1,18 @@
/*
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
namespace Ui::EditPeer {
constexpr auto kMaxGroupChannelTitle = 128;
constexpr auto kMaxUserFirstLastName = 64;
constexpr auto kMaxChannelDescription = 255;
constexpr auto kMinUsernameLength = 5;
constexpr auto kUsernameCheckTimeout = crl::time(200);
} // namespace Ui::EditPeer

View File

@@ -0,0 +1,78 @@
/*
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_peer_history_visibility_box.h"
#include "lang/lang_keys.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
void EditPeerHistoryVisibilityBox(
not_null<Ui::GenericBox*> box,
bool isLegacy,
Fn<void(HistoryVisibility)> savedCallback,
HistoryVisibility historyVisibilitySavedValue) {
const auto historyVisibility = std::make_shared<
Ui::RadioenumGroup<HistoryVisibility>
>(historyVisibilitySavedValue);
const auto addButton = [=](
not_null<Ui::RpWidget*> inner,
HistoryVisibility v) {
const auto button = Ui::CreateChild<Ui::AbstractButton>(inner.get());
inner->sizeValue(
) | rpl::on_next([=](const QSize &s) {
button->resize(s);
}, button->lifetime());
button->setClickedCallback([=] { historyVisibility->setValue(v); });
};
box->setTitle(tr::lng_manage_history_visibility_title());
box->addButton(tr::lng_settings_save(), [=] {
savedCallback(historyVisibility->current());
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->addSkip(st::editPeerHistoryVisibilityTopSkip);
const auto visible = box->addRow(object_ptr<Ui::VerticalLayout>(box));
visible->add(object_ptr<Ui::Radioenum<HistoryVisibility>>(
box,
historyVisibility,
HistoryVisibility::Visible,
tr::lng_manage_history_visibility_shown(tr::now),
st::defaultBoxCheckbox));
visible->add(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_manage_history_visibility_shown_about(),
st::editPeerPrivacyLabel),
st::editPeerPreHistoryLabelMargins);
addButton(visible, HistoryVisibility::Visible);
box->addSkip(st::editPeerHistoryVisibilityTopSkip);
const auto hidden = box->addRow(object_ptr<Ui::VerticalLayout>(box));
hidden->add(object_ptr<Ui::Radioenum<HistoryVisibility>>(
box,
historyVisibility,
HistoryVisibility::Hidden,
tr::lng_manage_history_visibility_hidden(tr::now),
st::defaultBoxCheckbox));
hidden->add(
object_ptr<Ui::FlatLabel>(
box,
(isLegacy
? tr::lng_manage_history_visibility_hidden_legacy
: tr::lng_manage_history_visibility_hidden_about)(),
st::editPeerPrivacyLabel),
st::editPeerPreHistoryLabelMargins);
addButton(hidden, HistoryVisibility::Hidden);
}

View File

@@ -0,0 +1,23 @@
/*
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
namespace Ui {
class GenericBox;
} // namespace Ui
enum class HistoryVisibility {
Visible,
Hidden,
};
void EditPeerHistoryVisibilityBox(
not_null<Ui::GenericBox*> box,
bool isLegacy,
Fn<void(HistoryVisibility)> savedCallback,
HistoryVisibility historyVisibilitySavedValue);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
/*
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/layers/box_content.h"
namespace Settings {
struct IconDescriptor;
} // namespace Settings
namespace style {
struct SettingsCountButton;
} // namespace style
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Ui {
class VerticalLayout;
class SettingsButton;
} // namespace Ui
class EditPeerInfoBox : public Ui::BoxContent {
public:
EditPeerInfoBox(
QWidget*,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
void setInnerFocus() override {
_focusRequests.fire({});
}
static bool Available(not_null<PeerData*> peer);
[[nodiscard]] static object_ptr<Ui::SettingsButton> CreateButton(
not_null<QWidget*> parent,
rpl::producer<QString> &&text,
rpl::producer<QString> &&count,
Fn<void()> callback,
const style::SettingsCountButton &st,
Settings::IconDescriptor &&descriptor);
[[nodiscard]] static object_ptr<Ui::SettingsButton> CreateButton(
not_null<QWidget*> parent,
rpl::producer<QString> &&text,
rpl::producer<TextWithEntities> &&labelText,
Fn<void()> callback,
const style::SettingsCountButton &st,
Settings::IconDescriptor &&descriptor);
protected:
void prepare() override;
private:
rpl::event_stream<> _focusRequests;
not_null<Window::SessionNavigation*> _navigation;
not_null<PeerData*> _peer;
};
void ShowEditChatPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
/*
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
template <typename Object>
class object_ptr;
class PeerData;
namespace Api {
struct InviteLink;
} // namespace Api
namespace Data {
class Thread;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class VerticalLayout;
class Show;
class BoxContent;
} // namespace Ui
[[nodiscard]] bool IsExpiredLink(const Api::InviteLink &data, TimeId now);
void AddSinglePeerRow(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
rpl::producer<QString> status,
Fn<void()> clicked = nullptr);
void AddSinglePeerRow(
not_null<Ui::VerticalLayout*> container,
not_null<Data::Thread*> thread,
rpl::producer<QString> status,
Fn<void()> clicked = nullptr);
void AddPermanentLinkBlock(
std::shared_ptr<Ui::Show> show,
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
not_null<UserData*> admin,
rpl::producer<Api::InviteLink> fromList);
void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link);
[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(
not_null<PeerData*> peer,
const QString &link,
const QString &copied = {});
[[nodiscard]] object_ptr<Ui::BoxContent> ShareInviteLinkBox(
not_null<Main::Session*> session,
const QString &link,
const QString &copied = {});
[[nodiscard]] object_ptr<Ui::BoxContent> InviteLinkQrBox(
PeerData *peer,
const QString &link,
rpl::producer<QString> title,
rpl::producer<QString> about);
[[nodiscard]] object_ptr<Ui::BoxContent> RevokeLinkBox(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
bool permanent = false);
[[nodiscard]] object_ptr<Ui::BoxContent> EditLinkBox(
not_null<PeerData*> peer,
const Api::InviteLink &data);
[[nodiscard]] object_ptr<Ui::BoxContent> DeleteLinkBox(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link);
[[nodiscard]] object_ptr<Ui::BoxContent> ShowInviteLinkBox(
not_null<PeerData*> peer,
const Api::InviteLink &link);
[[nodiscard]] QString PrepareRequestedRowStatus(TimeId date);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
/*
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/layers/generic_box.h"
class PeerData;
namespace Ui {
class SettingsButton;
} // namespace Ui
void ManageInviteLinksBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
not_null<UserData*> admin,
int count,
int revokedCount);
[[nodiscard]] object_ptr<Ui::SettingsButton> MakeCreateLinkButton(
not_null<QWidget*> parent,
rpl::producer<QString> text);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
/*
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 "base/object_ptr.h"
#include "data/data_chat_participant_status.h"
#include "history/admin_log/history_admin_log_filter_value.h"
namespace style {
struct SettingsButton;
} // namespace style
namespace Ui {
class GenericBox;
class RoundButton;
class RpWidget;
class VerticalLayout;
} // namespace Ui
namespace PowerSaving {
enum Flag : uint32;
using Flags = base::flags<Flag>;
} // namespace PowerSaving
namespace Data {
enum class ChatbotsPermission;
using ChatbotsPermissions = base::flags<ChatbotsPermission>;
} // namespace Data
template <typename Object>
class object_ptr;
namespace Window {
class SessionController;
class SessionNavigation;
} // namespace Window
struct EditPeerPermissionsBoxResult final {
ChatRestrictions rights;
int slowmodeSeconds = 0;
int boostsUnrestrict = 0;
int starsPerMessage = 0;
};
void ShowEditPeerPermissionsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> channelOrGroup,
Fn<void(EditPeerPermissionsBoxResult)> done);
[[nodiscard]] Fn<void()> AboutGigagroupCallback(
not_null<ChannelData*> channel,
not_null<Window::SessionController*> controller);
template <typename Flags>
struct EditFlagsLabel {
Flags flags;
QString label;
const style::icon *icon = nullptr;
};
template <typename Flags>
struct EditFlagsControl {
object_ptr<Ui::RpWidget> widget;
Fn<Flags()> value;
rpl::producer<Flags> changes;
};
template <typename Flags>
struct NestedEditFlagsLabels {
std::optional<rpl::producer<QString>> nestingLabel;
std::vector<EditFlagsLabel<Flags>> nested;
};
template <typename Flags>
struct EditFlagsDescriptor {
std::vector<NestedEditFlagsLabels<Flags>> labels;
base::flat_map<Flags, QString> disabledMessages;
const style::SettingsButton *st = nullptr;
rpl::producer<QString> forceDisabledMessage;
};
using RestrictionLabel = EditFlagsLabel<ChatRestrictions>;
[[nodiscard]] std::vector<RestrictionLabel> RestrictionLabels(
Data::RestrictionsSetOptions options);
using AdminRightLabel = EditFlagsLabel<ChatAdminRights>;
[[nodiscard]] std::vector<AdminRightLabel> AdminRightLabels(
Data::AdminRightsSetOptions options);
[[nodiscard]] auto CreateEditRestrictions(
QWidget *parent,
ChatRestrictions restrictions,
base::flat_map<ChatRestrictions, QString> disabledMessages,
Data::RestrictionsSetOptions options)
-> EditFlagsControl<ChatRestrictions>;
[[nodiscard]] auto CreateEditAdminRights(
QWidget *parent,
ChatAdminRights rights,
base::flat_map<ChatAdminRights, QString> disabledMessages,
Data::AdminRightsSetOptions options)
-> EditFlagsControl<ChatAdminRights>;
[[nodiscard]] ChatAdminRights DisabledByDefaultRestrictions(
not_null<PeerData*> peer);
[[nodiscard]] ChatRestrictions FixDependentRestrictions(
ChatRestrictions restrictions);
[[nodiscard]] ChatAdminRights AdminRightsForOwnershipTransfer(
Data::AdminRightsSetOptions options);
[[nodiscard]] auto CreateEditPowerSaving(
QWidget *parent,
PowerSaving::Flags flags,
rpl::producer<QString> forceDisabledMessage
) -> EditFlagsControl<PowerSaving::Flags>;
[[nodiscard]] auto CreateEditAdminLogFilter(
QWidget *parent,
AdminLog::FilterValue::Flags flags,
bool isChannel
) -> EditFlagsControl<AdminLog::FilterValue::Flags>;
[[nodiscard]] auto CreateEditChatbotPermissions(
QWidget *parent,
Data::ChatbotsPermissions flags
) -> EditFlagsControl<Data::ChatbotsPermissions>;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
/*
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 "data/data_peer.h"
namespace Data {
struct Reaction;
struct AllowedReactions;
} // namespace Data
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
struct EditAllowedReactionsArgs {
not_null<Window::SessionNavigation*> navigation;
int allowedCustomReactions = 0;
int customReactionsHardLimit = 0;
bool isGroup = false;
std::vector<Data::Reaction> list;
Data::AllowedReactions allowed;
Fn<void(int required)> askForBoosts;
Fn<void(const Data::AllowedReactions &)> save;
};
void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box,
EditAllowedReactionsArgs &&args);
void SaveAllowedReactions(
not_null<PeerData*> peer,
const Data::AllowedReactions &allowed);

View File

@@ -0,0 +1,760 @@
/*
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_peer_requests_box.h"
#include "api/api_invite_links.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/peers/edit_participants_box.h" // SubscribeToMigration
#include "boxes/peers/edit_peer_invite_link.h" // PrepareRequestedRowStatus
#include "boxes/peers/edit_peer_requests_box.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/history_view_requests_bar.h" // kRecentRequestsLimit
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/requests_list/info_requests_list_widget.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mtproto/sender.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/round_rect.h"
#include "ui/text/text_utilities.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
namespace {
constexpr auto kFirstPageCount = 16;
constexpr auto kPerPage = 200;
constexpr auto kServerSearchDelay = crl::time(1000);
constexpr auto kAcceptButton = 1;
constexpr auto kRejectButton = 2;
class RowDelegate {
public:
[[nodiscard]] virtual QSize rowAcceptButtonSize() = 0;
[[nodiscard]] virtual QSize rowRejectButtonSize() = 0;
virtual void rowPaintAccept(
Painter &p,
QRect geometry,
std::unique_ptr<Ui::RippleAnimation> &ripple,
int outerWidth,
bool over) = 0;
virtual void rowPaintReject(
Painter &p,
QRect geometry,
std::unique_ptr<Ui::RippleAnimation> &ripple,
int outerWidth,
bool over) = 0;
};
class Row final : public PeerListRow {
public:
Row(
not_null<RowDelegate*> delegate,
not_null<UserData*> user,
TimeId date);
int elementsCount() const override;
QRect elementGeometry(int element, int outerWidth) const override;
bool elementDisabled(int element) const override;
bool elementOnlySelect(int element) const override;
void elementAddRipple(
int element,
QPoint point,
Fn<void()> updateCallback) override;
void elementsStopLastRipple() override;
void elementsPaint(
Painter &p,
int outerWidth,
bool selected,
int selectedElement) override;
private:
const not_null<RowDelegate*> _delegate;
std::unique_ptr<Ui::RippleAnimation> _acceptRipple;
std::unique_ptr<Ui::RippleAnimation> _rejectRipple;
};
Row::Row(
not_null<RowDelegate*> delegate,
not_null<UserData*> user,
TimeId date)
: PeerListRow(user)
, _delegate(delegate) {
setCustomStatus(PrepareRequestedRowStatus(date));
}
int Row::elementsCount() const {
return 2;
}
QRect Row::elementGeometry(int element, int outerWidth) const {
switch (element) {
case kAcceptButton: {
const auto size = _delegate->rowAcceptButtonSize();
return QRect(st::requestAcceptPosition, size);
} break;
case kRejectButton: {
const auto accept = _delegate->rowAcceptButtonSize();
const auto size = _delegate->rowRejectButtonSize();
return QRect(
(st::requestAcceptPosition
+ QPoint(accept.width() + st::requestButtonsSkip, 0)),
size);
} break;
}
return QRect();
}
bool Row::elementDisabled(int element) const {
return false;
}
bool Row::elementOnlySelect(int element) const {
return true;
}
void Row::elementAddRipple(
int element,
QPoint point,
Fn<void()> updateCallback) {
const auto pointer = (element == kAcceptButton)
? &_acceptRipple
: (element == kRejectButton)
? &_rejectRipple
: nullptr;
if (!pointer) {
return;
}
auto &ripple = *pointer;
if (!ripple) {
auto mask = Ui::RippleAnimation::RoundRectMask(
(element == kAcceptButton
? _delegate->rowAcceptButtonSize()
: _delegate->rowRejectButtonSize()),
st::buttonRadius);
ripple = std::make_unique<Ui::RippleAnimation>(
(element == kAcceptButton
? st::requestsAcceptButton.ripple
: st::requestsRejectButton.ripple),
std::move(mask),
std::move(updateCallback));
}
ripple->add(point);
}
void Row::elementsStopLastRipple() {
if (_acceptRipple) {
_acceptRipple->lastStop();
}
if (_rejectRipple) {
_rejectRipple->lastStop();
}
}
void Row::elementsPaint(
Painter &p,
int outerWidth,
bool selected,
int selectedElement) {
const auto accept = elementGeometry(kAcceptButton, outerWidth);
const auto reject = elementGeometry(kRejectButton, outerWidth);
const auto over = [&](int element) {
return (selectedElement == element);
};
_delegate->rowPaintAccept(
p,
accept,
_acceptRipple,
outerWidth,
over(kAcceptButton));
_delegate->rowPaintReject(
p,
reject,
_rejectRipple,
outerWidth,
over(kRejectButton));
}
} // namespace
class RequestsBoxController::RowHelper final : public RowDelegate {
public:
explicit RowHelper(bool isGroup);
[[nodiscard]] QSize rowAcceptButtonSize() override;
[[nodiscard]] QSize rowRejectButtonSize() override;
void rowPaintAccept(
Painter &p,
QRect geometry,
std::unique_ptr<Ui::RippleAnimation> &ripple,
int outerWidth,
bool over) override;
void rowPaintReject(
Painter &p,
QRect geometry,
std::unique_ptr<Ui::RippleAnimation> &ripple,
int outerWidth,
bool over) override;
private:
void paintButton(
Painter &p,
QRect geometry,
const style::RoundButton &st,
const Ui::RoundRect &rect,
const Ui::RoundRect &rectOver,
std::unique_ptr<Ui::RippleAnimation> &ripple,
const QString &text,
int textWidth,
int outerWidth,
bool over);
Ui::RoundRect _acceptRect;
Ui::RoundRect _acceptRectOver;
Ui::RoundRect _rejectRect;
Ui::RoundRect _rejectRectOver;
QString _acceptText;
QString _rejectText;
int _acceptTextWidth = 0;
int _rejectTextWidth = 0;
};
RequestsBoxController::RowHelper::RowHelper(bool isGroup)
: _acceptRect(st::buttonRadius, st::requestsAcceptButton.textBg)
, _acceptRectOver(st::buttonRadius, st::requestsAcceptButton.textBgOver)
, _rejectRect(st::buttonRadius, st::requestsRejectButton.textBg)
, _rejectRectOver(st::buttonRadius, st::requestsRejectButton.textBgOver)
, _acceptText(isGroup
? tr::lng_group_requests_add(tr::now)
: tr::lng_group_requests_add_channel(tr::now))
, _rejectText(tr::lng_group_requests_dismiss(tr::now))
, _acceptTextWidth(st::requestsAcceptButton.style.font->width(_acceptText))
, _rejectTextWidth(st::requestsRejectButton.style.font->width(_rejectText)) {
}
RequestsBoxController::RequestsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer)
: PeerListController(CreateSearchController(peer))
, _navigation(navigation)
, _helper(std::make_unique<RowHelper>(!peer->isBroadcast()))
, _peer(peer)
, _api(&_peer->session().mtp()) {
setStyleOverrides(&st::requestsBoxList);
subscribeToMigration();
}
RequestsBoxController::~RequestsBoxController() = default;
void RequestsBoxController::Start(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer) {
navigation->showSection(
std::make_shared<Info::Memento>(
peer->migrateToOrMe(),
Info::Section::Type::RequestsList));
}
Main::Session &RequestsBoxController::session() const {
return _peer->session();
}
auto RequestsBoxController::CreateSearchController(not_null<PeerData*> peer)
-> std::unique_ptr<PeerListSearchController> {
return std::make_unique<RequestsBoxSearchController>(peer);
}
std::unique_ptr<PeerListRow> RequestsBoxController::createSearchRow(
not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return createRow(user);
}
return nullptr;
}
std::unique_ptr<PeerListRow> RequestsBoxController::createRestoredRow(
not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return createRow(user, _dates[user]);
}
return nullptr;
}
auto RequestsBoxController::saveState() const
-> std::unique_ptr<PeerListState> {
auto result = PeerListController::saveState();
auto my = std::make_unique<SavedState>();
my->dates = _dates;
my->offsetDate = _offsetDate;
my->offsetUser = _offsetUser;
my->allLoaded = _allLoaded;
my->wasLoading = (_loadRequestId != 0);
if (const auto search = searchController()) {
my->searchState = search->saveState();
}
result->controllerState = std::move(my);
return result;
}
void RequestsBoxController::restoreState(
std::unique_ptr<PeerListState> state) {
auto typeErasedState = state
? state->controllerState.get()
: nullptr;
if (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {
if (const auto requestId = base::take(_loadRequestId)) {
_api.request(requestId).cancel();
}
_dates = std::move(my->dates);
_offsetDate = my->offsetDate;
_offsetUser = my->offsetUser;
_allLoaded = my->allLoaded;
if (const auto search = searchController()) {
search->restoreState(std::move(my->searchState));
}
if (my->wasLoading) {
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
if (delegate()->peerListFullRowsCount() || _allLoaded) {
refreshDescription();
delegate()->peerListRefreshRows();
}
}
}
void RequestsBoxController::prepare() {
delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
delegate()->peerListSetTitle(_peer->isBroadcast()
? tr::lng_manage_peer_requests_channel()
: tr::lng_manage_peer_requests());
setDescriptionText(tr::lng_contacts_loading(tr::now));
setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
loadMoreRows();
}
void RequestsBoxController::loadMoreRows() {
if (searchController() && searchController()->loadMoreRows()) {
return;
} else if (_loadRequestId || _allLoaded) {
return;
}
// First query is small and fast, next loads a lot of rows.
const auto limit = _offsetDate ? kPerPage : kFirstPageCount;
using Flag = MTPmessages_GetChatInviteImporters::Flag;
_loadRequestId = _api.request(MTPmessages_GetChatInviteImporters(
MTP_flags(Flag::f_requested),
_peer->input(),
MTPstring(), // link
MTPstring(), // q
MTP_int(_offsetDate),
_offsetUser ? _offsetUser->inputUser() : MTP_inputUserEmpty(),
MTP_int(limit)
)).done([=](const MTPmessages_ChatInviteImporters &result) {
const auto firstLoad = !_offsetDate;
_loadRequestId = 0;
result.match([&](const MTPDmessages_chatInviteImporters &data) {
session().data().processUsers(data.vusers());
const auto &importers = data.vimporters().v;
auto &owner = _peer->owner();
for (const auto &importer : importers) {
importer.match([&](const MTPDchatInviteImporter &data) {
_offsetDate = data.vdate().v;
_offsetUser = owner.user(data.vuser_id());
appendRow(_offsetUser, _offsetDate);
});
}
// To be sure - wait for a whole empty result list.
_allLoaded = importers.isEmpty();
});
if (_allLoaded
|| (firstLoad && delegate()->peerListFullRowsCount() > 0)) {
refreshDescription();
}
delegate()->peerListRefreshRows();
}).fail([=] {
_loadRequestId = 0;
_allLoaded = true;
}).send();
}
void RequestsBoxController::refreshDescription() {
setDescriptionText((delegate()->peerListFullRowsCount() > 0)
? QString()
: _peer->isBroadcast()
? tr::lng_group_requests_none_channel(tr::now)
: tr::lng_group_requests_none(tr::now));
}
void RequestsBoxController::rowClicked(not_null<PeerListRow*> row) {
_navigation->showPeerInfo(row->peer());
}
void RequestsBoxController::rowElementClicked(
not_null<PeerListRow*> row,
int element) {
processRequest(row->peer()->asUser(), (element == kAcceptButton));
}
void RequestsBoxController::processRequest(
not_null<UserData*> user,
bool approved) {
const auto remove = [=] {
if (const auto row = delegate()->peerListFindRow(user->id.value)) {
delegate()->peerListRemoveRow(row);
refreshDescription();
delegate()->peerListRefreshRows();
}
static_cast<RequestsBoxSearchController*>(
searchController())->removeFromCache(user);
};
const auto done = crl::guard(this, [=] {
remove();
if (approved) {
delegate()->peerListUiShow()->showToast((_peer->isBroadcast()
? tr::lng_group_requests_was_added_channel
: tr::lng_group_requests_was_added)(
tr::now,
lt_user,
tr::bold(user->name()),
tr::marked));
}
});
const auto fail = crl::guard(this, remove);
session().api().inviteLinks().processRequest(
_peer,
QString(), // link
user,
approved,
done,
fail);
}
void RequestsBoxController::appendRow(
not_null<UserData*> user,
TimeId date) {
if (!delegate()->peerListFindRow(user->id.value)) {
_dates.emplace(user, date);
if (auto row = createRow(user, date)) {
delegate()->peerListAppendRow(std::move(row));
setDescriptionText(QString());
}
}
}
QSize RequestsBoxController::RowHelper::rowAcceptButtonSize() {
const auto &st = st::requestsAcceptButton;
return {
(st.width <= 0) ? (_acceptTextWidth - st.width) : st.width,
st.height,
};
}
QSize RequestsBoxController::RowHelper::rowRejectButtonSize() {
const auto &st = st::requestsRejectButton;
return {
(st.width <= 0) ? (_rejectTextWidth - st.width) : st.width,
st.height,
};
}
void RequestsBoxController::RowHelper::rowPaintAccept(
Painter &p,
QRect geometry,
std::unique_ptr<Ui::RippleAnimation> &ripple,
int outerWidth,
bool over) {
paintButton(
p,
geometry,
st::requestsAcceptButton,
_acceptRect,
_acceptRectOver,
ripple,
_acceptText,
_acceptTextWidth,
outerWidth,
over);
}
void RequestsBoxController::RowHelper::rowPaintReject(
Painter &p,
QRect geometry,
std::unique_ptr<Ui::RippleAnimation> &ripple,
int outerWidth,
bool over) {
paintButton(
p,
geometry,
st::requestsRejectButton,
_rejectRect,
_rejectRectOver,
ripple,
_rejectText,
_rejectTextWidth,
outerWidth,
over);
}
void RequestsBoxController::RowHelper::paintButton(
Painter &p,
QRect geometry,
const style::RoundButton &st,
const Ui::RoundRect &rect,
const Ui::RoundRect &rectOver,
std::unique_ptr<Ui::RippleAnimation> &ripple,
const QString &text,
int textWidth,
int outerWidth,
bool over) {
rect.paint(p, geometry);
if (over) {
rectOver.paint(p, geometry);
}
if (ripple) {
ripple->paint(p, geometry.x(), geometry.y(), outerWidth);
if (ripple->empty()) {
ripple = nullptr;
}
}
const auto textLeft = geometry.x()
+ ((geometry.width() - textWidth) / 2);
const auto textTop = geometry.y() + st.textTop;
p.setFont(st.style.font);
p.setPen(over ? st.textFgOver : st.textFg);
p.drawTextLeft(textLeft, textTop, outerWidth, text);
}
std::unique_ptr<PeerListRow> RequestsBoxController::createRow(
not_null<UserData*> user,
TimeId date) {
if (!date) {
const auto search = static_cast<RequestsBoxSearchController*>(
searchController());
date = search->dateForUser(user);
_dates.emplace(user, date);
}
return std::make_unique<Row>(_helper.get(), user, date);
}
void RequestsBoxController::subscribeToMigration() {
const auto chat = _peer->asChat();
if (!chat) {
return;
}
SubscribeToMigration(
chat,
lifetime(),
[=](not_null<ChannelData*> channel) { migrate(chat, channel); });
}
void RequestsBoxController::migrate(
not_null<ChatData*> chat,
not_null<ChannelData*> channel) {
_peer = channel;
}
RequestsBoxSearchController::RequestsBoxSearchController(
not_null<PeerData*> peer)
: _peer(peer)
, _api(&_peer->session().mtp()) {
_timer.setCallback([=] { searchOnServer(); });
}
void RequestsBoxSearchController::searchQuery(const QString &query) {
if (_query != query) {
_query = query;
_offsetDate = 0;
_offsetUser = nullptr;
_requestId = 0;
_allLoaded = false;
if (!_query.isEmpty() && !searchInCache()) {
_timer.callOnce(kServerSearchDelay);
} else {
_timer.cancel();
}
}
}
void RequestsBoxSearchController::searchOnServer() {
Expects(!_query.isEmpty());
loadMoreRows();
}
bool RequestsBoxSearchController::isLoading() {
return _timer.isActive() || _requestId;
}
void RequestsBoxSearchController::removeFromCache(not_null<UserData*> user) {
for (auto &entry : _cache) {
auto &items = entry.second.items;
const auto j = ranges::remove(items, user, &Item::user);
if (j != end(items)) {
entry.second.requestedCount -= (end(items) - j);
items.erase(j, end(items));
}
}
}
TimeId RequestsBoxSearchController::dateForUser(not_null<UserData*> user) {
if (const auto i = _dates.find(user); i != end(_dates)) {
return i->second;
}
return {};
}
auto RequestsBoxSearchController::saveState() const
-> std::unique_ptr<SavedStateBase> {
auto result = std::make_unique<SavedState>();
result->query = _query;
result->offsetDate = _offsetDate;
result->offsetUser = _offsetUser;
result->allLoaded = _allLoaded;
result->wasLoading = (_requestId != 0);
return result;
}
void RequestsBoxSearchController::restoreState(
std::unique_ptr<SavedStateBase> state) {
if (auto my = dynamic_cast<SavedState*>(state.get())) {
if (auto requestId = base::take(_requestId)) {
_api.request(requestId).cancel();
}
_cache.clear();
_queries.clear();
_allLoaded = my->allLoaded;
_offsetDate = my->offsetDate;
_offsetUser = my->offsetUser;
_query = my->query;
if (my->wasLoading) {
searchOnServer();
}
}
}
bool RequestsBoxSearchController::searchInCache() {
const auto i = _cache.find(_query);
if (i != _cache.cend()) {
_requestId = 0;
searchDone(
_requestId,
i->second.items,
i->second.requestedCount);
return true;
}
return false;
}
bool RequestsBoxSearchController::loadMoreRows() {
if (_query.isEmpty()) {
return false;
} else if (_allLoaded || isLoading()) {
return true;
}
// For search we request a lot of rows from the first query.
// (because we've waited for search request by timer already,
// so we don't expect it to be fast, but we want to fill cache).
const auto limit = kPerPage;
using Flag = MTPmessages_GetChatInviteImporters::Flag;
_requestId = _api.request(MTPmessages_GetChatInviteImporters(
MTP_flags(Flag::f_requested | Flag::f_q),
_peer->input(),
MTPstring(), // link
MTP_string(_query),
MTP_int(_offsetDate),
_offsetUser ? _offsetUser->inputUser() : MTP_inputUserEmpty(),
MTP_int(limit)
)).done([=](
const MTPmessages_ChatInviteImporters &result,
mtpRequestId requestId) {
auto items = std::vector<Item>();
result.match([&](const MTPDmessages_chatInviteImporters &data) {
const auto &importers = data.vimporters().v;
auto &owner = _peer->owner();
owner.processUsers(data.vusers());
items.reserve(importers.size());
for (const auto &importer : importers) {
importer.match([&](const MTPDchatInviteImporter &data) {
items.push_back({
owner.user(data.vuser_id()),
data.vdate().v,
});
});
}
});
searchDone(requestId, items, limit);
auto it = _queries.find(requestId);
if (it != _queries.cend()) {
const auto &query = it->second.text;
if (it->second.offsetDate == 0) {
auto &entry = _cache[query];
entry.items = std::move(items);
entry.requestedCount = limit;
}
_queries.erase(it);
}
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
if (_requestId == requestId) {
_requestId = 0;
_allLoaded = true;
delegate()->peerListSearchRefreshRows();
}
}).send();
auto entry = Query();
entry.text = _query;
entry.offsetDate = _offsetDate;
_queries.emplace(_requestId, entry);
return true;
}
void RequestsBoxSearchController::searchDone(
mtpRequestId requestId,
const std::vector<Item> &items,
int requestedCount) {
if (_requestId != requestId) {
return;
}
_requestId = 0;
if (!_offsetDate) {
_dates.clear();
}
for (const auto &[user, date] : items) {
_offsetDate = date;
_offsetUser = user;
_dates.emplace(user, date);
delegate()->peerListSearchAddRow(user);
}
if (items.size() < requestedCount) {
// We want cache to have full information about a query with
// small results count (that we don't need the second request).
// So we don't wait for empty list unlike the non-search case.
_allLoaded = true;
}
delegate()->peerListSearchRefreshRows();
}

View File

@@ -0,0 +1,149 @@
/*
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 "boxes/peer_list_box.h"
#include "base/weak_ptr.h"
#include "mtproto/sender.h"
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Ui {
class RippleAnimation;
} // namespace Ui
class RequestsBoxController final
: public PeerListController
, public base::has_weak_ptr {
public:
RequestsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
~RequestsBoxController();
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowElementClicked(
not_null<PeerListRow*> row,
int element) override;
void loadMoreRows() override;
std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
private:
class RowHelper;
struct SavedState : SavedStateBase {
using SearchStateBase = PeerListSearchController::SavedStateBase;
std::unique_ptr<SearchStateBase> searchState;
base::flat_map<not_null<UserData*>, TimeId> dates;
TimeId offsetDate = 0;
UserData *offsetUser = nullptr;
bool allLoaded = false;
bool wasLoading = false;
};
static std::unique_ptr<PeerListSearchController> CreateSearchController(
not_null<PeerData*> peer);
[[nodiscard]] std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user,
TimeId date = 0);
void appendRow(not_null<UserData*> user, TimeId date);
void refreshDescription();
void processRequest(not_null<UserData*> user, bool approved);
void subscribeToMigration();
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);
const not_null<Window::SessionNavigation*> _navigation;
const std::unique_ptr<RowHelper> _helper;
not_null<PeerData*> _peer;
MTP::Sender _api;
base::flat_map<not_null<UserData*>, TimeId> _dates;
TimeId _offsetDate = 0;
UserData *_offsetUser = nullptr;
mtpRequestId _loadRequestId = 0;
bool _allLoaded = false;
};
// Members, banned and restricted users server side search.
class RequestsBoxSearchController final : public PeerListSearchController {
public:
RequestsBoxSearchController(not_null<PeerData*> peer);
void searchQuery(const QString &query) override;
bool isLoading() override;
bool loadMoreRows() override;
void removeFromCache(not_null<UserData*> user);
[[nodiscard]] TimeId dateForUser(not_null<UserData*> user);
std::unique_ptr<SavedStateBase> saveState() const override;
void restoreState(std::unique_ptr<SavedStateBase> state) override;
private:
struct SavedState : SavedStateBase {
QString query;
TimeId offsetDate = 0;
UserData *offsetUser = nullptr;
bool allLoaded = false;
bool wasLoading = false;
};
struct Item {
not_null<UserData*> user;
TimeId date = 0;
};
struct CacheEntry {
std::vector<Item> items;
int requestedCount = 0;
};
struct Query {
QString text;
TimeId offsetDate = 0;
UserData *offsetUser = nullptr;
};
void searchOnServer();
bool searchInCache();
void searchDone(
mtpRequestId requestId,
const std::vector<Item> &items,
int requestedCount);
not_null<PeerData*> _peer;
MTP::Sender _api;
base::Timer _timer;
QString _query;
mtpRequestId _requestId = 0;
TimeId _offsetDate = 0;
UserData *_offsetUser = nullptr;
bool _allLoaded = false;
base::flat_map<QString, CacheEntry> _cache;
base::flat_map<mtpRequestId, Query> _queries;
base::flat_map<not_null<UserData*>, TimeId> _dates;
};

View File

@@ -0,0 +1,792 @@
/*
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_peer_type_box.h"
#include "main/main_session.h"
#include "boxes/add_contact_box.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/premium_limits_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/edit_peer_common.h"
#include "boxes/peers/edit_peer_info_box.h" // CreateButton.
#include "boxes/peers/edit_peer_invite_link.h"
#include "boxes/peers/edit_peer_invite_links.h"
#include "boxes/peers/edit_peer_usernames_list.h"
#include "boxes/username_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "mtproto/sender.h"
#include "ui/rp_widget.h"
#include "ui/vertical_list.h"
#include "ui/controls/userpic_button.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/fields/special_fields.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "settings/settings_common.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace {
class Controller : public base::has_weak_ptr {
public:
Controller(
Window::SessionNavigation *navigation,
std::shared_ptr<Ui::Show> show,
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
bool useLocationPhrases,
std::optional<EditPeerTypeData> dataSavedValue);
void createContent();
[[nodiscard]] QString getUsernameInput() const;
[[nodiscard]] std::vector<QString> usernamesOrder() const;
void setFocusUsername();
[[nodiscard]] rpl::producer<QString> getTitle() const {
return !_dataSavedValue
? tr::lng_create_invite_link_title()
: _isGroup
? tr::lng_manage_peer_group_type()
: tr::lng_manage_peer_channel_type();
}
[[nodiscard]] bool goodUsername() const {
return _goodUsername;
}
[[nodiscard]] Privacy getPrivacy() const {
return _controls.privacy->current();
}
[[nodiscard]] bool noForwards() const {
return _controls.noForwards->toggled();
}
[[nodiscard]] bool joinToWrite() const {
return _controls.joinToWrite && _controls.joinToWrite->toggled();
}
[[nodiscard]] bool requestToJoin() const {
return _controls.requestToJoin && _controls.requestToJoin->toggled();
}
[[nodiscard]] rpl::producer<int> scrollToRequests() const {
return _scrollToRequests.events();
}
void showError(rpl::producer<QString> text) {
_controls.usernameInput->showError();
showUsernameError(std::move(text));
}
private:
struct Controls {
std::shared_ptr<Ui::RadioenumGroup<Privacy>> privacy;
Ui::SlideWrap<Ui::RpWidget> *usernameWrap = nullptr;
Ui::UsernameInput *usernameInput = nullptr;
UsernamesList *usernamesList = nullptr;
base::unique_qptr<Ui::FlatLabel> usernameCheckResult;
Ui::SlideWrap<> *inviteLinkWrap = nullptr;
Ui::FlatLabel *inviteLink = nullptr;
Ui::SlideWrap<Ui::VerticalLayout> *whoSendWrap = nullptr;
Ui::SettingsButton *noForwards = nullptr;
Ui::SettingsButton *joinToWrite = nullptr;
Ui::SettingsButton *requestToJoin = nullptr;
};
Controls _controls;
object_ptr<Ui::RpWidget> createUsernameEdit();
object_ptr<Ui::RpWidget> createInviteLinkBlock();
void privacyChanged(Privacy value);
void checkUsernameAvailability();
void askUsernameRevoke();
void usernameChanged();
void showUsernameError(rpl::producer<QString> &&error);
void showUsernameGood();
void showUsernamePending();
void showUsernameEmpty();
void fillPrivaciesButtons(
not_null<Ui::VerticalLayout*> parent,
std::optional<Privacy> savedValue);
void addRoundButton(
not_null<Ui::VerticalLayout*> container,
Privacy value,
const QString &text,
rpl::producer<QString> about);
Window::SessionNavigation *_navigation = nullptr;
std::shared_ptr<Ui::Show> _show;
not_null<PeerData*> _peer;
bool _linkOnly = false;
MTP::Sender _api;
std::optional<EditPeerTypeData> _dataSavedValue;
bool _useLocationPhrases = false;
bool _isGroup = false;
bool _goodUsername = false;
base::unique_qptr<Ui::VerticalLayout> _wrap;
base::Timer _checkUsernameTimer;
mtpRequestId _checkUsernameRequestId = 0;
UsernameState _usernameState = UsernameState::Normal;
rpl::event_stream<UsernameCheckInfo> _usernameCheckInfo;
rpl::lifetime _usernameCheckInfoLifetime;
rpl::event_stream<int> _scrollToRequests;
rpl::lifetime _lifetime;
};
Controller::Controller(
Window::SessionNavigation *navigation,
std::shared_ptr<Ui::Show> show,
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
bool useLocationPhrases,
std::optional<EditPeerTypeData> dataSavedValue)
: _navigation(navigation)
, _show(show)
, _peer(peer)
, _linkOnly(!dataSavedValue.has_value())
, _api(&_peer->session().mtp())
, _dataSavedValue(dataSavedValue)
, _useLocationPhrases(useLocationPhrases)
, _isGroup(_peer->isChat() || _peer->isMegagroup())
, _goodUsername(_dataSavedValue
? !_dataSavedValue->username.isEmpty()
: (_peer->isChannel() && !_peer->asChannel()->editableUsername().isEmpty()))
, _wrap(container)
, _checkUsernameTimer([=] { checkUsernameAvailability(); }) {
_peer->updateFull();
}
void Controller::createContent() {
_controls = Controls();
fillPrivaciesButtons(
_wrap,
(_dataSavedValue
? _dataSavedValue->privacy
: std::optional<Privacy>()));
// Skip.
if (!_linkOnly) {
_wrap->add(object_ptr<Ui::BoxContentDivider>(_wrap));
}
//
_wrap->add(createInviteLinkBlock());
if (!_linkOnly) {
_wrap->add(createUsernameEdit());
}
using namespace Settings;
if (!_linkOnly) {
if (_peer->isMegagroup()) {
_controls.whoSendWrap = _wrap->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
_wrap.get(),
object_ptr<Ui::VerticalLayout>(_wrap.get())));
const auto wrap = _controls.whoSendWrap->entity();
Ui::AddSkip(wrap);
if (_dataSavedValue->hasDiscussionLink) {
Ui::AddSubsectionTitle(wrap, tr::lng_manage_peer_send_title());
_controls.joinToWrite = wrap->add(EditPeerInfoBox::CreateButton(
wrap,
tr::lng_manage_peer_send_only_members(),
rpl::single(QString()),
[=] {},
st::peerPermissionsButton,
{}
));
_controls.joinToWrite->toggleOn(
rpl::single(_dataSavedValue->joinToWrite)
)->toggledValue(
) | rpl::on_next([=](bool toggled) {
_dataSavedValue->joinToWrite = toggled;
}, wrap->lifetime());
} else {
_controls.whoSendWrap->toggle(
(_controls.privacy->current() == Privacy::HasUsername),
anim::type::instant);
}
auto joinToWrite = _controls.joinToWrite
? _controls.joinToWrite->toggledValue()
: rpl::single(true);
const auto requestToJoinWrap = wrap->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
wrap,
EditPeerInfoBox::CreateButton(
wrap,
tr::lng_manage_peer_send_approve_members(),
rpl::single(QString()),
[=] {},
st::peerPermissionsButton,
{})))->setDuration(0);
requestToJoinWrap->toggleOn(rpl::duplicate(joinToWrite));
_controls.requestToJoin = requestToJoinWrap->entity();
_controls.requestToJoin->toggleOn(
rpl::single(_dataSavedValue->requestToJoin)
)->toggledValue(
) | rpl::on_next([=](bool toggled) {
_dataSavedValue->requestToJoin = toggled;
}, wrap->lifetime());
Ui::AddSkip(wrap);
Ui::AddDividerText(
wrap,
rpl::conditional(
std::move(joinToWrite),
tr::lng_manage_peer_send_approve_members_about(),
tr::lng_manage_peer_send_only_members_about()));
}
Ui::AddSkip(_wrap.get());
Ui::AddSubsectionTitle(
_wrap.get(),
tr::lng_manage_peer_no_forwards_title());
_controls.noForwards = _wrap->add(EditPeerInfoBox::CreateButton(
_wrap.get(),
tr::lng_manage_peer_no_forwards(),
rpl::single(QString()),
[] {},
st::peerPermissionsButton,
{}));
_controls.noForwards->toggleOn(
rpl::single(_dataSavedValue->noForwards)
)->toggledValue(
) | rpl::on_next([=](bool toggled) {
_dataSavedValue->noForwards = toggled;
}, _wrap->lifetime());
Ui::AddSkip(_wrap.get());
Ui::AddDividerText(
_wrap.get(),
(_isGroup
? tr::lng_manage_peer_no_forwards_about
: tr::lng_manage_peer_no_forwards_about_channel)());
}
if (_linkOnly) {
_controls.inviteLinkWrap->show(anim::type::instant);
} else {
if (_controls.privacy->current() == Privacy::NoUsername) {
checkUsernameAvailability();
}
const auto forShowing = _dataSavedValue
? _dataSavedValue->privacy
: Privacy::NoUsername;
_controls.inviteLinkWrap->toggle(
(forShowing != Privacy::HasUsername),
anim::type::instant);
_controls.usernameWrap->toggle(
(forShowing == Privacy::HasUsername),
anim::type::instant);
}
}
void Controller::addRoundButton(
not_null<Ui::VerticalLayout*> container,
Privacy value,
const QString &text,
rpl::producer<QString> about) {
container->add(object_ptr<Ui::Radioenum<Privacy>>(
container,
_controls.privacy,
value,
text,
st::editPeerPrivacyBoxCheckbox));
container->add(object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
container,
object_ptr<Ui::FlatLabel>(
container,
std::move(about),
st::editPeerPrivacyLabel),
st::editPeerPrivacyLabelMargins));
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::editPeerPrivacyBottomSkip));
};
void Controller::fillPrivaciesButtons(
not_null<Ui::VerticalLayout*> parent,
std::optional<Privacy> savedValue) {
if (_linkOnly) {
return;
}
const auto result = parent->add(
object_ptr<Ui::PaddingWrap<Ui::VerticalLayout>>(
parent,
object_ptr<Ui::VerticalLayout>(parent),
st::editPeerPrivaciesMargins));
const auto container = result->entity();
const auto isPublic = _peer->isChannel()
&& _peer->asChannel()->hasUsername();
_controls.privacy = std::make_shared<Ui::RadioenumGroup<Privacy>>(
savedValue.value_or(
isPublic ? Privacy::HasUsername : Privacy::NoUsername));
addRoundButton(
container,
Privacy::HasUsername,
(_useLocationPhrases
? tr::lng_create_permanent_link_title
: _isGroup
? tr::lng_create_public_group_title
: tr::lng_create_public_channel_title)(tr::now),
(_isGroup
? tr::lng_create_public_group_about
: tr::lng_create_public_channel_about)());
addRoundButton(
container,
Privacy::NoUsername,
(_useLocationPhrases
? tr::lng_create_invite_link_title
: _isGroup
? tr::lng_create_private_group_title
: tr::lng_create_private_channel_title)(tr::now),
(_useLocationPhrases
? tr::lng_create_invite_link_about
: _isGroup
? tr::lng_create_private_group_about
: tr::lng_create_private_channel_about)());
_controls.privacy->setChangedCallback([=](Privacy value) {
privacyChanged(value);
});
}
void Controller::setFocusUsername() {
if (_controls.usernameInput) {
_controls.usernameInput->setFocus();
}
}
QString Controller::getUsernameInput() const {
return _controls.usernameInput->getLastText().trimmed();
}
std::vector<QString> Controller::usernamesOrder() const {
return _controls.usernamesList
? _controls.usernamesList->order()
: std::vector<QString>();
}
object_ptr<Ui::RpWidget> Controller::createUsernameEdit() {
Expects(_wrap != nullptr);
const auto channel = _peer->asChannel();
const auto username = (!_dataSavedValue || !channel)
? QString()
: channel->editableUsername();
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
_wrap,
object_ptr<Ui::VerticalLayout>(_wrap));
_controls.usernameWrap = result.data();
const auto container = result->entity();
using namespace Settings;
Ui::AddSkip(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_create_group_link(),
st::defaultSubsectionTitle),
st::defaultSubsectionTitlePadding);
const auto placeholder = container->add(
object_ptr<Ui::RpWidget>(container),
st::editPeerUsernameFieldMargins);
placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
_controls.usernameInput = Ui::AttachParentChild(
container,
object_ptr<Ui::UsernameInput>(
container,
st::setupChannelLink,
nullptr,
username,
_peer->session().createInternalLink(QString())));
_controls.usernameInput->heightValue(
) | rpl::on_next([placeholder](int height) {
placeholder->resize(placeholder->width(), height);
}, placeholder->lifetime());
placeholder->widthValue(
) | rpl::on_next([this](int width) {
_controls.usernameInput->resize(
width,
_controls.usernameInput->height());
}, placeholder->lifetime());
_controls.usernameInput->move(placeholder->pos());
AddUsernameCheckLabel(container, _usernameCheckInfo.events());
Ui::AddDividerText(
container,
tr::lng_create_channel_link_about());
if (channel) {
const auto focusCallback = [=] {
_scrollToRequests.fire(container->y());
_controls.usernameInput->setFocusFast();
};
_controls.usernamesList = container->add(object_ptr<UsernamesList>(
container,
channel,
_show,
focusCallback));
}
QObject::connect(
_controls.usernameInput,
&Ui::UsernameInput::changed,
[this] { usernameChanged(); });
const auto shown = (_controls.privacy->current() == Privacy::HasUsername);
result->toggle(shown, anim::type::instant);
return result;
}
void Controller::privacyChanged(Privacy value) {
const auto toggleInviteLink = [&] {
_controls.inviteLinkWrap->toggle(
(value != Privacy::HasUsername),
anim::type::instant);
};
const auto toggleEditUsername = [&] {
_controls.usernameWrap->toggle(
(value == Privacy::HasUsername),
anim::type::instant);
};
const auto toggleWhoSendWrap = [&] {
if (!_controls.whoSendWrap) {
return;
}
_controls.whoSendWrap->toggle(
(value == Privacy::HasUsername
|| (_dataSavedValue && _dataSavedValue->hasDiscussionLink)),
anim::type::instant);
};
const auto refreshVisibilities = [&] {
// Now first we need to hide that was shown.
// Otherwise box will change own Y position.
if (value == Privacy::HasUsername) {
toggleInviteLink();
toggleEditUsername();
toggleWhoSendWrap();
showUsernameEmpty();
checkUsernameAvailability();
} else {
toggleWhoSendWrap();
toggleEditUsername();
toggleInviteLink();
}
};
if (value == Privacy::HasUsername) {
if (_usernameState == UsernameState::TooMany) {
askUsernameRevoke();
return;
} else if (_usernameState == UsernameState::NotAvailable) {
_controls.privacy->setValue(Privacy::NoUsername);
return;
}
refreshVisibilities();
_controls.usernameInput->setDisplayFocused(true);
} else {
_api.request(base::take(_checkUsernameRequestId)).cancel();
_checkUsernameTimer.cancel();
refreshVisibilities();
}
setFocusUsername();
}
void Controller::checkUsernameAvailability() {
if (!_controls.usernameInput) {
return;
}
const auto initial = (_controls.privacy->current() != Privacy::HasUsername);
const auto checking = initial
? u".bad."_q
: getUsernameInput();
if (checking.size() < Ui::EditPeer::kMinUsernameLength) {
return;
}
if (_checkUsernameRequestId) {
_api.request(_checkUsernameRequestId).cancel();
}
const auto channel = _peer->migrateToOrMe()->asChannel();
const auto username = channel ? channel->editableUsername() : QString();
_checkUsernameRequestId = _api.request(MTPchannels_CheckUsername(
channel ? channel->inputChannel() : MTP_inputChannelEmpty(),
MTP_string(checking)
)).done([=](const MTPBool &result) {
_checkUsernameRequestId = 0;
if (initial) {
return;
}
if (!mtpIsTrue(result) && checking != username) {
showUsernameError(tr::lng_create_channel_link_occupied());
} else {
showUsernameGood();
}
}).fail([=](const MTP::Error &error) {
_checkUsernameRequestId = 0;
const auto &type = error.type();
_usernameState = UsernameState::Normal;
if (type == u"CHANNEL_PUBLIC_GROUP_NA"_q) {
_usernameState = UsernameState::NotAvailable;
_controls.privacy->setValue(Privacy::NoUsername);
} else if (type == u"CHANNELS_ADMIN_PUBLIC_TOO_MUCH"_q) {
_usernameState = UsernameState::TooMany;
if (_controls.privacy->current() == Privacy::HasUsername) {
askUsernameRevoke();
}
} else if (initial) {
if (_controls.privacy->current() == Privacy::HasUsername) {
showUsernameEmpty();
setFocusUsername();
}
} else if (type == u"USERNAME_INVALID"_q) {
showUsernameError(tr::lng_create_channel_link_invalid());
} else if (type == u"USERNAME_PURCHASE_AVAILABLE"_q) {
_goodUsername = false;
_usernameCheckInfo.fire(
UsernameCheckInfo::PurchaseAvailable(checking, _peer));
} else if (type == u"USERNAME_OCCUPIED"_q && checking != username) {
showUsernameError(tr::lng_create_channel_link_occupied());
}
}).send();
}
void Controller::askUsernameRevoke() {
_controls.privacy->setValue(Privacy::NoUsername);
const auto revokeCallback = crl::guard(this, [this] {
_usernameState = UsernameState::Normal;
_controls.privacy->setValue(Privacy::HasUsername);
checkUsernameAvailability();
});
_show->showBox(Box(PublicLinksLimitBox, _navigation, revokeCallback));
}
void Controller::usernameChanged() {
_goodUsername = false;
const auto username = getUsernameInput();
if (username.isEmpty()) {
showUsernameEmpty();
_checkUsernameTimer.cancel();
return;
}
const auto bad = ranges::any_of(username, [](QChar ch) {
return (ch < 'A' || ch > 'Z')
&& (ch < 'a' || ch > 'z')
&& (ch < '0' || ch > '9')
&& (ch != '_');
});
if (bad) {
showUsernameError(tr::lng_create_channel_link_bad_symbols());
} else if (username.size() < Ui::EditPeer::kMinUsernameLength) {
showUsernameError(tr::lng_create_channel_link_too_short());
} else {
showUsernamePending();
_checkUsernameTimer.callOnce(Ui::EditPeer::kUsernameCheckTimeout);
}
}
void Controller::showUsernameError(rpl::producer<QString> &&error) {
_goodUsername = false;
_usernameCheckInfoLifetime.destroy();
std::move(
error
) | rpl::map([](QString s) {
return UsernameCheckInfo{
.type = UsernameCheckInfo::Type::Error,
.text = { std::move(s) },
};
}) | rpl::start_to_stream(_usernameCheckInfo, _usernameCheckInfoLifetime);
}
void Controller::showUsernameGood() {
_goodUsername = true;
_usernameCheckInfoLifetime.destroy();
_usernameCheckInfo.fire({
.type = UsernameCheckInfo::Type::Good,
.text = { tr::lng_create_channel_link_available(tr::now) },
});
}
void Controller::showUsernamePending() {
_usernameCheckInfoLifetime.destroy();
_usernameCheckInfo.fire({
.type = UsernameCheckInfo::Type::Default,
.text = { .text = tr::lng_create_channel_link_pending(tr::now) },
});
}
void Controller::showUsernameEmpty() {
_usernameCheckInfoLifetime.destroy();
_usernameCheckInfo.fire({ .type = UsernameCheckInfo::Type::Default });
}
object_ptr<Ui::RpWidget> Controller::createInviteLinkBlock() {
Expects(_wrap != nullptr);
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
_wrap,
object_ptr<Ui::VerticalLayout>(_wrap));
_controls.inviteLinkWrap = result.data();
const auto container = result->entity();
using namespace Settings;
if (_dataSavedValue) {
Ui::AddSkip(container);
AddSubsectionTitle(container, tr::lng_create_permanent_link_title());
}
AddPermanentLinkBlock(
_show,
container,
_peer,
_peer->session().user(),
nullptr);
Ui::AddSkip(container);
Ui::AddDividerText(
container,
((_peer->isMegagroup() || _peer->asChat())
? tr::lng_group_invite_about_permanent_group()
: tr::lng_group_invite_about_permanent_channel()));
return result;
}
} // namespace
EditPeerTypeBox::EditPeerTypeBox(
QWidget*,
Window::SessionNavigation *navigation,
not_null<PeerData*> peer,
bool useLocationPhrases,
std::optional<FnMut<void(EditPeerTypeData)>> savedCallback,
std::optional<EditPeerTypeData> dataSaved,
std::optional<rpl::producer<QString>> usernameError)
: _navigation(navigation)
, _peer(peer)
, _useLocationPhrases(useLocationPhrases)
, _savedCallback(std::move(savedCallback))
, _dataSavedValue(dataSaved)
, _usernameError(usernameError) {
}
EditPeerTypeBox::EditPeerTypeBox(
QWidget*,
not_null<PeerData*> peer)
: EditPeerTypeBox(nullptr, nullptr, peer, {}, {}, {}) {
}
void EditPeerTypeBox::setInnerFocus() {
_focusRequests.fire({});
}
void EditPeerTypeBox::prepare() {
_peer->updateFull();
auto content = object_ptr<Ui::VerticalLayout>(this);
const auto controller = Ui::CreateChild<Controller>(
this,
_navigation,
uiShow(),
content.data(),
_peer,
_useLocationPhrases,
_dataSavedValue);
controller->scrollToRequests(
) | rpl::on_next([=, raw = content.data()](int y) {
scrollToY(raw->y() + y);
}, lifetime());
_focusRequests.events(
) | rpl::on_next(
[=] {
controller->setFocusUsername();
if (_usernameError.has_value()) {
controller->showError(std::move(*_usernameError));
_usernameError = std::nullopt;
}
},
lifetime());
controller->createContent();
setTitle(controller->getTitle());
if (_savedCallback.has_value()) {
addButton(tr::lng_settings_save(), [=] {
const auto v = controller->getPrivacy();
if ((v == Privacy::HasUsername) && !controller->goodUsername()) {
if (!controller->getUsernameInput().isEmpty()
|| controller->usernamesOrder().empty()) {
controller->setFocusUsername();
return;
}
}
auto local = std::move(*_savedCallback);
local(EditPeerTypeData{
.privacy = v,
.username = (v == Privacy::HasUsername
? controller->getUsernameInput()
: QString()),
.usernamesOrder = (v == Privacy::HasUsername
? controller->usernamesOrder()
: std::vector<QString>()),
.noForwards = controller->noForwards(),
.joinToWrite = controller->joinToWrite(),
.requestToJoin = controller->requestToJoin(),
}); // We don't need username with private type.
closeBox();
});
addButton(tr::lng_cancel(), [=] { closeBox(); });
} else {
addButton(tr::lng_close(), [=] { closeBox(); });
}
setDimensionsToContent(st::boxWideWidth, content.data());
setInnerWidget(std::move(content));
}

View File

@@ -0,0 +1,77 @@
/*
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/layers/box_content.h"
namespace style {
struct SettingsCountButton;
} // namespace style
namespace Ui {
class VerticalLayout;
class SettingsButton;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
enum class Privacy {
HasUsername,
NoUsername,
};
enum class UsernameState {
Normal,
TooMany,
NotAvailable,
};
struct EditPeerTypeData {
Privacy privacy = Privacy::NoUsername;
QString username;
std::vector<QString> usernamesOrder;
bool hasDiscussionLink = false;
bool noForwards = false;
bool joinToWrite = false;
bool requestToJoin = false;
};
class EditPeerTypeBox : public Ui::BoxContent {
public:
EditPeerTypeBox(
QWidget*,
Window::SessionNavigation *navigation,
not_null<PeerData*> peer,
bool useLocationPhrases,
std::optional<FnMut<void(EditPeerTypeData)>> savedCallback,
std::optional<EditPeerTypeData> dataSaved,
std::optional<rpl::producer<QString>> usernameError = {});
// For invite link only.
EditPeerTypeBox(
QWidget*,
not_null<PeerData*> peer);
protected:
void prepare() override;
void setInnerFocus() override;
private:
Window::SessionNavigation *_navigation = nullptr;
const not_null<PeerData*> _peer;
bool _useLocationPhrases = false;
std::optional<FnMut<void(EditPeerTypeData)>> _savedCallback;
std::optional<EditPeerTypeData> _dataSavedValue;
std::optional<rpl::producer<QString>> _usernameError;
rpl::event_stream<> _focusRequests;
};

View File

@@ -0,0 +1,426 @@
/*
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_peer_usernames_list.h"
#include "api/api_filter_updates.h"
#include "api/api_user_names.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/show.h"
#include "ui/painter.h"
#include "ui/text/text_utilities.h" // tr::rich.
#include "ui/toast/toast.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/vertical_layout_reorder.h"
#include "styles/style_boxes.h" // contactsStatusFont.
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtGui/QGuiApplication>
namespace {
class RightAction final : public Ui::RpWidget {
public:
RightAction(not_null<Ui::RpWidget*> parent);
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
};
RightAction::RightAction(not_null<Ui::RpWidget*> parent)
: RpWidget(parent) {
setCursor(style::cur_sizeall);
const auto &st = st::inviteLinkThreeDots;
resize(st.width, st.height);
}
void RightAction::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
st::usernamesReorderIcon.paintInCenter(p, rect());
}
void RightAction::mousePressEvent(QMouseEvent *e) {
}
} // namespace
class UsernamesList::Row final : public Ui::SettingsButton {
public:
Row(
not_null<Ui::RpWidget*> parent,
const Data::Username &data,
std::shared_ptr<Ui::Show> show,
QString status,
QString link);
[[nodiscard]] const Data::Username &username() const;
[[nodiscard]] not_null<Ui::RpWidget*> rightAction() const;
int resizeGetHeight(int newWidth) override;
protected:
void paintEvent(QPaintEvent *e) override;
private:
const style::PeerListItem &_st;
const Data::Username _data;
const QString _status;
const not_null<Ui::RpWidget*> _rightAction;
const QRect _iconRect;
std::shared_ptr<Ui::Show> _show;
Ui::Text::String _title;
base::unique_qptr<Ui::PopupMenu> _menu;
};
UsernamesList::Row::Row(
not_null<Ui::RpWidget*> parent,
const Data::Username &data,
std::shared_ptr<Ui::Show> show,
QString status,
QString link)
: Ui::SettingsButton(parent, rpl::never<QString>())
, _st(st::inviteLinkListItem)
, _data(data)
, _status(std::move(status))
, _rightAction(Ui::CreateChild<RightAction>(this))
, _iconRect(
_st.photoPosition.x() + st::inviteLinkIconSkip,
_st.photoPosition.y() + st::inviteLinkIconSkip,
_st.photoSize - st::inviteLinkIconSkip * 2,
_st.photoSize - st::inviteLinkIconSkip * 2)
, _show(show)
, _title(_st.nameStyle, '@' + data.username) {
base::install_event_filter(this, [=](not_null<QEvent*> e) {
if (e->type() != QEvent::ContextMenu) {
return base::EventFilterResult::Continue;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
_menu->addAction(
tr::lng_group_invite_context_copy(tr::now),
[=] {
QGuiApplication::clipboard()->setText(link);
show->showToast(
tr::lng_create_channel_link_copied(tr::now));
},
&st::menuIconCopy);
_menu->popup(QCursor::pos());
return base::EventFilterResult::Cancel;
});
_rightAction->setVisible(data.active);
sizeValue(
) | rpl::on_next([=](const QSize &s) {
_rightAction->moveToLeft(
s.width() - _rightAction->width() - st::inviteLinkThreeDotsSkip,
(s.height() - _rightAction->height()) / 2);
}, _rightAction->lifetime());
}
const Data::Username &UsernamesList::Row::username() const {
return _data;
}
not_null<Ui::RpWidget*> UsernamesList::Row::rightAction() const {
return _rightAction;
}
int UsernamesList::Row::resizeGetHeight(int newWidth) {
return _st.height;
}
void UsernamesList::Row::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto paintOver = (isOver() || isDown()) && !isDisabled();
Ui::SettingsButton::paintBg(p, e->rect(), paintOver);
Ui::SettingsButton::paintRipple(p, 0, 0);
const auto active = _data.active;
const auto &color = active ? st::msgFile1Bg : st::windowSubTextFg;
p.setPen(Qt::NoPen);
p.setBrush(color);
{
auto hq = PainterHighQualityEnabler(p);
p.drawEllipse(_iconRect);
}
(!active
? st::inviteLinkRevokedIcon
: st::inviteLinkIcon).paintInCenter(p, _iconRect);
p.setPen(_st.nameFg);
_title.drawLeft(
p,
_st.namePosition.x(),
_st.namePosition.y(),
width(),
width() - _st.namePosition.x());
p.setPen(active
? _st.statusFgActive
: paintOver
? _st.statusFgOver
: _st.statusFg);
p.setFont(st::contactsStatusFont);
p.drawTextLeft(
_st.statusPosition.x(),
_st.statusPosition.y(),
width() - _st.statusPosition.x(),
_status);
}
UsernamesList::UsernamesList(
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
Fn<void()> focusCallback)
: RpWidget(parent)
, _show(show)
, _peer(peer)
, _isBot(peer->isUser()
&& peer->asUser()->botInfo
&& peer->asUser()->botInfo->canEditInformation)
, _focusCallback(std::move(focusCallback)) {
{
auto &api = _peer->session().api();
const auto usernames = api.usernames().cacheFor(_peer->id);
if (!usernames.empty()) {
rebuild(usernames);
}
}
load();
rpl::merge(
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Username),
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Usernames)
) | rpl::on_next([=] {
load();
}, lifetime());
}
void UsernamesList::load() {
_loadLifetime = _peer->session().api().usernames().loadUsernames(
_peer
) | rpl::on_next([=](const Data::Usernames &usernames) {
if (usernames.empty()) {
_container = nullptr;
resize(0, 0);
} else {
rebuild(usernames);
}
});
}
void UsernamesList::rebuild(const Data::Usernames &usernames) {
if (_reorder) {
_reorder->cancel();
}
_rows.clear();
_rows.reserve(usernames.size());
_container = base::make_unique_q<Ui::VerticalLayout>(this);
{
Ui::AddSkip(_container);
_container->add(
object_ptr<Ui::FlatLabel>(
_container,
_peer->isSelf()
? tr::lng_usernames_subtitle()
: tr::lng_channel_usernames_subtitle(),
st::defaultSubsectionTitle),
st::defaultSubsectionTitlePadding);
}
const auto content = _container->add(
object_ptr<Ui::VerticalLayout>(_container));
for (const auto &username : usernames) {
const auto link = _peer->session().createInternalLinkFull(
username.username);
const auto status = (username.editable && _focusCallback)
? tr::lng_usernames_edit(tr::now)
: (username.editable && !username.active)
? tr::lng_usernames_non_active(tr::now)
: username.active
? tr::lng_usernames_active(tr::now)
: tr::lng_usernames_non_active(tr::now);
const auto row = content->add(
object_ptr<Row>(content, username, _show, status, link));
_rows.push_back(row);
row->addClickHandler([=] {
if (_reordering
|| (!_peer->isSelf() && !_peer->isChannel() && !_isBot)) {
return;
}
if (username.editable) {
if (_focusCallback) {
_focusCallback();
return;
}
if (_isBot) {
const auto hasActiveAuction = ranges::any_of(
usernames,
[](const Data::Username &u) {
return !u.editable && u.active;
});
if (!hasActiveAuction && username.active) {
return;
}
} else {
return;
}
}
auto text = _peer->isSelf()
? (username.active
? tr::lng_usernames_deactivate_description()
: tr::lng_usernames_activate_description())
: _isBot
? (username.active
? tr::lng_bot_usernames_deactivate_description()
: tr::lng_bot_usernames_activate_description())
: (username.active
? tr::lng_channel_usernames_deactivate_description()
: tr::lng_channel_usernames_activate_description());
auto confirmText = username.active
? tr::lng_usernames_deactivate_confirm()
: tr::lng_usernames_activate_confirm();
auto args = Ui::ConfirmBoxArgs{
.text = std::move(text),
.confirmed = crl::guard(this, [=](Fn<void()> close) {
auto &api = _peer->session().api();
_toggleLifetime = api.usernames().reorder(
_peer,
order()
) | rpl::on_done([=] {
auto &api = _peer->session().api();
_toggleLifetime = api.usernames().toggle(
_peer,
username.username,
!username.active
) | rpl::on_error_done([=](
Api::Usernames::Error error) {
if (error == Api::Usernames::Error::TooMuch) {
constexpr auto kMaxUsernames = 10.;
_show->showBox(
Ui::MakeInformBox(
tr::lng_usernames_activate_error(
lt_count,
rpl::single(kMaxUsernames),
tr::rich)));
}
if (error == Api::Usernames::Error::Flood) {
_show->showToast(
tr::lng_flood_error(tr::now));
}
load();
_toggleLifetime.destroy();
}, [=] {
_toggleLifetime.destroy();
});
});
close();
}),
.confirmText = std::move(confirmText),
};
_show->showBox(Ui::MakeConfirmBox(std::move(args)));
});
}
_reorder = std::make_unique<Ui::VerticalLayoutReorder>(content);
_reorder->setMouseEventProxy([=](int i) {
return _rows[i]->rightAction();
});
{
const auto it = ranges::find_if(usernames, [&](
const Data::Username username) {
return !username.active;
});
if (it != end(usernames)) {
const auto from = std::distance(begin(usernames), it);
const auto length = std::distance(it, end(usernames));
_reorder->addPinnedInterval(from, length);
if (from == 1) {
// Can't be reordered.
_rows[0]->rightAction()->hide();
}
}
}
_reorder->start();
_reorder->updates(
) | rpl::on_next([=](Ui::VerticalLayoutReorder::Single data) {
using State = Ui::VerticalLayoutReorder::State;
if (data.state == State::Started) {
++_reordering;
} else {
Ui::PostponeCall(content, [=] {
--_reordering;
});
if (data.state == State::Applied) {
base::reorder(
_rows,
data.oldPosition,
data.newPosition);
}
}
}, content->lifetime());
{
Ui::AddSkip(_container);
Ui::AddDividerText(
_container,
_peer->isSelf()
? tr::lng_usernames_description()
: _isBot
? tr::lng_bot_usernames_description()
: tr::lng_channel_usernames_description());
}
Ui::ResizeFitChild(this, _container.get());
content->show();
_container->show();
}
std::vector<QString> UsernamesList::order() const {
return ranges::views::all(
_rows
) | ranges::views::filter([](not_null<Row*> row) {
return row->username().active;
}) | ranges::views::transform([](not_null<Row*> row) {
return row->username().username;
}) | ranges::to_vector;
}
rpl::producer<> UsernamesList::save() {
return _peer->session().api().usernames().reorder(_peer, order());
}

View File

@@ -0,0 +1,56 @@
/*
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 "base/unique_qptr.h"
#include "ui/rp_widget.h"
class PeerData;
namespace Ui {
class VerticalLayout;
class VerticalLayoutReorder;
class Show;
} // namespace Ui
namespace Data {
struct Username;
} // namespace Data
class UsernamesList final : public Ui::RpWidget {
public:
UsernamesList(
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
Fn<void()> focusCallback);
[[nodiscard]] rpl::producer<> save();
[[nodiscard]] std::vector<QString> order() const;
private:
void rebuild(const std::vector<Data::Username> &usernames);
void load();
class Row;
const std::shared_ptr<Ui::Show> _show;
const not_null<PeerData*> _peer;
const bool _isBot = false;
Fn<void()> _focusCallback;
base::unique_qptr<Ui::VerticalLayout> _container;
std::unique_ptr<Ui::VerticalLayoutReorder> _reorder;
std::vector<Row*> _rows;
int _reordering = 0;
rpl::lifetime _loadLifetime;
rpl::lifetime _toggleLifetime;
};

View File

@@ -0,0 +1,933 @@
/*
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/peer_short_info_box.h"
#include "base/event_filter.h"
#include "core/application.h"
#include "info/profile/info_profile_text.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "ui/effects/radial_animation.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/wrap.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace {
using MenuCallback = Ui::Menu::MenuCallback;
constexpr auto kShadowMaxAlpha = 80;
constexpr auto kInactiveBarOpacity = 0.5;
} // namespace
struct PeerShortInfoCover::CustomLabelStyle {
explicit CustomLabelStyle(const style::FlatLabel &original);
style::complex_color textFg;
style::FlatLabel st;
float64 opacity = 1.;
};
struct PeerShortInfoCover::Radial {
explicit Radial(Fn<void()> &&callback);
void toggle(bool visible);
Ui::RadialAnimation radial;
Ui::Animations::Simple shownAnimation;
Fn<void()> callback;
base::Timer showTimer;
bool shown = false;
};
PeerShortInfoCover::Radial::Radial(Fn<void()> &&callback)
: radial(callback)
, callback(callback)
, showTimer([=] { toggle(true); }) {
}
void PeerShortInfoCover::Radial::toggle(bool visible) {
if (shown == visible) {
return;
}
shown = visible;
shownAnimation.start(
callback,
shown ? 0. : 1.,
shown ? 1. : 0.,
st::fadeWrapDuration);
}
PeerShortInfoCover::CustomLabelStyle::CustomLabelStyle(
const style::FlatLabel &original)
: textFg([=, c = original.textFg]{
auto result = c->c;
result.setAlphaF(result.alphaF() * opacity);
return result;
})
, st(original) {
st.textFg = textFg.color();
}
PeerShortInfoCover::PeerShortInfoCover(
not_null<QWidget*> parent,
const style::ShortInfoCover &st,
rpl::producer<QString> name,
rpl::producer<QString> status,
rpl::producer<PeerShortInfoUserpic> userpic,
Fn<bool()> videoPaused)
: _st(st)
, _owned(parent.get())
, _widget(_owned.data())
, _nameStyle(std::make_unique<CustomLabelStyle>(_st.name))
, _name(_widget.get(), std::move(name), _nameStyle->st)
, _statusStyle(std::make_unique<CustomLabelStyle>(_st.status))
, _status(_widget.get(), std::move(status), _statusStyle->st)
, _roundMask(Images::CornersMask(_st.radius))
, _roundMaskRetina(
Images::CornersMask(_st.radius / style::DevicePixelRatio()))
, _videoPaused(std::move(videoPaused)) {
_widget->setCursor(_cursor);
_widget->resize(_st.size, _st.size);
std::move(
userpic
) | rpl::on_next([=](PeerShortInfoUserpic &&value) {
applyUserpic(std::move(value));
applyAdditionalStatus(value.additionalStatus);
}, lifetime());
style::PaletteChanged(
) | rpl::on_next([=] {
refreshBarImages();
}, lifetime());
_widget->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(_widget.get());
paint(p);
}, lifetime());
base::install_event_filter(_widget.get(), [=](not_null<QEvent*> e) {
if (e->type() != QEvent::MouseButtonPress
&& e->type() != QEvent::MouseButtonDblClick) {
return base::EventFilterResult::Continue;
}
const auto mouse = static_cast<QMouseEvent*>(e.get());
const auto x = mouse->pos().x();
if (mouse->button() != Qt::LeftButton) {
return base::EventFilterResult::Continue;
} else if (/*_index > 0 && */x < _st.size / 3) {
_moveRequests.fire(-1);
} else if (/*_index + 1 < _count && */x >= _st.size / 3) {
_moveRequests.fire(1);
}
e->accept();
return base::EventFilterResult::Cancel;
});
refreshLabelsGeometry();
_roundedTopImage = QImage(
QSize(_st.size, _st.radius) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_roundedTopImage.setDevicePixelRatio(style::DevicePixelRatio());
_roundedTopImage.fill(Qt::transparent);
}
PeerShortInfoCover::~PeerShortInfoCover() = default;
not_null<Ui::RpWidget*> PeerShortInfoCover::widget() const {
return _widget;
}
object_ptr<Ui::RpWidget> PeerShortInfoCover::takeOwned() {
return std::move(_owned);
}
gsl::span<const QImage, 4> PeerShortInfoCover::roundMask() const {
return _roundMask;
}
void PeerShortInfoCover::setScrollTop(int scrollTop) {
_scrollTop = scrollTop;
_widget->update();
}
rpl::producer<int> PeerShortInfoCover::moveRequests() const {
return _moveRequests.events();
}
rpl::lifetime &PeerShortInfoCover::lifetime() {
return _widget->lifetime();
}
void PeerShortInfoCover::paint(QPainter &p) {
checkStreamedIsStarted();
auto frame = currentVideoFrame();
auto paused = _videoPaused && _videoPaused();
if (!frame.isNull()) {
frame = Images::Round(
std::move(frame),
_roundMaskRetina,
RectPart::TopLeft | RectPart::TopRight);
} else if (_userpicImage.isNull()) {
auto image = QImage(
_widget->size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::black);
_userpicImage = Images::Round(
std::move(image),
_roundMask,
RectPart::TopLeft | RectPart::TopRight);
}
paintCoverImage(p, frame.isNull() ? _userpicImage : frame);
paintBars(p);
paintShadow(p);
paintRadial(p);
if (_videoInstance && _videoInstance->ready() && !paused) {
_videoInstance->markFrameShown();
}
}
void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) {
const auto roundedWidth = _st.size;
const auto roundedHeight = _st.radius;
const auto covered = (_st.size - _scrollTop);
if (covered <= 0) {
return;
} else if (!_scrollTop) {
p.drawImage(_widget->rect(), image);
return;
}
const auto fill = covered - roundedHeight;
const auto top = _widget->height() - fill;
const auto factor = style::DevicePixelRatio();
if (fill > 0) {
const auto t = roundedHeight + _scrollTop;
p.drawImage(
QRect(0, t, roundedWidth, roundedWidth - t),
image,
QRect(
0,
t * factor,
roundedWidth * factor,
(roundedWidth - t) * factor));
}
if (covered <= 0) {
return;
}
const auto rounded = std::min(covered, roundedHeight);
const auto from = top - rounded;
auto q = QPainter(&_roundedTopImage);
q.drawImage(
QRect(0, 0, roundedWidth, rounded),
image,
QRect(0, _scrollTop * factor, roundedWidth * factor, rounded * factor));
q.end();
_roundedTopImage = Images::Round(
std::move(_roundedTopImage),
_roundMask,
RectPart::TopLeft | RectPart::TopRight);
p.drawImage(
QRect(0, from, roundedWidth, rounded),
_roundedTopImage,
QRect(0, 0, roundedWidth * factor, rounded * factor));
}
void PeerShortInfoCover::paintBars(QPainter &p) {
const auto height = _st.linePadding * 2 + _st.line;
const auto factor = style::DevicePixelRatio();
if (_shadowTop.isNull()) {
_shadowTop = Images::GenerateShadow(height, kShadowMaxAlpha, 0);
_shadowTop = Images::Round(
_shadowTop.scaled(QSize(_st.size, height) * factor),
_roundMask,
RectPart::TopLeft | RectPart::TopRight);
}
const auto shadowRect = QRect(0, _scrollTop, _st.size, height);
p.drawImage(
shadowRect,
_shadowTop,
QRect(0, 0, _shadowTop.width(), height * factor));
const auto hiddenAt = _st.size - _st.namePosition.y();
if (!_smallWidth || _scrollTop >= hiddenAt) {
return;
}
const auto start = _st.linePadding;
const auto y = _scrollTop + start;
const auto skip = _st.lineSkip;
const auto full = (_st.size - 2 * start - (_count - 1) * skip);
const auto single = full / float64(_count);
const auto masterOpacity = 1. - (_scrollTop / float64(hiddenAt));
const auto inactiveOpacity = masterOpacity * kInactiveBarOpacity;
for (auto i = 0; i != _count; ++i) {
const auto left = start + i * (single + skip);
const auto right = left + single;
const auto x = qRound(left);
const auto small = (qRound(right) == qRound(left) + _smallWidth);
const auto width = small ? _smallWidth : _largeWidth;
const auto &image = small ? _barSmall : _barLarge;
const auto min = 2 * ((_st.line + 1) / 2);
const auto minProgress = min / float64(width);
const auto videoProgress = (_videoInstance && _videoDuration > 0);
const auto progress = (i != _index)
? 0.
: videoProgress
? std::max(_videoPosition / float64(_videoDuration), minProgress)
: (_videoInstance ? 0. : 1.);
if (progress == 1. && !videoProgress) {
p.setOpacity(masterOpacity);
p.drawImage(x, y, image);
} else {
p.setOpacity(inactiveOpacity);
p.drawImage(x, y, image);
if (progress > 0.) {
const auto paint = qRound(progress * width);
const auto right = paint / 2;
const auto left = paint - right;
p.setOpacity(masterOpacity);
p.drawImage(
QRect(x, y, left, _st.line),
image,
QRect(0, 0, left * factor, image.height()));
p.drawImage(
QRect(x + left, y, right, _st.line),
image,
QRect(left * factor, 0, right * factor, image.height()));
}
}
}
p.setOpacity(1.);
}
void PeerShortInfoCover::paintShadow(QPainter &p) {
if (_shadowBottom.isNull()) {
_shadowBottom = Images::GenerateShadow(
_st.shadowHeight,
0,
kShadowMaxAlpha);
}
const auto shadowTop = _st.size - _st.shadowHeight;
if (_scrollTop >= shadowTop) {
_name->hide();
_status->hide();
return;
}
const auto opacity = 1. - (_scrollTop / float64(shadowTop));
_nameStyle->opacity = opacity;
_nameStyle->textFg.refresh();
_name->show();
_statusStyle->opacity = opacity;
_statusStyle->textFg.refresh();
_status->show();
p.setOpacity(opacity);
const auto shadowRect = QRect(
0,
shadowTop,
_st.size,
_st.shadowHeight);
const auto factor = style::DevicePixelRatio();
p.drawImage(
shadowRect,
_shadowBottom,
QRect(
0,
0,
_shadowBottom.width(),
_st.shadowHeight * factor));
p.setOpacity(1.);
}
void PeerShortInfoCover::paintRadial(QPainter &p) {
const auto infinite = _videoInstance && _videoInstance->waitingShown();
if (!_radial && !infinite) {
return;
}
const auto radial = radialRect();
const auto line = _st.radialAnimation.thickness;
const auto arc = radial.marginsRemoved(
{ line, line, line, line });
const auto infiniteOpacity = _videoInstance
? _videoInstance->waitingOpacity()
: 0.;
const auto radialState = _radial
? _radial->radial.computeState()
: Ui::RadialState();
if (_radial) {
updateRadialState();
}
const auto radialOpacity = _radial
? (_radial->shownAnimation.value(_radial->shown ? 1. : 0.)
* radialState.shown)
: 0.;
auto hq = PainterHighQualityEnabler(p);
p.setOpacity(std::max(infiniteOpacity, radialOpacity));
p.setPen(Qt::NoPen);
p.setBrush(st::radialBg);
p.drawEllipse(radial);
if (radialOpacity > 0.) {
p.setOpacity(radialOpacity);
auto pen = _st.radialAnimation.color->p;
pen.setWidth(line);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.drawArc(arc, radialState.arcFrom, radialState.arcLength);
}
if (infinite) {
p.setOpacity(1.);
Ui::InfiniteRadialAnimation::Draw(
p,
_videoInstance->waitingState(),
arc.topLeft(),
arc.size(),
_st.size,
_st.radialAnimation.color,
line);
}
}
QImage PeerShortInfoCover::currentVideoFrame() const {
const auto size = QSize(_st.size, _st.size);
const auto request = Media::Streaming::FrameRequest{
.resize = size,
.outer = size,
};
return (_videoInstance
&& _videoInstance->player().ready()
&& !_videoInstance->player().videoSize().isEmpty())
? _videoInstance->frame(request)
: QImage();
}
void PeerShortInfoCover::applyAdditionalStatus(const QString &status) {
if (status.isEmpty()) {
if (_additionalStatus) {
_additionalStatus.destroy();
refreshLabelsGeometry();
}
return;
}
if (_additionalStatus) {
_additionalStatus->setText(status);
} else {
_additionalStatus.create(_widget.get(), status, _statusStyle->st);
_additionalStatus->show();
refreshLabelsGeometry();
}
}
void PeerShortInfoCover::applyUserpic(PeerShortInfoUserpic &&value) {
if (_index != value.index) {
_index = value.index;
_widget->update();
}
if (_count != value.count) {
_count = value.count;
refreshCoverCursor();
refreshBarImages();
_widget->update();
}
if (value.photo.isNull()) {
const auto videoChanged = _videoInstance
? (_videoInstance->shared() != value.videoDocument)
: (value.videoDocument != nullptr);
auto frame = videoChanged ? currentVideoFrame() : QImage();
if (!frame.isNull()) {
_userpicImage = Images::Round(
std::move(frame),
_roundMask,
RectPart::TopLeft | RectPart::TopRight);
}
} else if (_userpicImage.cacheKey() != value.photo.cacheKey()) {
_userpicImage = std::move(value.photo);
_widget->update();
}
if (!value.videoDocument) {
clearVideo();
} else if (!_videoInstance
|| _videoInstance->shared() != value.videoDocument) {
using namespace Media::Streaming;
_videoInstance = std::make_unique<Instance>(
std::move(value.videoDocument),
[=] { videoWaiting(); });
_videoStartPosition = value.videoStartPosition;
_videoInstance->lockPlayer();
_videoInstance->player().updates(
) | rpl::on_next_error([=](Update &&update) {
handleStreamingUpdate(std::move(update));
}, [=](Error &&error) {
handleStreamingError(std::move(error));
}, _videoInstance->lifetime());
if (_videoInstance->ready()) {
streamingReady(base::duplicate(_videoInstance->info()));
}
if (!_videoInstance->valid()) {
clearVideo();
}
}
_photoLoadingProgress = value.photoLoadingProgress;
updateRadialState();
}
void PeerShortInfoCover::updateRadialState() {
const auto progress = _videoInstance ? 1. : _photoLoadingProgress;
if (_radial) {
_radial->radial.update(progress, (progress == 1.), crl::now());
}
_widget->update(radialRect());
if (progress == 1.) {
if (!_radial) {
return;
}
_radial->showTimer.cancel();
_radial->toggle(false);
if (!_radial->shownAnimation.animating()) {
_radial = nullptr;
}
return;
} else if (!_radial) {
_radial = std::make_unique<Radial>([=] { updateRadialState(); });
_radial->radial.update(progress, false, crl::now());
_radial->showTimer.callOnce(st::fadeWrapDuration);
return;
} else if (!_radial->showTimer.isActive()) {
_radial->toggle(true);
}
}
void PeerShortInfoCover::clearVideo() {
_videoInstance = nullptr;
_videoStartPosition = _videoPosition = _videoDuration = 0;
}
void PeerShortInfoCover::checkStreamedIsStarted() {
if (!_videoInstance) {
return;
} else if (_videoInstance->paused()) {
_videoInstance->resume();
}
if (!_videoInstance
|| _videoInstance->active()
|| _videoInstance->failed()) {
return;
}
auto options = Media::Streaming::PlaybackOptions();
options.position = _videoStartPosition;
options.mode = Media::Streaming::Mode::Video;
options.loop = true;
_videoInstance->play(options);
}
void PeerShortInfoCover::handleStreamingUpdate(
Media::Streaming::Update &&update) {
using namespace Media::Streaming;
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [](PreloadedVideo) {
}, [&](UpdateVideo update) {
_videoPosition = update.position;
_widget->update();
}, [](PreloadedAudio) {
}, [](UpdateAudio) {
}, [](WaitingForData) {
}, [](SpeedEstimate) {
}, [](MutedByOther) {
}, [](Finished) {
});
}
void PeerShortInfoCover::handleStreamingError(
Media::Streaming::Error &&error) {
//_streamedPhoto->setVideoPlaybackFailed();
//_streamedPhoto = nullptr;
clearVideo();
}
void PeerShortInfoCover::streamingReady(Media::Streaming::Information &&info) {
_videoPosition = info.video.state.position;
_videoDuration = info.video.state.duration;
_widget->update();
}
void PeerShortInfoCover::refreshCoverCursor() {
const auto cursor = (_count > 1)
? style::cur_pointer
: style::cur_default;
if (_cursor != cursor) {
_cursor = cursor;
_widget->setCursor(_cursor);
}
}
void PeerShortInfoCover::refreshBarImages() {
if (_count < 2) {
_smallWidth = _largeWidth = 0;
_barSmall = _barLarge = QImage();
return;
}
const auto width = _st.size - 2 * _st.linePadding;
_smallWidth = (width - (_count - 1) * _st.lineSkip) / _count;
if (_smallWidth < _st.line) {
_smallWidth = _largeWidth = 0;
_barSmall = _barLarge = QImage();
return;
}
_largeWidth = _smallWidth + 1;
const auto makeBar = [&](int size) {
const auto radius = _st.line / 2.;
auto result = QImage(
QSize(size, _st.line) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::groupCallVideoTextFg);
p.drawRoundedRect(0, 0, size, _st.line, radius, radius);
p.end();
return result;
};
_barSmall = makeBar(_smallWidth);
_barLarge = makeBar(_largeWidth);
}
void PeerShortInfoCover::refreshLabelsGeometry() {
const auto statusTop = _st.size
- _st.statusPosition.y()
- _status->height();
const auto diff = _st.namePosition.y()
- _name->height()
- _st.statusPosition.y();
if (_additionalStatus) {
_additionalStatus->moveToLeft(
_status->x(),
statusTop - diff - _additionalStatus->height());
}
_name->moveToLeft(
_st.namePosition.x(),
_st.size
- _st.namePosition.y()
- _name->height()
- (_additionalStatus ? (diff + _additionalStatus->height()) : 0),
_st.size);
_status->moveToLeft(_st.statusPosition.x(), statusTop, _st.size);
}
QRect PeerShortInfoCover::radialRect() const {
const auto cover = _widget->rect();
const auto size = st::boxLoadingSize;
return QRect(
cover.x() + (cover.width() - size) / 2,
cover.y() + (cover.height() - size) / 2,
size,
size);
}
void PeerShortInfoCover::videoWaiting() {
if (!anim::Disabled()) {
_widget->update(radialRect());
}
}
PeerShortInfoBox::PeerShortInfoBox(
QWidget*,
PeerShortInfoType type,
rpl::producer<PeerShortInfoFields> fields,
rpl::producer<QString> status,
rpl::producer<PeerShortInfoUserpic> userpic,
Fn<bool()> videoPaused,
const style::ShortInfoBox *stOverride)
: _st(stOverride ? *stOverride : st::shortInfoBox)
, _type(type)
, _fields(std::move(fields))
, _topRoundBackground(this)
, _scroll(this, st::shortInfoScroll)
, _rows(
_scroll->setOwnedWidget(
object_ptr<Ui::VerticalLayout>(
_scroll.data())))
, _cover(
_rows.get(),
st::shortInfoCover,
nameValue(),
std::move(status),
std::move(userpic),
std::move(videoPaused)) {
_rows->add(_cover.takeOwned());
_scroll->scrolls(
) | rpl::on_next([=] {
_cover.setScrollTop(_scroll->scrollTop());
}, _cover.lifetime());
}
PeerShortInfoBox::~PeerShortInfoBox() = default;
rpl::producer<> PeerShortInfoBox::openRequests() const {
return _openRequests.events();
}
rpl::producer<int> PeerShortInfoBox::moveRequests() const {
return _cover.moveRequests();
}
void PeerShortInfoBox::prepare() {
addButton(tr::lng_close(), [=] { closeBox(); });
if (_type != PeerShortInfoType::Self) {
// Perhaps a new lang key should be added for opening a group.
addLeftButton(
(_type == PeerShortInfoType::User)
? tr::lng_profile_send_message()
: (_type == PeerShortInfoType::Group)
? tr::lng_view_button_group()
: tr::lng_profile_view_channel(),
[=] { _openRequests.fire({}); });
}
prepareRows();
setNoContentMargin(true);
_topRoundBackground->resize(st::shortInfoWidth, st::boxRadius);
_topRoundBackground->paintRequest(
) | rpl::on_next([=] {
if (const auto use = fillRoundedTopHeight()) {
const auto width = _topRoundBackground->width();
const auto top = _topRoundBackground->height() - use;
const auto factor = style::DevicePixelRatio();
QPainter(_topRoundBackground.data()).drawImage(
QRect(0, top, width, use),
_roundedTop,
QRect(0, top * factor, width * factor, use * factor));
}
}, _topRoundBackground->lifetime());
_roundedTop = QImage(
_topRoundBackground->size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_roundedTop.setDevicePixelRatio(style::DevicePixelRatio());
refreshRoundedTopImage(getDelegate()->style().bg->c);
setCustomCornersFilling(RectPart::FullTop);
setDimensionsToContent(st::shortInfoWidth, _rows);
}
void PeerShortInfoBox::prepareRows() {
using namespace Info::Profile;
auto addInfoLineGeneric = [&](
rpl::producer<QString> &&label,
rpl::producer<TextWithEntities> &&text,
const style::FlatLabel &textSt) {
auto line = CreateTextWithLabel(
_rows,
rpl::duplicate(label) | rpl::map(tr::marked),
rpl::duplicate(text),
_st.label,
textSt,
st::shortInfoLabeledPadding);
_rows->add(object_ptr<Ui::OverrideMargins>(
_rows.get(),
std::move(line.wrap)));
rpl::combine(
std::move(label),
std::move(text)
) | rpl::on_next([=] {
_rows->resizeToWidth(st::shortInfoWidth);
}, _rows->lifetime());
//line.text->setClickHandlerFilter(infoClickFilter);
return line.text;
};
auto addInfoLine = [&](
rpl::producer<QString> &&label,
rpl::producer<TextWithEntities> &&text,
const style::FlatLabel &textSt) {
return addInfoLineGeneric(
std::move(label),
std::move(text),
textSt);
};
auto addInfoOneLine = [&](
rpl::producer<QString> &&label,
rpl::producer<TextWithEntities> &&text,
const QString &contextCopyText) {
auto result = addInfoLine(
std::move(label),
std::move(text),
_st.labeledOneLine);
result->setDoubleClickSelectsParagraph(true);
result->setContextCopyText(contextCopyText);
return result;
};
addInfoOneLine(
tr::lng_settings_channel_label(),
channelValue(),
tr::lng_context_copy_link(tr::now));
addInfoOneLine(
tr::lng_info_link_label(),
linkValue(),
tr::lng_context_copy_link(tr::now));
addInfoOneLine(
tr::lng_info_mobile_label(),
phoneValue() | rpl::map(tr::marked),
tr::lng_profile_copy_phone(tr::now));
auto label = _fields.current().isBio
? tr::lng_info_bio_label()
: tr::lng_info_about_label();
addInfoLine(std::move(label), aboutValue(), _st.labeled);
addInfoOneLine(
tr::lng_info_username_label(),
usernameValue() | rpl::map(tr::marked),
tr::lng_context_copy_mention(tr::now));
addInfoOneLine(
birthdayLabel(),
birthdayValue() | rpl::map(tr::marked),
tr::lng_mediaview_copy(tr::now));
addInfoLine(
tr::lng_info_notes_label(),
noteValue(),
_st.labeled);
}
void PeerShortInfoBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_rows->resizeToWidth(st::shortInfoWidth);
_scroll->resize(st::shortInfoWidth, height());
_scroll->move(0, 0);
_topRoundBackground->move(0, 0);
}
int PeerShortInfoBox::fillRoundedTopHeight() {
const auto roundedHeight = _topRoundBackground->height();
const auto scrollTop = _scroll->scrollTop();
const auto covered = (st::shortInfoWidth - scrollTop);
if (covered >= roundedHeight) {
return 0;
}
const auto &color = getDelegate()->style().bg->c;
if (_roundedTopColor != color) {
refreshRoundedTopImage(color);
}
return roundedHeight - covered;
}
void PeerShortInfoBox::refreshRoundedTopImage(const QColor &color) {
_roundedTopColor = color;
_roundedTop.fill(color);
_roundedTop = Images::Round(
std::move(_roundedTop),
_cover.roundMask(),
RectPart::TopLeft | RectPart::TopRight);
}
rpl::producer<MenuCallback> PeerShortInfoBox::fillMenuRequests() const {
return _fillMenuRequests.events();
}
void PeerShortInfoBox::contextMenuEvent(QContextMenuEvent *e) {
_menuHolder = nullptr;
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
_fillMenuRequests.fire(Ui::Menu::CreateAddActionCallback(menu));
_menuHolder.reset(menu);
if (menu->empty()) {
_menuHolder = nullptr;
return;
}
menu->popup(e->globalPos());
}
rpl::producer<QString> PeerShortInfoBox::nameValue() const {
return _fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return fields.name;
}) | rpl::distinct_until_changed();
}
rpl::producer<TextWithEntities> PeerShortInfoBox::channelValue() const {
return _fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return tr::link(fields.channelName, fields.channelLink);
}) | rpl::distinct_until_changed();
}
rpl::producer<TextWithEntities> PeerShortInfoBox::linkValue() const {
return _fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return tr::link(fields.link, fields.link);
}) | rpl::distinct_until_changed();
}
rpl::producer<QString> PeerShortInfoBox::phoneValue() const {
return _fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return fields.phone;
}) | rpl::distinct_until_changed();
}
rpl::producer<QString> PeerShortInfoBox::usernameValue() const {
return _fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return fields.username;
}) | rpl::distinct_until_changed();
}
rpl::producer<QString> PeerShortInfoBox::birthdayLabel() const {
return Info::Profile::BirthdayLabelText(_fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return fields.birthday;
}) | rpl::distinct_until_changed());
}
rpl::producer<QString> PeerShortInfoBox::birthdayValue() const {
return Info::Profile::BirthdayValueText(_fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return fields.birthday;
}) | rpl::distinct_until_changed());
}
rpl::producer<TextWithEntities> PeerShortInfoBox::aboutValue() const {
return _fields.value() | rpl::map([](const PeerShortInfoFields &fields) {
return fields.about;
}) | rpl::distinct_until_changed();
}
rpl::producer<TextWithEntities> PeerShortInfoBox::noteValue() const {
return _fields.value() | rpl::map([](const PeerShortInfoFields &fields) {
return fields.note;
}) | rpl::distinct_until_changed();
}

View File

@@ -0,0 +1,212 @@
/*
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 "data/data_birthday.h"
#include "ui/layers/box_content.h"
namespace style {
struct ShortInfoCover;
struct ShortInfoBox;
} // namespace style
namespace Ui::Menu {
struct MenuCallback;
} // namespace Ui::Menu
namespace Media::Streaming {
class Document;
class Instance;
struct Update;
enum class Error;
struct Information;
} // namespace Media::Streaming
namespace Ui {
class VerticalLayout;
class RpWidget;
} // namespace Ui
enum class PeerShortInfoType {
Self,
User,
Group,
Channel,
};
struct PeerShortInfoFields {
QString name;
QString channelName;
QString channelLink;
QString phone;
QString link;
TextWithEntities about;
QString username;
Data::Birthday birthday;
TextWithEntities note;
bool isBio = false;
};
struct PeerShortInfoUserpic {
int index = 0;
int count = 0;
QImage photo;
float64 photoLoadingProgress = 0.;
std::shared_ptr<Media::Streaming::Document> videoDocument;
crl::time videoStartPosition = 0;
QString additionalStatus;
};
class PeerShortInfoCover final {
public:
PeerShortInfoCover(
not_null<QWidget*> parent,
const style::ShortInfoCover &st,
rpl::producer<QString> name,
rpl::producer<QString> status,
rpl::producer<PeerShortInfoUserpic> userpic,
Fn<bool()> videoPaused);
~PeerShortInfoCover();
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
[[nodiscard]] object_ptr<Ui::RpWidget> takeOwned();
[[nodiscard]] gsl::span<const QImage, 4> roundMask() const;
void setScrollTop(int scrollTop);
[[nodiscard]] rpl::producer<int> moveRequests() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct CustomLabelStyle;
struct Radial;
void paint(QPainter &p);
void paintCoverImage(QPainter &p, const QImage &image);
void paintBars(QPainter &p);
void paintShadow(QPainter &p);
void paintRadial(QPainter &p);
[[nodiscard]] QImage currentVideoFrame() const;
void applyUserpic(PeerShortInfoUserpic &&value);
void applyAdditionalStatus(const QString &status);
[[nodiscard]] QRect radialRect() const;
void videoWaiting();
void checkStreamedIsStarted();
void handleStreamingUpdate(Media::Streaming::Update &&update);
void handleStreamingError(Media::Streaming::Error &&error);
void streamingReady(Media::Streaming::Information &&info);
void clearVideo();
void updateRadialState();
void refreshCoverCursor();
void refreshBarImages();
void refreshLabelsGeometry();
const style::ShortInfoCover &_st;
object_ptr<Ui::RpWidget> _owned;
const not_null<Ui::RpWidget*> _widget;
std::unique_ptr<CustomLabelStyle> _nameStyle;
object_ptr<Ui::FlatLabel> _name;
std::unique_ptr<CustomLabelStyle> _statusStyle;
object_ptr<Ui::FlatLabel> _status;
object_ptr<Ui::FlatLabel> _additionalStatus = { nullptr };
std::array<QImage, 4> _roundMask;
std::array<QImage, 4> _roundMaskRetina;
QImage _userpicImage;
QImage _roundedTopImage;
QImage _barSmall;
QImage _barLarge;
QImage _shadowTop;
int _scrollTop = 0;
int _smallWidth = 0;
int _largeWidth = 0;
int _index = 0;
int _count = 0;
style::cursor _cursor = style::cur_default;
std::unique_ptr<Media::Streaming::Instance> _videoInstance;
crl::time _videoStartPosition = 0;
crl::time _videoPosition = 0;
crl::time _videoDuration = 0;
Fn<bool()> _videoPaused;
QImage _shadowBottom;
std::unique_ptr<Radial> _radial;
float64 _photoLoadingProgress = 0.;
rpl::event_stream<int> _moveRequests;
};
class PeerShortInfoBox final : public Ui::BoxContent {
public:
PeerShortInfoBox(
QWidget*,
PeerShortInfoType type,
rpl::producer<PeerShortInfoFields> fields,
rpl::producer<QString> status,
rpl::producer<PeerShortInfoUserpic> userpic,
Fn<bool()> videoPaused,
const style::ShortInfoBox *stOverride);
~PeerShortInfoBox();
[[nodiscard]] rpl::producer<> openRequests() const;
[[nodiscard]] rpl::producer<int> moveRequests() const;
[[nodiscard]] auto fillMenuRequests() const
-> rpl::producer<Ui::Menu::MenuCallback>;
protected:
void contextMenuEvent(QContextMenuEvent *e) override;
private:
void prepare() override;
void prepareRows();
void resizeEvent(QResizeEvent *e) override;
void refreshRoundedTopImage(const QColor &color);
int fillRoundedTopHeight();
[[nodiscard]] rpl::producer<QString> nameValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> channelValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> linkValue() const;
[[nodiscard]] rpl::producer<QString> phoneValue() const;
[[nodiscard]] rpl::producer<QString> usernameValue() const;
[[nodiscard]] rpl::producer<QString> birthdayLabel() const;
[[nodiscard]] rpl::producer<QString> birthdayValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> aboutValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> noteValue() const;
const style::ShortInfoBox &_st;
const PeerShortInfoType _type = PeerShortInfoType::User;
rpl::variable<PeerShortInfoFields> _fields;
QColor _roundedTopColor;
QImage _roundedTop;
object_ptr<Ui::RpWidget> _topRoundBackground;
object_ptr<Ui::ScrollArea> _scroll;
not_null<Ui::VerticalLayout*> _rows;
PeerShortInfoCover _cover;
base::unique_qptr<Ui::RpWidget> _menuHolder;
rpl::event_stream<Ui::Menu::MenuCallback> _fillMenuRequests;
rpl::event_stream<> _openRequests;
};

View File

@@ -0,0 +1,568 @@
/*
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/prepare_short_info_box.h"
#include "base/unixtime.h"
#include "boxes/peers/peer_short_info_box.h"
#include "core/application.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_file_origin.h"
#include "data/data_peer.h"
#include "data/data_peer_values.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
#include "data/data_user.h"
#include "data/data_user_photos.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/delayed_activation.h" // PreventDelayedActivation
#include "ui/text/format_values.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "window/window_session_controller.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"
namespace {
constexpr auto kOverviewLimit = 48;
struct UserpicState {
PeerShortInfoUserpic current;
std::optional<UserPhotosSlice> userSlice;
PhotoId userpicPhotoId = PeerData::kUnknownPhotoId;
Ui::PeerUserpicView userpicView;
std::shared_ptr<Data::PhotoMedia> photoView;
std::vector<std::shared_ptr<Data::PhotoMedia>> photoPreloads;
InMemoryKey userpicKey;
PhotoId photoId = PeerData::kUnknownPhotoId;
std::array<QImage, 4> roundMask;
int size = 0;
bool waitingFull = false;
bool waitingLoad = false;
};
void GenerateImage(
not_null<UserpicState*> state,
QImage image,
bool blurred = false) {
using namespace Images;
const auto size = state->size;
const auto ratio = style::DevicePixelRatio();
const auto options = blurred ? Option::Blur : Option();
state->current.photo = Images::Round(
Images::Prepare(
std::move(image),
QSize(size, size) * ratio,
{ .options = options, .outer = { size, size } }),
state->roundMask,
RectPart::TopLeft | RectPart::TopRight);
}
void GenerateImage(
not_null<UserpicState*> state,
not_null<Image*> image,
bool blurred = false) {
GenerateImage(state, image->original(), blurred);
}
void ProcessUserpic(
not_null<PeerData*> peer,
not_null<UserpicState*> state) {
state->current.videoDocument = nullptr;
state->userpicKey = peer->userpicUniqueKey(state->userpicView);
if (!state->userpicView.cloud) {
GenerateImage(
state,
PeerData::GenerateUserpicImage(
peer,
state->userpicView,
st::shortInfoWidth * style::DevicePixelRatio(),
0),
false);
state->current.photoLoadingProgress = 1.;
state->photoView = nullptr;
return;
}
peer->loadUserpic();
if (Ui::PeerUserpicLoading(state->userpicView)) {
state->current.photoLoadingProgress = 0.;
state->current.photo = QImage();
state->waitingLoad = true;
return;
}
GenerateImage(state, *state->userpicView.cloud, true);
state->current.photoLoadingProgress = peer->userpicPhotoId() ? 0. : 1.;
state->photoView = nullptr;
}
void Preload(
not_null<PeerData*> peer,
not_null<UserpicState*> state) {
auto taken = base::take(state->photoPreloads);
if (state->userSlice && state->userSlice->size() > 0) {
const auto preload = [&](int index) {
const auto photo = peer->owner().photo(
(*state->userSlice)[index]);
const auto current = (peer->userpicPhotoId() == photo->id);
const auto origin = current
? peer->userpicPhotoOrigin()
: Data::FileOriginUserPhoto(peerToUser(peer->id), photo->id);
state->photoPreloads.push_back(photo->createMediaView());
if (photo->hasVideo()) {
state->photoPreloads.back()->videoWanted(
Data::PhotoSize::Large,
origin);
} else {
state->photoPreloads.back()->wanted(
Data::PhotoSize::Large,
origin);
}
};
const auto skip = (state->userSlice->size() == state->current.count)
? 0
: 1;
if (state->current.index - skip > 0) {
preload(state->current.index - skip - 1);
} else if (!state->current.index && state->current.count > 1) {
preload(state->userSlice->size() - 1);
}
if (state->current.index - skip + 1 < state->userSlice->size()) {
preload(state->current.index - skip + 1);
} else if (!skip && state->current.index > 0) {
preload(0);
}
}
}
void ProcessFullPhoto(
not_null<PeerData*> peer,
not_null<UserpicState*> state,
not_null<PhotoData*> photo) {
using PhotoSize = Data::PhotoSize;
const auto current = (peer->userpicPhotoId() == photo->id);
const auto video = photo->hasVideo();
const auto originCurrent = peer->userpicPhotoOrigin();
const auto originOther = peer->isUser()
? Data::FileOriginUserPhoto(peerToUser(peer->id), photo->id)
: originCurrent;
const auto origin = current ? originCurrent : originOther;
const auto was = base::take(state->current.videoDocument);
const auto view = photo->createMediaView();
if (!video) {
view->wanted(PhotoSize::Large, origin);
}
if (const auto image = view->image(PhotoSize::Large)) {
GenerateImage(state, image);
Preload(peer, state);
state->photoView = nullptr;
state->current.photoLoadingProgress = 1.;
} else {
if (const auto thumbnail = view->image(PhotoSize::Thumbnail)) {
GenerateImage(state, thumbnail, true);
} else if (const auto small = view->image(PhotoSize::Small)) {
GenerateImage(state, small, true);
} else {
if (current) {
ProcessUserpic(peer, state);
}
if (!current || state->current.photo.isNull()) {
if (const auto blurred = view->thumbnailInline()) {
GenerateImage(state, blurred, true);
} else {
state->current.photo = QImage();
}
}
}
state->waitingLoad = !video;
state->photoView = view;
state->current.photoLoadingProgress = photo->progress();
}
if (!video) {
return;
}
state->current.videoDocument = peer->owner().streaming().sharedDocument(
photo,
origin);
state->current.videoStartPosition = photo->videoStartPosition();
state->photoView = nullptr;
state->current.photoLoadingProgress = 1.;
}
} // namespace
[[nodiscard]] rpl::producer<PeerShortInfoFields> FieldsValue(
not_null<PeerData*> peer) {
using UpdateFlag = Data::PeerUpdate::Flag;
return peer->session().changes().peerFlagsValue(
peer,
(UpdateFlag::Name
| UpdateFlag::PersonalChannel
| UpdateFlag::PhoneNumber
| UpdateFlag::Username
| UpdateFlag::About
| UpdateFlag::Birthday
| UpdateFlag::ContactNote)
) | rpl::map([=] {
const auto user = peer->asUser();
const auto username = peer->username();
const auto channelId = user ? user->personalChannelId() : 0;
const auto channel = channelId
? user->owner().channel(channelId).get()
: nullptr;
const auto channelUsername = channel
? channel->username()
: QString();
const auto hasChannel = !channelUsername.isEmpty();
return PeerShortInfoFields{
.name = peer->name(),
.channelName = hasChannel ? channel->name() : QString(),
.channelLink = (hasChannel
? channel->session().createInternalLinkFull(channelUsername)
: QString()),
.phone = user ? Ui::FormatPhone(user->phone()) : QString(),
.link = ((user || username.isEmpty())
? QString()
: peer->session().createInternalLinkFull(username)),
.about = Info::Profile::AboutWithEntities(peer, peer->about()),
.username = ((user && !username.isEmpty())
? ('@' + username)
: QString()),
.birthday = user ? user->birthday() : Data::Birthday(),
.note = user ? user->note() : TextWithEntities(),
.isBio = (user && !user->isBot()),
};
});
}
[[nodiscard]] rpl::producer<QString> StatusValue(not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
const auto now = base::unixtime::now();
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto timer = lifetime.make_state<base::Timer>();
const auto push = [=] {
consumer.put_next(Data::OnlineText(user, now));
timer->callOnce(Data::OnlineChangeTimeout(user, now));
};
timer->setCallback(push);
push();
return lifetime;
};
}
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Members
) | rpl::map([=] {
const auto chat = peer->asChat();
const auto channel = peer->asChannel();
const auto count = std::max({
chat ? chat->count : channel->membersCount(),
chat ? int(chat->participants.size()) : 0,
0,
});
return (chat && !chat->amIn())
? tr::lng_chat_status_unaccessible(tr::now)
: (count > 0)
? ((channel && channel->isBroadcast())
? tr::lng_chat_status_subscribers(
tr::now,
lt_count_decimal,
count)
: tr::lng_chat_status_members(
tr::now,
lt_count_decimal,
count))
: ((channel && channel->isBroadcast())
? tr::lng_channel_status(tr::now)
: tr::lng_group_status(tr::now));
});
}
void ValidatePhotoId(
not_null<UserpicState*> state,
PhotoId oldUserpicPhotoId) {
if (state->userSlice) {
const auto count = state->userSlice->size();
const auto hasOld = state->userSlice->indexOf(
oldUserpicPhotoId).has_value();
const auto hasNew = state->userSlice->indexOf(
state->userpicPhotoId).has_value();
const auto shift = (hasNew ? 0 : 1);
const auto fullCount = count + shift;
state->current.count = fullCount;
if (hasOld && !hasNew && state->current.index + 1 < fullCount) {
++state->current.index;
} else if (!hasOld && hasNew && state->current.index > 0) {
--state->current.index;
}
const auto index = state->current.index;
if (!index || index >= fullCount) {
state->current.index = 0;
state->photoId = state->userpicPhotoId;
} else {
state->photoId = (*state->userSlice)[index - shift];
}
} else {
state->photoId = state->userpicPhotoId;
}
}
bool ProcessCurrent(
not_null<PeerData*> peer,
not_null<UserpicState*> state) {
const auto userpicPhotoId = peer->userpicPhotoId();
const auto userpicPhoto = (userpicPhotoId
&& (userpicPhotoId != PeerData::kUnknownPhotoId)
&& (state->userpicPhotoId != userpicPhotoId))
? peer->owner().photo(userpicPhotoId).get()
: (state->photoId == userpicPhotoId && state->photoView)
? state->photoView->owner().get()
: nullptr;
state->waitingFull = (state->userpicPhotoId != userpicPhotoId)
&& ((userpicPhotoId == PeerData::kUnknownPhotoId)
|| (userpicPhotoId && userpicPhoto->isNull()));
if (state->waitingFull) {
peer->updateFullForced();
}
const auto oldUserpicPhotoId = state->waitingFull
? state->userpicPhotoId
: std::exchange(state->userpicPhotoId, userpicPhotoId);
const auto changedUserpic = (state->userpicKey
!= peer->userpicUniqueKey(state->userpicView));
const auto wasIndex = state->current.index;
const auto wasCount = state->current.count;
const auto wasPhotoId = state->photoId;
ValidatePhotoId(state, oldUserpicPhotoId);
const auto changedInSlice = (state->current.index != wasIndex)
|| (state->current.count != wasCount);
const auto changedPhotoId = (state->photoId != wasPhotoId);
const auto photo = (state->photoId == state->userpicPhotoId
&& userpicPhoto)
? userpicPhoto
: (state->photoId
&& (state->photoId != PeerData::kUnknownPhotoId)
&& changedPhotoId)
? peer->owner().photo(state->photoId).get()
: state->photoView
? state->photoView->owner().get()
: nullptr;
state->current.additionalStatus = (!peer->isUser())
? QString()
: ((state->photoId == userpicPhotoId)
&& peer->asUser()->hasPersonalPhoto())
? tr::lng_profile_photo_by_you(tr::now)
: ((state->current.index == (state->current.count - 1))
&& SyncUserFallbackPhotoViewer(peer->asUser()) == state->photoId)
? tr::lng_profile_public_photo(tr::now)
: QString();
state->waitingLoad = false;
if (!changedPhotoId
&& (state->current.index > 0 || !changedUserpic)
&& !state->photoView
&& (!state->current.photo.isNull()
|| state->current.videoDocument)) {
return changedInSlice;
} else if (photo && !photo->isNull()) {
ProcessFullPhoto(peer, state, photo);
} else if (state->current.index > 0) {
return changedInSlice;
} else {
ProcessUserpic(peer, state);
}
return true;
}
[[nodiscard]] PreparedShortInfoUserpic UserpicValue(
not_null<PeerData*> peer,
const style::ShortInfoCover &st,
rpl::producer<UserPhotosSlice> slices,
Fn<bool(not_null<UserpicState*>)> customProcess) {
const auto moveRequests = std::make_shared<rpl::event_stream<int>>();
auto move = [=](int shift) {
moveRequests->fire_copy(shift);
};
const auto size = st.size;
const auto radius = st.radius;
auto value = [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto state = lifetime.make_state<UserpicState>();
state->size = size;
state->roundMask = Images::CornersMask(radius);
const auto push = [=](bool force = false) {
if (customProcess(state) || force) {
consumer.put_next_copy(state->current);
}
};
using UpdateFlag = Data::PeerUpdate::Flag;
peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Photo | UpdateFlag::FullInfo
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.flags & UpdateFlag::Photo) || state->waitingFull;
}) | rpl::on_next([=] {
push();
}, lifetime);
rpl::duplicate(
slices
) | rpl::on_next([=](UserPhotosSlice &&slice) {
state->userSlice = std::move(slice);
push();
}, lifetime);
moveRequests->events(
) | rpl::filter([=] {
return (state->current.count > 1);
}) | rpl::on_next([=](int shift) {
state->current.index = std::clamp(
((state->current.index + shift + state->current.count)
% state->current.count),
0,
state->current.count - 1);
push(true);
}, lifetime);
peer->session().downloaderTaskFinished(
) | rpl::filter([=] {
return state->waitingLoad
&& (state->photoView
? (!!state->photoView->image(Data::PhotoSize::Large))
: (!Ui::PeerUserpicLoading(state->userpicView)));
}) | rpl::on_next([=] {
push();
}, lifetime);
return lifetime;
};
return { .value = std::move(value), .move = std::move(move) };
}
object_ptr<Ui::BoxContent> PrepareShortInfoBox(
not_null<PeerData*> peer,
Fn<void()> open,
Fn<bool()> videoPaused,
Fn<void(Ui::Menu::MenuCallback)> menuFiller,
const style::ShortInfoBox *stOverride) {
const auto type = peer->isSelf()
? PeerShortInfoType::Self
: peer->isUser()
? PeerShortInfoType::User
: peer->isBroadcast()
? PeerShortInfoType::Channel
: PeerShortInfoType::Group;
auto userpic = PrepareShortInfoUserpic(peer, st::shortInfoCover);
auto result = Box<PeerShortInfoBox>(
type,
FieldsValue(peer),
StatusValue(peer),
std::move(userpic.value),
std::move(videoPaused),
stOverride);
if (menuFiller) {
result->fillMenuRequests(
) | rpl::on_next([=](Ui::Menu::MenuCallback callback) {
menuFiller(std::move(callback));
}, result->lifetime());
}
result->openRequests(
) | rpl::on_next(open, result->lifetime());
result->moveRequests(
) | rpl::on_next(userpic.move, result->lifetime());
return result;
}
object_ptr<Ui::BoxContent> PrepareShortInfoBox(
not_null<PeerData*> peer,
std::shared_ptr<ChatHelpers::Show> show,
const style::ShortInfoBox *stOverride) {
const auto open = [=] {
if (const auto window = show->resolveWindow()) {
window->showPeerHistory(peer);
}
};
const auto videoIsPaused = [=] {
return show->paused(Window::GifPauseReason::Layer);
};
auto menuFiller = [=](Ui::Menu::MenuCallback addAction) {
const auto peerSeparateId = Window::SeparateId(peer);
const auto window = show->resolveWindow();
if (window && window->windowId() != peerSeparateId) {
addAction(tr::lng_context_new_window(tr::now), [=] {
Ui::PreventDelayedActivation();
window->showInNewWindow(peer);
}, &st::menuIconNewWindow);
}
};
return PrepareShortInfoBox(
peer,
open,
videoIsPaused,
std::move(menuFiller),
stOverride);
}
object_ptr<Ui::BoxContent> PrepareShortInfoBox(
not_null<PeerData*> peer,
not_null<Window::SessionNavigation*> navigation,
const style::ShortInfoBox *stOverride) {
return PrepareShortInfoBox(peer, navigation->uiShow(), stOverride);
}
rpl::producer<QString> PrepareShortInfoStatus(not_null<PeerData*> peer) {
return StatusValue(peer);
}
PreparedShortInfoUserpic PrepareShortInfoUserpic(
not_null<PeerData*> peer,
const style::ShortInfoCover &st) {
auto slices = peer->isUser()
? UserPhotosReversedViewer(
&peer->session(),
UserPhotosSlice::Key(peerToUser(peer->asUser()->id), PhotoId()),
kOverviewLimit,
kOverviewLimit)
: rpl::never<UserPhotosSlice>();
auto process = [=](not_null<UserpicState*> state) {
return ProcessCurrent(peer, state);
};
return UserpicValue(peer, st, std::move(slices), std::move(process));
}
PreparedShortInfoUserpic PrepareShortInfoFallbackUserpic(
not_null<PeerData*> peer,
const style::ShortInfoCover &st) {
Expects(peer->isUser());
const auto photoId = SyncUserFallbackPhotoViewer(peer->asUser());
auto slices = photoId
? rpl::single<UserPhotosSlice>(UserPhotosSlice(
Storage::UserPhotosKey(peerToUser(peer->id), *photoId),
std::deque<PhotoId>({ *photoId }),
1,
1,
1))
: (rpl::never<UserPhotosSlice>() | rpl::type_erased);
auto process = [=](not_null<UserpicState*> state) {
if (photoId) {
ProcessFullPhoto(peer, state, peer->owner().photo(*photoId));
return true;
}
return false;
};
return UserpicValue(peer, st, std::move(slices), std::move(process));
}

View File

@@ -0,0 +1,68 @@
/*
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 "base/object_ptr.h"
class PeerData;
namespace style {
struct ShortInfoCover;
struct ShortInfoBox;
} // namespace style
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui::Menu {
struct MenuCallback;
} // namespace Ui::Menu
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
struct PeerShortInfoUserpic;
struct PreparedShortInfoUserpic {
rpl::producer<PeerShortInfoUserpic> value;
Fn<void(int)> move;
};
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
not_null<PeerData*> peer,
Fn<void()> open,
Fn<bool()> videoPaused,
Fn<void(Ui::Menu::MenuCallback)> menuFiller,
const style::ShortInfoBox *stOverride = nullptr);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
not_null<PeerData*> peer,
std::shared_ptr<ChatHelpers::Show> show,
const style::ShortInfoBox *stOverride = nullptr);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
not_null<PeerData*> peer,
not_null<Window::SessionNavigation*> navigation,
const style::ShortInfoBox *stOverride = nullptr);
[[nodiscard]] rpl::producer<QString> PrepareShortInfoStatus(
not_null<PeerData*> peer);
[[nodiscard]] PreparedShortInfoUserpic PrepareShortInfoUserpic(
not_null<PeerData*> peer,
const style::ShortInfoCover &st);
[[nodiscard]] PreparedShortInfoUserpic PrepareShortInfoFallbackUserpic(
not_null<PeerData*> peer,
const style::ShortInfoCover &st);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
/*
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 "base/object_ptr.h"
namespace style {
struct UserpicsRow;
} // namespace style
class ChannelData;
namespace Data {
struct UniqueGift;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
struct BoostCounters;
struct BoostFeatures;
class BoxContent;
class RpWidget;
} // namespace Ui
struct TakenBoostSlot {
int id = 0;
TimeId expires = 0;
PeerId peerId = 0;
TimeId cooldown = 0;
};
struct ForChannelBoostSlots {
std::vector<int> free;
std::vector<int> already;
std::vector<TakenBoostSlot> other;
};
[[nodiscard]] ForChannelBoostSlots ParseForChannelBoostSlots(
not_null<ChannelData*> channel,
const QVector<MTPMyBoost> &boosts);
[[nodiscard]] Ui::BoostCounters ParseBoostCounters(
const MTPpremium_BoostsStatus &status);
[[nodiscard]] Ui::BoostFeatures LookupBoostFeatures(
not_null<ChannelData*> channel);
[[nodiscard]] int BoostsForGift(not_null<Main::Session*> session);
object_ptr<Ui::BoxContent> ReassignBoostsBox(
not_null<ChannelData*> to,
std::vector<TakenBoostSlot> from,
Fn<void(std::vector<int> slots, int groups, int channels)> reassign,
Fn<void()> cancel);
enum class UserpicsTransferType {
BoostReplace,
StarRefJoin,
AuctionRecipient,
};
[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> from,
not_null<PeerData*> to,
UserpicsTransferType type);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> peers,
const style::UserpicsRow &st,
int limit);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateGiftTransfer(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Data::UniqueGift> unique,
not_null<PeerData*> to);

View File

@@ -0,0 +1,228 @@
/*
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/toggle_topics_box.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/settings_common.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
enum class LayoutType {
Tabs,
List
};
class LayoutButton final : public Ui::RippleButton {
public:
LayoutButton(
QWidget *parent,
LayoutType type,
std::shared_ptr<Ui::RadioenumGroup<LayoutType>> group);
private:
void paintEvent(QPaintEvent *e) override;
QImage prepareRippleMask() const override;
Ui::FlatLabel _text;
Ui::Animations::Simple _activeAnimation;
bool _active = false;
};
LayoutButton::LayoutButton(
QWidget *parent,
LayoutType type,
std::shared_ptr<Ui::RadioenumGroup<LayoutType>> group)
: RippleButton(parent, st::defaultRippleAnimationBgOver)
, _text(this, st::topicsLayoutButtonLabel)
, _active(group->current() == type) {
_text.setText(type == LayoutType::Tabs
? tr::lng_edit_topics_tabs(tr::now)
: tr::lng_edit_topics_list(tr::now));
const auto iconColorOverride = [=] {
return anim::color(
st::windowSubTextFg,
st::windowActiveTextFg,
_activeAnimation.value(_active ? 1. : 0.));
};
const auto iconSize = st::topicsLayoutButtonIconSize;
auto [iconWidget, iconAnimate] = Settings::CreateLottieIcon(
this,
{
.name = (type == LayoutType::Tabs
? u"topics_tabs"_q
: u"topics_list"_q),
.color = &st::windowSubTextFg,
.sizeOverride = { iconSize, iconSize },
.colorizeUsingAlpha = true,
},
st::topicsLayoutButtonIconPadding,
iconColorOverride);
const auto icon = iconWidget.release();
setClickedCallback([=] {
group->setValue(type);
iconAnimate(anim::repeat::once);
});
group->value() | rpl::on_next([=](LayoutType value) {
const auto active = (value == type);
_text.setTextColorOverride(active
? st::windowFgActive->c
: std::optional<QColor>());
if (_active == active) {
return;
}
_active = active;
_text.update();
_activeAnimation.start([=] {
icon->update();
}, _active ? 0. : 1., _active ? 0. : 1., st::fadeWrapDuration);
}, lifetime());
_text.paintRequest() | rpl::on_next([=](QRect clip) {
if (_active) {
auto p = QPainter(&_text);
auto hq = PainterHighQualityEnabler(p);
const auto radius = _text.height() / 2.;
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgActive);
p.drawRoundedRect(_text.rect(), radius, radius);
}
}, _text.lifetime());
const auto padding = st::topicsLayoutButtonPadding;
const auto skip = st::topicsLayoutButtonSkip;
const auto text = _text.height();
resize(
padding.left() + icon->width() + padding.right(),
padding.top() + icon->height() + skip + text + padding.bottom());
icon->move(padding.left(), padding.top());
_text.move(
(width() - _text.width()) / 2,
padding.top() + icon->height() + skip);
}
void LayoutButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto rippleBg = anim::color(
st::windowBgOver,
st::lightButtonBgOver,
_activeAnimation.value(_active ? 1. : 0.));
paintRipple(p, QPoint(), &rippleBg);
}
QImage LayoutButton::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(size(), st::boxRadius);
}
} // namespace
void ToggleTopicsBox(
not_null<Ui::GenericBox*> box,
bool enabled,
bool tabs,
Fn<void(bool enabled, bool tabs)> callback) {
box->setTitle(tr::lng_forum_topics_switch());
box->setWidth(st::boxWideWidth);
const auto container = box->verticalLayout();
Settings::AddDividerTextWithLottie(container, {
.lottie = u"topics"_q,
.lottieSize = st::settingsFilterIconSize,
.lottieMargins = st::settingsFilterIconPadding,
.showFinished = box->showFinishes(),
.about = tr::lng_edit_topics_about(
tr::rich
),
.aboutMargins = st::settingsFilterDividerLabelPadding,
});
Ui::AddSkip(container);
const auto toggle = container->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_edit_topics_enable(),
st::settingsButtonNoIcon));
toggle->toggleOn(rpl::single(enabled));
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
const auto group = std::make_shared<Ui::RadioenumGroup<LayoutType>>(tabs
? LayoutType::Tabs
: LayoutType::List);
const auto layoutWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto layout = layoutWrap->entity();
Ui::AddSubsectionTitle(layout, tr::lng_edit_topics_layout());
const auto buttons = layout->add(
object_ptr<Ui::RpWidget>(layout),
QMargins(0, 0, 0, st::defaultVerticalListSkip * 2));
const auto tabsButton = Ui::CreateChild<LayoutButton>(
buttons,
LayoutType::Tabs,
group);
const auto listButton = Ui::CreateChild<LayoutButton>(
buttons,
LayoutType::List,
group);
buttons->resize(container->width(), tabsButton->height());
buttons->widthValue() | rpl::on_next([=](int outer) {
const auto skip = st::boxRowPadding.left() - st::boxRadius;
tabsButton->moveToLeft(skip, 0, outer);
listButton->moveToRight(skip, 0, outer);
}, buttons->lifetime());
Ui::AddDividerText(
layout,
tr::lng_edit_topics_layout_about(tr::rich));
layoutWrap->toggle(enabled, anim::type::instant);
toggle->toggledChanges(
) | rpl::on_next([=](bool checked) {
layoutWrap->toggle(checked, anim::type::normal);
}, layoutWrap->lifetime());
box->addButton(tr::lng_settings_save(), [=] {
const auto enabledValue = toggle->toggled();
const auto tabsValue = (group->current() == LayoutType::Tabs);
callback(enabledValue, tabsValue);
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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/layers/generic_box.h"
namespace Ui {
void ToggleTopicsBox(
not_null<Ui::GenericBox*> box,
bool enabled,
bool tabs,
Fn<void(bool enabled, bool tabs)> callback);
} // namespace Ui

View File

@@ -0,0 +1,295 @@
/*
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/verify_peers_box.h"
#include "apiwrap.h"
#include "boxes/peer_list_controllers.h"
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
namespace {
constexpr auto kSetupVerificationToastDuration = 4 * crl::time(1000);
class Controller final : public ChatsListBoxController {
public:
Controller(not_null<Main::Session*> session, not_null<UserData*> bot)
: ChatsListBoxController(session)
, _bot(bot) {
}
Main::Session &session() const override;
void rowClicked(gsl::not_null<PeerListRow*> row) override;
private:
std::unique_ptr<Row> createRow(not_null<History*> history) override;
void prepareViewHook() override;
void confirmAdd(not_null<PeerData*> peer);
void confirmRemove(not_null<PeerData*> peer);
const not_null<UserData*> _bot;
};
void Setup(
not_null<UserData*> bot,
not_null<PeerData*> peer,
QString description,
Fn<void(QString)> done) {
using Flag = MTPbots_SetCustomVerification::Flag;
bot->session().api().request(MTPbots_SetCustomVerification(
MTP_flags(Flag::f_bot
| Flag::f_enabled
| (description.isEmpty() ? Flag() : Flag::f_custom_description)),
bot->inputUser(),
peer->input(),
MTP_string(description)
)).done([=] {
done(QString());
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
void Remove(
not_null<UserData*> bot,
not_null<PeerData*> peer,
Fn<void(QString)> done) {
bot->session().api().request(MTPbots_SetCustomVerification(
MTP_flags(MTPbots_SetCustomVerification::Flag::f_bot),
bot->inputUser(),
peer->input(),
MTPstring()
)).done([=] {
done(QString());
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
Main::Session &Controller::session() const {
return _bot->session();
}
void Controller::rowClicked(gsl::not_null<PeerListRow*> row) {
const auto peer = row->peer();
const auto details = peer->botVerifyDetails();
const auto already = details && (details->botId == peerToUser(_bot->id));
if (already) {
confirmRemove(peer);
} else {
confirmAdd(peer);
}
}
void Controller::confirmAdd(not_null<PeerData*> peer) {
const auto bot = _bot;
const auto show = delegate()->peerListUiShow();
show->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
Ui::InputField *field = nullptr;
QString description;
bool sent = false;
};
const auto settings = bot->botInfo
? bot->botInfo->verifierSettings.get()
: nullptr;
const auto modify = settings && settings->canModifyDescription;
const auto state = std::make_shared<State>(State{
.description = settings ? settings->customDescription : QString()
});
const auto limit = session().appConfig().get<int>(
u"bot_verification_description_length_limit"_q,
70);
const auto send = [=] {
if (modify && state->description.size() > limit) {
state->field->showError();
return;
} else if (state->sent) {
return;
}
state->sent = true;
const auto weak = base::make_weak(box);
const auto description = modify ? state->description : QString();
Setup(bot, peer, description, [=](QString error) {
if (error.isEmpty()) {
if (const auto strong = weak.get()) {
strong->closeBox();
}
show->showToast({
.text = PeerVerifyPhrases(peer).sent(
tr::now,
lt_name,
tr::bold(peer->shortName()),
tr::marked),
.duration = kSetupVerificationToastDuration,
});
} else {
state->sent = false;
show->showToast(error);
}
});
};
const auto phrases = PeerVerifyPhrases(peer);
Ui::ConfirmBox(box, {
.text = phrases.text(
lt_name,
rpl::single(tr::bold(peer->shortName())),
tr::marked),
.confirmed = send,
.confirmText = phrases.submit(),
.title = phrases.title(),
});
if (!modify) {
return;
}
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_bot_verify_description_label(),
QMargins(0, 0, 0, -st::defaultSubsectionTitlePadding.bottom()));
const auto field = box->addRow(object_ptr<Ui::InputField>(
box,
st::createPollField,
Ui::InputField::Mode::NoNewlines,
rpl::single(state->description),
state->description
), st::createPollFieldPadding);
state->field = field;
box->setFocusCallback([=] {
field->setFocusFast();
});
Ui::AddSkip(box->verticalLayout());
field->changes() | rpl::on_next([=] {
state->description = field->getLastText();
}, field->lifetime());
field->setMaxLength(limit * 2);
Ui::AddLengthLimitLabel(field, limit);
Ui::AddDividerText(box->verticalLayout(), phrases.about());
}));
}
void Controller::confirmRemove(not_null<PeerData*> peer) {
const auto bot = _bot;
const auto show = delegate()->peerListUiShow();
show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto sent = std::make_shared<bool>();
const auto send = [=] {
if (*sent) {
return;
}
*sent = true;
const auto weak = base::make_weak(box);
Remove(bot, peer, [=](QString error) {
if (error.isEmpty()) {
if (const auto strong = weak.get()) {
strong->closeBox();
}
show->showToast(tr::lng_bot_verify_remove_done(tr::now));
} else {
*sent = false;
show->showToast(error);
}
});
};
Ui::ConfirmBox(box, {
.text = PeerVerifyPhrases(peer).remove(),
.confirmed = send,
.confirmText = tr::lng_bot_verify_remove_submit(),
.confirmStyle = &st::attentionBoxButton,
.title = tr::lng_bot_verify_remove_title(),
});
}));
}
auto Controller::createRow(not_null<History*> history)
-> std::unique_ptr<Row> {
const auto peer = history->peer;
const auto may = peer->isUser() || peer->isChannel();
return may ? std::make_unique<Row>(history) : nullptr;
}
void Controller::prepareViewHook() {
}
} // namespace
object_ptr<Ui::BoxContent> MakeVerifyPeersBox(
not_null<Window::SessionController*> window,
not_null<UserData*> bot) {
const auto session = &window->session();
auto controller = std::make_unique<Controller>(session, bot);
auto init = [=](not_null<PeerListBox*> box) {
box->setTitle(tr::lng_bot_verify_title());
box->addButton(tr::lng_box_done(), [=] {
box->closeBox();
});
};
return Box<PeerListBox>(std::move(controller), std::move(init));
}
BotVerifyPhrases PeerVerifyPhrases(not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
if (user->isBot()) {
return {
.title = tr::lng_bot_verify_bot_title,
.text = tr::lng_bot_verify_bot_text,
.about = tr::lng_bot_verify_bot_about,
.submit = tr::lng_bot_verify_bot_submit,
.sent = tr::lng_bot_verify_bot_sent,
.remove = tr::lng_bot_verify_bot_remove,
};
} else {
return {
.title = tr::lng_bot_verify_user_title,
.text = tr::lng_bot_verify_user_text,
.about = tr::lng_bot_verify_user_about,
.submit = tr::lng_bot_verify_user_submit,
.sent = tr::lng_bot_verify_user_sent,
.remove = tr::lng_bot_verify_user_remove,
};
}
} else if (peer->isBroadcast()) {
return {
.title = tr::lng_bot_verify_channel_title,
.text = tr::lng_bot_verify_channel_text,
.about = tr::lng_bot_verify_channel_about,
.submit = tr::lng_bot_verify_channel_submit,
.sent = tr::lng_bot_verify_channel_sent,
.remove = tr::lng_bot_verify_channel_remove,
};
}
return {
.title = tr::lng_bot_verify_group_title,
.text = tr::lng_bot_verify_group_text,
.about = tr::lng_bot_verify_group_about,
.submit = tr::lng_bot_verify_group_submit,
.sent = tr::lng_bot_verify_group_sent,
.remove = tr::lng_bot_verify_group_remove,
};
}

View File

@@ -0,0 +1,36 @@
/*
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 "base/object_ptr.h"
#include "lang/lang_keys.h"
class PeerData;
class UserData;
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
[[nodiscard]] object_ptr<Ui::BoxContent> MakeVerifyPeersBox(
not_null<Window::SessionController*> window,
not_null<UserData*> bot);
struct BotVerifyPhrases {
tr::phrase<> title;
tr::phrase<lngtag_name> text;
tr::phrase<> about;
tr::phrase<> submit;
tr::phrase<lngtag_name> sent;
tr::phrase<> remove;
};
[[nodiscard]] BotVerifyPhrases PeerVerifyPhrases(not_null<PeerData*> peer);