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