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 "ui/chat/group_call_bar.h"
9 
10 #include "ui/chat/group_call_userpics.h"
11 #include "ui/widgets/shadow.h"
12 #include "ui/widgets/buttons.h"
13 #include "lang/lang_keys.h"
14 #include "base/unixtime.h"
15 #include "styles/style_chat.h"
16 #include "styles/style_calls.h"
17 #include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
18 #include "styles/palette.h"
19 
20 #include <QtGui/QtEvents>
21 
22 namespace Ui {
23 
GroupCallScheduledLeft(TimeId date)24 GroupCallScheduledLeft::GroupCallScheduledLeft(TimeId date)
25 : _date(date)
26 , _datePrecise(computePreciseDate())
27 , _timer([=] { update(); }) {
28 	update();
29 	base::unixtime::updates(
__anon8c3c4a060202null30 	) | rpl::start_with_next([=] {
31 		restart();
32 	}, _lifetime);
33 }
34 
computePreciseDate() const35 crl::time GroupCallScheduledLeft::computePreciseDate() const {
36 	return crl::now() + (_date - base::unixtime::now()) * crl::time(1000);
37 }
38 
setDate(TimeId date)39 void GroupCallScheduledLeft::setDate(TimeId date) {
40 	if (_date == date) {
41 		return;
42 	}
43 	_date = date;
44 	restart();
45 }
46 
restart()47 void GroupCallScheduledLeft::restart() {
48 	_datePrecise = computePreciseDate();
49 	_timer.cancel();
50 	update();
51 }
52 
text(Negative negative) const53 rpl::producer<QString> GroupCallScheduledLeft::text(Negative negative) const {
54 	return (negative == Negative::Show)
55 		? _text.value()
56 		: _textNonNegative.value();
57 }
58 
late() const59 rpl::producer<bool> GroupCallScheduledLeft::late() const {
60 	return _late.value();
61 }
62 
update()63 void GroupCallScheduledLeft::update() {
64 	const auto now = crl::now();
65 	const auto duration = (_datePrecise - now);
66 	const auto left = crl::time(base::SafeRound(std::abs(duration) / 1000.));
67 	const auto late = (duration < 0) && (left > 0);
68 	_late = late;
69 	constexpr auto kDay = 24 * 60 * 60;
70 	if (left >= kDay) {
71 		const auto days = (left / kDay);
72 		_textNonNegative = tr::lng_group_call_duration_days(
73 			tr::now,
74 			lt_count,
75 			days);
76 		_text = late
77 			? tr::lng_group_call_duration_days(tr::now, lt_count, -days)
78 			: _textNonNegative.current();
79 	} else {
80 		const auto hours = left / (60 * 60);
81 		const auto minutes = (left % (60 * 60)) / 60;
82 		const auto seconds = (left % 60);
83 		_textNonNegative = (hours > 0)
84 			? (u"%1:%2:%3"_q
85 				.arg(hours, 2, 10, QChar('0'))
86 				.arg(minutes, 2, 10, QChar('0'))
87 				.arg(seconds, 2, 10, QChar('0')))
88 			: (u"%1:%2"_q
89 				.arg(minutes, 2, 10, QChar('0'))
90 				.arg(seconds, 2, 10, QChar('0')));
91 		_text = (late ? QString(QChar(0x2212)) : QString())
92 			+ _textNonNegative.current();
93 	}
94 	if (left >= kDay) {
95 		_timer.callOnce((left % kDay) * crl::time(1000));
96 	} else {
97 		const auto fraction = (std::abs(duration) + 500) % 1000;
98 		if (fraction < 400 || fraction > 600) {
99 			const auto next = std::abs(duration) % 1000;
100 			_timer.callOnce((duration < 0) ? (1000 - next) : next);
101 		} else if (!_timer.isActive()) {
102 			_timer.callEach(1000);
103 		}
104 	}
105 }
106 
GroupCallBar(not_null<QWidget * > parent,rpl::producer<GroupCallBarContent> content,rpl::producer<bool> && hideBlobs)107 GroupCallBar::GroupCallBar(
108 	not_null<QWidget*> parent,
109 	rpl::producer<GroupCallBarContent> content,
110 	rpl::producer<bool> &&hideBlobs)
111 : _wrap(parent, object_ptr<RpWidget>(parent))
112 , _inner(_wrap.entity())
113 , _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
114 , _userpics(std::make_unique<GroupCallUserpics>(
115 		st::historyGroupCallUserpics,
116 		std::move(hideBlobs),
117 		[=] { updateUserpics(); })) {
118 	_wrap.hide(anim::type::instant);
119 	_shadow->hide();
120 
121 	_wrap.entity()->paintRequest(
__anon8c3c4a060402(QRect clip) 122 	) | rpl::start_with_next([=](QRect clip) {
123 		QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
124 	}, lifetime());
125 	_wrap.setAttribute(Qt::WA_OpaquePaintEvent);
126 
127 	auto copy = std::move(
128 		content
129 	) | rpl::start_spawning(_wrap.lifetime());
130 
131 	rpl::duplicate(
132 		copy
__anon8c3c4a060502(GroupCallBarContent &&content) 133 	) | rpl::start_with_next([=](GroupCallBarContent &&content) {
134 		_content = content;
135 		_userpics->update(_content.users, !_wrap.isHidden());
136 		_inner->update();
137 		refreshScheduledProcess();
138 	}, lifetime());
139 	if (!_open && !_join) {
140 		refreshScheduledProcess();
141 	}
142 
143 	std::move(
144 		copy
__anon8c3c4a060602(const GroupCallBarContent &content) 145 	) | rpl::map([=](const GroupCallBarContent &content) {
146 		return !content.shown;
147 	}) | rpl::start_with_next_done([=](bool hidden) {
148 		_shouldBeShown = !hidden;
149 		if (!_forceHidden) {
150 			_wrap.toggle(_shouldBeShown, anim::type::normal);
151 		}
__anon8c3c4a060802null152 	}, [=] {
153 		_forceHidden = true;
154 		_wrap.toggle(false, anim::type::normal);
155 	}, lifetime());
156 
157 	setupInner();
158 }
159 
160 GroupCallBar::~GroupCallBar() = default;
161 
refreshOpenBrush()162 void GroupCallBar::refreshOpenBrush() {
163 	Expects(_open != nullptr);
164 
165 	const auto width = _open->width();
166 	if (_openBrushForWidth == width) {
167 		return;
168 	}
169 	auto gradient = QLinearGradient(QPoint(width, 0), QPoint(0, 0));
170 	gradient.setStops(QGradientStops{
171 		{ 0.0, st::groupCallForceMutedBar1->c },
172 		{ .7, st::groupCallForceMutedBar2->c },
173 		{ 1.0, st::groupCallForceMutedBar3->c }
174 	});
175 	_openBrushOverride = QBrush(std::move(gradient));
176 	_openBrushForWidth = width;
177 	_open->setBrushOverride(_openBrushOverride);
178 }
179 
refreshScheduledProcess()180 void GroupCallBar::refreshScheduledProcess() {
181 	const auto date = _content.scheduleDate;
182 	if (!date) {
183 		if (_scheduledProcess) {
184 			_scheduledProcess = nullptr;
185 			_open = nullptr;
186 			_openBrushForWidth = 0;
187 		}
188 		if (!_join) {
189 			_join = std::make_unique<RoundButton>(
190 				_inner.get(),
191 				tr::lng_group_call_join(),
192 				st::groupCallTopBarJoin);
193 			setupRightButton(_join.get());
194 		}
195 	} else if (!_scheduledProcess) {
196 		_scheduledProcess = std::make_unique<GroupCallScheduledLeft>(date);
197 		_join = nullptr;
198 		_open = std::make_unique<RoundButton>(
199 			_inner.get(),
200 			_scheduledProcess->text(GroupCallScheduledLeft::Negative::Show),
201 			st::groupCallTopBarOpen);
202 		setupRightButton(_open.get());
203 		_open->widthValue(
204 		) | rpl::start_with_next([=] {
205 			refreshOpenBrush();
206 		}, _open->lifetime());
207 	} else {
208 		_scheduledProcess->setDate(date);
209 	}
210 }
211 
setupInner()212 void GroupCallBar::setupInner() {
213 	_inner->resize(0, st::historyReplyHeight);
214 	_inner->paintRequest(
215 	) | rpl::start_with_next([=](QRect rect) {
216 		auto p = Painter(_inner);
217 		paint(p);
218 	}, _inner->lifetime());
219 
220 	// Clicks.
221 	_inner->setCursor(style::cur_pointer);
222 	_inner->events(
223 	) | rpl::filter([=](not_null<QEvent*> event) {
224 		return (event->type() == QEvent::MouseButtonPress);
225 	}) | rpl::map([=] {
226 		return _inner->events(
227 		) | rpl::filter([=](not_null<QEvent*> event) {
228 			return (event->type() == QEvent::MouseButtonRelease);
229 		}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {
230 			return _inner->rect().contains(
231 				static_cast<QMouseEvent*>(event.get())->pos());
232 		});
233 	}) | rpl::flatten_latest(
234 	) | rpl::map([] {
235 		return rpl::empty_value();
236 	}) | rpl::start_to_stream(_barClicks, _inner->lifetime());
237 
238 	_wrap.geometryValue(
239 	) | rpl::start_with_next([=](QRect rect) {
240 		updateShadowGeometry(rect);
241 		updateControlsGeometry(rect);
242 	}, _inner->lifetime());
243 }
244 
setupRightButton(not_null<RoundButton * > button)245 void GroupCallBar::setupRightButton(not_null<RoundButton*> button) {
246 	rpl::combine(
247 		_inner->widthValue(),
248 		button->widthValue()
249 	) | rpl::start_with_next([=](int outerWidth, int) {
250 		// Skip shadow of the bar above.
251 		const auto top = (st::historyReplyHeight
252 			- st::lineWidth
253 			- button->height()) / 2 + st::lineWidth;
254 		button->moveToRight(top, top, outerWidth);
255 	}, button->lifetime());
256 
257 	button->clicks() | rpl::start_to_stream(_joinClicks, button->lifetime());
258 }
259 
paint(Painter & p)260 void GroupCallBar::paint(Painter &p) {
261 	p.fillRect(_inner->rect(), st::historyComposeAreaBg);
262 
263 	const auto left = st::topBarArrowPadding.right();
264 	const auto titleTop = st::msgReplyPadding.top();
265 	const auto textTop = titleTop + st::msgServiceNameFont->height;
266 	const auto width = _inner->width();
267 	const auto &font = st::defaultMessageBar.title.font;
268 	p.setPen(st::defaultMessageBar.textFg);
269 	p.setFont(font);
270 
271 	const auto available = (_join ? _join->x() : _open->x()) - left;
272 	const auto titleWidth = font->width(_content.title);
273 	p.drawTextLeft(
274 		left,
275 		titleTop,
276 		width,
277 		(!_content.scheduleDate
278 			? (_content.livestream
279 				? tr::lng_group_call_title_channel
280 				: tr::lng_group_call_title)(tr::now)
281 			: _content.title.isEmpty()
282 			? (_content.livestream
283 				? tr::lng_group_call_scheduled_title_channel
284 				: tr::lng_group_call_scheduled_title)(tr::now)
285 			: (titleWidth > available)
286 			? font->elided(_content.title, available)
287 			: _content.title));
288 	p.setPen(st::historyStatusFg);
289 	p.setFont(st::defaultMessageBar.text.font);
290 	const auto when = [&] {
291 		if (!_content.scheduleDate) {
292 			return QString();
293 		}
294 		const auto parsed = base::unixtime::parse(_content.scheduleDate);
295 		const auto date = parsed.date();
296 		const auto time = parsed.time().toString(
297 			QLocale::system().timeFormat(QLocale::ShortFormat));
298 		const auto today = QDate::currentDate();
299 		if (date == today) {
300 			return tr::lng_group_call_starts_today(tr::now, lt_time, time);
301 		} else if (date == today.addDays(1)) {
302 			return tr::lng_group_call_starts_tomorrow(
303 				tr::now,
304 				lt_time,
305 				time);
306 		} else {
307 			return tr::lng_group_call_starts_date(
308 				tr::now,
309 				lt_date,
310 				langDayOfMonthFull(date),
311 				lt_time,
312 				time);
313 		}
314 	}();
315 	p.drawTextLeft(
316 		left,
317 		textTop,
318 		width,
319 		(_content.scheduleDate
320 			? (_content.title.isEmpty()
321 				? tr::lng_group_call_starts_short
322 				: _content.livestream
323 				? tr::lng_group_call_starts_channel
324 				: tr::lng_group_call_starts)(tr::now, lt_when, when)
325 			: _content.count > 0
326 			? tr::lng_group_call_members(tr::now, lt_count, _content.count)
327 			: tr::lng_group_call_no_members(tr::now)));
328 
329 	const auto size = st::historyGroupCallUserpics.size;
330 	// Skip shadow of the bar above.
331 	const auto top = (st::historyReplyHeight - st::lineWidth - size) / 2
332 		+ st::lineWidth;
333 	_userpics->paint(p, _inner->width() / 2, top, size);
334 }
335 
updateControlsGeometry(QRect wrapGeometry)336 void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
337 	const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
338 	if (_shadow->isHidden() != hidden) {
339 		_shadow->setVisible(!hidden);
340 	}
341 }
342 
setShadowGeometryPostprocess(Fn<QRect (QRect)> postprocess)343 void GroupCallBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {
344 	_shadowGeometryPostprocess = std::move(postprocess);
345 	updateShadowGeometry(_wrap.geometry());
346 }
347 
updateShadowGeometry(QRect wrapGeometry)348 void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {
349 	const auto regular = QRect(
350 		wrapGeometry.x(),
351 		wrapGeometry.y() + wrapGeometry.height(),
352 		wrapGeometry.width(),
353 		st::lineWidth);
354 	_shadow->setGeometry(_shadowGeometryPostprocess
355 		? _shadowGeometryPostprocess(regular)
356 		: regular);
357 }
358 
updateUserpics()359 void GroupCallBar::updateUserpics() {
360 	const auto widget = _wrap.entity();
361 	const auto middle = widget->width() / 2;
362 	const auto width = _userpics->maxWidth();
363 	widget->update(
364 		(middle - width / 2),
365 		0,
366 		width,
367 		widget->height());
368 }
369 
show()370 void GroupCallBar::show() {
371 	if (!_forceHidden) {
372 		return;
373 	}
374 	_forceHidden = false;
375 	if (_shouldBeShown) {
376 		_wrap.show(anim::type::instant);
377 		_shadow->show();
378 	}
379 }
380 
hide()381 void GroupCallBar::hide() {
382 	if (_forceHidden) {
383 		return;
384 	}
385 	_forceHidden = true;
386 	_wrap.hide(anim::type::instant);
387 	_shadow->hide();
388 }
389 
raise()390 void GroupCallBar::raise() {
391 	_wrap.raise();
392 	_shadow->raise();
393 }
394 
finishAnimating()395 void GroupCallBar::finishAnimating() {
396 	_wrap.finishAnimating();
397 }
398 
move(int x,int y)399 void GroupCallBar::move(int x, int y) {
400 	_wrap.move(x, y);
401 }
402 
resizeToWidth(int width)403 void GroupCallBar::resizeToWidth(int width) {
404 	_wrap.entity()->resizeToWidth(width);
405 	_inner->resizeToWidth(width);
406 }
407 
height() const408 int GroupCallBar::height() const {
409 	return !_forceHidden
410 		? _wrap.height()
411 		: _shouldBeShown
412 		? st::historyReplyHeight
413 		: 0;
414 }
415 
heightValue() const416 rpl::producer<int> GroupCallBar::heightValue() const {
417 	return _wrap.heightValue();
418 }
419 
barClicks() const420 rpl::producer<> GroupCallBar::barClicks() const {
421 	return _barClicks.events();
422 }
423 
joinClicks() const424 rpl::producer<> GroupCallBar::joinClicks() const {
425 	return _joinClicks.events() | rpl::to_empty;
426 }
427 
428 } // namespace Ui
429