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
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:
424
Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
Normal file
424
Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
84
Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h
Normal file
84
Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h
Normal 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);
|
||||
2049
Telegram/SourceFiles/boxes/peers/add_participants_box.cpp
Normal file
2049
Telegram/SourceFiles/boxes/peers/add_participants_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
227
Telegram/SourceFiles/boxes/peers/add_participants_box.h
Normal file
227
Telegram/SourceFiles/boxes/peers/add_participants_box.h
Normal 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;
|
||||
|
||||
};
|
||||
546
Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
Normal file
546
Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
Normal 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)));
|
||||
}
|
||||
24
Telegram/SourceFiles/boxes/peers/choose_peer_box.h
Normal file
24
Telegram/SourceFiles/boxes/peers/choose_peer_box.h
Normal 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);
|
||||
899
Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp
Normal file
899
Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp
Normal 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 ¬e,
|
||||
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();
|
||||
}
|
||||
26
Telegram/SourceFiles/boxes/peers/edit_contact_box.h
Normal file
26
Telegram/SourceFiles/boxes/peers/edit_contact_box.h
Normal 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);
|
||||
365
Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp
Normal file
365
Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp
Normal 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));
|
||||
}
|
||||
34
Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.h
Normal file
34
Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.h
Normal 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);
|
||||
609
Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp
Normal file
609
Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp
Normal 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);
|
||||
}
|
||||
41
Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h
Normal file
41
Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h
Normal 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);
|
||||
77
Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp
Normal file
77
Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp
Normal 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;
|
||||
}
|
||||
19
Telegram/SourceFiles/boxes/peers/edit_members_visible.h
Normal file
19
Telegram/SourceFiles/boxes/peers/edit_members_visible.h
Normal 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);
|
||||
982
Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp
Normal file
982
Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp
Normal 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;
|
||||
}
|
||||
189
Telegram/SourceFiles/boxes/peers/edit_participant_box.h
Normal file
189
Telegram/SourceFiles/boxes/peers/edit_participant_box.h
Normal 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;
|
||||
|
||||
};
|
||||
2439
Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
Normal file
2439
Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
366
Telegram/SourceFiles/boxes/peers/edit_participants_box.h
Normal file
366
Telegram/SourceFiles/boxes/peers/edit_participants_box.h
Normal 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;
|
||||
|
||||
};
|
||||
2832
Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
Normal file
2832
Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
69
Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
Normal file
69
Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
Normal 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);
|
||||
18
Telegram/SourceFiles/boxes/peers/edit_peer_common.h
Normal file
18
Telegram/SourceFiles/boxes/peers/edit_peer_common.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
3023
Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
Normal file
3023
Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
69
Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h
Normal file
69
Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h
Normal 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);
|
||||
1756
Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
Normal file
1756
Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
Normal file
File diff suppressed because it is too large
Load Diff
85
Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h
Normal file
85
Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h
Normal 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);
|
||||
1082
Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp
Normal file
1082
Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp
Normal file
File diff suppressed because it is too large
Load Diff
27
Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h
Normal file
27
Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h
Normal 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);
|
||||
1507
Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp
Normal file
1507
Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
132
Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h
Normal file
132
Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h
Normal 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>;
|
||||
1033
Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp
Normal file
1033
Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp
Normal file
File diff suppressed because it is too large
Load Diff
42
Telegram/SourceFiles/boxes/peers/edit_peer_reactions.h
Normal file
42
Telegram/SourceFiles/boxes/peers/edit_peer_reactions.h
Normal 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);
|
||||
760
Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp
Normal file
760
Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp
Normal 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();
|
||||
}
|
||||
149
Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h
Normal file
149
Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h
Normal 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;
|
||||
|
||||
};
|
||||
792
Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp
Normal file
792
Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp
Normal 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));
|
||||
}
|
||||
77
Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h
Normal file
77
Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h
Normal 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;
|
||||
|
||||
};
|
||||
426
Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp
Normal file
426
Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp
Normal 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());
|
||||
}
|
||||
56
Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h
Normal file
56
Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h
Normal 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;
|
||||
|
||||
};
|
||||
933
Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
Normal file
933
Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
Normal 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();
|
||||
}
|
||||
212
Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
Normal file
212
Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
Normal 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;
|
||||
|
||||
};
|
||||
568
Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
Normal file
568
Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
Normal 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));
|
||||
}
|
||||
68
Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h
Normal file
68
Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h
Normal 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);
|
||||
1039
Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
Normal file
1039
Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
84
Telegram/SourceFiles/boxes/peers/replace_boost_box.h
Normal file
84
Telegram/SourceFiles/boxes/peers/replace_boost_box.h
Normal 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);
|
||||
228
Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp
Normal file
228
Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp
Normal 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
|
||||
20
Telegram/SourceFiles/boxes/peers/toggle_topics_box.h
Normal file
20
Telegram/SourceFiles/boxes/peers/toggle_topics_box.h
Normal 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
|
||||
295
Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp
Normal file
295
Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp
Normal 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,
|
||||
};
|
||||
}
|
||||
36
Telegram/SourceFiles/boxes/peers/verify_peers_box.h
Normal file
36
Telegram/SourceFiles/boxes/peers/verify_peers_box.h
Normal 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);
|
||||
Reference in New Issue
Block a user