1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "calls/group/calls_choose_join_as.h"
9
10 #include "calls/group/calls_group_common.h"
11 #include "calls/group/calls_group_menu.h"
12 #include "data/data_peer.h"
13 #include "data/data_user.h"
14 #include "data/data_channel.h"
15 #include "data/data_session.h"
16 #include "data/data_group_call.h"
17 #include "main/main_session.h"
18 #include "main/main_account.h"
19 #include "lang/lang_keys.h"
20 #include "apiwrap.h"
21 #include "ui/layers/generic_box.h"
22 #include "ui/boxes/choose_date_time.h"
23 #include "ui/text/text_utilities.h"
24 #include "boxes/peer_list_box.h"
25 #include "base/unixtime.h"
26 #include "base/timer_rpl.h"
27 #include "styles/style_boxes.h"
28 #include "styles/style_layers.h"
29 #include "styles/style_calls.h"
30
31 namespace Calls::Group {
32 namespace {
33
34 constexpr auto kLabelRefreshInterval = 10 * crl::time(1000);
35
36 using Context = ChooseJoinAsProcess::Context;
37
38 class ListController : public PeerListController {
39 public:
40 ListController(
41 std::vector<not_null<PeerData*>> list,
42 not_null<PeerData*> selected);
43
44 Main::Session &session() const override;
45 void prepare() override;
46 void rowClicked(not_null<PeerListRow*> row) override;
47
48 [[nodiscard]] not_null<PeerData*> selected() const;
49
50 private:
51 std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);
52
53 std::vector<not_null<PeerData*>> _list;
54 not_null<PeerData*> _selected;
55
56 };
57
ListController(std::vector<not_null<PeerData * >> list,not_null<PeerData * > selected)58 ListController::ListController(
59 std::vector<not_null<PeerData*>> list,
60 not_null<PeerData*> selected)
61 : PeerListController()
62 , _list(std::move(list))
63 , _selected(selected) {
64 }
65
session() const66 Main::Session &ListController::session() const {
67 return _selected->session();
68 }
69
createRow(not_null<PeerData * > peer)70 std::unique_ptr<PeerListRow> ListController::createRow(
71 not_null<PeerData*> peer) {
72 auto result = std::make_unique<PeerListRow>(peer);
73 if (peer->isSelf()) {
74 result->setCustomStatus(
75 tr::lng_group_call_join_as_personal(tr::now));
76 } else if (const auto channel = peer->asChannel()) {
77 result->setCustomStatus(
78 (channel->isMegagroup()
79 ? tr::lng_chat_status_members
80 : tr::lng_chat_status_subscribers)(
81 tr::now,
82 lt_count,
83 channel->membersCount()));
84 }
85 return result;
86 }
87
prepare()88 void ListController::prepare() {
89 delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
90 for (const auto &peer : _list) {
91 auto row = createRow(peer);
92 const auto raw = row.get();
93 delegate()->peerListAppendRow(std::move(row));
94 if (peer == _selected) {
95 delegate()->peerListSetRowChecked(raw, true);
96 raw->finishCheckedAnimation();
97 }
98 }
99 delegate()->peerListRefreshRows();
100 }
101
rowClicked(not_null<PeerListRow * > row)102 void ListController::rowClicked(not_null<PeerListRow*> row) {
103 const auto peer = row->peer();
104 if (peer == _selected) {
105 return;
106 }
107 const auto previous = delegate()->peerListFindRow(_selected->id.value);
108 Assert(previous != nullptr);
109 delegate()->peerListSetRowChecked(previous, false);
110 delegate()->peerListSetRowChecked(row, true);
111 _selected = peer;
112 }
113
selected() const114 not_null<PeerData*> ListController::selected() const {
115 return _selected;
116 }
117
ScheduleGroupCallBox(not_null<Ui::GenericBox * > box,const JoinInfo & info,Fn<void (JoinInfo)> done)118 void ScheduleGroupCallBox(
119 not_null<Ui::GenericBox*> box,
120 const JoinInfo &info,
121 Fn<void(JoinInfo)> done) {
122 const auto send = [=](TimeId date) {
123 box->closeBox();
124
125 auto copy = info;
126 copy.scheduleDate = date;
127 done(std::move(copy));
128 };
129 const auto livestream = info.peer->isBroadcast();
130 const auto duration = box->lifetime().make_state<
131 rpl::variable<QString>>();
132 auto description = (info.peer->isBroadcast()
133 ? tr::lng_group_call_schedule_notified_channel
134 : tr::lng_group_call_schedule_notified_group)(
135 lt_duration,
136 duration->value());
137
138 const auto now = QDateTime::currentDateTime();
139 const auto min = [] {
140 return base::unixtime::serialize(
141 QDateTime::currentDateTime().addSecs(12));
142 };
143 const auto max = [] {
144 return base::unixtime::serialize(
145 QDateTime(QDate::currentDate().addDays(8), QTime(0, 0))) - 1;
146 };
147
148 // At least half an hour later, at zero minutes/seconds.
149 const auto schedule = QDateTime(
150 now.date(),
151 QTime(now.time().hour(), 0)
152 ).addSecs(60 * 60 * (now.time().minute() < 30 ? 1 : 2));
153
154 auto descriptor = Ui::ChooseDateTimeBox(box, {
155 .title = (livestream
156 ? tr::lng_group_call_schedule_title_channel()
157 : tr::lng_group_call_schedule_title()),
158 .submit = tr::lng_schedule_button(),
159 .done = send,
160 .min = min,
161 .time = base::unixtime::serialize(schedule),
162 .max = max,
163 .description = std::move(description),
164 });
165
166 using namespace rpl::mappers;
167 *duration = rpl::combine(
168 rpl::single(
169 rpl::empty_value()
170 ) | rpl::then(base::timer_each(kLabelRefreshInterval)),
171 std::move(descriptor.values) | rpl::filter(_1 != 0),
172 _2
173 ) | rpl::map([](TimeId date) {
174 const auto now = base::unixtime::now();
175 const auto duration = (date - now);
176 if (duration >= 24 * 60 * 60) {
177 return tr::lng_group_call_duration_days(
178 tr::now,
179 lt_count,
180 duration / (24 * 60 * 60));
181 } else if (duration >= 60 * 60) {
182 return tr::lng_group_call_duration_hours(
183 tr::now,
184 lt_count,
185 duration / (60 * 60));
186 }
187 return tr::lng_group_call_duration_minutes(
188 tr::now,
189 lt_count,
190 std::max(duration / 60, 1));
191 });
192 }
193
ChooseJoinAsBox(not_null<Ui::GenericBox * > box,Context context,JoinInfo info,Fn<void (JoinInfo)> done)194 void ChooseJoinAsBox(
195 not_null<Ui::GenericBox*> box,
196 Context context,
197 JoinInfo info,
198 Fn<void(JoinInfo)> done) {
199 box->setWidth(st::groupCallJoinAsWidth);
200 const auto livestream = info.peer->isBroadcast();
201 box->setTitle([&] {
202 switch (context) {
203 case Context::Create: return livestream
204 ? tr::lng_group_call_start_as_header_channel()
205 : tr::lng_group_call_start_as_header();
206 case Context::Join:
207 case Context::JoinWithConfirm: return livestream
208 ? tr::lng_group_call_join_as_header_channel()
209 : tr::lng_group_call_join_as_header();
210 case Context::Switch: return tr::lng_group_call_display_as_header();
211 }
212 Unexpected("Context in ChooseJoinAsBox.");
213 }());
214 const auto &labelSt = (context == Context::Switch)
215 ? st::groupCallJoinAsLabel
216 : st::confirmPhoneAboutLabel;
217 box->addRow(object_ptr<Ui::FlatLabel>(
218 box,
219 tr::lng_group_call_join_as_about(),
220 labelSt));
221
222 auto &lifetime = box->lifetime();
223 const auto delegate = lifetime.make_state<
224 PeerListContentDelegateSimple
225 >();
226 const auto controller = lifetime.make_state<ListController>(
227 info.possibleJoinAs,
228 info.joinAs);
229 if (context == Context::Switch) {
230 controller->setStyleOverrides(
231 &st::groupCallJoinAsList,
232 &st::groupCallMultiSelect);
233 } else {
234 controller->setStyleOverrides(
235 &st::peerListJoinAsList,
236 nullptr);
237 }
238 const auto content = box->addRow(
239 object_ptr<PeerListContent>(box, controller),
240 style::margins());
241 delegate->setContent(content);
242 controller->setDelegate(delegate);
243 auto next = (context == Context::Switch)
244 ? tr::lng_settings_save()
245 : tr::lng_continue();
246 if (context == Context::Create) {
247 const auto makeLink = [](const QString &text) {
248 return Ui::Text::Link(text);
249 };
250 const auto label = box->addRow(object_ptr<Ui::FlatLabel>(
251 box,
252 tr::lng_group_call_or_schedule(
253 lt_link,
254 (livestream
255 ? tr::lng_group_call_schedule_channel
256 : tr::lng_group_call_schedule)(makeLink),
257 Ui::Text::WithEntities),
258 labelSt));
259 label->setClickHandlerFilter([=](const auto&...) {
260 auto withJoinAs = info;
261 withJoinAs.joinAs = controller->selected();
262 box->getDelegate()->show(
263 Box(ScheduleGroupCallBox, withJoinAs, done));
264 return false;
265 });
266 }
267 box->addButton(std::move(next), [=] {
268 auto copy = info;
269 copy.joinAs = controller->selected();
270 done(std::move(copy));
271 });
272 box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
273 }
274
CreateOrJoinConfirmation(not_null<PeerData * > peer,ChooseJoinAsProcess::Context context,bool joinAsAlreadyUsed)275 [[nodiscard]] TextWithEntities CreateOrJoinConfirmation(
276 not_null<PeerData*> peer,
277 ChooseJoinAsProcess::Context context,
278 bool joinAsAlreadyUsed) {
279 const auto existing = peer->groupCall();
280 if (!existing) {
281 return { peer->isBroadcast()
282 ? tr::lng_group_call_create_sure_channel(tr::now)
283 : tr::lng_group_call_create_sure(tr::now) };
284 }
285 const auto channel = peer->asChannel();
286 const auto anonymouseAdmin = channel
287 && ((channel->isMegagroup() && channel->amAnonymous())
288 || (channel->isBroadcast()
289 && (channel->amCreator() || channel->hasAdminRights())));
290 if (anonymouseAdmin && !joinAsAlreadyUsed) {
291 return { tr::lng_group_call_join_sure_personal(tr::now) };
292 } else if (context != ChooseJoinAsProcess::Context::JoinWithConfirm) {
293 return {};
294 }
295 const auto name = !existing->title().isEmpty()
296 ? existing->title()
297 : peer->name;
298 return (peer->isBroadcast()
299 ? tr::lng_group_call_join_confirm_channel
300 : tr::lng_group_call_join_confirm)(
301 tr::now,
302 lt_chat,
303 Ui::Text::Bold(name),
304 Ui::Text::WithEntities);
305 }
306
307 } // namespace
308
~ChooseJoinAsProcess()309 ChooseJoinAsProcess::~ChooseJoinAsProcess() {
310 if (_request) {
311 _request->peer->session().api().request(_request->id).cancel();
312 }
313 }
314
start(not_null<PeerData * > peer,Context context,Fn<void (object_ptr<Ui::BoxContent>)> showBox,Fn<void (QString)> showToast,Fn<void (JoinInfo)> done,PeerData * changingJoinAsFrom)315 void ChooseJoinAsProcess::start(
316 not_null<PeerData*> peer,
317 Context context,
318 Fn<void(object_ptr<Ui::BoxContent>)> showBox,
319 Fn<void(QString)> showToast,
320 Fn<void(JoinInfo)> done,
321 PeerData *changingJoinAsFrom) {
322 Expects(done != nullptr);
323
324 const auto session = &peer->session();
325 if (_request) {
326 if (_request->peer == peer) {
327 _request->context = context;
328 _request->showBox = std::move(showBox);
329 _request->showToast = std::move(showToast);
330 _request->done = std::move(done);
331 return;
332 }
333 session->api().request(_request->id).cancel();
334 _request = nullptr;
335 }
336 _request = std::make_unique<ChannelsListRequest>(
337 ChannelsListRequest{
338 .peer = peer,
339 .showBox = std::move(showBox),
340 .showToast = std::move(showToast),
341 .done = std::move(done),
342 .context = context });
343 session->account().sessionChanges(
344 ) | rpl::start_with_next([=] {
345 _request = nullptr;
346 }, _request->lifetime);
347
348 const auto finish = [=](JoinInfo info) {
349 const auto done = std::move(_request->done);
350 const auto box = _request->box;
351 _request = nullptr;
352 done(std::move(info));
353 if (const auto strong = box.data()) {
354 strong->closeBox();
355 }
356 };
357 _request->id = session->api().request(MTPphone_GetGroupCallJoinAs(
358 _request->peer->input
359 )).done([=](const MTPphone_JoinAsPeers &result) {
360 const auto peer = _request->peer;
361 const auto self = peer->session().user();
362 auto info = JoinInfo{ .peer = peer, .joinAs = self };
363 auto list = result.match([&](const MTPDphone_joinAsPeers &data) {
364 session->data().processUsers(data.vusers());
365 session->data().processChats(data.vchats());
366 const auto &peers = data.vpeers().v;
367 auto list = std::vector<not_null<PeerData*>>();
368 list.reserve(peers.size());
369 for (const auto &peer : peers) {
370 const auto peerId = peerFromMTP(peer);
371 if (const auto peer = session->data().peerLoaded(peerId)) {
372 if (!ranges::contains(list, not_null{ peer })) {
373 list.push_back(peer);
374 }
375 }
376 }
377 return list;
378 });
379 const auto selectedId = peer->groupCallDefaultJoinAs();
380 if (list.empty()) {
381 _request->showToast(Lang::Hard::ServerError());
382 return;
383 }
384 info.joinAs = [&]() -> not_null<PeerData*> {
385 const auto loaded = selectedId
386 ? session->data().peerLoaded(selectedId)
387 : nullptr;
388 return (changingJoinAsFrom
389 && ranges::contains(list, not_null{ changingJoinAsFrom }))
390 ? not_null(changingJoinAsFrom)
391 : (loaded && ranges::contains(list, not_null{ loaded }))
392 ? not_null(loaded)
393 : ranges::contains(list, self)
394 ? self
395 : list.front();
396 }();
397 info.possibleJoinAs = std::move(list);
398
399 const auto onlyByMe = (info.possibleJoinAs.size() == 1)
400 && (info.possibleJoinAs.front() == self);
401
402 // We already joined this voice chat, just rejoin with the same.
403 const auto byAlreadyUsed = selectedId
404 && (info.joinAs->id == selectedId)
405 && (peer->groupCall() != nullptr);
406
407 if (!changingJoinAsFrom && (onlyByMe || byAlreadyUsed)) {
408 auto confirmation = CreateOrJoinConfirmation(
409 peer,
410 context,
411 byAlreadyUsed);
412 if (confirmation.text.isEmpty()) {
413 finish(info);
414 return;
415 }
416 const auto livestream = peer->isBroadcast();
417 const auto creating = !peer->groupCall();
418 if (creating) {
419 confirmation
420 .append("\n\n")
421 .append(tr::lng_group_call_or_schedule(
422 tr::now,
423 lt_link,
424 Ui::Text::Link((livestream
425 ? tr::lng_group_call_schedule_channel
426 : tr::lng_group_call_schedule)(tr::now)),
427 Ui::Text::WithEntities));
428 }
429 const auto guard = base::make_weak(&_request->guard);
430 const auto safeFinish = crl::guard(guard, [=] { finish(info); });
431 const auto filter = [=](const auto &...) {
432 if (guard) {
433 _request->showBox(Box(
434 ScheduleGroupCallBox,
435 info,
436 crl::guard(guard, finish)));
437 }
438 return false;
439 };
440 auto box = ConfirmBox({
441 .text = confirmation,
442 .button = (creating
443 ? tr::lng_create_group_create()
444 : tr::lng_group_call_join()),
445 .callback = crl::guard(guard, [=] { finish(info); }),
446 .st = &st::boxLabel,
447 .filter = filter,
448 });
449 box->boxClosing(
450 ) | rpl::start_with_next([=] {
451 _request = nullptr;
452 }, _request->lifetime);
453
454 _request->box = box.data();
455 _request->showBox(std::move(box));
456 return;
457 }
458 auto box = Box(
459 ChooseJoinAsBox,
460 context,
461 std::move(info),
462 crl::guard(&_request->guard, finish));
463 box->boxClosing(
464 ) | rpl::start_with_next([=] {
465 _request = nullptr;
466 }, _request->lifetime);
467
468 _request->box = box.data();
469 _request->showBox(std::move(box));
470 }).fail([=](const MTP::Error &error) {
471 finish({
472 .peer = _request->peer,
473 .joinAs = _request->peer->session().user(),
474 });
475 }).send();
476 }
477
478 } // namespace Calls::Group
479