Support task lists view/update/actions.

This commit is contained in:
John Preston 2025-06-06 18:24:44 +04:00
parent 06db13a0ab
commit a97d1b8669
27 changed files with 1983 additions and 43 deletions

View file

@ -178,6 +178,8 @@ PRIVATE
api/api_statistics_sender.h
api/api_text_entities.cpp
api/api_text_entities.h
api/api_todo_lists.cpp
api/api_todo_lists.h
api/api_toggling_media.cpp
api/api_toggling_media.h
api/api_transcribes.cpp
@ -649,6 +651,8 @@ PRIVATE
data/data_streaming.h
data/data_thread.cpp
data/data_thread.h
data/data_todo_list.cpp
data/data_todo_list.h
data/data_types.cpp
data/data_types.h
data/data_unread_value.cpp
@ -812,6 +816,8 @@ PRIVATE
history/view/media/history_view_story_mention.h
history/view/media/history_view_theme_document.cpp
history/view/media/history_view_theme_document.h
history/view/media/history_view_todo_list.cpp
history/view/media/history_view_todo_list.h
history/view/media/history_view_unique_gift.cpp
history/view/media/history_view_unique_gift.h
history/view/media/history_view_userpic_suggestion.cpp

View file

@ -2257,8 +2257,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group.";
"lng_action_direct_messages_enabled" = "Channel enabled Direct Messages.";
"lng_action_direct_messages_paid#one" = "Channel allows Direct Messages for {count} Star each.";
"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each";
"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each.";
"lng_action_direct_messages_disabled" = "Channel disabled Direct Messages.";
"lng_action_todo_marked_done" = "{from} marked {tasks} as done.";
"lng_action_todo_marked_done_self" = "You marked {tasks} as done.";
"lng_action_todo_marked_not_done" = "{from} marked {tasks} as not done.";
"lng_action_todo_marked_not_done_self" = "You marked {tasks} as not done.";
"lng_action_todo_added" = "{from} added {tasks} to the list.";
"lng_action_todo_added_self" = "You added {tasks} to the list.";
"lng_action_todo_tasks_fallback#one" = "task";
"lng_action_todo_tasks_fallback#other" = "{count} tasks";
"lng_action_todo_tasks_and_one" = "{tasks}, {task}";
"lng_action_todo_tasks_and_last" = "{tasks} and {task}";
"lng_you_paid_stars#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars.";
@ -3791,6 +3801,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_in_dlg_sticker_emoji" = "{emoji} Sticker";
"lng_in_dlg_poll" = "Poll";
"lng_in_dlg_story" = "Story";
"lng_in_dlg_todo_list" = "To-Do List";
"lng_in_dlg_story_expired" = "Expired story";
"lng_in_dlg_media_count#one" = "{count} media";
"lng_in_dlg_media_count#other" = "{count} media";
@ -5844,6 +5855,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_show_more#other" = "Show more ({count})";
"lng_polls_votes_collapse" = "Collapse";
"lng_todo_title" = "To-Do List";
"lng_todo_title_group" = "Group To-Do List";
"lng_todo_completed#one" = "{count} of {total} completed";
"lng_todo_completed#other" = "{count} of {total} completed";
"lng_todo_completed_none" = "None of {total} completed";
"lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM.";
"lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM.";
"lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}.";

View file

@ -0,0 +1,204 @@
/*
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 "api/api_todo_lists.h"
//#include "api/api_common.h"
//#include "api/api_updates.h"
#include "apiwrap.h"
//#include "base/random.h"
//#include "data/business/data_shortcut_messages.h"
//#include "data/data_changes.h"
//#include "data/data_histories.h"
#include "data/data_todo_list.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
//#include "history/history_item_helpers.h" // ShouldSendSilent
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kSendTogglesDelay = 3 * crl::time(1000);
[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId) {
return TimeId(msgId >> 32);
}
} // namespace
TodoLists::TodoLists(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance())
, _sendTimer([=] { sendAccumulatedToggles(false); }) {
}
//
//void TodoLists::create(
// const PollData &data,
// SendAction action,
// Fn<void()> done,
// Fn<void()> fail) {
// _session->api().sendAction(action);
//
// const auto history = action.history;
// const auto peer = history->peer;
// const auto topicRootId = action.replyTo.messageId
// ? action.replyTo.topicRootId
// : 0;
// const auto monoforumPeerId = action.replyTo.monoforumPeerId;
// auto sendFlags = MTPmessages_SendMedia::Flags(0);
// if (action.replyTo) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
// }
// const auto clearCloudDraft = action.clearDraft;
// if (clearCloudDraft) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
// history->clearLocalDraft(topicRootId, monoforumPeerId);
// history->clearCloudDraft(topicRootId, monoforumPeerId);
// history->startSavingCloudDraft(topicRootId, monoforumPeerId);
// }
// const auto silentPost = ShouldSendSilent(peer, action.options);
// const auto starsPaid = std::min(
// peer->starsPerMessageChecked(),
// action.options.starsApproved);
// if (silentPost) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
// }
// if (action.options.scheduled) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
// }
// if (action.options.shortcutId) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
// }
// if (action.options.effectId) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
// }
// if (starsPaid) {
// action.options.starsApproved -= starsPaid;
// sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
// }
// const auto sendAs = action.options.sendAs;
// if (sendAs) {
// sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
// }
// auto &histories = history->owner().histories();
// const auto randomId = base::RandomValue<uint64>();
// histories.sendPreparedMessage(
// history,
// action.replyTo,
// randomId,
// Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
// MTP_flags(sendFlags),
// peer->input,
// Data::Histories::ReplyToPlaceholder(),
// PollDataToInputMedia(&data),
// MTP_string(),
// MTP_long(randomId),
// MTPReplyMarkup(),
// MTPVector<MTPMessageEntity>(),
// MTP_int(action.options.scheduled),
// (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
// Data::ShortcutIdToMTP(_session, action.options.shortcutId),
// MTP_long(action.options.effectId),
// MTP_long(starsPaid)
// ), [=](const MTPUpdates &result, const MTP::Response &response) {
// if (clearCloudDraft) {
// history->finishSavingCloudDraft(
// topicRootId,
// monoforumPeerId,
// UnixtimeFromMsgId(response.outerMsgId));
// }
// _session->changes().historyUpdated(
// history,
// (action.options.scheduled
// ? Data::HistoryUpdate::Flag::ScheduledSent
// : Data::HistoryUpdate::Flag::MessageSent));
// done();
// }, [=](const MTP::Error &error, const MTP::Response &response) {
// if (clearCloudDraft) {
// history->finishSavingCloudDraft(
// topicRootId,
// monoforumPeerId,
// UnixtimeFromMsgId(response.outerMsgId));
// }
// fail();
// });
//}
void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) {
auto &entry = _toggles[itemId];
if (completed) {
if (!entry.completed.emplace(id).second) {
return;
}
} else {
if (!entry.incompleted.emplace(id).second) {
return;
}
}
entry.scheduled = crl::now();
if (!entry.requestId && !_sendTimer.isActive()) {
_sendTimer.callOnce(kSendTogglesDelay);
}
}
void TodoLists::sendAccumulatedToggles(bool force) {
const auto now = crl::now();
auto nearest = crl::time(0);
for (auto &[itemId, entry] : _toggles) {
if (entry.requestId) {
continue;
}
const auto wait = entry.scheduled + kSendTogglesDelay - now;
if (wait <= 0) {
entry.scheduled = 0;
send(itemId, entry);
} else if (!nearest || nearest > wait) {
nearest = wait;
}
}
if (nearest > 0) {
_sendTimer.callOnce(nearest);
}
}
void TodoLists::send(FullMsgId itemId, Accumulated &entry) {
const auto item = _session->data().message(itemId);
if (!item) {
return;
}
auto completed = entry.completed
| ranges::views::transform([](int id) { return MTP_int(id); });
auto incompleted = entry.incompleted
| ranges::views::transform([](int id) { return MTP_int(id); });
entry.requestId = _api.request(MTPmessages_ToggleTodoCompleted(
item->history()->peer->input,
MTP_int(item->id),
MTP_vector_from_range(completed),
MTP_vector_from_range(incompleted)
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
finishRequest(itemId);
}).fail([=](const MTP::Error &error) {
finishRequest(itemId);
}).send();
entry.completed.clear();
entry.incompleted.clear();
}
void TodoLists::finishRequest(FullMsgId itemId) {
auto &entry = _toggles[itemId];
entry.requestId = 0;
if (entry.completed.empty() && entry.incompleted.empty()) {
_toggles.remove(itemId);
} else {
sendAccumulatedToggles(false);
}
}
} // namespace Api

View file

@ -0,0 +1,56 @@
/*
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 "base/timer.h"
#include "mtproto/sender.h"
class ApiWrap;
class HistoryItem;
struct TodoListData;
namespace Main {
class Session;
} // namespace Main
namespace Api {
struct SendAction;
class TodoLists final {
public:
explicit TodoLists(not_null<ApiWrap*> api);
//void create(
// const PollData &data,
// SendAction action,
// Fn<void()> done,
// Fn<void()> fail);
void toggleCompletion(FullMsgId itemId, int id, bool completed);
private:
struct Accumulated {
base::flat_set<int> completed;
base::flat_set<int> incompleted;
crl::time scheduled = 0;
mtpRequestId requestId = 0;
};
void sendAccumulatedToggles(bool force);
void send(FullMsgId itemId, Accumulated &entry);
void finishRequest(FullMsgId itemId);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<FullMsgId, Accumulated> _toggles;
base::Timer _sendTimer;
};
} // namespace Api

View file

@ -1916,7 +1916,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
// Update web page anyway.
session().data().processWebpage(d.vwebpage());
session().data().sendWebPageGamePollNotifications();
session().data().sendWebPageGamePollTodoListNotifications();
updateAndApply(d.vpts().v, d.vpts_count().v, update);
} break;
@ -1926,7 +1926,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
// Update web page anyway.
session().data().processWebpage(d.vwebpage());
session().data().sendWebPageGamePollNotifications();
session().data().sendWebPageGamePollTodoListNotifications();
auto channel = session().data().channelLoaded(d.vchannel_id());
if (channel && !_handlingChannelDifference) {

View file

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_polls.h"
#include "api/api_sending.h"
#include "api/api_text_entities.h"
#include "api/api_todo_lists.h"
#include "api/api_self_destruct.h"
#include "api/api_sensitive_content.h"
#include "api/api_global_privacy.h"
@ -178,6 +179,7 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
, _confirmPhone(std::make_unique<Api::ConfirmPhone>(this))
, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
, _polls(std::make_unique<Api::Polls>(this))
, _todoLists(std::make_unique<Api::TodoLists>(this))
, _chatParticipants(std::make_unique<Api::ChatParticipants>(this))
, _unreadThings(std::make_unique<Api::UnreadThings>(this))
, _ringtones(std::make_unique<Api::Ringtones>(this))
@ -2574,7 +2576,10 @@ void ApiWrap::refreshFileReference(
});
}
void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &result, mtpRequestId req) {
void ApiWrap::gotWebPages(
ChannelData *channel,
const MTPmessages_Messages &result,
mtpRequestId req) {
WebPageData::ApplyChanges(_session, channel, result);
for (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) {
if (i->second == req) {
@ -2588,7 +2593,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu
++i;
}
}
_session->data().sendWebPageGamePollNotifications();
_session->data().sendWebPageGamePollTodoListNotifications();
}
void ApiWrap::updateStickers() {
@ -4792,6 +4797,10 @@ Api::Polls &ApiWrap::polls() {
return *_polls;
}
Api::TodoLists &ApiWrap::todoLists() {
return *_todoLists;
}
Api::ChatParticipants &ApiWrap::chatParticipants() {
return *_chatParticipants;
}

View file

@ -77,6 +77,7 @@ class ConfirmPhone;
class PeerPhoto;
class PeerColors;
class Polls;
class TodoLists;
class ChatParticipants;
class UnreadThings;
class Ringtones;
@ -413,6 +414,7 @@ public:
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
[[nodiscard]] Api::PeerPhoto &peerPhoto();
[[nodiscard]] Api::Polls &polls();
[[nodiscard]] Api::TodoLists &todoLists();
[[nodiscard]] Api::ChatParticipants &chatParticipants();
[[nodiscard]] Api::UnreadThings &unreadThings();
[[nodiscard]] Api::Ringtones &ringtones();
@ -764,6 +766,7 @@ private:
const std::unique_ptr<Api::ConfirmPhone> _confirmPhone;
const std::unique_ptr<Api::PeerPhoto> _peerPhoto;
const std::unique_ptr<Api::Polls> _polls;
const std::unique_ptr<Api::TodoLists> _todoLists;
const std::unique_ptr<Api::ChatParticipants> _chatParticipants;
const std::unique_ptr<Api::UnreadThings> _unreadThings;
const std::unique_ptr<Api::Ringtones> _ringtones;

View file

@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_web_page.h"
#include "history/view/media/history_view_poll.h"
#include "history/view/media/history_view_theme_document.h"
#include "history/view/media/history_view_todo_list.h"
#include "history/view/media/history_view_slot_machine.h"
#include "history/view/media/history_view_dice.h"
#include "history/view/media/history_view_service_box.h"
@ -65,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_origin.h"
#include "data/data_stories.h"
#include "data/data_story.h"
#include "data/data_todo_list.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
@ -645,6 +647,10 @@ PollData *Media::poll() const {
return nullptr;
}
TodoListData *Media::todolist() const {
return nullptr;
}
const WallPaper *Media::paper() const {
return nullptr;
}
@ -2315,6 +2321,67 @@ std::unique_ptr<HistoryView::Media> MediaPoll::createView(
return std::make_unique<HistoryView::Poll>(message, _poll);
}
MediaTodoList::MediaTodoList(
not_null<HistoryItem*> parent,
not_null<TodoListData*> todolist)
: Media(parent)
, _todolist(todolist) {
}
MediaTodoList::~MediaTodoList() {
}
std::unique_ptr<Media> MediaTodoList::clone(not_null<HistoryItem*> parent) {
return std::make_unique<MediaTodoList>(parent, _todolist);
}
TodoListData *MediaTodoList::todolist() const {
return _todolist;
}
TextWithEntities MediaTodoList::notificationText() const {
return TextWithEntities()
.append(QChar(0x2611))
.append(QChar(' '))
.append(Ui::Text::Colorized(_todolist->title));
}
QString MediaTodoList::pinnedTextSubstring() const {
return QChar(171) + _todolist->title.text + QChar(187);
}
TextForMimeData MediaTodoList::clipboardText() const {
auto result = TextWithEntities();
result
.append(u"[ "_q)
.append(tr::lng_in_dlg_todo_list(tr::now))
.append(u" : "_q)
.append(_todolist->title)
.append(u" ]"_q);
for (const auto &item : _todolist->items) {
result.append(u"\n- "_q).append(item.text);
}
return TextForMimeData::Rich(std::move(result));
}
bool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) {
return false;
}
bool MediaTodoList::updateSentMedia(const MTPMessageMedia &media) {
return false;
}
std::unique_ptr<HistoryView::Media> MediaTodoList::createView(
not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent,
HistoryView::Element *replacing) {
return std::make_unique<HistoryView::TodoList>(
message,
_todolist,
replacing);
}
MediaDice::MediaDice(not_null<HistoryItem*> parent, QString emoji, int value)
: Media(parent)
, _emoji(emoji)

View file

@ -196,6 +196,7 @@ public:
virtual const GiftCode *gift() const;
virtual CloudImage *location() const;
virtual PollData *poll() const;
virtual TodoListData *todolist() const;
virtual const WallPaper *paper() const;
virtual bool paperForBoth() const;
virtual FullStoryId storyId() const;
@ -610,6 +611,33 @@ private:
};
class MediaTodoList final : public Media {
public:
MediaTodoList(
not_null<HistoryItem*> parent,
not_null<TodoListData*> todolist);
~MediaTodoList();
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
TodoListData *todolist() const override;
TextWithEntities notificationText() const override;
QString pinnedTextSubstring() const override;
TextForMimeData clipboardText() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;
std::unique_ptr<HistoryView::Media> createView(
not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent,
HistoryView::Element *replacing = nullptr) override;
private:
not_null<TodoListData*> _todolist;
};
class MediaDice final : public Media {
public:
MediaDice(not_null<HistoryItem*> parent, QString emoji, int value);

View file

@ -75,6 +75,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_premium_limits.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_todo_list.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "base/call_delayed.h"
@ -1710,6 +1711,16 @@ void Session::requestPollViewRepaint(not_null<const PollData*> poll) {
}
}
void Session::requestTodoListViewRepaint(
not_null<const TodoListData*> todolist) {
if (const auto i = _todoListViews.find(todolist)
; i != _todoListViews.end()) {
for (const auto &view : i->second) {
requestViewResize(view);
}
}
}
void Session::documentLoadProgress(not_null<DocumentData*> document) {
requestDocumentViewRepaint(document);
_documentLoadProgress.fire_copy(document);
@ -4098,6 +4109,39 @@ not_null<PollData*> Session::processPoll(const MTPDmessageMediaPoll &data) {
return result;
}
not_null<TodoListData*> Session::todoList(TodoListId id) {
auto i = _todoLists.find(id);
if (i == _todoLists.cend()) {
i = _todoLists.emplace(
id,
std::make_unique<TodoListData>(this, id)).first;
}
return i->second.get();
}
not_null<TodoListData*> Session::processTodoList(
TodoListId id,
const MTPTodoList &todolist) {
const auto &data = todolist.data();
const auto result = todoList(id);
const auto changed = result->applyChanges(data);
if (changed) {
notifyTodoListUpdateDelayed(result);
}
return result;
}
not_null<TodoListData*> Session::processTodoList(
TodoListId id,
const MTPDmessageMediaToDo &data) {
const auto result = processTodoList(id, data.vtodo());
const auto changed = result->applyCompletions(data.vcompletions());
if (changed) {
notifyTodoListUpdateDelayed(result);
}
return result;
}
void Session::checkPollsClosings() {
const auto now = base::unixtime::now();
auto closest = 0;
@ -4308,6 +4352,24 @@ void Session::unregisterPollView(
}
}
void Session::registerTodoListView(
not_null<const TodoListData*> todolist,
not_null<ViewElement*> view) {
_todoListViews[todolist].insert(view);
}
void Session::unregisterTodoListView(
not_null<const TodoListData*> todolist,
not_null<ViewElement*> view) {
const auto i = _todoListViews.find(todolist);
if (i != _todoListViews.end()) {
auto &items = i->second;
if (items.remove(view) && items.empty()) {
_todoListViews.erase(i);
}
}
}
void Session::registerContactView(
UserId contactId,
not_null<ViewElement*> view) {
@ -4488,37 +4550,54 @@ QString Session::findContactPhone(UserId contactId) const {
return QString();
}
bool Session::hasPendingWebPageGamePollNotification() const {
bool Session::hasPendingWebPageGamePollTodoListNotification() const {
return !_webpagesUpdated.empty()
|| !_gamesUpdated.empty()
|| !_pollsUpdated.empty();
|| !_pollsUpdated.empty()
|| !_todoListsUpdated.empty();
}
void Session::notifyWebPageUpdateDelayed(not_null<WebPageData*> page) {
const auto invoke = !hasPendingWebPageGamePollNotification();
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
_webpagesUpdated.insert(page);
if (invoke) {
crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); });
crl::on_main(_session, [=] {
sendWebPageGamePollTodoListNotifications();
});
}
}
void Session::notifyGameUpdateDelayed(not_null<GameData*> game) {
const auto invoke = !hasPendingWebPageGamePollNotification();
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
_gamesUpdated.insert(game);
if (invoke) {
crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); });
crl::on_main(_session, [=] {
sendWebPageGamePollTodoListNotifications();
});
}
}
void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {
const auto invoke = !hasPendingWebPageGamePollNotification();
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
_pollsUpdated.insert(poll);
if (invoke) {
crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); });
crl::on_main(_session, [=] {
sendWebPageGamePollTodoListNotifications();
});
}
}
void Session::sendWebPageGamePollNotifications() {
void Session::notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist) {
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
_todoListsUpdated.insert(todolist);
if (invoke) {
crl::on_main(_session, [=] {
sendWebPageGamePollTodoListNotifications();
});
}
}
void Session::sendWebPageGamePollTodoListNotifications() {
auto resize = std::vector<not_null<ViewElement*>>();
for (const auto &page : base::take(_webpagesUpdated)) {
_webpageUpdates.fire_copy(page);
@ -4537,6 +4616,12 @@ void Session::sendWebPageGamePollNotifications() {
resize.insert(end(resize), begin(i->second), end(i->second));
}
}
for (const auto &todolist : base::take(_todoListsUpdated)) {
if (const auto i = _todoListViews.find(todolist)
; i != _todoListViews.end()) {
resize.insert(end(resize), begin(i->second), end(i->second));
}
}
for (const auto &view : resize) {
requestViewResize(view);
}

View file

@ -536,6 +536,7 @@ public:
void requestDocumentViewRepaint(not_null<const DocumentData*> document);
void markMediaRead(not_null<const DocumentData*> document);
void requestPollViewRepaint(not_null<const PollData*> poll);
void requestTodoListViewRepaint(not_null<const TodoListData*> todolist);
void photoLoadProgress(not_null<PhotoData*> photo);
void photoLoadDone(not_null<PhotoData*> photo);
@ -690,6 +691,14 @@ public:
not_null<PollData*> processPoll(const MTPPoll &data);
not_null<PollData*> processPoll(const MTPDmessageMediaPoll &data);
[[nodiscard]] not_null<TodoListData*> todoList(TodoListId id);
not_null<TodoListData*> processTodoList(
TodoListId id,
const MTPTodoList &todolist);
not_null<TodoListData*> processTodoList(
TodoListId id,
const MTPDmessageMediaToDo &data);
[[nodiscard]] not_null<CloudImage*> location(
const LocationPoint &point);
@ -729,6 +738,12 @@ public:
void unregisterPollView(
not_null<const PollData*> poll,
not_null<ViewElement*> view);
void registerTodoListView(
not_null<const TodoListData*> todolist,
not_null<ViewElement*> view);
void unregisterTodoListView(
not_null<const TodoListData*> todolist,
not_null<ViewElement*> view);
void registerContactView(
UserId contactId,
not_null<ViewElement*> view);
@ -758,8 +773,9 @@ public:
void notifyWebPageUpdateDelayed(not_null<WebPageData*> page);
void notifyGameUpdateDelayed(not_null<GameData*> game);
void notifyPollUpdateDelayed(not_null<PollData*> poll);
[[nodiscard]] bool hasPendingWebPageGamePollNotification() const;
void sendWebPageGamePollNotifications();
void notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist);
[[nodiscard]] bool hasPendingWebPageGamePollTodoListNotification() const;
void sendWebPageGamePollTodoListNotifications();
[[nodiscard]] rpl::producer<not_null<WebPageData*>> webPageUpdates() const;
void channelDifferenceTooLong(not_null<ChannelData*> channel);
@ -1066,6 +1082,9 @@ private:
std::unordered_map<
PollId,
std::unique_ptr<PollData>> _polls;
std::map<
TodoListId,
std::unique_ptr<TodoListData>> _todoLists;
std::unordered_map<
GameId,
std::unique_ptr<GameData>> _games;
@ -1078,6 +1097,9 @@ private:
std::unordered_map<
not_null<const PollData*>,
base::flat_set<not_null<ViewElement*>>> _pollViews;
std::unordered_map<
not_null<const TodoListData*>,
base::flat_set<not_null<ViewElement*>>> _todoListViews;
std::unordered_map<
UserId,
base::flat_set<not_null<HistoryItem*>>> _contactItems;
@ -1094,6 +1116,7 @@ private:
base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
base::flat_set<not_null<GameData*>> _gamesUpdated;
base::flat_set<not_null<PollData*>> _pollsUpdated;
base::flat_set<not_null<TodoListData*>> _todoListsUpdated;
rpl::event_stream<not_null<WebPageData*>> _webpageUpdates;
rpl::event_stream<not_null<ChannelData*>> _channelDifferenceTooLong;

View file

@ -0,0 +1,232 @@
/*
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 "data/data_todo_list.h"
#include "api/api_text_entities.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "base/call_delayed.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "api/api_text_entities.h"
#include "ui/text/text_options.h"
namespace {
constexpr auto kShortPollTimeout = 30 * crl::time(1000);
const TodoListItem *ItemById(const std::vector<TodoListItem> &list, int id) {
const auto i = ranges::find(list, id, &TodoListItem::id);
return (i != end(list)) ? &*i : nullptr;
}
TodoListItem *ItemById(std::vector<TodoListItem> &list, int id) {
return const_cast<TodoListItem*>(ItemById(std::as_const(list), id));
}
} // namespace
TodoListData::TodoListData(not_null<Data::Session*> owner, TodoListId id)
: id(id)
, _owner(owner) {
}
Data::Session &TodoListData::owner() const {
return *_owner;
}
Main::Session &TodoListData::session() const {
return _owner->session();
}
bool TodoListData::applyChanges(const MTPDtodoList &todolist) {
const auto newTitle = TextWithEntities{
.text = qs(todolist.vtitle().data().vtext()),
.entities = Api::EntitiesFromMTP(
&session(),
todolist.vtitle().data().ventities().v),
};
const auto newFlags = (todolist.is_others_can_append()
? Flag::OthersCanAppend
: Flag())
| (todolist.is_others_can_complete() ? Flag::OthersCanComplete
: Flag());
auto newItems = ranges::views::all(
todolist.vlist().v
) | ranges::views::transform([&](const MTPTodoItem &item) {
return TodoListItemFromMTP(&session(), item);
}) | ranges::views::take(
kMaxOptions
) | ranges::to_vector;
const auto changed1 = (title != newTitle) || (_flags != newFlags);
const auto changed2 = (items != newItems);
if (!changed1 && !changed2) {
return false;
}
if (changed1) {
title = newTitle;
_flags = newFlags;
}
if (changed2) {
std::swap(items, newItems);
for (const auto &old : newItems) {
if (const auto current = itemById(old.id)) {
current->completedBy = old.completedBy;
current->completionDate = old.completionDate;
}
}
}
++version;
return true;
}
bool TodoListData::applyCompletions(
const MTPVector<MTPTodoCompletion> *completions) {
auto changed = false;
const auto lookup = [&](int id) {
if (!completions) {
return (const MTPDtodoCompletion*)nullptr;
}
const auto proj = [](const MTPTodoCompletion &completion) {
return completion.data().vid().v;
};
const auto i = ranges::find(completions->v, id, proj);
return (i != completions->v.end()) ? &i->data() : nullptr;
};
for (auto &item : items) {
const auto completion = lookup(item.id);
const auto by = (completion && completion->vcompleted_by().v)
? owner().user(UserId(completion->vcompleted_by().v)).get()
: nullptr;
const auto date = completion ? completion->vdate().v : TimeId();
if (item.completedBy != by || item.completionDate != date) {
item.completedBy = by;
item.completionDate = date;
changed = true;
}
}
if (changed) {
++version;
}
return changed;
}
void TodoListData::apply(
not_null<HistoryItem*> item,
const MTPDmessageActionTodoCompletions &data) {
for (const auto &id : data.vcompleted().v) {
if (const auto task = itemById(id.v)) {
task->completedBy = item->from();
task->completionDate = item->date();
}
}
for (const auto &id : data.vincompleted().v) {
if (const auto task = itemById(id.v)) {
task->completedBy = nullptr;
task->completionDate = TimeId();
}
}
owner().notifyTodoListUpdateDelayed(this);
}
void TodoListData::apply(const MTPDmessageActionTodoAppendTasks &data) {
const auto limit = TodoListData::kMaxOptions;
for (const auto &task : data.vlist().v) {
if (items.size() < limit) {
const auto parsed = TodoListItemFromMTP(
&session(),
task);
if (!itemById(parsed.id)) {
items.push_back(std::move(parsed));
}
}
}
owner().notifyTodoListUpdateDelayed(this);
}
TodoListItem *TodoListData::itemById(int id) {
return ItemById(items, id);
}
const TodoListItem *TodoListData::itemById(int id) const {
return ItemById(items, id);
}
void TodoListData::setFlags(Flags flags) {
if (_flags != flags) {
_flags = flags;
++version;
}
}
TodoListData::Flags TodoListData::flags() const {
return _flags;
}
bool TodoListData::othersCanAppend() const {
return (_flags & Flag::OthersCanAppend);
}
bool TodoListData::othersCanComplete() const {
return (_flags & Flag::OthersCanComplete);
}
MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) {
const auto convert = [&](const TodoListItem &item) {
return MTP_todoItem(
MTP_int(item.id),
MTP_textWithEntities(
MTP_string(item.text.text),
Api::EntitiesToMTP(
&todolist->session(),
item.text.entities)));
};
auto items = QVector<MTPTodoItem>();
items.reserve(todolist->items.size());
ranges::transform(
todolist->items,
ranges::back_inserter(items),
convert);
using Flag = MTPDtodoList::Flag;
const auto flags = Flag()
| (todolist->othersCanAppend()
? Flag::f_others_can_append
: Flag())
| (todolist->othersCanComplete()
? Flag::f_others_can_complete
: Flag());
return MTP_todoList(
MTP_flags(flags),
MTP_textWithEntities(
MTP_string(todolist->title.text),
Api::EntitiesToMTP(
&todolist->session(),
todolist->title.entities)),
MTP_vector<MTPTodoItem>(items));
}
MTPInputMedia TodoListDataToInputMedia(
not_null<const TodoListData*> todolist) {
return MTP_inputMediaTodo(TodoListDataToMTP(todolist));
}
TodoListItem TodoListItemFromMTP(
not_null<Main::Session*> session,
const MTPTodoItem &item) {
const auto &data = item.data();
return {
.text = TextWithEntities{
.text = qs(data.vtitle().data().vtext()),
.entities = Api::EntitiesFromMTP(
session,
data.vtitle().data().ventities().v),
},
.id = data.vid().v,
};
}

View file

@ -0,0 +1,79 @@
/*
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
namespace Data {
class Session;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
struct TodoListItem {
TextWithEntities text;
PeerData *completedBy = nullptr;
TimeId completionDate = 0;
int id = 0;
friend inline bool operator==(
const TodoListItem &,
const TodoListItem &) = default;
};
struct TodoListData {
TodoListData(not_null<Data::Session*> owner, TodoListId id);
[[nodiscard]] Data::Session &owner() const;
[[nodiscard]] Main::Session &session() const;
enum class Flag {
OthersCanAppend = 0x01,
OthersCanComplete = 0x02,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
bool applyChanges(const MTPDtodoList &todolist);
bool applyCompletions(const MTPVector<MTPTodoCompletion> *completions);
void apply(
not_null<HistoryItem*> item,
const MTPDmessageActionTodoCompletions &data);
void apply(const MTPDmessageActionTodoAppendTasks &data);
[[nodiscard]] TodoListItem *itemById(int id);
[[nodiscard]] const TodoListItem *itemById(int id) const;
void setFlags(Flags flags);
[[nodiscard]] Flags flags() const;
[[nodiscard]] bool othersCanAppend() const;
[[nodiscard]] bool othersCanComplete() const;
TodoListId id;
TextWithEntities title;
std::vector<TodoListItem> items;
int version = 0;
static constexpr auto kMaxOptions = 32;
private:
bool applyCompletionToItems(const MTPTodoCompletion *result);
const not_null<Data::Session*> _owner;
Flags _flags = Flags();
};
[[nodiscard]] MTPTodoList TodoListDataToMTP(
not_null<const TodoListData*> todolist);
[[nodiscard]] MTPInputMedia TodoListDataToInputMedia(
not_null<const TodoListData*> todolist);
[[nodiscard]] TodoListItem TodoListItemFromMTP(
not_null<Main::Session*> session,
const MTPTodoItem &item);

View file

@ -128,6 +128,7 @@ struct WebPageData;
struct GameData;
struct BotAppData;
struct PollData;
struct TodoListData;
using PhotoId = uint64;
using VideoId = uint64;
@ -136,6 +137,7 @@ using DocumentId = uint64;
using WebPageId = uint64;
using GameId = uint64;
using PollId = uint64;
using TodoListId = FullMsgId;
using WallPaperId = uint64;
using CallId = uint64;
using BotAppId = uint64;

View file

@ -372,7 +372,7 @@ void WebPageData::ApplyChanges(
}, [&](const auto &) {
});
}
session->data().sendWebPageGamePollNotifications();
session->data().sendWebPageGamePollTodoListNotifications();
}
QString WebPageData::displayedSiteName() const {

View file

@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_document.h"
#include "data/data_histories.h"
#include "data/data_history_messages.h"
#include "data/data_todo_list.h"
#include "lang/lang_keys.h"
#include "apiwrap.h"
#include "api/api_chat_participants.h"
@ -1331,6 +1332,28 @@ void History::applyServiceChanges(
Core::App().calls().showConferenceInvite(user, item->id);
}
}
}, [&](const MTPDmessageActionTodoCompletions &data) {
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
const auto list = done->msg
? done->msg
: owner().message(peer, done->msgId);
if (const auto media = list ? list->media() : nullptr) {
if (const auto todolist = media->todolist()) {
todolist->apply(item, data);
}
}
}
}, [&](const MTPDmessageActionTodoAppendTasks &data) {
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
const auto list = done->msg
? done->msg
: owner().message(peer, done->msgId);
if (const auto media = list ? list->media() : nullptr) {
if (const auto todolist = media->todolist()) {
todolist->apply(data);
}
}
}
}, [](const auto &) {
});
}

View file

@ -62,6 +62,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "data/data_group_call.h" // Data::GroupCall::id().
#include "data/data_poll.h" // PollData::publicVotes.
#include "data/data_todo_list.h"
#include "data/data_stories.h"
#include "data/data_web_page.h"
#include "chat_helpers/stickers_gift_box_pack.h"
@ -358,7 +359,9 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
item,
item->history()->owner().processPoll(media));
}, [&](const MTPDmessageMediaToDo &media) -> Result {
return nullptr; // #TODO todo
return std::make_unique<Data::MediaTodoList>(
item,
item->history()->owner().processTodoList(item->fullId(), media));
}, [&](const MTPDmessageMediaDice &media) -> Result {
return std::make_unique<Data::MediaDice>(
item,
@ -820,6 +823,10 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() {
return same;
} else if (const auto results = Get<HistoryServiceGiveawayResults>()) {
return results;
} else if (const auto done = Get<HistoryServiceTodoCompletions>()) {
return done;
} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {
return append;
}
return nullptr;
}
@ -877,6 +884,10 @@ void HistoryItem::updateDependentServiceText() {
updateServiceText(prepareGameScoreText());
} else if (Has<HistoryServicePayment>()) {
updateServiceText(preparePaymentSentText());
} else if (Has<HistoryServiceTodoCompletions>()) {
updateServiceText(prepareTodoCompletionsText());
} else if (Has<HistoryServiceTodoAppendTasks>()) {
updateServiceText(prepareTodoAppendTasksText());
}
}
@ -4528,12 +4539,32 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
refund->transactionId = qs(data.vcharge().data().vid());
const auto id = fullId();
refund->link = std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto window = my.sessionWindow.get()) {
Settings::ShowRefundInfoBox(window, id);
}
});
} else if (type == mtpc_messageActionTodoCompletions) {
const auto &data = action.c_messageActionTodoCompletions();
UpdateComponents(HistoryServiceTodoCompletions::Bit());
const auto done = Get<HistoryServiceTodoCompletions>();
done->completed = data.vcompleted().v
| ranges::views::transform(&MTPint::v)
| ranges::to_vector;
done->incompleted = data.vincompleted().v
| ranges::views::transform(&MTPint::v)
| ranges::to_vector;
} else if (type == mtpc_messageActionTodoAppendTasks) {
const auto session = &_history->session();
const auto &data = action.c_messageActionTodoAppendTasks();
UpdateComponents(HistoryServiceTodoAppendTasks::Bit());
const auto append = Get<HistoryServiceTodoAppendTasks>();
append->list = ranges::views::all(
data.vlist().v
) | ranges::views::transform([&](const MTPTodoItem &item) {
return TodoListItemFromMTP(session, item);
}) | ranges::to_vector;
}
if (const auto replyTo = message.vreply_to()) {
replyTo->match([&](const MTPDmessageReplyHeader &data) {
@ -5874,14 +5905,12 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
return result;
};
auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &action) {
auto result = PreparedServiceText(); // #TODO todo
return result;
auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &) {
return prepareTodoCompletionsText();
};
auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &action) {
auto result = PreparedServiceText(); // #TODO todo
return result;
auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &) {
return prepareTodoAppendTasksText();
};
auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText {
@ -6549,6 +6578,92 @@ PreparedServiceText HistoryItem::prepareCallScheduledText(
return result;
}
PreparedServiceText HistoryItem::composeTodoIncompleted(
not_null<HistoryServiceTodoCompletions*> done) {
const auto tasks = ComposeTodoTasksList(done->msg, done->incompleted);
if (out()) {
return {
tr::lng_action_todo_marked_not_done_self(
tr::now,
lt_tasks,
tasks,
Ui::Text::WithEntities),
};
}
return {
.text = tr::lng_action_todo_marked_not_done(
tr::now,
lt_from,
fromLinkText(),
lt_tasks,
tasks,
Ui::Text::WithEntities),
.links = { fromLink() },
};
}
PreparedServiceText HistoryItem::composeTodoCompleted(
not_null<HistoryServiceTodoCompletions*> done) {
const auto tasks = ComposeTodoTasksList(done->msg, done->completed);
if (out()) {
return {
tr::lng_action_todo_marked_done_self(
tr::now,
lt_tasks,
tasks,
Ui::Text::WithEntities),
};
}
return {
.text = tr::lng_action_todo_marked_done(
tr::now,
lt_from,
fromLinkText(),
lt_tasks,
tasks,
Ui::Text::WithEntities),
.links = { fromLink() },
};
}
PreparedServiceText HistoryItem::prepareTodoCompletionsText() {
auto result = PreparedServiceText();
const auto done = Get<HistoryServiceTodoCompletions>();
Assert(done != nullptr);
return done->completed.empty()
? composeTodoIncompleted(done)
: composeTodoCompleted(done);
}
PreparedServiceText HistoryItem::prepareTodoAppendTasksText() {
auto result = PreparedServiceText();
auto append = Get<HistoryServiceTodoAppendTasks>();
Assert(append != nullptr);
const auto tasks = ComposeTodoTasksList(append);
if (out()) {
return {
tr::lng_action_todo_added_self(
tr::now,
lt_tasks,
tasks,
Ui::Text::WithEntities),
};
}
return {
.text = tr::lng_action_todo_added(
tr::now,
lt_from,
fromLinkText(),
lt_tasks,
tasks,
Ui::Text::WithEntities),
.links = { fromLink() },
};
return result;
}
TextWithEntities HistoryItem::fromLinkText() const {
return Ui::Text::Link(st::wrap_rtl(_from->name()), 1);
}

View file

@ -23,6 +23,7 @@ struct HistoryMessageReplyMarkup;
struct HistoryMessageTranslation;
struct HistoryMessageForwarded;
struct HistoryServiceDependentData;
struct HistoryServiceTodoCompletions;
enum class HistorySelfDestructType;
struct PreparedServiceText;
struct MessageFactcheck;
@ -644,6 +645,13 @@ private:
CallId linkCallId);
[[nodiscard]] PreparedServiceText prepareCallScheduledText(
TimeId scheduleDate);
[[nodiscard]] PreparedServiceText prepareTodoCompletionsText();
[[nodiscard]] PreparedServiceText prepareTodoAppendTasksText();
[[nodiscard]] PreparedServiceText composeTodoIncompleted(
not_null<HistoryServiceTodoCompletions*> done);
[[nodiscard]] PreparedServiceText composeTodoCompleted(
not_null<HistoryServiceTodoCompletions*> done);
[[nodiscard]] PreparedServiceText prepareServiceTextForMessage(
const MTPMessageMedia &media,

View file

@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_click_handler.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_todo_list.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "api/api_bot.h"
@ -70,6 +71,38 @@ base::options::toggle FastButtonsModeOption({
.description = "Trigger inline keyboard buttons by 1-9 keyboard keys.",
});
[[nodiscard]] TextWithEntities ComposeTodoTasksList(
int fullCount,
const std::vector<TextWithEntities> &names) {
const auto count = int(names.size());
if (!count) {
return tr::lng_action_todo_tasks_fallback(
tr::now,
lt_count,
fullCount,
Ui::Text::WithEntities);
} else if (count == 1) {
return names.front();
}
auto full = names.front();
for (auto i = 1; i != count - 1; ++i) {
full = tr::lng_action_todo_tasks_and_one(
tr::now,
lt_tasks,
full,
lt_task,
names[i],
Ui::Text::WithEntities);
}
return tr::lng_action_todo_tasks_and_last(
tr::now,
lt_tasks,
full,
lt_task,
names.back(),
Ui::Text::WithEntities);
}
} // namespace
const char kOptionFastButtonsMode[] = "fast-buttons-mode";
@ -1225,6 +1258,38 @@ MessageFactcheck FromMTP(
return result;
}
TextWithEntities ComposeTodoTasksList(
HistoryItem *itemWithList,
const std::vector<int> &ids) {
const auto media = itemWithList ? itemWithList->media() : nullptr;
const auto list = media ? media->todolist() : nullptr;
auto names = std::vector<TextWithEntities>();
if (list) {
names.reserve(ids.size());
for (const auto &id : ids) {
const auto i = ranges::find(list->items, id, &TodoListItem::id);
if (i == end(list->items)) {
names.clear();
break;
}
names.push_back(
TextWithEntities().append('"').append(i->text).append('"'));
}
}
return ComposeTodoTasksList(ids.size(), names);
}
TextWithEntities ComposeTodoTasksList(
not_null<HistoryServiceTodoAppendTasks*> append) {
auto names = std::vector<TextWithEntities>();
names.reserve(append->list.size());
for (const auto &task : append->list) {
names.push_back(
TextWithEntities().append('"').append(task.text).append('"'));
}
return ComposeTodoTasksList(names.size(), names);
}
HistoryDocumentCaptioned::HistoryDocumentCaptioned()
: caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) {
}

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/message_bubble.h"
struct WebPageData;
struct TodoListItem;
class VoiceSeekClickHandler;
class ReplyKeyboard;
@ -661,6 +662,26 @@ struct HistoryServiceTopicInfo
}
};
struct HistoryServiceTodoCompletions
: RuntimeComponent<HistoryServiceTodoCompletions, HistoryItem>
, HistoryServiceDependentData {
std::vector<int> completed;
std::vector<int> incompleted;
};
[[nodiscard]] TextWithEntities ComposeTodoTasksList(
HistoryItem *itemWithList,
const std::vector<int> &ids);
struct HistoryServiceTodoAppendTasks
: RuntimeComponent<HistoryServiceTodoAppendTasks, HistoryItem>
, HistoryServiceDependentData {
std::vector<TodoListItem> list;
};
[[nodiscard]] TextWithEntities ComposeTodoTasksList(
not_null<HistoryServiceTodoAppendTasks*> append);
struct HistoryServiceGameScore
: RuntimeComponent<HistoryServiceGameScore, HistoryItem>
, HistoryServiceDependentData {

View file

@ -598,12 +598,15 @@ void MonoforumSenderBar::Paint(
});
}
void ServicePreMessage::init(PreparedServiceText string) {
void ServicePreMessage::init(
PreparedServiceText string,
ClickHandlerPtr fullClickHandler) {
text = Ui::Text::String(
st::serviceTextStyle,
string.text,
kMarkupTextOptions,
st::msgMinWidth);
handler = std::move(fullClickHandler);
for (auto i = 0; i != int(string.links.size()); ++i) {
text.setLink(i + 1, string.links[i]);
}
@ -687,10 +690,16 @@ ClickHandlerPtr ServicePreMessage::textState(
if (trect.contains(point)) {
auto textRequest = request.forText();
textRequest.align = style::al_center;
return text.getState(
const auto link = text.getState(
point - trect.topLeft(),
trect.width(),
textRequest).link;
if (link) {
return link;
}
}
if (handler && rect.contains(point)) {
return handler;
}
return {};
}
@ -1282,6 +1291,16 @@ void Element::validateText() {
? _textItem->customTextLinks()
: contextDependentText.links;
setTextWithLinks(markedText, customLinks);
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
if (!done->completed.empty() && !done->incompleted.empty()) {
setServicePreMessage(
item->composeTodoIncompleted(done),
done->lnk);
} else {
setServicePreMessage({});
}
}
} else {
const auto unavailable = item->computeUnavailableReason();
if (!unavailable.isEmpty()) {
@ -1606,11 +1625,13 @@ void Element::setDisplayDate(bool displayDate) {
}
}
void Element::setServicePreMessage(PreparedServiceText text) {
void Element::setServicePreMessage(
PreparedServiceText text,
ClickHandlerPtr fullClickHandler) {
if (!text.text.empty()) {
AddComponents(ServicePreMessage::Bit());
const auto service = Get<ServicePreMessage>();
service->init(std::move(text));
service->init(std::move(text), std::move(fullClickHandler));
setPendingResize();
} else if (Has<ServicePreMessage>()) {
RemoveComponents(ServicePreMessage::Bit());

View file

@ -309,7 +309,7 @@ private:
// Any HistoryView::Element can have this Component for
// displaying some text in layout of a service message above the message.
struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> {
void init(PreparedServiceText string);
void init(PreparedServiceText string, ClickHandlerPtr fullClickHandler);
int resizeToWidth(int newWidth, ElementChatMode mode);
@ -324,6 +324,7 @@ struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> {
QRect g) const;
Ui::Text::String text;
ClickHandlerPtr handler;
int width = 0;
int height = 0;
@ -456,7 +457,9 @@ public:
// For blocks context this should be called only from recountDisplayDate().
void setDisplayDate(bool displayDate);
void setServicePreMessage(PreparedServiceText text);
void setServicePreMessage(
PreparedServiceText text,
ClickHandlerPtr fullClickHandler = nullptr);
bool computeIsAttachToPrevious(not_null<Element*> previous);

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_abstract_structure.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_todo_list.h"
#include "info/profile/info_profile_cover.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/reaction_fly_animation.h"
@ -448,16 +449,14 @@ void Service::animateReaction(Ui::ReactionFlyAnimationArgs &&args) {
}
QSize Service::performCountCurrentSize(int newWidth) {
auto newHeight = displayedDateHeight();
if (const auto bar = Get<UnreadBar>()) {
newHeight += bar->height();
}
if (const auto monoforumBar = Get<MonoforumSenderBar>()) {
newHeight += monoforumBar->height();
}
auto newHeight = marginTop();
data()->resolveDependent();
if (const auto service = Get<ServicePreMessage>()) {
service->resizeToWidth(newWidth, delegate()->elementChatMode());
}
if (isHidden()) {
return { newWidth, newHeight };
}
@ -465,9 +464,7 @@ QSize Service::performCountCurrentSize(int newWidth) {
const auto mediaDisplayed = media && media->isDisplayed();
auto contentWidth = newWidth;
if (mediaDisplayed && media->hideServiceText()) {
newHeight += st::msgServiceMargin.top()
+ media->resizeGetHeight(newWidth)
+ st::msgServiceMargin.bottom();
newHeight += media->resizeGetHeight(newWidth) + marginBottom();
} else if (!text().isEmpty()) {
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
@ -481,12 +478,15 @@ QSize Service::performCountCurrentSize(int newWidth) {
newHeight += (contentWidth >= maxWidth())
? minHeight()
: textHeightFor(nwidth);
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom();
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom();
if (mediaDisplayed) {
const auto mediaWidth = std::min(media->maxWidth(), nwidth);
newHeight += st::msgServiceMargin.top()
+ media->resizeGetHeight(mediaWidth);
}
newHeight += marginBottom();
} else {
newHeight -= st::msgServiceMargin.top();
}
if (_reactions) {
@ -523,7 +523,7 @@ bool Service::isHidden() const {
}
int Service::marginTop() const {
auto result = st::msgServiceMargin.top();
auto result = isHidden() ? 0 : st::msgServiceMargin.top();
result += displayedDateHeight();
if (const auto bar = Get<UnreadBar>()) {
result += bar->height();
@ -531,6 +531,9 @@ int Service::marginTop() const {
if (const auto monoforumBar = Get<MonoforumSenderBar>()) {
result += monoforumBar->height();
}
if (const auto service = Get<ServicePreMessage>()) {
result += service->height;
}
return result;
}
@ -566,6 +569,10 @@ void Service::draw(Painter &p, const PaintContext &context) const {
}
}
if (const auto service = Get<ServicePreMessage>()) {
service->paint(p, context, g, delegate()->elementChatMode());
}
if (isHidden()) {
return;
}
@ -667,6 +674,13 @@ TextState Service::textState(QPoint point, StateRequest request) const {
return result;
}
if (const auto service = Get<ServicePreMessage>()) {
result.link = service->textState(point, request, g);
if (result.link) {
return result;
}
}
if (_reactions) {
const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();
const auto reactionsLeft = 0;
@ -724,6 +738,10 @@ TextState Service::textState(QPoint point, StateRequest request) const {
result.link = custom->link;
} else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) {
result.link = payment->link;
} else if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
result.link = done->lnk;
} else if (const auto append = item->Get<HistoryServiceTodoAppendTasks>()) {
result.link = append->lnk;
} else if (media && data()->showSimilarChannels()) {
result = media->textState(mediaPoint, request);
}

View file

@ -589,6 +589,10 @@ QImage Media::locationTakeImage() {
return QImage();
}
std::vector<Media::TodoTaskInfo> Media::takeTasksInfo() {
return {};
}
TextState Media::getStateGrouped(
const QRect &geometry,
RectParts sides,

View file

@ -209,6 +209,14 @@ public:
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements);
virtual QImage locationTakeImage();
struct TodoTaskInfo {
int id = 0;
PeerData *completedBy = nullptr;
TimeId completionDate = TimeId();
};
virtual std::vector<TodoTaskInfo> takeTasksInfo();
virtual void checkAnimation() {
}

View file

@ -0,0 +1,712 @@
/*
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/media/history_view_todo_list.h"
#include "base/unixtime.h"
#include "core/ui_integration.h" // TextContext
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "calls/calls_instance.h"
#include "ui/chat/message_bubble.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/text/format_values.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/toast/toast.h"
#include "ui/painter.h"
#include "data/data_media_types.h"
#include "data/data_poll.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "base/unixtime.h"
#include "base/timer.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "api/api_todo_lists.h"
#include "styles/style_chat.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
namespace HistoryView {
namespace {
constexpr auto kShowRecentVotersCount = 3;
constexpr auto kRotateSegments = 8;
constexpr auto kRotateAmplitude = 3.;
constexpr auto kScaleSegments = 2;
constexpr auto kScaleAmplitude = 0.03;
constexpr auto kLargestRadialDuration = 30 * crl::time(1000);
constexpr auto kCriticalCloseDuration = 5 * crl::time(1000);
} // namespace
struct TodoList::Task {
Task();
void fillData(
not_null<TodoListData*> todolist,
const TodoListItem &original,
Ui::Text::MarkedContext context);
Ui::Text::String text;
PeerData *completedBy = nullptr;
mutable Ui::PeerUserpicView userpic;
TimeId completionDate = 0;
int id = 0;
ClickHandlerPtr handler;
Ui::Animations::Simple selectedAnimation;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
};
TodoList::Task::Task() : text(st::msgMinWidth / 2) {
}
void TodoList::Task::fillData(
not_null<TodoListData*> todolist,
const TodoListItem &original,
Ui::Text::MarkedContext context) {
id = original.id;
if (original.completedBy) {
completedBy = original.completedBy;
}
completionDate = original.completionDate;
if (!text.isEmpty() && text.toTextWithEntities() == original.text) {
return;
}
text.setMarkedText(
st::historyPollAnswerStyle,
original.text,
Ui::WebpageTextTitleOptions(),
context);
}
TodoList::TodoList(
not_null<Element*> parent,
not_null<TodoListData*> todolist,
Element *replacing)
: Media(parent)
, _todolist(todolist)
, _title(st::msgMinWidth / 2) {
history()->owner().registerTodoListView(_todolist, _parent);
if (const auto media = replacing ? replacing->media() : nullptr) {
const auto info = media->takeTasksInfo();
if (!info.empty()) {
setupPreviousState(info);
}
}
}
void TodoList::setupPreviousState(const std::vector<TodoTaskInfo> &info) {
// If we restore state from the view we're replacing we'll be able to
// animate the changes properly.
updateTasks(true);
for (auto &task : _tasks) {
const auto i = ranges::find(info, task.id, &TodoTaskInfo::id);
if (i != end(info)) {
task.completedBy = i->completedBy;
task.completionDate = i->completionDate;
}
}
}
QSize TodoList::countOptimalSize() {
updateTexts();
const auto paddings = st::msgPadding.left() + st::msgPadding.right();
auto maxWidth = st::msgFileMinWidth;
accumulate_max(maxWidth, paddings + _title.maxWidth());
for (const auto &task : _tasks) {
accumulate_max(
maxWidth,
paddings
+ st::historyPollAnswerPadding.left()
+ task.text.maxWidth()
+ st::historyPollAnswerPadding.right());
}
const auto tasksHeight = ranges::accumulate(ranges::views::all(
_tasks
) | ranges::views::transform([](const Task &task) {
return st::historyPollAnswerPadding.top()
+ task.text.minHeight()
+ st::historyPollAnswerPadding.bottom();
}), 0);
const auto bottomButtonHeight = st::historyPollBottomButtonSkip;
auto minHeight = st::historyPollQuestionTop
+ _title.minHeight()
+ st::historyPollSubtitleSkip
+ st::msgDateFont->height
+ st::historyPollAnswersSkip
+ tasksHeight
+ st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height
+ st::msgPadding.bottom();
if (!isBubbleTop()) {
minHeight -= st::msgFileTopMinus;
}
return { maxWidth, minHeight };
}
bool TodoList::canComplete() const {
return (_parent->data()->out() || _todolist->othersCanComplete())
&& _parent->data()->isRegular();
}
int TodoList::countTaskTop(
const Task &task,
int innerWidth) const {
auto tshift = st::historyPollQuestionTop;
if (!isBubbleTop()) {
tshift -= st::msgFileTopMinus;
}
tshift += _title.countHeight(innerWidth) + st::historyPollSubtitleSkip;
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
const auto i = ranges::find(
_tasks,
&task,
[](const Task &task) { return &task; });
const auto countHeight = [&](const Task &task) {
return countTaskHeight(task, innerWidth);
};
tshift += ranges::accumulate(
begin(_tasks),
i,
0,
ranges::plus(),
countHeight);
return tshift;
}
int TodoList::countTaskHeight(
const Task &task,
int innerWidth) const {
const auto answerWidth = innerWidth
- st::historyPollAnswerPadding.left()
- st::historyPollAnswerPadding.right();
return st::historyPollAnswerPadding.top()
+ task.text.countHeight(answerWidth)
+ st::historyPollAnswerPadding.bottom();
}
QSize TodoList::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
const auto innerWidth = newWidth
- st::msgPadding.left()
- st::msgPadding.right();
const auto tasksHeight = ranges::accumulate(ranges::views::all(
_tasks
) | ranges::views::transform([&](const Task &task) {
return countTaskHeight(task, innerWidth);
}), 0);
const auto bottomButtonHeight = st::historyPollBottomButtonSkip;
auto newHeight = st::historyPollQuestionTop
+ _title.countHeight(innerWidth)
+ st::historyPollSubtitleSkip
+ st::msgDateFont->height
+ st::historyPollAnswersSkip
+ tasksHeight
+ st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height
+ st::msgPadding.bottom();
if (!isBubbleTop()) {
newHeight -= st::msgFileTopMinus;
}
return { newWidth, newHeight };
}
void TodoList::updateTexts() {
if (_todoListVersion == _todolist->version) {
return;
}
const auto skipAnimations = _tasks.empty();
_todoListVersion = _todolist->version;
if (_title.toTextWithEntities() != _todolist->title) {
auto options = Ui::WebpageTextTitleOptions();
options.maxw = options.maxh = 0;
_title.setMarkedText(
st::historyPollQuestionStyle,
_todolist->title,
options,
Core::TextContext({
.session = &_todolist->session(),
.repaint = [=] { repaint(); },
.customEmojiLoopLimit = 2,
}));
}
if (_flags != _todolist->flags() || _subtitle.isEmpty()) {
using Flag = PollData::Flag;
_flags = _todolist->flags();
_subtitle.setText(
st::msgDateTextStyle,
(parent()->history()->peer->isUser()
? tr::lng_todo_title(tr::now)
: tr::lng_todo_title_group(tr::now)));
}
updateTasks(skipAnimations);
}
void TodoList::updateTasks(bool skipAnimations) {
const auto context = Core::TextContext({
.session = &_todolist->session(),
.repaint = [=] { repaint(); },
.customEmojiLoopLimit = 2,
});
const auto changed = !ranges::equal(
_tasks,
_todolist->items,
ranges::equal_to(),
&Task::id,
&TodoListItem::id);
if (!changed) {
auto &&tasks = ranges::views::zip(_tasks, _todolist->items);
for (auto &&[task, original] : tasks) {
const auto wasDate = task.completionDate;
task.fillData(_todolist, original, context);
if (!skipAnimations && (!wasDate != !task.completionDate)) {
startToggleAnimation(task);
}
}
return;
}
_tasks = ranges::views::all(
_todolist->items
) | ranges::views::transform([&](const TodoListItem &item) {
auto result = Task();
result.id = item.id;
result.fillData(_todolist, item, context);
return result;
}) | ranges::to_vector;
for (auto &task : _tasks) {
task.handler = createTaskClickHandler(task);
}
}
ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) {
const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id);
}));
}
void TodoList::startToggleAnimation(Task &task) {
const auto selected = (task.completionDate != 0);
task.selectedAnimation.start(
[=] { repaint(); },
selected ? 0. : 1.,
selected ? 1. : 0.,
st::defaultCheck.duration);
}
void TodoList::toggleCompletion(int id) {
const auto i = ranges::find(
_tasks,
id,
&Task::id);
if (i == end(_tasks)) {
return;
}
const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) {
i->completedBy = _parent->history()->session().user();
}
startToggleAnimation(*i);
repaint();
history()->session().api().todoLists().toggleCompletion(
_parent->data()->fullId(),
id,
!selected);
}
void TodoList::updateCompletionStatus() {
const auto incompleted = int(ranges::count(
_todolist->items,
nullptr,
&TodoListItem::completedBy));
const auto total = int(_todolist->items.size());
if (_total == total
&& _incompleted == incompleted
&& !_completionStatusLabel.isEmpty()) {
return;
}
_total = total;
_incompleted = incompleted;
const auto totalText = QString::number(total);
const auto string = (incompleted == total)
? tr::lng_todo_completed_none(tr::now, lt_total, totalText)
: tr::lng_todo_completed(
tr::now,
lt_count,
total - incompleted,
lt_total,
totalText);
_completionStatusLabel.setText(st::msgDateTextStyle, string);
}
void TodoList::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
auto paintw = width();
const auto stm = context.messageStyle();
const auto padding = st::msgPadding;
auto tshift = st::historyPollQuestionTop;
if (!isBubbleTop()) {
tshift -= st::msgFileTopMinus;
}
paintw -= padding.left() + padding.right();
p.setPen(stm->historyTextFg);
_title.drawLeft(p, padding.left(), tshift, paintw, width(), style::al_left, 0, -1, context.selection);
tshift += _title.countHeight(paintw) + st::historyPollSubtitleSkip;
p.setPen(stm->msgDateFg);
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
auto heavy = false;
auto created = false;
auto &&tasks = ranges::views::zip(
_tasks,
ranges::views::ints(0, int(_tasks.size())));
for (const auto &[task, index] : tasks) {
const auto was = !task.userpic.null();
const auto height = paintTask(
p,
task,
padding.left(),
tshift,
paintw,
width(),
context);
if (was) {
heavy = true;
} else if (!task.userpic.null()) {
created = true;
}
tshift += height;
}
if (!heavy && created) {
history()->owner().registerHeavyViewPart(_parent);
}
paintBottom(p, padding.left(), tshift, paintw, context);
}
void TodoList::paintBottom(
Painter &p,
int left,
int top,
int paintw,
const PaintContext &context) const {
const auto stringtop = top
+ st::msgPadding.bottom()
+ st::historyPollBottomButtonTop;
const auto stm = context.messageStyle();
p.setPen(stm->msgDateFg);
_completionStatusLabel.draw(p, left, stringtop, paintw, style::al_top);
}
void TodoList::radialAnimationCallback() const {
if (!anim::Disabled()) {
repaint();
}
}
int TodoList::paintTask(
Painter &p,
const Task &task,
int left,
int top,
int width,
int outerWidth,
const PaintContext &context) const {
const auto height = countTaskHeight(task, width);
const auto stm = context.messageStyle();
const auto aleft = left + st::historyPollAnswerPadding.left();
const auto awidth = width
- st::historyPollAnswerPadding.left()
- st::historyPollAnswerPadding.right();
if (task.ripple) {
p.setOpacity(st::historyPollRippleOpacity);
task.ripple->paint(
p,
left - st::msgPadding.left(),
top,
outerWidth,
&stm->msgWaveformInactive->c);
if (task.ripple->empty()) {
task.ripple.reset();
}
p.setOpacity(1.);
}
paintRadio(p, task, left, top, context);
top += st::historyPollAnswerPadding.top();
p.setPen(stm->historyTextFg);
task.text.drawLeft(p, aleft, top, awidth, outerWidth);
return height;
}
void TodoList::paintRadio(
Painter &p,
const Task &task,
int left,
int top,
const PaintContext &context) const {
top += st::historyPollAnswerPadding.top();
const auto stm = context.messageStyle();
PainterHighQualityEnabler hq(p);
const auto &radio = st::historyPollRadio;
const auto over = ClickHandler::showAsActive(task.handler);
const auto &regular = stm->msgDateFg;
const auto checkmark = task.selectedAnimation.value(
task.completionDate ? 1. : 0.);
const auto o = p.opacity();
if (checkmark < 1.) {
p.setBrush(Qt::NoBrush);
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
}
const auto rect = QRectF(left, top, radio.diameter, radio.diameter).marginsRemoved(QMarginsF(radio.thickness / 2., radio.thickness / 2., radio.thickness / 2., radio.thickness / 2.));
if (checkmark > 0. && task.completedBy) {
const auto skip = st::lineWidth;
const auto userpic = QRect(
left + (radio.diameter / 2) + skip,
top + skip,
radio.diameter - 2 * skip,
radio.diameter - 2 * skip);
if (checkmark < 1.) {
p.save();
p.setOpacity(checkmark);
p.translate(QRectF(userpic).center());
const auto ratio = 0.4 + 0.6 * checkmark;
p.scale(ratio, ratio);
p.translate(-QRectF(userpic).center());
}
task.completedBy->paintUserpic(
p,
task.userpic,
userpic.left(),
userpic.top(),
userpic.width());
if (checkmark < 1.) {
p.restore();
}
}
if (checkmark < 1.) {
auto pen = regular->p;
pen.setWidth(radio.thickness);
p.setPen(pen);
p.drawEllipse(rect);
}
if (checkmark > 0.) {
const auto removeFull = (radio.diameter / 2 - radio.thickness);
const auto removeNow = removeFull * (1. - checkmark);
const auto color = stm->msgFileThumbLinkFg;
auto pen = color->p;
pen.setWidth(radio.thickness);
p.setPen(pen);
p.setBrush(color);
p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow }));
const auto &icon = stm->historyPollChosen;
icon.paint(p, left + (radio.diameter - icon.width()) / 2, top + (radio.diameter - icon.height()) / 2, width());
const auto stm = context.messageStyle();
auto bgpen = stm->msgBg->p;
bgpen.setWidth(st::lineWidth);
const auto outline = QRect(left, top, radio.diameter, radio.diameter);
const auto paintContent = [&](QPainter &p) {
p.setPen(bgpen);
p.setBrush(Qt::NoBrush);
PainterHighQualityEnabler hq(p);
p.drawEllipse(outline);
};
if (usesBubblePattern(context)) {
const auto add = st::lineWidth * 3;
const auto target = outline.marginsAdded(
{ add, add, add, add });
Ui::PaintPatternBubblePart(
p,
context.viewport,
context.bubblesPattern->pixmap,
target,
paintContent,
_userpicCircleCache);
} else {
paintContent(p);
}
}
p.setOpacity(o);
}
TextSelection TodoList::adjustSelection(
TextSelection selection,
TextSelectType type) const {
return _title.adjustSelection(selection, type);
}
uint16 TodoList::fullSelectionLength() const {
return _title.length();
}
TextForMimeData TodoList::selectedText(TextSelection selection) const {
return _title.toTextForMimeData(selection);
}
TextState TodoList::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
const auto can = canComplete();
const auto padding = st::msgPadding;
auto paintw = width();
auto tshift = st::historyPollQuestionTop;
if (!isBubbleTop()) {
tshift -= st::msgFileTopMinus;
}
paintw -= padding.left() + padding.right();
const auto questionH = _title.countHeight(paintw);
if (QRect(padding.left(), tshift, paintw, questionH).contains(point)) {
result = TextState(_parent, _title.getState(
point - QPoint(padding.left(), tshift),
paintw,
request.forText()));
return result;
}
tshift += questionH + st::historyPollSubtitleSkip;
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
for (const auto &task : _tasks) {
const auto height = countTaskHeight(task, paintw);
if (point.y() >= tshift && point.y() < tshift + height) {
if (can) {
_lastLinkPoint = point;
result.link = task.handler;
} else if (task.completionDate) {
result.customTooltip = true;
using Flag = Ui::Text::StateRequest::Flag;
if (request.flags & Flag::LookupCustomTooltip) {
result.customTooltipText = langDateTimeFull(
base::unixtime::parse(task.completionDate));
}
}
return result;
}
tshift += height;
}
return result;
}
void TodoList::clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) {
if (!handler) return;
const auto i = ranges::find(
_tasks,
handler,
&Task::handler);
if (i != end(_tasks)) {
toggleRipple(*i, pressed);
}
}
void TodoList::unloadHeavyPart() {
for (auto &task : _tasks) {
task.userpic = {};
}
}
bool TodoList::hasHeavyPart() const {
for (auto &task : _tasks) {
if (!task.userpic.null()) {
return true;
}
}
return false;
}
std::vector<Media::TodoTaskInfo> TodoList::takeTasksInfo() {
if (_tasks.empty()) {
return {};
}
return _tasks | ranges::views::transform([](const Task &task) {
return TodoTaskInfo{
.id = task.id,
.completedBy = task.completedBy,
.completionDate = task.completionDate,
};
}) | ranges::to_vector;
}
void TodoList::toggleRipple(Task &task, bool pressed) {
if (pressed) {
const auto outerWidth = width();
const auto innerWidth = outerWidth
- st::msgPadding.left()
- st::msgPadding.right();
if (!task.ripple) {
auto mask = Ui::RippleAnimation::RectMask(QSize(
outerWidth,
countTaskHeight(task, innerWidth)));
task.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
[=] { repaint(); });
}
const auto top = countTaskTop(task, innerWidth);
task.ripple->add(_lastLinkPoint - QPoint(0, top));
} else if (task.ripple) {
task.ripple->lastStop();
}
}
int TodoList::bottomButtonHeight() const {
const auto skip = st::historyPollChoiceRight.height()
- st::historyPollFillingBottom
- st::historyPollFillingHeight
- (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2;
return st::historyPollTotalVotesSkip
- skip
+ st::historyPollBottomButtonSkip
+ st::msgDateFont->height
+ st::msgPadding.bottom();
}
TodoList::~TodoList() {
history()->owner().unregisterTodoListView(_todolist, _parent);
if (hasHeavyPart()) {
unloadHeavyPart();
_parent->checkHeavyPart();
}
}
} // namespace HistoryView

View file

@ -0,0 +1,131 @@
/*
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 "history/view/media/history_view_media.h"
#include "ui/effects/animations.h"
#include "data/data_todo_list.h"
#include "base/weak_ptr.h"
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class Message;
class TodoList final : public Media {
public:
TodoList(
not_null<Element*> parent,
not_null<TodoListData*> todolist,
Element *replacing);
~TodoList();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return true;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override;
TextForMimeData selectedText(TextSelection selection) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) override;
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
std::vector<TodoTaskInfo> takeTasksInfo() override;
private:
struct Task;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] bool canComplete() const;
[[nodiscard]] int countTaskTop(
const Task &task,
int innerWidth) const;
[[nodiscard]] int countTaskHeight(
const Task &task,
int innerWidth) const;
[[nodiscard]] ClickHandlerPtr createTaskClickHandler(
const Task &task);
void updateTexts();
void updateTasks(bool skipAnimations);
void startToggleAnimation(Task &task);
void updateCompletionStatus();
void setupPreviousState(const std::vector<TodoTaskInfo> &info);
int paintTask(
Painter &p,
const Task &task,
int left,
int top,
int width,
int outerWidth,
const PaintContext &context) const;
void paintRadio(
Painter &p,
const Task &task,
int left,
int top,
const PaintContext &context) const;
void paintBottom(
Painter &p,
int left,
int top,
int paintw,
const PaintContext &context) const;
void radialAnimationCallback() const;
void toggleRipple(Task &task, bool pressed);
void toggleCompletion(int id);
[[nodiscard]] int bottomButtonHeight() const;
const not_null<TodoListData*> _todolist;
int _todoListVersion = 0;
int _total = 0;
int _incompleted = 0;
TodoListData::Flags _flags = TodoListData::Flags();
Ui::Text::String _title;
Ui::Text::String _subtitle;
std::vector<Task> _tasks;
Ui::Text::String _completionStatusLabel;
mutable QPoint _lastLinkPoint;
mutable QImage _userpicCircleCache;
mutable QImage _fillingIconCache;
};
} // namespace HistoryView