tdesktop/Telegram/SourceFiles/history/view/history_view_send_action.cpp

419 lines
13 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_send_action.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "history/history.h"
#include "lang/lang_instance.h" // Instance::supportChoosingStickerReplacement
#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 kStatusShowClientsideChooseSticker = 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())
, _st(st::dialogsTextStyle)
, _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 MTPDsendMessageChooseStickerAction &) {
emplaceAction(
Type::ChooseSticker,
kStatusShowClientsideChooseSticker);
}, [&](const MTPDsendMessageEmojiInteraction &) {
Unexpected("EmojiInteraction here.");
}, [&](const MTPDsendMessageEmojiInteractionSeen &) {
// #TODO interaction
}, [&](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) {
const auto animationWidth = _sendActionAnimation.width();
const auto extraAnimationWidth = _animationLeft
? animationWidth * 2
: 0;
const auto left =
(availableWidth < _animationLeft + extraAnimationWidth)
? 0
: _animationLeft;
_sendActionAnimation.paint(
p,
color,
left + x,
y + st::normalFont->ascent,
outerWidth,
ms);
// availableWidth should be the same
// if an animation is in the middle of text.
if (!left) {
x += animationWidth;
availableWidth -= _animationLeft
? extraAnimationWidth
: 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 animationLeft = 0;
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;
const auto sendActionString = [](
Type type,
const QString &name) -> QString {
switch (type) {
case Type::RecordVideo: return name.isEmpty()
? tr::lng_send_action_record_video({})
: tr::lng_user_action_record_video({}, lt_user, name);
case Type::UploadVideo: return name.isEmpty()
? tr::lng_send_action_upload_video({})
: tr::lng_user_action_upload_video({}, lt_user, name);
case Type::RecordVoice: return name.isEmpty()
? tr::lng_send_action_record_audio({})
: tr::lng_user_action_record_audio({}, lt_user, name);
case Type::UploadVoice: return name.isEmpty()
? tr::lng_send_action_upload_audio({})
: tr::lng_user_action_upload_audio({}, lt_user, name);
case Type::RecordRound: return name.isEmpty()
? tr::lng_send_action_record_round({})
: tr::lng_user_action_record_round({}, lt_user, name);
case Type::UploadRound: return name.isEmpty()
? tr::lng_send_action_upload_round({})
: tr::lng_user_action_upload_round({}, lt_user, name);
case Type::UploadPhoto: return name.isEmpty()
? tr::lng_send_action_upload_photo({})
: tr::lng_user_action_upload_photo({}, lt_user, name);
case Type::UploadFile: return name.isEmpty()
? tr::lng_send_action_upload_file({})
: tr::lng_user_action_upload_file({}, lt_user, name);
case Type::ChooseLocation:
case Type::ChooseContact: return name.isEmpty()
? tr::lng_typing({})
: tr::lng_user_typing({}, lt_user, name);
case Type::ChooseSticker: return name.isEmpty()
? tr::lng_send_action_choose_sticker({})
: tr::lng_user_action_choose_sticker({}, lt_user, name);
default: break;
};
return QString();
};
for (const auto &[user, action] : _sendActions) {
const auto isNamed = !_history->peer->isUser();
newTypingString = sendActionString(
action.type,
isNamed ? user->firstName : QString());
if (!newTypingString.isEmpty()) {
_sendActionAnimation.start(action.type);
// Add an animation to the middle of text.
const auto &lang = Lang::GetInstance();
if (lang.supportChoosingStickerReplacement()
&& (action.type == Type::ChooseSticker)) {
const auto index = newTypingString.size()
- lang.rightIndexChoosingStickerReplacement(
isNamed);
animationLeft = Ui::Text::String(
_st,
newTypingString.mid(0, index)).maxWidth();
if (!_spacesCount) {
// We have to use QFontMetricsF instead of
// FontData::spacew for more precise calculation.
const auto mf = QFontMetricsF(_st.font->f);
_spacesCount = base::SafeRound(
_sendActionAnimation.widthNoMargins()
/ mf.horizontalAdvance(' '));
}
newTypingString = newTypingString.replace(
index,
Lang::kChoosingStickerReplacement.utf8().size(),
QString().fill(' ', _spacesCount).constData(),
_spacesCount);
}
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 (_animationLeft != animationLeft) {
_animationLeft = animationLeft;
}
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())) {
const auto height = std::max(
st::normalFont->height,
st::dialogsMiniPreviewTop + st::dialogsMiniPreview);
_history->peer->owner().sendActionManager().updateAnimation({
_history,
0,
_sendActionAnimation.width() + _animationLeft,
height,
(force || sendActionChanged)
});
}
if (force
|| speakingChanged
|| (speakingResult && !anim::Disabled())) {
_history->peer->owner().sendActionManager().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