mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-07-15 02:02:52 +02:00
Support task lists view/update/actions.
This commit is contained in:
parent
06db13a0ab
commit
a97d1b8669
27 changed files with 1983 additions and 43 deletions
|
@ -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
|
||||
|
|
|
@ -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}.";
|
||||
|
|
204
Telegram/SourceFiles/api/api_todo_lists.cpp
Normal file
204
Telegram/SourceFiles/api/api_todo_lists.cpp
Normal 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
|
56
Telegram/SourceFiles/api/api_todo_lists.h
Normal file
56
Telegram/SourceFiles/api/api_todo_lists.h
Normal 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
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
232
Telegram/SourceFiles/data/data_todo_list.cpp
Normal file
232
Telegram/SourceFiles/data/data_todo_list.cpp
Normal 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,
|
||||
};
|
||||
}
|
79
Telegram/SourceFiles/data/data_todo_list.h
Normal file
79
Telegram/SourceFiles/data/data_todo_list.h
Normal 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);
|
|
@ -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;
|
||||
|
|
|
@ -372,7 +372,7 @@ void WebPageData::ApplyChanges(
|
|||
}, [&](const auto &) {
|
||||
});
|
||||
}
|
||||
session->data().sendWebPageGamePollNotifications();
|
||||
session->data().sendWebPageGamePollTodoListNotifications();
|
||||
}
|
||||
|
||||
QString WebPageData::displayedSiteName() const {
|
||||
|
|
|
@ -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 &) {
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()) {
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -589,6 +589,10 @@ QImage Media::locationTakeImage() {
|
|||
return QImage();
|
||||
}
|
||||
|
||||
std::vector<Media::TodoTaskInfo> Media::takeTasksInfo() {
|
||||
return {};
|
||||
}
|
||||
|
||||
TextState Media::getStateGrouped(
|
||||
const QRect &geometry,
|
||||
RectParts sides,
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ®ular = 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
|
131
Telegram/SourceFiles/history/view/media/history_view_todo_list.h
Normal file
131
Telegram/SourceFiles/history/view/media/history_view_todo_list.h
Normal 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
|
Loading…
Add table
Reference in a new issue