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