Show send action animations in Replies thread.

This commit is contained in:
John Preston 2020-09-29 11:36:30 +03:00
parent 433c147dd0
commit e8df47c926
20 changed files with 597 additions and 332 deletions

View File

@ -596,6 +596,8 @@ PRIVATE
history/view/history_view_schedule_box.h
history/view/history_view_scheduled_section.cpp
history/view/history_view_scheduled_section.h
history/view/history_view_send_action.cpp
history/view/history_view_send_action.h
history/view/history_view_service_message.cpp
history/view/history_view_service_message.h
history/view/history_view_top_bar_widget.cpp

View File

@ -1535,7 +1535,12 @@ void Updates::feedUpdate(const MTPUpdate &update) {
const auto user = session().data().userLoaded(d.vuser_id().v);
if (history && user) {
const auto when = requestingDifference() ? 0 : base::unixtime::now();
session().data().registerSendAction(history, user, d.vaction(), when);
session().data().registerSendAction(
history,
MsgId(),
user,
d.vaction(),
when);
}
} break;
@ -1548,7 +1553,12 @@ void Updates::feedUpdate(const MTPUpdate &update) {
: session().data().userLoaded(d.vuser_id().v);
if (history && user) {
const auto when = requestingDifference() ? 0 : base::unixtime::now();
session().data().registerSendAction(history, user, d.vaction(), when);
session().data().registerSendAction(
history,
MsgId(),
user,
d.vaction(),
when);
}
} break;
@ -1559,9 +1569,17 @@ void Updates::feedUpdate(const MTPUpdate &update) {
const auto user = (d.vuser_id().v == session().userId())
? nullptr
: session().data().userLoaded(d.vuser_id().v);
if (history && user && !d.vtop_msg_id().value_or_empty()) {
const auto when = requestingDifference() ? 0 : base::unixtime::now();
session().data().registerSendAction(history, user, d.vaction(), when);
if (history && user) {
const auto when = requestingDifference()
? 0
: base::unixtime::now();
const auto rootId = d.vtop_msg_id().value_or_empty();
session().data().registerSendAction(
history,
rootId,
user,
d.vaction(),
when);
}
} break;

View File

@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_components.h"
#include "history/view/media/history_view_media.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_send_action.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "storage/storage_account.h"
#include "storage/storage_encrypted_file.h"
@ -868,25 +869,97 @@ void Session::cancelForwarding(not_null<History*> history) {
Data::HistoryUpdate::Flag::ForwardDraft);
}
HistoryView::SendActionPainter *Session::lookupSendActionPainter(
not_null<History*> history,
MsgId rootId) {
if (!rootId) {
return history->sendActionPainter();
}
const auto i = _sendActionPainters.find(history);
if (i == end(_sendActionPainters)) {
return nullptr;
}
const auto j = i->second.find(rootId);
return (j == end(i->second)) ? nullptr : j->second.lock().get();
}
void Session::registerSendAction(
not_null<History*> history,
MsgId rootId,
not_null<UserData*> user,
const MTPSendMessageAction &action,
TimeId when) {
if (history->updateSendActionNeedsAnimating(user, action)) {
if (history->peer->isSelf()) {
return;
}
const auto sendAction = lookupSendActionPainter(history, rootId);
if (!sendAction) {
return;
}
if (sendAction->updateNeedsAnimating(user, action)) {
user->madeAction(when);
const auto i = _sendActions.find(history);
if (!_sendActions.contains(history)) {
_sendActions.emplace(history, crl::now());
const auto i = _sendActions.find(std::pair{ history, rootId });
if (!_sendActions.contains(std::pair{ history, rootId })) {
_sendActions.emplace(std::pair{ history, rootId }, crl::now());
_sendActionsAnimation.start();
}
}
}
auto Session::repliesSendActionPainter(
not_null<History*> history,
MsgId rootId)
-> std::shared_ptr<SendActionPainter> {
auto &weak = _sendActionPainters[history][rootId];
if (auto strong = weak.lock()) {
return strong;
}
auto result = std::make_shared<SendActionPainter>(history);
weak = result;
return result;
}
void Session::repliesSendActionPainterRemoved(
not_null<History*> history,
MsgId rootId) {
const auto i = _sendActionPainters.find(history);
if (i == end(_sendActionPainters)) {
return;
}
const auto j = i->second.find(rootId);
if (j == end(i->second) || j->second.lock()) {
return;
}
i->second.erase(j);
if (i->second.empty()) {
_sendActionPainters.erase(i);
}
}
void Session::repliesSendActionPaintersClear(
not_null<History*> history,
not_null<UserData*> user) {
auto &map = _sendActionPainters[history];
for (auto i = map.begin(); i != map.end();) {
if (auto strong = i->second.lock()) {
strong->clear(user);
++i;
} else {
i = map.erase(i);
}
}
if (map.empty()) {
_sendActionPainters.erase(history);
}
}
bool Session::sendActionsAnimationCallback(crl::time now) {
for (auto i = begin(_sendActions); i != end(_sendActions);) {
if (i->first->updateSendActionNeedsAnimating(now)) {
const auto sendAction = lookupSendActionPainter(
i->first.first,
i->first.second);
if (sendAction->updateNeedsAnimating(now)) {
++i;
} else {
i = _sendActions.erase(i);

View File

@ -31,6 +31,7 @@ namespace HistoryView {
struct Group;
class Element;
class ElementDelegate;
class SendActionPainter;
} // namespace HistoryView
namespace Main {
@ -170,6 +171,7 @@ public:
void registerSendAction(
not_null<History*> history,
MsgId rootId,
not_null<UserData*> user,
const MTPSendMessageAction &action,
TimeId when);
@ -377,6 +379,17 @@ public:
-> rpl::producer<SendActionAnimationUpdate>;
void updateSendActionAnimation(SendActionAnimationUpdate &&update);
using SendActionPainter = HistoryView::SendActionPainter;
[[nodiscard]] std::shared_ptr<SendActionPainter> repliesSendActionPainter(
not_null<History*> history,
MsgId rootId);
void repliesSendActionPainterRemoved(
not_null<History*> history,
MsgId rootId);
void repliesSendActionPaintersClear(
not_null<History*> history,
not_null<UserData*> user);
[[nodiscard]] int unreadBadge() const;
[[nodiscard]] bool unreadBadgeMuted() const;
[[nodiscard]] int unreadBadgeIgnoreOne(const Dialogs::Key &key) const;
@ -759,6 +772,9 @@ private:
TimeId date);
bool sendActionsAnimationCallback(crl::time now);
[[nodiscard]] SendActionPainter *lookupSendActionPainter(
not_null<History*> history,
MsgId rootId);
void setWallpapers(const QVector<MTPWallPaper> &data, int32 hash);
@ -819,7 +835,9 @@ private:
std::vector<FullMsgId> _selfDestructItems;
// When typing in this history started.
base::flat_map<not_null<History*>, crl::time> _sendActions;
base::flat_map<
std::pair<not_null<History*>, MsgId>,
crl::time> _sendActions;
Ui::Animations::Basic _sendActionsAnimation;
std::unordered_map<
@ -920,6 +938,11 @@ private:
std::unique_ptr<Streaming> _streaming;
std::unique_ptr<MediaRotation> _mediaRotation;
std::unique_ptr<Histories> _histories;
base::flat_map<
not_null<History*>,
base::flat_map<
MsgId,
std::weak_ptr<SendActionPainter>>> _sendActionPainters;
std::unique_ptr<Stickers> _stickers;
MsgId _nonHistoryEntryId = ServerMaxMsgId;

View File

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "support/support_helper.h"
#include "main/main_session.h"
#include "history/view/history_view_send_action.h"
#include "history/history_item_components.h"
#include "history/history_item.h"
#include "history/history.h"
@ -362,7 +363,7 @@ void paintRow(
p.setFont(st::dialogsTextFont);
auto &color = active ? st::dialogsTextFgServiceActive : (selected ? st::dialogsTextFgServiceOver : st::dialogsTextFgService);
if (history && !history->paintSendAction(p, nameleft, texttop, availableWidth, fullWidth, color, ms)) {
if (history && !history->sendActionPainter()->paint(p, nameleft, texttop, availableWidth, fullWidth, color, ms)) {
if (history->cloudDraftTextCache.isEmpty()) {
auto draftWrapped = textcmdLink(1, tr::lng_dialogs_text_from_wrapped(tr::now, lt_from, tr::lng_from_draft(tr::now)));
auto draftText = supportMode
@ -389,7 +390,7 @@ void paintRow(
auto &color = active ? st::dialogsTextFgServiceActive : (selected ? st::dialogsTextFgServiceOver : st::dialogsTextFgService);
p.setFont(st::dialogsTextFont);
if (history && !history->paintSendAction(p, nameleft, texttop, availableWidth, fullWidth, color, ms)) {
if (history && !history->sendActionPainter()->paint(p, nameleft, texttop, availableWidth, fullWidth, color, ms)) {
// Empty history
}
} else if (!item->isEmpty()) {
@ -741,7 +742,7 @@ void RowPainter::paint(
texttop,
availableWidth,
st::dialogsTextFont->height);
const auto actionWasPainted = history ? history->paintSendAction(
const auto actionWasPainted = history ? history->sendActionPainter()->paint(
p,
itemRect.x(),
itemRect.y(),

View File

@ -524,7 +524,8 @@ void Widget::refreshFolderTopBar() {
}
_folderTopBar->setActiveChat(
_openedFolder,
HistoryView::TopBarWidget::Section::History);
HistoryView::TopBarWidget::Section::History,
nullptr);
} else {
_folderTopBar.destroy();
}

View File

@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/history.h"
#include "api/api_send_progress.h"
#include "history/view/history_view_element.h"
#include "history/history_message.h"
#include "history/history_service.h"
@ -50,18 +49,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kStatusShowClientsideTyping = 6000;
constexpr auto kStatusShowClientsideRecordVideo = 6000;
constexpr auto kStatusShowClientsideUploadVideo = 6000;
constexpr auto kStatusShowClientsideRecordVoice = 6000;
constexpr auto kStatusShowClientsideUploadVoice = 6000;
constexpr auto kStatusShowClientsideRecordRound = 6000;
constexpr auto kStatusShowClientsideUploadRound = 6000;
constexpr auto kStatusShowClientsideUploadPhoto = 6000;
constexpr auto kStatusShowClientsideUploadFile = 6000;
constexpr auto kStatusShowClientsideChooseLocation = 6000;
constexpr auto kStatusShowClientsideChooseContact = 6000;
constexpr auto kStatusShowClientsidePlayGame = 10000;
constexpr auto kNewBlockEachMessage = 50;
constexpr auto kSkipCloudDraftsFor = TimeId(3);
@ -74,7 +61,7 @@ History::History(not_null<Data::Session*> owner, PeerId peerId)
, peer(owner->peer(peerId))
, cloudDraftTextCache(st::dialogsTextWidthMin)
, _mute(owner->notifyIsMuted(peer))
, _sendActionText(st::dialogsTextWidthMin) {
, _sendActionPainter(this) {
if (const auto user = peer->asUser()) {
if (user->isBot()) {
_outboxReadBefore = std::numeric_limits<MsgId>::max();
@ -350,219 +337,6 @@ void History::setForwardDraft(MessageIdsList &&items) {
_forwardDraft = std::move(items);
}
bool History::updateSendActionNeedsAnimating(
not_null<UserData*> user,
const MTPSendMessageAction &action) {
if (peer->isSelf()) {
return false;
}
using Type = Api::SendProgressType;
if (action.type() == mtpc_sendMessageCancelAction) {
clearSendAction(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 MTPDsendMessageCancelAction &) {
Unexpected("CancelAction here.");
});
return updateSendActionNeedsAnimating(now, true);
}
bool History::paintSendAction(
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;
}
bool History::updateSendActionNeedsAnimating(crl::time now, bool force) {
auto changed = force;
for (auto i = begin(_typing); i != end(_typing);) {
if (now >= i->second) {
i = _typing.erase(i);
changed = true;
} else {
++i;
}
}
for (auto i = begin(_sendActions); i != end(_sendActions);) {
if (now >= i->second.until) {
i = _sendActions.erase(i);
changed = true;
} else {
++i;
}
}
if (changed) {
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 = 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,
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 = 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.stop();
}
if (_sendActionString != newTypingString) {
_sendActionString = newTypingString;
_sendActionText.setText(
st::dialogsTextStyle,
_sendActionString,
Ui::NameTextOptions());
}
}
const auto result = (!_typing.empty() || !_sendActions.empty());
if (changed || (result && !anim::Disabled())) {
owner().updateSendActionAnimation({
this,
_sendActionAnimation.width(),
st::normalFont->height,
changed
});
}
return result;
}
HistoryItem *History::createItem(
const MTPMessage &message,
MTPDmessage_ClientFlags clientFlags,
@ -1229,23 +1003,6 @@ void History::applyServiceChanges(
}
}
void History::clearSendAction(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) {
updateSendActionNeedsAnimating(updateAtMs, true);
}
}
void History::mainViewRemoved(
not_null<HistoryBlock*> block,
not_null<HistoryView::Element*> view) {
@ -1266,7 +1023,8 @@ void History::newItemAdded(not_null<HistoryItem*> item) {
item->indexAsNewItem();
if (const auto from = item->from() ? item->from()->asUser() : nullptr) {
if (from == item->author()) {
clearSendAction(from);
_sendActionPainter.clear(from);
owner().repliesSendActionPaintersClear(this, from);
}
from->madeAction(item->date());
}

View File

@ -10,7 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_types.h"
#include "data/data_peer.h"
#include "dialogs/dialogs_entry.h"
#include "ui/effects/send_action_animations.h"
#include "history/view/history_view_send_action.h"
#include "base/observer.h"
#include "base/timer.h"
#include "base/variant.h"
@ -23,11 +23,6 @@ class HistoryItem;
class HistoryMessage;
class HistoryService;
namespace Api {
enum class SendProgressType;
struct SendProgress;
} // namespace Api
namespace Main {
class Session;
} // namespace Main
@ -278,22 +273,10 @@ public:
bool hasPendingResizedItems() const;
void setHasPendingResizedItems();
bool paintSendAction(
Painter &p,
int x,
int y,
int availableWidth,
int outerWidth,
style::color color,
crl::time now);
// Interface for Histories
bool updateSendActionNeedsAnimating(
crl::time now,
bool force = false);
bool updateSendActionNeedsAnimating(
not_null<UserData*> user,
const MTPSendMessageAction &action);
[[nodiscard]] auto sendActionPainter()
-> not_null<HistoryView::SendActionPainter*> {
return &_sendActionPainter;
}
void clearLastKeyboard();
@ -514,7 +497,6 @@ private:
void addEdgesToSharedMedia();
void addItemsToLists(const std::vector<not_null<HistoryItem*>> &items);
void clearSendAction(not_null<UserData*> from);
bool clearUnreadOnClientSide() const;
bool skipUnreadUpdate() const;
@ -583,11 +565,7 @@ private:
QString _topPromotedMessage;
QString _topPromotedType;
base::flat_map<not_null<UserData*>, crl::time> _typing;
base::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions;
QString _sendActionString;
Ui::Text::String _sendActionText;
Ui::SendActionAnimation _sendActionAnimation;
HistoryView::SendActionPainter _sendActionPainter;
std::deque<not_null<HistoryItem*>> _notifications;

View File

@ -1792,7 +1792,8 @@ void HistoryWidget::showHistory(
_topBar->setActiveChat(
_history,
HistoryView::TopBarWidget::Section::History);
HistoryView::TopBarWidget::Section::History,
_history->sendActionPainter());
updateTopBarSelection();
if (_channel) {
@ -1881,7 +1882,8 @@ void HistoryWidget::showHistory(
} else {
_topBar->setActiveChat(
Dialogs::Key(),
HistoryView::TopBarWidget::Section::History);
HistoryView::TopBarWidget::Section::History,
nullptr);
updateTopBarSelection();
clearFieldText();

View File

@ -501,7 +501,8 @@ ComposeControls::ComposeControls(
tr::lng_message_ph()))
, _header(std::make_unique<FieldHeader>(
_wrap.get(),
&_window->session().data())) {
&_window->session().data()))
, _textUpdateEvents(TextUpdateEvent::SendTyping) {
init();
}
@ -640,13 +641,13 @@ void ComposeControls::clear() {
}
void ComposeControls::setText(const TextWithTags &textWithTags) {
//_textUpdateEvents = events;
_textUpdateEvents = TextUpdateEvents();
_field->setTextWithTags(textWithTags, Ui::InputField::HistoryAction::Clear/*fieldHistoryAction*/);
auto cursor = _field->textCursor();
cursor.movePosition(QTextCursor::End);
_field->setTextCursor(cursor);
//_textUpdateEvents = TextUpdateEvent::SaveDraft
// | TextUpdateEvent::SendTyping;
_textUpdateEvents = /*TextUpdateEvent::SaveDraft
| */TextUpdateEvent::SendTyping;
//previewCancel();
//_previewCancelled = false;
@ -738,7 +739,7 @@ void ComposeControls::initField() {
//Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); });
Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); });
//Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); });
//Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); });
Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); });
InitMessageField(_window, _field);
const auto suggestions = Ui::Emoji::SuggestionsController::Init(
_parent,
@ -747,6 +748,21 @@ void ComposeControls::initField() {
_raiseEmojiSuggestions = [=] { suggestions->raise(); };
}
void ComposeControls::fieldChanged() {
if (/*!_inlineBot
&& */!_header->isEditingMessage()
&& (_textUpdateEvents & TextUpdateEvent::SendTyping)) {
_sendActionUpdates.fire(Api::SendProgress{
Api::SendProgressType::Typing,
crl::now() + 5 * crl::time(1000),
});
}
}
rpl::producer<Api::SendProgress> ComposeControls::sendActionUpdates() const {
return _sendActionUpdates.events();
}
void ComposeControls::initTabbedSelector() {
if (_window->hasTabbedSelectorOwnership()) {
createTabbedPanel();

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "api/api_common.h"
#include "api/api_send_progress.h"
#include "base/unique_qptr.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
@ -93,6 +94,7 @@ public:
[[nodiscard]] rpl::producer<not_null<QKeyEvent*>> keyEvents() const;
[[nodiscard]] auto inlineResultChosen() const
-> rpl::producer<ChatHelpers::TabbedSelector::InlineChosen>;
[[nodiscard]] rpl::producer<Api::SendProgress> sendActionUpdates() const;
using MimeDataHook = Fn<bool(
not_null<const QMimeData*> data,
@ -124,6 +126,13 @@ public:
void hidePanelsAnimated();
private:
enum class TextUpdateEvent {
//SaveDraft = (1 << 0),
SendTyping = (1 << 1),
};
using TextUpdateEvents = base::flags<TextUpdateEvent>;
friend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; };
void init();
void initField();
void initTabbedSelector();
@ -136,6 +145,7 @@ private:
void paintBackground(QRect clip);
void escape();
void fieldChanged();
void toggleTabbedSelectorMode();
void createTabbedPanel();
void setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);
@ -163,8 +173,10 @@ private:
rpl::event_stream<FileChosen> _fileChosen;
rpl::event_stream<PhotoChosen> _photoChosen;
rpl::event_stream<ChatHelpers::TabbedSelector::InlineChosen> _inlineResultChosen;
rpl::event_stream<Api::SendProgress> _sendActionUpdates;
TextWithTags _localSavedText;
TextUpdateEvents _textUpdateEvents;
//bool _recording = false;
//bool _inField = false;

View File

@ -127,6 +127,7 @@ RepliesWidget::RepliesWidget(
, _rootId(rootId)
, _root(lookupRoot())
, _areComments(computeAreComments())
, _sendAction(history->owner().repliesSendActionPainter(history, rootId))
, _topBar(this, controller)
, _topBarShadow(this)
, _composeControls(std::make_unique<ComposeControls>(
@ -141,7 +142,10 @@ RepliesWidget::RepliesWidget(
setupRoot();
setupRootView();
_topBar->setActiveChat(_history, TopBarWidget::Section::Replies);
_topBar->setActiveChat(
_history,
TopBarWidget::Section::Replies,
_sendAction.get());
_topBar->move(0, 0);
_topBar->resizeToWidth(width());
@ -199,6 +203,14 @@ RepliesWidget::RepliesWidget(
_composeControls->replyToMessage(fullId);
}, _inner->lifetime());
_composeControls->sendActionUpdates(
) | rpl::start_with_next([=] {
session().sendProgressManager().update(
_history,
_rootId,
Api::SendProgressType::Typing);
}, lifetime());
_history->session().changes().messageUpdates(
Data::MessageUpdate::Flag::Destroyed
) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
@ -226,6 +238,8 @@ RepliesWidget::~RepliesWidget() {
if (_readRequestTimer.isActive()) {
sendReadTillRequest();
}
base::take(_sendAction);
_history->owner().repliesSendActionPainterRemoved(_history, _rootId);
}
void RepliesWidget::sendReadTillRequest() {
@ -827,6 +841,12 @@ void RepliesWidget::send(Api::SendOptions options) {
session().api().sendMessage(std::move(message));
_composeControls->clear();
session().sendProgressManager().update(
_history,
_rootId,
Api::SendProgressType::Typing,
-1);
//_saveDraftText = true;
//_saveDraftStart = crl::now();
//onDraftSave();

View File

@ -57,6 +57,7 @@ class Element;
class TopBarWidget;
class RepliesMemento;
class ComposeControls;
class SendActionPainter;
class RepliesWidget final
: public Window::SectionWidget
@ -237,6 +238,7 @@ private:
HistoryItem *_root = nullptr;
std::shared_ptr<Data::RepliesList> _replies;
rpl::variable<bool> _areComments = false;
std::shared_ptr<SendActionPainter> _sendAction;
QPointer<ListWidget> _inner;
object_ptr<TopBarWidget> _topBar;
object_ptr<Ui::PlainShadow> _topBarShadow;

View File

@ -104,7 +104,10 @@ ScheduledWidget::ScheduledWidget(
controller,
ComposeControls::Mode::Scheduled))
, _scrollDown(_scroll, st::historyToDown) {
_topBar->setActiveChat(_history, TopBarWidget::Section::Scheduled);
_topBar->setActiveChat(
_history,
TopBarWidget::Section::Scheduled,
nullptr);
_topBar->move(0, 0);
_topBar->resizeToWidth(width());

View File

@ -0,0 +1,267 @@
/*
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 "history/history.h"
#include "lang/lang_keys.h"
#include "ui/effects/animations.h"
#include "ui/text_options.h"
#include "styles/style_dialogs.h"
namespace HistoryView {
namespace {
constexpr auto kStatusShowClientsideTyping = 6000;
constexpr auto kStatusShowClientsideRecordVideo = 6000;
constexpr auto kStatusShowClientsideUploadVideo = 6000;
constexpr auto kStatusShowClientsideRecordVoice = 6000;
constexpr auto kStatusShowClientsideUploadVoice = 6000;
constexpr auto kStatusShowClientsideRecordRound = 6000;
constexpr auto kStatusShowClientsideUploadRound = 6000;
constexpr auto kStatusShowClientsideUploadPhoto = 6000;
constexpr auto kStatusShowClientsideUploadFile = 6000;
constexpr auto kStatusShowClientsideChooseLocation = 6000;
constexpr auto kStatusShowClientsideChooseContact = 6000;
constexpr auto kStatusShowClientsidePlayGame = 10000;
} // namespace
SendActionPainter::SendActionPainter(not_null<History*> history)
: _history(history)
, _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 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;
}
bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
auto changed = force;
for (auto i = begin(_typing); i != end(_typing);) {
if (now >= i->second) {
i = _typing.erase(i);
changed = true;
} else {
++i;
}
}
for (auto i = begin(_sendActions); i != end(_sendActions);) {
if (now >= i->second.until) {
i = _sendActions.erase(i);
changed = true;
} else {
++i;
}
}
if (changed) {
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.stop();
}
if (_sendActionString != newTypingString) {
_sendActionString = newTypingString;
_sendActionText.setText(
st::dialogsTextStyle,
_sendActionString,
Ui::NameTextOptions());
}
}
const auto result = (!_typing.empty() || !_sendActions.empty());
if (changed || (result && !anim::Disabled())) {
_history->peer->owner().updateSendActionAnimation({
_history,
_sendActionAnimation.width(),
st::normalFont->height,
changed
});
}
return result;
}
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

View File

@ -0,0 +1,53 @@
/*
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
*/
#pragma once
#include "ui/effects/send_action_animations.h"
#include "api/api_send_progress.h"
class UserData;
namespace Api {
enum class SendProgressType;
struct SendProgress;
} // namespace Api
namespace HistoryView {
class SendActionPainter final {
public:
explicit SendActionPainter(not_null<History*> history);
bool paint(
Painter &p,
int x,
int y,
int availableWidth,
int outerWidth,
style::color color,
crl::time now);
bool updateNeedsAnimating(
crl::time now,
bool force = false);
bool updateNeedsAnimating(
not_null<UserData*> user,
const MTPSendMessageAction &action);
void clear(not_null<UserData*> from);
private:
const not_null<History*> _history;
base::flat_map<not_null<UserData*>, crl::time> _typing;
base::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions;
QString _sendActionString;
Ui::Text::String _sendActionText;
Ui::SendActionAnimation _sendActionAnimation;
};
} // namespace HistoryView

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <rpl/combine.h>
#include <rpl/combine_previous.h>
#include "history/history.h"
#include "history/view/history_view_send_action.h"
#include "boxes/add_contact_box.h"
#include "boxes/confirm_box.h"
#include "info/info_memento.h"
@ -330,12 +331,9 @@ void TopBarWidget::paintTopBar(Painter &p) {
const auto folder = _activeChat.folder();
if (folder
|| history->peer->sharedMediaInfo()
|| (_section == Section::Scheduled)
|| !_customTitleText.isEmpty()) {
|| (_section == Section::Scheduled)) {
// #TODO feed name emoji.
auto text = !_customTitleText.isEmpty()
? _customTitleText
: (_section == Section::Scheduled)
auto text = (_section == Section::Scheduled)
? ((history && history->peer->isSelf())
? tr::lng_reminder_messages(tr::now)
: tr::lng_scheduled_messages(tr::now))
@ -355,6 +353,28 @@ void TopBarWidget::paintTopBar(Painter &p) {
(height() - st::historySavedFont->height) / 2,
width(),
text);
} else if (_section == Section::Replies) {
p.setPen(st::dialogsNameFg);
p.setFont(st::semiboldFont);
p.drawTextLeft(
nameleft,
nametop,
width(),
tr::lng_manage_discussion_group(tr::now));
p.setFont(st::dialogsTextFont);
if (!paintConnectingState(p, nameleft, statustop, width())
&& !_sendAction->paint(
p,
nameleft,
statustop,
availableWidth,
width(),
st::historyStatusFgTyping,
crl::now())) {
p.setPen(st::historyStatusFg);
p.drawTextLeft(nameleft, statustop, width(), _customTitleText);
}
} else if (const auto history = _activeChat.history()) {
const auto peer = history->peer;
const auto &text = peer->topBarNameText();
@ -382,18 +402,15 @@ void TopBarWidget::paintTopBar(Painter &p) {
namewidth);
p.setFont(st::dialogsTextFont);
if (paintConnectingState(p, nameleft, statustop, width())) {
return;
} else if (history->paintSendAction(
if (!paintConnectingState(p, nameleft, statustop, width())
&& !_sendAction->paint(
p,
nameleft,
statustop,
availableWidth,
availableWidth,
width(),
st::historyStatusFgTyping,
crl::now())) {
return;
} else {
paintStatus(p, nameleft, statustop, availableWidth, width());
}
}
@ -486,12 +503,16 @@ void TopBarWidget::backClicked() {
}
}
void TopBarWidget::setActiveChat(Dialogs::Key chat, Section section) {
void TopBarWidget::setActiveChat(
Dialogs::Key chat,
Section section,
SendActionPainter *sendAction) {
if (_activeChat == chat && _section == section) {
return;
}
_activeChat = chat;
_section = section;
_sendAction = sendAction;
_back->clearState();
update();

View File

@ -32,6 +32,8 @@ class SessionController;
namespace HistoryView {
class SendActionPainter;
class TopBarWidget : public Ui::RpWidget, private base::Subscriber {
public:
struct SelectedState {
@ -62,7 +64,10 @@ public:
}
void setAnimatingMode(bool enabled);
void setActiveChat(Dialogs::Key chat, Section section);
void setActiveChat(
Dialogs::Key chat,
Section section,
SendActionPainter *sendAction);
void setCustomTitle(const QString &title);
rpl::producer<> forwardSelectionRequest() const {
@ -159,6 +164,8 @@ private:
bool _animatingMode = false;
std::unique_ptr<Ui::InfiniteRadialAnimation> _connecting;
SendActionPainter *_sendAction = nullptr;
base::Timer _onlineUpdater;
rpl::event_stream<> _forwardSelection;

View File

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "ui/image/image_location_factory.h"
#include "history/history_item.h"
#include "history/history.h"
#include "core/mime_type.h"
#include "main/main_session.h"
#include "apiwrap.h"
@ -235,11 +236,7 @@ void Uploader::processPhotoProgress(const FullMsgId &newId) {
const auto photo = item->media()
? item->media()->photo()
: nullptr;
session->sendProgressManager().update(
item->history(),
Api::SendProgressType::UploadPhoto,
0);
session->data().requestItemRepaint(item);
sendProgressUpdate(item, Api::SendProgressType::UploadPhoto);
}
}
@ -254,23 +251,14 @@ void Uploader::processDocumentProgress(const FullMsgId &newId) {
const auto progress = (document && document->uploading())
? document->uploadingData->offset
: 0;
session->sendProgressManager().update(
item->history(),
sendAction,
progress);
session->data().requestItemRepaint(item);
sendProgressUpdate(item, sendAction, progress);
}
}
void Uploader::processPhotoFailed(const FullMsgId &newId) {
const auto session = &_api->session();
if (const auto item = session->data().message(newId)) {
session->sendProgressManager().update(
item->history(),
Api::SendProgressType::UploadPhoto,
-1);
session->data().requestItemRepaint(item);
sendProgressUpdate(item, Api::SendProgressType::UploadPhoto, -1);
}
}
@ -282,14 +270,25 @@ void Uploader::processDocumentFailed(const FullMsgId &newId) {
const auto sendAction = (document && document->isVoiceMessage())
? Api::SendProgressType::UploadVoice
: Api::SendProgressType::UploadFile;
session->sendProgressManager().update(
item->history(),
sendAction,
-1);
session->data().requestItemRepaint(item);
sendProgressUpdate(item, sendAction, -1);
}
}
void Uploader::sendProgressUpdate(
not_null<HistoryItem*> item,
Api::SendProgressType type,
int progress) {
const auto history = item->history();
auto &manager = _api->session().sendProgressManager();
manager.update(history, type, progress);
if (const auto replyTo = item->replyToTop()) {
if (history->peer->isMegagroup()) {
manager.update(history, replyTo, type, progress);
}
}
_api->session().data().requestItemRepaint(item);
}
Uploader::~Uploader() {
clear();
}

View File

@ -16,6 +16,10 @@ class ApiWrap;
struct FileLoadResult;
struct SendMediaReady;
namespace Api {
enum class SendProgressType;
} // namespace Api
namespace Main {
class Session;
} // namespace Main
@ -128,7 +132,12 @@ private:
void currentFailed();
not_null<ApiWrap*> _api;
void sendProgressUpdate(
not_null<HistoryItem*> item,
Api::SendProgressType type,
int progress = 0);
const not_null<ApiWrap*> _api;
base::flat_map<mtpRequestId, QByteArray> requestsSent;
base::flat_map<mtpRequestId, int32> docRequestsSent;
base::flat_map<mtpRequestId, int32> dcMap;