331 lines
10 KiB
C++
331 lines
10 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "history/view/history_view_send_action.h"
|
|
|
|
#include "data/data_user.h"
|
|
#include "data/data_session.h"
|
|
#include "main/main_session.h"
|
|
#include "history/history.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "ui/effects/animations.h"
|
|
#include "ui/text/text_options.h"
|
|
#include "styles/style_dialogs.h"
|
|
|
|
namespace HistoryView {
|
|
namespace {
|
|
|
|
constexpr auto kStatusShowClientsideTyping = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideRecordVideo = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideUploadVideo = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideRecordVoice = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideUploadVoice = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideRecordRound = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideUploadRound = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideUploadPhoto = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideUploadFile = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideChooseLocation = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideChooseContact = 6 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsidePlayGame = 10 * crl::time(1000);
|
|
constexpr auto kStatusShowClientsideSpeaking = 6 * crl::time(1000);
|
|
|
|
} // namespace
|
|
|
|
SendActionPainter::SendActionPainter(not_null<History*> history)
|
|
: _history(history)
|
|
, _weak(&_history->session())
|
|
, _sendActionText(st::dialogsTextWidthMin) {
|
|
}
|
|
|
|
bool SendActionPainter::updateNeedsAnimating(
|
|
not_null<UserData*> user,
|
|
const MTPSendMessageAction &action) {
|
|
using Type = Api::SendProgressType;
|
|
if (action.type() == mtpc_sendMessageCancelAction) {
|
|
clear(user);
|
|
return false;
|
|
}
|
|
|
|
const auto now = crl::now();
|
|
const auto emplaceAction = [&](
|
|
Type type,
|
|
crl::time duration,
|
|
int progress = 0) {
|
|
_sendActions.emplace_or_assign(user, type, now + duration, progress);
|
|
};
|
|
action.match([&](const MTPDsendMessageTypingAction &) {
|
|
_typing.emplace_or_assign(user, now + kStatusShowClientsideTyping);
|
|
}, [&](const MTPDsendMessageRecordVideoAction &) {
|
|
emplaceAction(Type::RecordVideo, kStatusShowClientsideRecordVideo);
|
|
}, [&](const MTPDsendMessageRecordAudioAction &) {
|
|
emplaceAction(Type::RecordVoice, kStatusShowClientsideRecordVoice);
|
|
}, [&](const MTPDsendMessageRecordRoundAction &) {
|
|
emplaceAction(Type::RecordRound, kStatusShowClientsideRecordRound);
|
|
}, [&](const MTPDsendMessageGeoLocationAction &) {
|
|
emplaceAction(Type::ChooseLocation, kStatusShowClientsideChooseLocation);
|
|
}, [&](const MTPDsendMessageChooseContactAction &) {
|
|
emplaceAction(Type::ChooseContact, kStatusShowClientsideChooseContact);
|
|
}, [&](const MTPDsendMessageUploadVideoAction &data) {
|
|
emplaceAction(
|
|
Type::UploadVideo,
|
|
kStatusShowClientsideUploadVideo,
|
|
data.vprogress().v);
|
|
}, [&](const MTPDsendMessageUploadAudioAction &data) {
|
|
emplaceAction(
|
|
Type::UploadVoice,
|
|
kStatusShowClientsideUploadVoice,
|
|
data.vprogress().v);
|
|
}, [&](const MTPDsendMessageUploadRoundAction &data) {
|
|
emplaceAction(
|
|
Type::UploadRound,
|
|
kStatusShowClientsideUploadRound,
|
|
data.vprogress().v);
|
|
}, [&](const MTPDsendMessageUploadPhotoAction &data) {
|
|
emplaceAction(
|
|
Type::UploadPhoto,
|
|
kStatusShowClientsideUploadPhoto,
|
|
data.vprogress().v);
|
|
}, [&](const MTPDsendMessageUploadDocumentAction &data) {
|
|
emplaceAction(
|
|
Type::UploadFile,
|
|
kStatusShowClientsideUploadFile,
|
|
data.vprogress().v);
|
|
}, [&](const MTPDsendMessageGamePlayAction &) {
|
|
const auto i = _sendActions.find(user);
|
|
if ((i == end(_sendActions))
|
|
|| (i->second.type == Type::PlayGame)
|
|
|| (i->second.until <= now)) {
|
|
emplaceAction(Type::PlayGame, kStatusShowClientsidePlayGame);
|
|
}
|
|
}, [&](const MTPDspeakingInGroupCallAction &) {
|
|
_speaking.emplace_or_assign(
|
|
user,
|
|
now + kStatusShowClientsideSpeaking);
|
|
}, [&](const MTPDsendMessageHistoryImportAction &) {
|
|
}, [&](const MTPDsendMessageCancelAction &) {
|
|
Unexpected("CancelAction here.");
|
|
});
|
|
return updateNeedsAnimating(now, true);
|
|
}
|
|
|
|
bool SendActionPainter::paint(
|
|
Painter &p,
|
|
int x,
|
|
int y,
|
|
int availableWidth,
|
|
int outerWidth,
|
|
style::color color,
|
|
crl::time ms) {
|
|
if (_sendActionAnimation) {
|
|
_sendActionAnimation.paint(
|
|
p,
|
|
color,
|
|
x,
|
|
y + st::normalFont->ascent,
|
|
outerWidth,
|
|
ms);
|
|
auto animationWidth = _sendActionAnimation.width();
|
|
x += animationWidth;
|
|
availableWidth -= animationWidth;
|
|
p.setPen(color);
|
|
_sendActionText.drawElided(p, x, y, availableWidth);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SendActionPainter::paintSpeaking(
|
|
Painter &p,
|
|
int x,
|
|
int y,
|
|
int outerWidth,
|
|
style::color color,
|
|
crl::time ms) {
|
|
if (_speakingAnimation) {
|
|
_speakingAnimation.paint(
|
|
p,
|
|
color,
|
|
x,
|
|
y,
|
|
outerWidth,
|
|
ms);
|
|
} else {
|
|
Ui::SendActionAnimation::PaintSpeakingIdle(
|
|
p,
|
|
color,
|
|
x,
|
|
y,
|
|
outerWidth);
|
|
}
|
|
}
|
|
|
|
bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
|
|
if (!_weak) {
|
|
return false;
|
|
}
|
|
auto sendActionChanged = false;
|
|
auto speakingChanged = false;
|
|
for (auto i = begin(_typing); i != end(_typing);) {
|
|
if (now >= i->second) {
|
|
i = _typing.erase(i);
|
|
sendActionChanged = true;
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
for (auto i = begin(_speaking); i != end(_speaking);) {
|
|
if (now >= i->second) {
|
|
i = _speaking.erase(i);
|
|
speakingChanged = true;
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
for (auto i = begin(_sendActions); i != end(_sendActions);) {
|
|
if (now >= i->second.until) {
|
|
i = _sendActions.erase(i);
|
|
sendActionChanged = true;
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
const auto wasSpeakingAnimation = !!_speakingAnimation;
|
|
if (force || sendActionChanged || speakingChanged) {
|
|
QString newTypingString;
|
|
auto typingCount = _typing.size();
|
|
if (typingCount > 2) {
|
|
newTypingString = tr::lng_many_typing(tr::now, lt_count, typingCount);
|
|
} else if (typingCount > 1) {
|
|
newTypingString = tr::lng_users_typing(
|
|
tr::now,
|
|
lt_user,
|
|
begin(_typing)->first->firstName,
|
|
lt_second_user,
|
|
(end(_typing) - 1)->first->firstName);
|
|
} else if (typingCount) {
|
|
newTypingString = _history->peer->isUser()
|
|
? tr::lng_typing(tr::now)
|
|
: tr::lng_user_typing(
|
|
tr::now,
|
|
lt_user,
|
|
begin(_typing)->first->firstName);
|
|
} else if (!_sendActions.empty()) {
|
|
// Handles all actions except game playing.
|
|
using Type = Api::SendProgressType;
|
|
auto sendActionString = [](Type type, const QString &name) -> QString {
|
|
switch (type) {
|
|
case Type::RecordVideo: return name.isEmpty() ? tr::lng_send_action_record_video(tr::now) : tr::lng_user_action_record_video(tr::now, lt_user, name);
|
|
case Type::UploadVideo: return name.isEmpty() ? tr::lng_send_action_upload_video(tr::now) : tr::lng_user_action_upload_video(tr::now, lt_user, name);
|
|
case Type::RecordVoice: return name.isEmpty() ? tr::lng_send_action_record_audio(tr::now) : tr::lng_user_action_record_audio(tr::now, lt_user, name);
|
|
case Type::UploadVoice: return name.isEmpty() ? tr::lng_send_action_upload_audio(tr::now) : tr::lng_user_action_upload_audio(tr::now, lt_user, name);
|
|
case Type::RecordRound: return name.isEmpty() ? tr::lng_send_action_record_round(tr::now) : tr::lng_user_action_record_round(tr::now, lt_user, name);
|
|
case Type::UploadRound: return name.isEmpty() ? tr::lng_send_action_upload_round(tr::now) : tr::lng_user_action_upload_round(tr::now, lt_user, name);
|
|
case Type::UploadPhoto: return name.isEmpty() ? tr::lng_send_action_upload_photo(tr::now) : tr::lng_user_action_upload_photo(tr::now, lt_user, name);
|
|
case Type::UploadFile: return name.isEmpty() ? tr::lng_send_action_upload_file(tr::now) : tr::lng_user_action_upload_file(tr::now, lt_user, name);
|
|
case Type::ChooseLocation:
|
|
case Type::ChooseContact: return name.isEmpty() ? tr::lng_typing(tr::now) : tr::lng_user_typing(tr::now, lt_user, name);
|
|
default: break;
|
|
};
|
|
return QString();
|
|
};
|
|
for (const auto &[user, action] : _sendActions) {
|
|
newTypingString = sendActionString(
|
|
action.type,
|
|
_history->peer->isUser() ? QString() : user->firstName);
|
|
if (!newTypingString.isEmpty()) {
|
|
_sendActionAnimation.start(action.type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Everyone in sendActions are playing a game.
|
|
if (newTypingString.isEmpty()) {
|
|
int playingCount = _sendActions.size();
|
|
if (playingCount > 2) {
|
|
newTypingString = tr::lng_many_playing_game(
|
|
tr::now,
|
|
lt_count,
|
|
playingCount);
|
|
} else if (playingCount > 1) {
|
|
newTypingString = tr::lng_users_playing_game(
|
|
tr::now,
|
|
lt_user,
|
|
begin(_sendActions)->first->firstName,
|
|
lt_second_user,
|
|
(end(_sendActions) - 1)->first->firstName);
|
|
} else {
|
|
newTypingString = _history->peer->isUser()
|
|
? tr::lng_playing_game(tr::now)
|
|
: tr::lng_user_playing_game(
|
|
tr::now,
|
|
lt_user,
|
|
begin(_sendActions)->first->firstName);
|
|
}
|
|
_sendActionAnimation.start(Type::PlayGame);
|
|
}
|
|
}
|
|
if (typingCount > 0) {
|
|
_sendActionAnimation.start(Api::SendProgressType::Typing);
|
|
} else if (newTypingString.isEmpty()) {
|
|
_sendActionAnimation.tryToFinish();
|
|
}
|
|
if (_sendActionString != newTypingString) {
|
|
_sendActionString = newTypingString;
|
|
_sendActionText.setText(
|
|
st::dialogsTextStyle,
|
|
_sendActionString,
|
|
Ui::NameTextOptions());
|
|
}
|
|
if (_speaking.empty()) {
|
|
_speakingAnimation.tryToFinish();
|
|
} else {
|
|
_speakingAnimation.start(Api::SendProgressType::Speaking);
|
|
}
|
|
} else if (_speaking.empty() && _speakingAnimation) {
|
|
_speakingAnimation.tryToFinish();
|
|
}
|
|
const auto sendActionResult = !_typing.empty() || !_sendActions.empty();
|
|
const auto speakingResult = !_speaking.empty() || wasSpeakingAnimation;
|
|
if (force
|
|
|| sendActionChanged
|
|
|| (sendActionResult && !anim::Disabled())) {
|
|
_history->peer->owner().updateSendActionAnimation({
|
|
_history,
|
|
_sendActionAnimation.width(),
|
|
st::normalFont->height,
|
|
(force || sendActionChanged)
|
|
});
|
|
}
|
|
if (force
|
|
|| speakingChanged
|
|
|| (speakingResult && !anim::Disabled())) {
|
|
_history->peer->owner().updateSpeakingAnimation({
|
|
_history
|
|
});
|
|
}
|
|
return sendActionResult || speakingResult;
|
|
}
|
|
|
|
void SendActionPainter::clear(not_null<UserData*> from) {
|
|
auto updateAtMs = crl::time(0);
|
|
auto i = _typing.find(from);
|
|
if (i != _typing.cend()) {
|
|
updateAtMs = crl::now();
|
|
i->second = updateAtMs;
|
|
}
|
|
auto j = _sendActions.find(from);
|
|
if (j != _sendActions.cend()) {
|
|
if (!updateAtMs) updateAtMs = crl::now();
|
|
j->second.until = updateAtMs;
|
|
}
|
|
if (updateAtMs) {
|
|
updateNeedsAnimating(updateAtMs, true);
|
|
}
|
|
}
|
|
|
|
} // namespace HistoryView
|