mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-07 23:53:58 +02:00
Add carousel animation for emoji fingerprint.
This commit is contained in:
parent
b1b2798be1
commit
29d87f692a
15 changed files with 644 additions and 103 deletions
|
@ -4967,6 +4967,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_confcall_not_accessible" = "This call is no longer accessible.";
|
||||
"lng_confcall_participants_limit" = "This call reached the participants limit.";
|
||||
"lng_confcall_sure_remove" = "Remove {user} from the call?";
|
||||
"lng_confcall_e2e_badge" = "End-to-End Encrypted";
|
||||
"lng_confcall_e2e_badge_small" = "E2E Encrypted";
|
||||
|
||||
"lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages.";
|
||||
|
||||
|
|
|
@ -1316,7 +1316,7 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) {
|
|||
user->madeAction(base::unixtime::now());
|
||||
}
|
||||
}
|
||||
ClearMediaAsExpired(item);
|
||||
item->clearMediaAsExpired();
|
||||
}
|
||||
} else {
|
||||
// Perhaps it was an unread mention!
|
||||
|
|
|
@ -1669,3 +1669,14 @@ groupCallLinkMenu: IconButton(confcallLinkMenu) {
|
|||
color: groupCallMembersBgOver;
|
||||
}
|
||||
}
|
||||
|
||||
confcallFingerprintBottomSkip: 8px;
|
||||
confcallFingerprintMargins: margins(8px, 5px, 8px, 5px);
|
||||
confcallFingerprintTextMargins: margins(3px, 3px, 3px, 0px);
|
||||
confcallFingerprintText: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMembersFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(10px semibold);
|
||||
}
|
||||
}
|
||||
confcallFingerprintSkip: 2px;
|
||||
|
|
|
@ -7,11 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
|
||||
#include "base/random.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "data/data_user.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
@ -20,7 +23,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTooltipShowTimeoutMs = 1000;
|
||||
constexpr auto kTooltipShowTimeoutMs = crl::time(1000);
|
||||
constexpr auto kCarouselOneDuration = crl::time(1000);// crl::time(100);
|
||||
constexpr auto kStartTimeShift = crl::time(500);// crl::time(50);
|
||||
constexpr auto kEmojiInFingerprint = 4;
|
||||
constexpr auto kEmojiInCarousel = 10;
|
||||
|
||||
const ushort Data[] = {
|
||||
0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,
|
||||
|
@ -109,8 +116,11 @@ const ushort Offsets[] = {
|
|||
620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,
|
||||
642, 643, 644, 646, 648, 650, 652, 654, 656, 658 };
|
||||
|
||||
constexpr auto kEmojiCount = (base::array_size(Offsets) - 1);
|
||||
|
||||
uint64 ComputeEmojiIndex(bytes::const_span bytes) {
|
||||
Expects(bytes.size() == 8);
|
||||
|
||||
return ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56)
|
||||
| (gsl::to_integer<uint64>(bytes[1]) << 48)
|
||||
| (gsl::to_integer<uint64>(bytes[2]) << 40)
|
||||
|
@ -121,6 +131,17 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) {
|
|||
| (gsl::to_integer<uint64>(bytes[7]));
|
||||
}
|
||||
|
||||
[[nodiscard]] EmojiPtr EmojiByIndex(int index) {
|
||||
Expects(index >= 0 && index < kEmojiCount);
|
||||
|
||||
const auto offset = Offsets[index];
|
||||
const auto size = Offsets[index + 1] - offset;
|
||||
const auto string = QString::fromRawData(
|
||||
reinterpret_cast<const QChar*>(Data + offset),
|
||||
size);
|
||||
return Ui::Emoji::Find(string);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
|
||||
|
@ -133,22 +154,13 @@ std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
|
|||
std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
bytes::const_span fingerprint) {
|
||||
auto result = std::vector<EmojiPtr>();
|
||||
constexpr auto EmojiCount = (base::array_size(Offsets) - 1);
|
||||
constexpr auto kPartSize = 8;
|
||||
for (auto partOffset = 0
|
||||
; partOffset != fingerprint.size()
|
||||
; partOffset += kPartSize) {
|
||||
auto value = ComputeEmojiIndex(
|
||||
const auto value = ComputeEmojiIndex(
|
||||
fingerprint.subspan(partOffset, kPartSize));
|
||||
auto index = value % EmojiCount;
|
||||
auto offset = Offsets[index];
|
||||
auto size = Offsets[index + 1] - offset;
|
||||
auto string = QString::fromRawData(
|
||||
reinterpret_cast<const QChar*>(Data + offset),
|
||||
size);
|
||||
auto emoji = Ui::Emoji::Find(string);
|
||||
Assert(emoji != nullptr);
|
||||
result.push_back(emoji);
|
||||
result.push_back(EmojiByIndex(value % kEmojiCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -294,4 +306,407 @@ base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
|||
return result;
|
||||
}
|
||||
|
||||
FingerprintBadge SetupFingerprintBadge(
|
||||
rpl::lifetime &on,
|
||||
rpl::producer<QByteArray> fingerprint) {
|
||||
struct State {
|
||||
FingerprintBadgeState data;
|
||||
Ui::Animations::Basic animation;
|
||||
Fn<void(crl::time)> update;
|
||||
rpl::event_stream<> repaints;
|
||||
};
|
||||
const auto state = on.make_state<State>();
|
||||
|
||||
state->update = [=](crl::time now) {
|
||||
// speed-up-duration = 2 * one / speed.
|
||||
const auto one = 1.;
|
||||
const auto speedUpDuration = 2 * kCarouselOneDuration;
|
||||
const auto speed0 = one / kCarouselOneDuration;
|
||||
|
||||
auto updated = false;
|
||||
auto animating = false;
|
||||
for (auto &entry : state->data.entries) {
|
||||
if (!entry.time) {
|
||||
continue;
|
||||
}
|
||||
animating = true;
|
||||
if (entry.time >= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updated = true;
|
||||
const auto elapsed = (now - entry.time) * 1.;
|
||||
entry.time = now;
|
||||
|
||||
Assert(!entry.emoji || entry.sliding.size() > 1);
|
||||
const auto slideCount = entry.emoji
|
||||
? (int(entry.sliding.size()) - 1) * one
|
||||
: (kEmojiInCarousel + (elapsed / kCarouselOneDuration));
|
||||
const auto finalPosition = slideCount * one;
|
||||
const auto distance = finalPosition - entry.position;
|
||||
|
||||
const auto accelerate0 = speed0 - entry.speed;
|
||||
const auto decelerate0 = speed0;
|
||||
const auto acceleration0 = speed0 / speedUpDuration;
|
||||
const auto taccelerate0 = accelerate0 / acceleration0;
|
||||
const auto tdecelerate0 = decelerate0 / acceleration0;
|
||||
const auto paccelerate0 = entry.speed * taccelerate0
|
||||
+ acceleration0 * taccelerate0 * taccelerate0 / 2.;
|
||||
const auto pdecelerate0 = 0
|
||||
+ acceleration0 * tdecelerate0 * tdecelerate0 / 2.;
|
||||
const auto ttozero = entry.speed / acceleration0;
|
||||
if (paccelerate0 + pdecelerate0 <= distance) {
|
||||
// We have time to accelerate to speed0,
|
||||
// maybe go some time on speed0 and then decelerate to 0.
|
||||
const auto uaccelerate0 = std::min(taccelerate0, elapsed);
|
||||
const auto left = distance - paccelerate0 - pdecelerate0;
|
||||
const auto tconstant = left / speed0;
|
||||
const auto uconstant = std::min(
|
||||
tconstant,
|
||||
elapsed - uaccelerate0);
|
||||
const auto udecelerate0 = std::min(
|
||||
tdecelerate0,
|
||||
elapsed - uaccelerate0 - uconstant);
|
||||
if (udecelerate0 >= tdecelerate0) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
entry.position += entry.speed * uaccelerate0
|
||||
+ acceleration0 * uaccelerate0 * uaccelerate0 / 2.
|
||||
+ speed0 * uconstant
|
||||
+ speed0 * udecelerate0
|
||||
- acceleration0 * udecelerate0 * udecelerate0 / 2.;
|
||||
entry.speed += acceleration0
|
||||
* (uaccelerate0 - udecelerate0);
|
||||
}
|
||||
} else if (acceleration0 * ttozero * ttozero / 2 <= distance) {
|
||||
// We have time to accelerate at least for some time >= 0,
|
||||
// and then decelerate to 0 to make it to final position.
|
||||
//
|
||||
// peak = entry.speed + acceleration0 * t
|
||||
// tdecelerate = peak / acceleration0
|
||||
// distance = entry.speed * t
|
||||
// + acceleration0 * t * t / 2
|
||||
// + acceleration0 * tdecelerate * tdecelerate / 2
|
||||
const auto det = entry.speed * entry.speed / 2
|
||||
+ distance * acceleration0;
|
||||
const auto t = std::max(
|
||||
(sqrt(det) - entry.speed) / acceleration0,
|
||||
0.);
|
||||
|
||||
const auto taccelerate = t;
|
||||
const auto uaccelerate = std::min(taccelerate, elapsed);
|
||||
const auto tdecelerate = t + (entry.speed / acceleration0);
|
||||
const auto udecelerate = std::min(
|
||||
tdecelerate,
|
||||
elapsed - uaccelerate);
|
||||
if (udecelerate >= tdecelerate) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
const auto topspeed = entry.speed
|
||||
+ acceleration0 * taccelerate;
|
||||
entry.position += entry.speed * uaccelerate
|
||||
+ acceleration0 * uaccelerate * uaccelerate / 2.
|
||||
+ topspeed * udecelerate
|
||||
- acceleration0 * udecelerate * udecelerate / 2.;
|
||||
entry.speed += acceleration0
|
||||
* (uaccelerate - udecelerate);
|
||||
}
|
||||
} else {
|
||||
// We just need to decelerate to 0,
|
||||
// faster than acceleration0.
|
||||
Assert(entry.speed > 0);
|
||||
const auto tdecelerate = 2 * distance / entry.speed;
|
||||
const auto udecelerate = std::min(tdecelerate, elapsed);
|
||||
if (udecelerate >= tdecelerate) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
const auto a = entry.speed / tdecelerate;
|
||||
entry.position += entry.speed * udecelerate
|
||||
- a * udecelerate * udecelerate / 2;
|
||||
entry.speed -= a * udecelerate;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.position >= kEmojiInCarousel) {
|
||||
entry.position -= qFloor(entry.position / kEmojiInCarousel)
|
||||
* kEmojiInCarousel;
|
||||
}
|
||||
while (entry.position >= 1.) {
|
||||
Assert(!entry.sliding.empty());
|
||||
entry.position -= 1.;
|
||||
entry.sliding.erase(begin(entry.sliding));
|
||||
if (entry.emoji && entry.sliding.size() < 2) {
|
||||
entry = { .emoji = entry.emoji };
|
||||
break;
|
||||
} else if (entry.sliding.empty()) {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
}
|
||||
if (!entry.emoji
|
||||
&& entry.position > 0.
|
||||
&& entry.sliding.size() < 2) {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
}
|
||||
if (!animating) {
|
||||
state->animation.stop();
|
||||
} else if (updated) {
|
||||
state->repaints.fire({});
|
||||
}
|
||||
};
|
||||
state->animation.init(state->update);
|
||||
state->data.entries.resize(kEmojiInFingerprint);
|
||||
|
||||
const auto fillCarousel = [=](
|
||||
int index,
|
||||
base::BufferedRandom<uint32> &buffered) {
|
||||
auto &entry = state->data.entries[index];
|
||||
auto indices = std::vector<int>();
|
||||
indices.reserve(kEmojiInCarousel);
|
||||
auto count = kEmojiCount;
|
||||
for (auto i = 0; i != kEmojiInCarousel; ++i, --count) {
|
||||
auto index = base::RandomIndex(count, buffered);
|
||||
for (const auto &already : indices) {
|
||||
if (index >= already) {
|
||||
++index;
|
||||
}
|
||||
}
|
||||
indices.push_back(index);
|
||||
}
|
||||
|
||||
entry.carousel.clear();
|
||||
entry.carousel.reserve(kEmojiInCarousel);
|
||||
for (const auto index : indices) {
|
||||
entry.carousel.push_back(EmojiByIndex(index));
|
||||
}
|
||||
};
|
||||
|
||||
const auto startTo = [=](
|
||||
int index,
|
||||
EmojiPtr emoji,
|
||||
crl::time now,
|
||||
base::BufferedRandom<uint32> &buffered) {
|
||||
auto &entry = state->data.entries[index];
|
||||
if ((entry.emoji == emoji) && (emoji || entry.time)) {
|
||||
return;
|
||||
} else if (!entry.time) {
|
||||
Assert(entry.sliding.empty());
|
||||
|
||||
if (entry.emoji) {
|
||||
entry.sliding.push_back(entry.emoji);
|
||||
} else if (emoji) {
|
||||
// Just initialize if we get emoji right from the start.
|
||||
entry.emoji = emoji;
|
||||
return;
|
||||
}
|
||||
entry.time = now + index * kStartTimeShift;
|
||||
|
||||
fillCarousel(index, buffered);
|
||||
}
|
||||
entry.emoji = emoji;
|
||||
if (entry.emoji) {
|
||||
entry.sliding.push_back(entry.emoji);
|
||||
} else {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
};
|
||||
|
||||
std::move(
|
||||
fingerprint
|
||||
) | rpl::start_with_next([=](const QByteArray &fingerprint) {
|
||||
auto buffered = base::BufferedRandom<uint32>(
|
||||
kEmojiInCarousel * kEmojiInFingerprint);
|
||||
const auto now = crl::now();
|
||||
const auto emoji = (fingerprint.size() >= 32)
|
||||
? ComputeEmojiFingerprint(
|
||||
bytes::make_span(fingerprint).subspan(0, 32))
|
||||
: std::vector<EmojiPtr>();
|
||||
state->update(now);
|
||||
|
||||
if (emoji.size() == kEmojiInFingerprint) {
|
||||
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
|
||||
startTo(i, emoji[i], now, buffered);
|
||||
}
|
||||
} else {
|
||||
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
|
||||
startTo(i, nullptr, now, buffered);
|
||||
}
|
||||
}
|
||||
if (!state->animation.animating()) {
|
||||
state->animation.start();
|
||||
}
|
||||
}, on);
|
||||
|
||||
return { .state = &state->data, .repaints = state->repaints.events() };
|
||||
}
|
||||
|
||||
void SetupFingerprintBadgeWidget(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<const FingerprintBadgeState*> state,
|
||||
rpl::producer<> repaints) {
|
||||
auto &lifetime = widget->lifetime();
|
||||
|
||||
const auto button = Ui::CreateChild<Ui::AbstractButton>(widget);
|
||||
button->show();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
QString(),
|
||||
st::confcallFingerprintText);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
label->show();
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto esize = Ui::Emoji::GetSizeNormal();
|
||||
const auto size = esize / ratio;
|
||||
widget->widthValue() | rpl::start_with_next([=](int width) {
|
||||
static_assert(!(kEmojiInFingerprint % 2));
|
||||
|
||||
const auto available = width
|
||||
- st::confcallFingerprintMargins.left()
|
||||
- st::confcallFingerprintMargins.right()
|
||||
- (kEmojiInFingerprint * size)
|
||||
- (kEmojiInFingerprint - 2) * st::confcallFingerprintSkip
|
||||
- st::confcallFingerprintTextMargins.left()
|
||||
- st::confcallFingerprintTextMargins.right();
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
label->setText(tr::lng_confcall_e2e_badge(tr::now));
|
||||
if (label->textMaxWidth() > available) {
|
||||
label->setText(tr::lng_confcall_e2e_badge_small(tr::now));
|
||||
}
|
||||
const auto use = std::min(available, label->textMaxWidth());
|
||||
label->resizeToWidth(use);
|
||||
|
||||
const auto ontheleft = kEmojiInFingerprint / 2;
|
||||
const auto ontheside = ontheleft * size
|
||||
+ (ontheleft - 1) * st::confcallFingerprintSkip;
|
||||
const auto text = QRect(
|
||||
(width - use) / 2,
|
||||
(st::confcallFingerprintMargins.top()
|
||||
+ st::confcallFingerprintTextMargins.top()),
|
||||
use,
|
||||
label->height());
|
||||
const auto textOuter = text.marginsAdded(
|
||||
st::confcallFingerprintTextMargins);
|
||||
const auto withEmoji = QRect(
|
||||
textOuter.x() - ontheside,
|
||||
textOuter.y(),
|
||||
textOuter.width() + ontheside * 2,
|
||||
size);
|
||||
const auto outer = withEmoji.marginsAdded(
|
||||
st::confcallFingerprintMargins);
|
||||
|
||||
button->setGeometry(outer);
|
||||
label->moveToLeft(text.x() - outer.x(), text.y() - outer.y(), width);
|
||||
|
||||
widget->resize(
|
||||
width,
|
||||
button->height() + st::confcallFingerprintBottomSkip);
|
||||
}, lifetime);
|
||||
|
||||
const auto cache = lifetime.make_state<FingerprintBadgeCache>();
|
||||
button->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(button);
|
||||
|
||||
const auto outer = button->rect();
|
||||
const auto radius = outer.height() / 2.;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::groupCallMembersBg);
|
||||
p.drawRoundedRect(outer, radius, radius);
|
||||
p.setClipRect(outer);
|
||||
|
||||
const auto withEmoji = outer.marginsRemoved(
|
||||
st::confcallFingerprintMargins);
|
||||
p.translate(withEmoji.topLeft());
|
||||
|
||||
const auto text = label->geometry();
|
||||
const auto textOuter = text.marginsAdded(
|
||||
st::confcallFingerprintTextMargins);
|
||||
const auto count = int(state->entries.size());
|
||||
cache->entries.resize(count);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
PaintFingerprintEntry(
|
||||
p,
|
||||
state->entries[i],
|
||||
cache->entries[i],
|
||||
esize);
|
||||
if (i + 1 == count / 2) {
|
||||
p.translate(size + textOuter.width(), 0);
|
||||
} else {
|
||||
p.translate(size + st::confcallFingerprintSkip, 0);
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
std::move(repaints) | rpl::start_with_next([=] {
|
||||
button->update();
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
void PaintFingerprintEntry(
|
||||
QPainter &p,
|
||||
const FingerprintBadgeState::Entry &entry,
|
||||
FingerprintBadgeCache::Entry &cache,
|
||||
int esize) {
|
||||
const auto stationary = !entry.time;
|
||||
if (stationary) {
|
||||
Ui::Emoji::Draw(p, entry.emoji, esize, 0, 0);
|
||||
return;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = esize / ratio;
|
||||
const auto add = 4;
|
||||
const auto height = size + 2 * add;
|
||||
const auto validateCache = [&](int index, EmojiPtr e) {
|
||||
if (cache.emoji.size() <= index) {
|
||||
cache.emoji.reserve(entry.carousel.size() + 2);
|
||||
cache.emoji.resize(index + 1);
|
||||
}
|
||||
auto &emoji = cache.emoji[index];
|
||||
if (emoji.ptr != e) {
|
||||
emoji.ptr = e;
|
||||
emoji.image = QImage(
|
||||
QSize(size, height) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
emoji.image.setDevicePixelRatio(ratio);
|
||||
emoji.image.fill(Qt::transparent);
|
||||
auto q = QPainter(&emoji.image);
|
||||
Ui::Emoji::Draw(q, e, esize, 0, add);
|
||||
q.end();
|
||||
|
||||
//emoji.image = Images::Blur(
|
||||
// std::move(emoji.image),
|
||||
// false,
|
||||
// Qt::Vertical);
|
||||
}
|
||||
return &emoji;
|
||||
};
|
||||
auto shift = entry.position * height - add;
|
||||
p.translate(0, shift);
|
||||
for (const auto &e : entry.sliding) {
|
||||
const auto index = [&] {
|
||||
const auto i = ranges::find(entry.carousel, e);
|
||||
if (i != end(entry.carousel)) {
|
||||
return int(i - begin(entry.carousel));
|
||||
}
|
||||
return int(entry.carousel.size())
|
||||
+ ((e == entry.sliding.back()) ? 1 : 0);
|
||||
}();
|
||||
const auto entry = validateCache(index, e);
|
||||
p.drawImage(0, 0, entry->image);
|
||||
p.translate(0, -height);
|
||||
shift -= height;
|
||||
}
|
||||
p.translate(0, -shift);
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -26,4 +26,45 @@ class Call;
|
|||
not_null<QWidget*> parent,
|
||||
not_null<Call*> call);
|
||||
|
||||
struct FingerprintBadgeState {
|
||||
struct Entry {
|
||||
EmojiPtr emoji = nullptr;
|
||||
std::vector<EmojiPtr> sliding;
|
||||
std::vector<EmojiPtr> carousel;
|
||||
crl::time time = 0;
|
||||
float64 speed = 0.;
|
||||
float64 position = 0.;
|
||||
int added = 0;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
};
|
||||
struct FingerprintBadge {
|
||||
not_null<const FingerprintBadgeState*> state;
|
||||
rpl::producer<> repaints;
|
||||
};
|
||||
FingerprintBadge SetupFingerprintBadge(
|
||||
rpl::lifetime &on,
|
||||
rpl::producer<QByteArray> fingerprint);
|
||||
|
||||
void SetupFingerprintBadgeWidget(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<const FingerprintBadgeState*> state,
|
||||
rpl::producer<> repaints);
|
||||
|
||||
struct FingerprintBadgeCache {
|
||||
struct Emoji {
|
||||
EmojiPtr ptr = nullptr;
|
||||
QImage image;
|
||||
};
|
||||
struct Entry {
|
||||
std::vector<Emoji> emoji;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
};
|
||||
void PaintFingerprintEntry(
|
||||
QPainter &p,
|
||||
const FingerprintBadgeState::Entry &entry,
|
||||
FingerprintBadgeCache::Entry &cache,
|
||||
int esize);
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "calls/group/calls_volume_item.h"
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
|
@ -1745,6 +1746,9 @@ Members::Members(
|
|||
, _listController(std::make_unique<Controller>(call, parent, mode))
|
||||
, _layout(_scroll->setOwnedWidget(
|
||||
object_ptr<Ui::VerticalLayout>(_scroll.data())))
|
||||
, _fingerprint(call->conference()
|
||||
? _layout->add(object_ptr<Ui::RpWidget>(_layout.get()))
|
||||
: nullptr)
|
||||
, _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get())))
|
||||
, _viewport(
|
||||
std::make_unique<Viewport>(
|
||||
|
@ -1753,6 +1757,7 @@ Members::Members(
|
|||
backend)) {
|
||||
setupList();
|
||||
setupAddMember(call);
|
||||
setupFingerprint();
|
||||
setContent(_list);
|
||||
setupFakeRoundCorners();
|
||||
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
|
||||
|
@ -1854,6 +1859,8 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
_canInviteByLink = canInviteByLinkByPeer(channel);
|
||||
});
|
||||
|
||||
const auto baseIndex = _layout->count() - 2;
|
||||
|
||||
rpl::combine(
|
||||
_canAddMembers.value(),
|
||||
_canInviteByLink.value(),
|
||||
|
@ -1887,7 +1894,7 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
addMember->resizeToWidth(_layout->width());
|
||||
delete _addMemberButton.current();
|
||||
_addMemberButton = addMember.data();
|
||||
_layout->insert(3, std::move(addMember));
|
||||
_layout->insert(baseIndex, std::move(addMember));
|
||||
if (conference) {
|
||||
auto shareLink = Settings::CreateButtonWithIcon(
|
||||
_layout.get(),
|
||||
|
@ -1901,7 +1908,7 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
shareLink->resizeToWidth(_layout->width());
|
||||
delete _shareLinkButton.current();
|
||||
_shareLinkButton = shareLink.data();
|
||||
_layout->insert(4, std::move(shareLink));
|
||||
_layout->insert(baseIndex + 1, std::move(shareLink));
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
|
@ -2000,6 +2007,23 @@ void Members::setupList() {
|
|||
}, _scroll->lifetime());
|
||||
}
|
||||
|
||||
void Members::setupFingerprint() {
|
||||
if (const auto raw = _fingerprint) {
|
||||
auto badge = SetupFingerprintBadge(
|
||||
raw->lifetime(),
|
||||
_call->emojiHashValue());
|
||||
std::move(badge.repaints) | rpl::start_to_stream(
|
||||
_fingerprintRepaints,
|
||||
raw->lifetime());
|
||||
_fingerprintState = badge.state;
|
||||
|
||||
SetupFingerprintBadgeWidget(
|
||||
raw,
|
||||
_fingerprintState,
|
||||
_fingerprintRepaints.events());
|
||||
}
|
||||
}
|
||||
|
||||
void Members::trackViewportGeometry() {
|
||||
_call->videoEndpointLargeValue(
|
||||
) | rpl::start_with_next([=](const VideoEndpoint &large) {
|
||||
|
|
|
@ -25,6 +25,7 @@ class GroupCall;
|
|||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct FingerprintBadgeState;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
@ -96,6 +97,7 @@ private:
|
|||
void setupAddMember(not_null<GroupCall*> call);
|
||||
void resizeToList();
|
||||
void setupList();
|
||||
void setupFingerprint();
|
||||
void setupFakeRoundCorners();
|
||||
|
||||
void trackViewportGeometry();
|
||||
|
@ -106,6 +108,9 @@ private:
|
|||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
std::unique_ptr<Controller> _listController;
|
||||
not_null<Ui::VerticalLayout*> _layout;
|
||||
const not_null<Ui::RpWidget*> _fingerprint;
|
||||
rpl::event_stream<> _fingerprintRepaints;
|
||||
const FingerprintBadgeState *_fingerprintState = nullptr;
|
||||
const not_null<Ui::RpWidget*> _videoWrap;
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
|
||||
|
|
|
@ -426,10 +426,12 @@ HistoryItem::HistoryItem(
|
|||
} else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive)
|
||||
|| (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) {
|
||||
createServiceFromMtp(data);
|
||||
setReactions(data.vreactions());
|
||||
applyTTL(data);
|
||||
} else if (checked == MediaCheckResult::HasStoryMention) {
|
||||
setMedia(*data.vmedia());
|
||||
createServiceFromMtp(data);
|
||||
setReactions(data.vreactions());
|
||||
applyTTL(data);
|
||||
} else {
|
||||
createComponents(data);
|
||||
|
@ -1830,8 +1832,18 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) {
|
|||
auto updatedText = checkedMedia
|
||||
? edition.textWithEntities
|
||||
: EnsureNonEmpty(edition.textWithEntities);
|
||||
auto serviceText = (!checkedMedia
|
||||
&& edition.textWithEntities.empty()
|
||||
&& edition.mtpMedia)
|
||||
? prepareServiceTextForMessage(
|
||||
*edition.mtpMedia,
|
||||
edition.isMediaUnread)
|
||||
: PreparedServiceText();
|
||||
if (updatingSavedLocalEdit) {
|
||||
Get<HistoryMessageSavedMediaData>()->text = std::move(updatedText);
|
||||
} else if (!serviceText.text.empty()) {
|
||||
setServiceText(std::move(serviceText));
|
||||
addToSharedMediaIndex();
|
||||
} else {
|
||||
setText(std::move(updatedText));
|
||||
addToSharedMediaIndex();
|
||||
|
@ -1920,6 +1932,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) {
|
|||
addToSharedMediaIndex();
|
||||
finishEdition(-1);
|
||||
_flags &= ~MessageFlag::DisplayFromChecked;
|
||||
|
||||
updateReactions(message.vreactions());
|
||||
} else if (isService()) {
|
||||
if (const auto reply = Get<HistoryMessageReply>()) {
|
||||
reply->clearData(this);
|
||||
|
@ -1930,6 +1944,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) {
|
|||
applyServiceDateEdition(message);
|
||||
finishEdition(-1);
|
||||
_flags &= ~MessageFlag::DisplayFromChecked;
|
||||
|
||||
updateReactions(message.vreactions());
|
||||
}
|
||||
const auto nowSublist = savedSublist();
|
||||
if (wasSublist && nowSublist != wasSublist) {
|
||||
|
@ -2091,6 +2107,31 @@ void HistoryItem::contributeToSlowmode(TimeId realDate) {
|
|||
}
|
||||
}
|
||||
|
||||
void HistoryItem::clearMediaAsExpired() {
|
||||
const auto media = this->media();
|
||||
if (!media->ttlSeconds()) {
|
||||
return;
|
||||
}
|
||||
if (const auto document = media->document()) {
|
||||
applyEditionToHistoryCleared();
|
||||
auto text = (document->isVideoFile()
|
||||
? tr::lng_ttl_video_expired
|
||||
: document->isVoiceMessage()
|
||||
? tr::lng_ttl_voice_expired
|
||||
: document->isVideoMessage()
|
||||
? tr::lng_ttl_round_expired
|
||||
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
||||
updateServiceText({ std::move(text) });
|
||||
_flags |= MessageFlag::ReactionsAllowed;
|
||||
} else if (media->photo()) {
|
||||
applyEditionToHistoryCleared();
|
||||
updateServiceText({
|
||||
tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
_flags |= MessageFlag::ReactionsAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) {
|
||||
if (!isRegular()) {
|
||||
return;
|
||||
|
@ -4169,9 +4210,86 @@ void HistoryItem::refreshSentMedia(const MTPMessageMedia *media) {
|
|||
}
|
||||
}
|
||||
|
||||
PreparedServiceText HistoryItem::prepareServiceTextForMessage(
|
||||
const MTPMessageMedia &media,
|
||||
bool unread) {
|
||||
return media.match([&](const MTPDmessageMediaStory &data) {
|
||||
return prepareStoryMentionText();
|
||||
}, [&](const MTPDmessageMediaPhoto &data) -> PreparedServiceText {
|
||||
if (unread) {
|
||||
const auto ttl = data.vttl_seconds();
|
||||
Assert(ttl != nullptr);
|
||||
|
||||
if (out()) {
|
||||
return {
|
||||
tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
} else {
|
||||
auto result = PreparedServiceText();
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_ttl_photo_received(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
}
|
||||
}, [&](const MTPDmessageMediaDocument &data) -> PreparedServiceText {
|
||||
if (unread) {
|
||||
const auto ttl = data.vttl_seconds();
|
||||
Assert(ttl != nullptr);
|
||||
|
||||
if (data.is_video()) {
|
||||
if (out()) {
|
||||
return {
|
||||
tr::lng_ttl_video_sent(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities)
|
||||
};
|
||||
} else {
|
||||
auto result = PreparedServiceText();
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_ttl_video_received(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
}
|
||||
} else if (out()) {
|
||||
auto text = (data.is_voice()
|
||||
? tr::lng_ttl_voice_sent
|
||||
: data.is_round()
|
||||
? tr::lng_ttl_round_sent
|
||||
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
||||
return { std::move(text) };
|
||||
}
|
||||
return {};
|
||||
} else {
|
||||
auto text = (data.is_video()
|
||||
? tr::lng_ttl_video_expired
|
||||
: data.is_voice()
|
||||
? tr::lng_ttl_voice_expired
|
||||
: data.is_round()
|
||||
? tr::lng_ttl_round_expired
|
||||
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
||||
return { std::move(text) };
|
||||
}
|
||||
}, [](const auto &) {
|
||||
return PreparedServiceText();
|
||||
});
|
||||
}
|
||||
|
||||
void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
|
||||
AddComponents(HistoryServiceData::Bit());
|
||||
|
||||
_flags |= MessageFlag::ReactionsAllowed;
|
||||
|
||||
const auto unread = message.is_media_unread();
|
||||
const auto media = message.vmedia();
|
||||
Assert(media != nullptr);
|
||||
|
@ -4182,24 +4300,6 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
|
|||
Assert(ttl != nullptr);
|
||||
|
||||
setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, *ttl);
|
||||
if (out()) {
|
||||
setServiceText({
|
||||
tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
} else {
|
||||
auto result = PreparedServiceText();
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_ttl_photo_received(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
setServiceText(std::move(result));
|
||||
}
|
||||
} else {
|
||||
setServiceText({
|
||||
tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
}
|
||||
}, [&](const MTPDmessageMediaDocument &data) {
|
||||
if (unread) {
|
||||
|
@ -4210,49 +4310,11 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
|
|||
setSelfDestruct(
|
||||
HistoryServiceSelfDestruct::Type::Video,
|
||||
*ttl);
|
||||
if (out()) {
|
||||
setServiceText({
|
||||
tr::lng_ttl_video_sent(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities)
|
||||
});
|
||||
} else {
|
||||
auto result = PreparedServiceText();
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_ttl_video_received(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
setServiceText(std::move(result));
|
||||
}
|
||||
} else if (out()) {
|
||||
auto text = (data.is_voice()
|
||||
? tr::lng_ttl_voice_sent
|
||||
: data.is_round()
|
||||
? tr::lng_ttl_round_sent
|
||||
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
||||
setServiceText({ std::move(text) });
|
||||
}
|
||||
} else {
|
||||
auto text = (data.is_video()
|
||||
? tr::lng_ttl_video_expired
|
||||
: data.is_voice()
|
||||
? tr::lng_ttl_voice_expired
|
||||
: data.is_round()
|
||||
? tr::lng_ttl_round_expired
|
||||
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
||||
setServiceText({ std::move(text) });
|
||||
}
|
||||
}, [&](const MTPDmessageMediaStory &data) {
|
||||
setServiceText(prepareStoryMentionText());
|
||||
}, [](const auto &) {
|
||||
Unexpected("Media type in HistoryItem::createServiceFromMtp()");
|
||||
});
|
||||
}, [](const auto &) {});
|
||||
|
||||
if (const auto reactions = message.vreactions()) {
|
||||
updateReactions(reactions);
|
||||
}
|
||||
setServiceText(prepareServiceTextForMessage(*media, unread));
|
||||
}
|
||||
|
||||
void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
|
||||
|
|
|
@ -380,6 +380,8 @@ public:
|
|||
void updateReplyMarkup(HistoryMessageMarkupData &&markup);
|
||||
void contributeToSlowmode(TimeId realDate = 0);
|
||||
|
||||
void clearMediaAsExpired();
|
||||
|
||||
void addToUnreadThings(HistoryUnreadThings::AddType type);
|
||||
void destroyHistoryEntry();
|
||||
[[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const;
|
||||
|
@ -670,6 +672,10 @@ private:
|
|||
[[nodiscard]] PreparedServiceText prepareCallScheduledText(
|
||||
TimeId scheduleDate);
|
||||
|
||||
[[nodiscard]] PreparedServiceText prepareServiceTextForMessage(
|
||||
const MTPMessageMedia &media,
|
||||
bool unread);
|
||||
|
||||
void flagSensitiveContent();
|
||||
[[nodiscard]] PeerData *computeDisplayFrom() const;
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ HistoryMessageEdition::HistoryMessageEdition(
|
|||
not_null<Main::Session*> session,
|
||||
const MTPDmessage &message) {
|
||||
isEditHide = message.is_edit_hide();
|
||||
isMediaUnread = message.is_media_unread();
|
||||
editDate = message.vedit_date().value_or(-1);
|
||||
textWithEntities = TextWithEntities{
|
||||
qs(message.vmessage()),
|
||||
|
|
|
@ -20,6 +20,7 @@ struct HistoryMessageEdition {
|
|||
const MTPDmessage &message);
|
||||
|
||||
bool isEditHide = false;
|
||||
bool isMediaUnread = false;
|
||||
int editDate = 0;
|
||||
int views = -1;
|
||||
int forwards = -1;
|
||||
|
|
|
@ -1195,30 +1195,6 @@ void ShowTrialTranscribesToast(int left, TimeId until) {
|
|||
});
|
||||
}
|
||||
|
||||
void ClearMediaAsExpired(not_null<HistoryItem*> item) {
|
||||
if (const auto media = item->media()) {
|
||||
if (!media->ttlSeconds()) {
|
||||
return;
|
||||
}
|
||||
if (const auto document = media->document()) {
|
||||
item->applyEditionToHistoryCleared();
|
||||
auto text = (document->isVideoFile()
|
||||
? tr::lng_ttl_video_expired
|
||||
: document->isVoiceMessage()
|
||||
? tr::lng_ttl_voice_expired
|
||||
: document->isVideoMessage()
|
||||
? tr::lng_ttl_round_expired
|
||||
: tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
|
||||
item->updateServiceText(PreparedServiceText{ std::move(text) });
|
||||
} else if (media->photo()) {
|
||||
item->applyEditionToHistoryCleared();
|
||||
item->updateServiceText(PreparedServiceText{
|
||||
tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ItemsForwardSendersCount(const HistoryItemsList &list) {
|
||||
auto peers = base::flat_set<not_null<PeerData*>>();
|
||||
auto names = base::flat_set<QString>();
|
||||
|
|
|
@ -257,7 +257,5 @@ ClickHandlerPtr JumpToStoryClickHandler(
|
|||
|
||||
void ShowTrialTranscribesToast(int left, TimeId until);
|
||||
|
||||
void ClearMediaAsExpired(not_null<HistoryItem*> item);
|
||||
|
||||
[[nodiscard]] int ItemsForwardSendersCount(const HistoryItemsList &list);
|
||||
[[nodiscard]] int ItemsForwardCaptionsCount(const HistoryItemsList &list);
|
||||
|
|
|
@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "media/audio/media_audio.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "history/history_item_helpers.h" // ClearMediaAsExpired.
|
||||
#include "history/history_item.h"
|
||||
#include "history/history.h"
|
||||
#include "core/click_handler_types.h" // kDocumentFilenameTooltipProperty.
|
||||
#include "history/view/history_view_element.h"
|
||||
|
@ -330,7 +330,7 @@ Document::Document(
|
|||
}
|
||||
if (const auto item = data->message(fullId)) {
|
||||
// Destroys this.
|
||||
ClearMediaAsExpired(item);
|
||||
item->clearMediaAsExpired();
|
||||
}
|
||||
}, *lifetime);
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
|
@ -176,7 +175,7 @@ Gif::Gif(
|
|||
if (!isOut) {
|
||||
if (const auto item = data->message(fullId)) {
|
||||
// Destroys this.
|
||||
ClearMediaAsExpired(item);
|
||||
item->clearMediaAsExpired();
|
||||
}
|
||||
}
|
||||
}, *lifetime);
|
||||
|
|
Loading…
Add table
Reference in a new issue