mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-08-22 18:27:17 +00:00
Support task lists view/update/actions.
This commit is contained in:
parent
06db13a0ab
commit
a97d1b8669
@ -178,6 +178,8 @@ PRIVATE
|
|||||||
api/api_statistics_sender.h
|
api/api_statistics_sender.h
|
||||||
api/api_text_entities.cpp
|
api/api_text_entities.cpp
|
||||||
api/api_text_entities.h
|
api/api_text_entities.h
|
||||||
|
api/api_todo_lists.cpp
|
||||||
|
api/api_todo_lists.h
|
||||||
api/api_toggling_media.cpp
|
api/api_toggling_media.cpp
|
||||||
api/api_toggling_media.h
|
api/api_toggling_media.h
|
||||||
api/api_transcribes.cpp
|
api/api_transcribes.cpp
|
||||||
@ -649,6 +651,8 @@ PRIVATE
|
|||||||
data/data_streaming.h
|
data/data_streaming.h
|
||||||
data/data_thread.cpp
|
data/data_thread.cpp
|
||||||
data/data_thread.h
|
data/data_thread.h
|
||||||
|
data/data_todo_list.cpp
|
||||||
|
data/data_todo_list.h
|
||||||
data/data_types.cpp
|
data/data_types.cpp
|
||||||
data/data_types.h
|
data/data_types.h
|
||||||
data/data_unread_value.cpp
|
data/data_unread_value.cpp
|
||||||
@ -812,6 +816,8 @@ PRIVATE
|
|||||||
history/view/media/history_view_story_mention.h
|
history/view/media/history_view_story_mention.h
|
||||||
history/view/media/history_view_theme_document.cpp
|
history/view/media/history_view_theme_document.cpp
|
||||||
history/view/media/history_view_theme_document.h
|
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.cpp
|
||||||
history/view/media/history_view_unique_gift.h
|
history/view/media/history_view_unique_gift.h
|
||||||
history/view/media/history_view_userpic_suggestion.cpp
|
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_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_enabled" = "Channel enabled Direct Messages.";
|
||||||
"lng_action_direct_messages_paid#one" = "Channel allows Direct Messages for {count} Star each.";
|
"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_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#one" = "You paid {count} Star.";
|
||||||
"lng_you_paid_stars#other" = "You paid {count} Stars.";
|
"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_sticker_emoji" = "{emoji} Sticker";
|
||||||
"lng_in_dlg_poll" = "Poll";
|
"lng_in_dlg_poll" = "Poll";
|
||||||
"lng_in_dlg_story" = "Story";
|
"lng_in_dlg_story" = "Story";
|
||||||
|
"lng_in_dlg_todo_list" = "To-Do List";
|
||||||
"lng_in_dlg_story_expired" = "Expired story";
|
"lng_in_dlg_story_expired" = "Expired story";
|
||||||
"lng_in_dlg_media_count#one" = "{count} media";
|
"lng_in_dlg_media_count#one" = "{count} media";
|
||||||
"lng_in_dlg_media_count#other" = "{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_show_more#other" = "Show more ({count})";
|
||||||
"lng_polls_votes_collapse" = "Collapse";
|
"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" = "PLEASE UPDATE YOUR OPERATING SYSTEM.";
|
||||||
"lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT 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}.";
|
"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.
|
// Update web page anyway.
|
||||||
session().data().processWebpage(d.vwebpage());
|
session().data().processWebpage(d.vwebpage());
|
||||||
session().data().sendWebPageGamePollNotifications();
|
session().data().sendWebPageGamePollTodoListNotifications();
|
||||||
|
|
||||||
updateAndApply(d.vpts().v, d.vpts_count().v, update);
|
updateAndApply(d.vpts().v, d.vpts_count().v, update);
|
||||||
} break;
|
} break;
|
||||||
@ -1926,7 +1926,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
|||||||
|
|
||||||
// Update web page anyway.
|
// Update web page anyway.
|
||||||
session().data().processWebpage(d.vwebpage());
|
session().data().processWebpage(d.vwebpage());
|
||||||
session().data().sendWebPageGamePollNotifications();
|
session().data().sendWebPageGamePollTodoListNotifications();
|
||||||
|
|
||||||
auto channel = session().data().channelLoaded(d.vchannel_id());
|
auto channel = session().data().channelLoaded(d.vchannel_id());
|
||||||
if (channel && !_handlingChannelDifference) {
|
if (channel && !_handlingChannelDifference) {
|
||||||
|
@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "api/api_polls.h"
|
#include "api/api_polls.h"
|
||||||
#include "api/api_sending.h"
|
#include "api/api_sending.h"
|
||||||
#include "api/api_text_entities.h"
|
#include "api/api_text_entities.h"
|
||||||
|
#include "api/api_todo_lists.h"
|
||||||
#include "api/api_self_destruct.h"
|
#include "api/api_self_destruct.h"
|
||||||
#include "api/api_sensitive_content.h"
|
#include "api/api_sensitive_content.h"
|
||||||
#include "api/api_global_privacy.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))
|
, _confirmPhone(std::make_unique<Api::ConfirmPhone>(this))
|
||||||
, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
|
, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
|
||||||
, _polls(std::make_unique<Api::Polls>(this))
|
, _polls(std::make_unique<Api::Polls>(this))
|
||||||
|
, _todoLists(std::make_unique<Api::TodoLists>(this))
|
||||||
, _chatParticipants(std::make_unique<Api::ChatParticipants>(this))
|
, _chatParticipants(std::make_unique<Api::ChatParticipants>(this))
|
||||||
, _unreadThings(std::make_unique<Api::UnreadThings>(this))
|
, _unreadThings(std::make_unique<Api::UnreadThings>(this))
|
||||||
, _ringtones(std::make_unique<Api::Ringtones>(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);
|
WebPageData::ApplyChanges(_session, channel, result);
|
||||||
for (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) {
|
for (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) {
|
||||||
if (i->second == req) {
|
if (i->second == req) {
|
||||||
@ -2588,7 +2593,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu
|
|||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_session->data().sendWebPageGamePollNotifications();
|
_session->data().sendWebPageGamePollTodoListNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiWrap::updateStickers() {
|
void ApiWrap::updateStickers() {
|
||||||
@ -4792,6 +4797,10 @@ Api::Polls &ApiWrap::polls() {
|
|||||||
return *_polls;
|
return *_polls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Api::TodoLists &ApiWrap::todoLists() {
|
||||||
|
return *_todoLists;
|
||||||
|
}
|
||||||
|
|
||||||
Api::ChatParticipants &ApiWrap::chatParticipants() {
|
Api::ChatParticipants &ApiWrap::chatParticipants() {
|
||||||
return *_chatParticipants;
|
return *_chatParticipants;
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ class ConfirmPhone;
|
|||||||
class PeerPhoto;
|
class PeerPhoto;
|
||||||
class PeerColors;
|
class PeerColors;
|
||||||
class Polls;
|
class Polls;
|
||||||
|
class TodoLists;
|
||||||
class ChatParticipants;
|
class ChatParticipants;
|
||||||
class UnreadThings;
|
class UnreadThings;
|
||||||
class Ringtones;
|
class Ringtones;
|
||||||
@ -413,6 +414,7 @@ public:
|
|||||||
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
|
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
|
||||||
[[nodiscard]] Api::PeerPhoto &peerPhoto();
|
[[nodiscard]] Api::PeerPhoto &peerPhoto();
|
||||||
[[nodiscard]] Api::Polls &polls();
|
[[nodiscard]] Api::Polls &polls();
|
||||||
|
[[nodiscard]] Api::TodoLists &todoLists();
|
||||||
[[nodiscard]] Api::ChatParticipants &chatParticipants();
|
[[nodiscard]] Api::ChatParticipants &chatParticipants();
|
||||||
[[nodiscard]] Api::UnreadThings &unreadThings();
|
[[nodiscard]] Api::UnreadThings &unreadThings();
|
||||||
[[nodiscard]] Api::Ringtones &ringtones();
|
[[nodiscard]] Api::Ringtones &ringtones();
|
||||||
@ -764,6 +766,7 @@ private:
|
|||||||
const std::unique_ptr<Api::ConfirmPhone> _confirmPhone;
|
const std::unique_ptr<Api::ConfirmPhone> _confirmPhone;
|
||||||
const std::unique_ptr<Api::PeerPhoto> _peerPhoto;
|
const std::unique_ptr<Api::PeerPhoto> _peerPhoto;
|
||||||
const std::unique_ptr<Api::Polls> _polls;
|
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::ChatParticipants> _chatParticipants;
|
||||||
const std::unique_ptr<Api::UnreadThings> _unreadThings;
|
const std::unique_ptr<Api::UnreadThings> _unreadThings;
|
||||||
const std::unique_ptr<Api::Ringtones> _ringtones;
|
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_web_page.h"
|
||||||
#include "history/view/media/history_view_poll.h"
|
#include "history/view/media/history_view_poll.h"
|
||||||
#include "history/view/media/history_view_theme_document.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_slot_machine.h"
|
||||||
#include "history/view/media/history_view_dice.h"
|
#include "history/view/media/history_view_dice.h"
|
||||||
#include "history/view/media/history_view_service_box.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_file_origin.h"
|
||||||
#include "data/data_stories.h"
|
#include "data/data_stories.h"
|
||||||
#include "data/data_story.h"
|
#include "data/data_story.h"
|
||||||
|
#include "data/data_todo_list.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "main/main_session_settings.h"
|
#include "main/main_session_settings.h"
|
||||||
@ -645,6 +647,10 @@ PollData *Media::poll() const {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TodoListData *Media::todolist() const {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const WallPaper *Media::paper() const {
|
const WallPaper *Media::paper() const {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -2315,6 +2321,67 @@ std::unique_ptr<HistoryView::Media> MediaPoll::createView(
|
|||||||
return std::make_unique<HistoryView::Poll>(message, _poll);
|
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)
|
MediaDice::MediaDice(not_null<HistoryItem*> parent, QString emoji, int value)
|
||||||
: Media(parent)
|
: Media(parent)
|
||||||
, _emoji(emoji)
|
, _emoji(emoji)
|
||||||
|
@ -196,6 +196,7 @@ public:
|
|||||||
virtual const GiftCode *gift() const;
|
virtual const GiftCode *gift() const;
|
||||||
virtual CloudImage *location() const;
|
virtual CloudImage *location() const;
|
||||||
virtual PollData *poll() const;
|
virtual PollData *poll() const;
|
||||||
|
virtual TodoListData *todolist() const;
|
||||||
virtual const WallPaper *paper() const;
|
virtual const WallPaper *paper() const;
|
||||||
virtual bool paperForBoth() const;
|
virtual bool paperForBoth() const;
|
||||||
virtual FullStoryId storyId() 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 {
|
class MediaDice final : public Media {
|
||||||
public:
|
public:
|
||||||
MediaDice(not_null<HistoryItem*> parent, QString emoji, int value);
|
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_premium_limits.h"
|
||||||
#include "data/data_forum.h"
|
#include "data/data_forum.h"
|
||||||
#include "data/data_forum_topic.h"
|
#include "data/data_forum_topic.h"
|
||||||
|
#include "data/data_todo_list.h"
|
||||||
#include "base/platform/base_platform_info.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "base/call_delayed.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) {
|
void Session::documentLoadProgress(not_null<DocumentData*> document) {
|
||||||
requestDocumentViewRepaint(document);
|
requestDocumentViewRepaint(document);
|
||||||
_documentLoadProgress.fire_copy(document);
|
_documentLoadProgress.fire_copy(document);
|
||||||
@ -4098,6 +4109,39 @@ not_null<PollData*> Session::processPoll(const MTPDmessageMediaPoll &data) {
|
|||||||
return result;
|
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() {
|
void Session::checkPollsClosings() {
|
||||||
const auto now = base::unixtime::now();
|
const auto now = base::unixtime::now();
|
||||||
auto closest = 0;
|
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(
|
void Session::registerContactView(
|
||||||
UserId contactId,
|
UserId contactId,
|
||||||
not_null<ViewElement*> view) {
|
not_null<ViewElement*> view) {
|
||||||
@ -4488,37 +4550,54 @@ QString Session::findContactPhone(UserId contactId) const {
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::hasPendingWebPageGamePollNotification() const {
|
bool Session::hasPendingWebPageGamePollTodoListNotification() const {
|
||||||
return !_webpagesUpdated.empty()
|
return !_webpagesUpdated.empty()
|
||||||
|| !_gamesUpdated.empty()
|
|| !_gamesUpdated.empty()
|
||||||
|| !_pollsUpdated.empty();
|
|| !_pollsUpdated.empty()
|
||||||
|
|| !_todoListsUpdated.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::notifyWebPageUpdateDelayed(not_null<WebPageData*> page) {
|
void Session::notifyWebPageUpdateDelayed(not_null<WebPageData*> page) {
|
||||||
const auto invoke = !hasPendingWebPageGamePollNotification();
|
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
|
||||||
_webpagesUpdated.insert(page);
|
_webpagesUpdated.insert(page);
|
||||||
if (invoke) {
|
if (invoke) {
|
||||||
crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); });
|
crl::on_main(_session, [=] {
|
||||||
|
sendWebPageGamePollTodoListNotifications();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::notifyGameUpdateDelayed(not_null<GameData*> game) {
|
void Session::notifyGameUpdateDelayed(not_null<GameData*> game) {
|
||||||
const auto invoke = !hasPendingWebPageGamePollNotification();
|
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
|
||||||
_gamesUpdated.insert(game);
|
_gamesUpdated.insert(game);
|
||||||
if (invoke) {
|
if (invoke) {
|
||||||
crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); });
|
crl::on_main(_session, [=] {
|
||||||
|
sendWebPageGamePollTodoListNotifications();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {
|
void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {
|
||||||
const auto invoke = !hasPendingWebPageGamePollNotification();
|
const auto invoke = !hasPendingWebPageGamePollTodoListNotification();
|
||||||
_pollsUpdated.insert(poll);
|
_pollsUpdated.insert(poll);
|
||||||
if (invoke) {
|
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*>>();
|
auto resize = std::vector<not_null<ViewElement*>>();
|
||||||
for (const auto &page : base::take(_webpagesUpdated)) {
|
for (const auto &page : base::take(_webpagesUpdated)) {
|
||||||
_webpageUpdates.fire_copy(page);
|
_webpageUpdates.fire_copy(page);
|
||||||
@ -4537,6 +4616,12 @@ void Session::sendWebPageGamePollNotifications() {
|
|||||||
resize.insert(end(resize), begin(i->second), end(i->second));
|
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) {
|
for (const auto &view : resize) {
|
||||||
requestViewResize(view);
|
requestViewResize(view);
|
||||||
}
|
}
|
||||||
|
@ -536,6 +536,7 @@ public:
|
|||||||
void requestDocumentViewRepaint(not_null<const DocumentData*> document);
|
void requestDocumentViewRepaint(not_null<const DocumentData*> document);
|
||||||
void markMediaRead(not_null<const DocumentData*> document);
|
void markMediaRead(not_null<const DocumentData*> document);
|
||||||
void requestPollViewRepaint(not_null<const PollData*> poll);
|
void requestPollViewRepaint(not_null<const PollData*> poll);
|
||||||
|
void requestTodoListViewRepaint(not_null<const TodoListData*> todolist);
|
||||||
|
|
||||||
void photoLoadProgress(not_null<PhotoData*> photo);
|
void photoLoadProgress(not_null<PhotoData*> photo);
|
||||||
void photoLoadDone(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 MTPPoll &data);
|
||||||
not_null<PollData*> processPoll(const MTPDmessageMediaPoll &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(
|
[[nodiscard]] not_null<CloudImage*> location(
|
||||||
const LocationPoint &point);
|
const LocationPoint &point);
|
||||||
|
|
||||||
@ -729,6 +738,12 @@ public:
|
|||||||
void unregisterPollView(
|
void unregisterPollView(
|
||||||
not_null<const PollData*> poll,
|
not_null<const PollData*> poll,
|
||||||
not_null<ViewElement*> view);
|
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(
|
void registerContactView(
|
||||||
UserId contactId,
|
UserId contactId,
|
||||||
not_null<ViewElement*> view);
|
not_null<ViewElement*> view);
|
||||||
@ -758,8 +773,9 @@ public:
|
|||||||
void notifyWebPageUpdateDelayed(not_null<WebPageData*> page);
|
void notifyWebPageUpdateDelayed(not_null<WebPageData*> page);
|
||||||
void notifyGameUpdateDelayed(not_null<GameData*> game);
|
void notifyGameUpdateDelayed(not_null<GameData*> game);
|
||||||
void notifyPollUpdateDelayed(not_null<PollData*> poll);
|
void notifyPollUpdateDelayed(not_null<PollData*> poll);
|
||||||
[[nodiscard]] bool hasPendingWebPageGamePollNotification() const;
|
void notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist);
|
||||||
void sendWebPageGamePollNotifications();
|
[[nodiscard]] bool hasPendingWebPageGamePollTodoListNotification() const;
|
||||||
|
void sendWebPageGamePollTodoListNotifications();
|
||||||
[[nodiscard]] rpl::producer<not_null<WebPageData*>> webPageUpdates() const;
|
[[nodiscard]] rpl::producer<not_null<WebPageData*>> webPageUpdates() const;
|
||||||
|
|
||||||
void channelDifferenceTooLong(not_null<ChannelData*> channel);
|
void channelDifferenceTooLong(not_null<ChannelData*> channel);
|
||||||
@ -1066,6 +1082,9 @@ private:
|
|||||||
std::unordered_map<
|
std::unordered_map<
|
||||||
PollId,
|
PollId,
|
||||||
std::unique_ptr<PollData>> _polls;
|
std::unique_ptr<PollData>> _polls;
|
||||||
|
std::map<
|
||||||
|
TodoListId,
|
||||||
|
std::unique_ptr<TodoListData>> _todoLists;
|
||||||
std::unordered_map<
|
std::unordered_map<
|
||||||
GameId,
|
GameId,
|
||||||
std::unique_ptr<GameData>> _games;
|
std::unique_ptr<GameData>> _games;
|
||||||
@ -1078,6 +1097,9 @@ private:
|
|||||||
std::unordered_map<
|
std::unordered_map<
|
||||||
not_null<const PollData*>,
|
not_null<const PollData*>,
|
||||||
base::flat_set<not_null<ViewElement*>>> _pollViews;
|
base::flat_set<not_null<ViewElement*>>> _pollViews;
|
||||||
|
std::unordered_map<
|
||||||
|
not_null<const TodoListData*>,
|
||||||
|
base::flat_set<not_null<ViewElement*>>> _todoListViews;
|
||||||
std::unordered_map<
|
std::unordered_map<
|
||||||
UserId,
|
UserId,
|
||||||
base::flat_set<not_null<HistoryItem*>>> _contactItems;
|
base::flat_set<not_null<HistoryItem*>>> _contactItems;
|
||||||
@ -1094,6 +1116,7 @@ private:
|
|||||||
base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
|
base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
|
||||||
base::flat_set<not_null<GameData*>> _gamesUpdated;
|
base::flat_set<not_null<GameData*>> _gamesUpdated;
|
||||||
base::flat_set<not_null<PollData*>> _pollsUpdated;
|
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<WebPageData*>> _webpageUpdates;
|
||||||
rpl::event_stream<not_null<ChannelData*>> _channelDifferenceTooLong;
|
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 GameData;
|
||||||
struct BotAppData;
|
struct BotAppData;
|
||||||
struct PollData;
|
struct PollData;
|
||||||
|
struct TodoListData;
|
||||||
|
|
||||||
using PhotoId = uint64;
|
using PhotoId = uint64;
|
||||||
using VideoId = uint64;
|
using VideoId = uint64;
|
||||||
@ -136,6 +137,7 @@ using DocumentId = uint64;
|
|||||||
using WebPageId = uint64;
|
using WebPageId = uint64;
|
||||||
using GameId = uint64;
|
using GameId = uint64;
|
||||||
using PollId = uint64;
|
using PollId = uint64;
|
||||||
|
using TodoListId = FullMsgId;
|
||||||
using WallPaperId = uint64;
|
using WallPaperId = uint64;
|
||||||
using CallId = uint64;
|
using CallId = uint64;
|
||||||
using BotAppId = uint64;
|
using BotAppId = uint64;
|
||||||
|
@ -372,7 +372,7 @@ void WebPageData::ApplyChanges(
|
|||||||
}, [&](const auto &) {
|
}, [&](const auto &) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
session->data().sendWebPageGamePollNotifications();
|
session->data().sendWebPageGamePollTodoListNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString WebPageData::displayedSiteName() const {
|
QString WebPageData::displayedSiteName() const {
|
||||||
|
@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_histories.h"
|
#include "data/data_histories.h"
|
||||||
#include "data/data_history_messages.h"
|
#include "data/data_history_messages.h"
|
||||||
|
#include "data/data_todo_list.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "api/api_chat_participants.h"
|
#include "api/api_chat_participants.h"
|
||||||
@ -1331,6 +1332,28 @@ void History::applyServiceChanges(
|
|||||||
Core::App().calls().showConferenceInvite(user, item->id);
|
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 &) {
|
}, [](const auto &) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "data/data_group_call.h" // Data::GroupCall::id().
|
#include "data/data_group_call.h" // Data::GroupCall::id().
|
||||||
#include "data/data_poll.h" // PollData::publicVotes.
|
#include "data/data_poll.h" // PollData::publicVotes.
|
||||||
|
#include "data/data_todo_list.h"
|
||||||
#include "data/data_stories.h"
|
#include "data/data_stories.h"
|
||||||
#include "data/data_web_page.h"
|
#include "data/data_web_page.h"
|
||||||
#include "chat_helpers/stickers_gift_box_pack.h"
|
#include "chat_helpers/stickers_gift_box_pack.h"
|
||||||
@ -358,7 +359,9 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
|
|||||||
item,
|
item,
|
||||||
item->history()->owner().processPoll(media));
|
item->history()->owner().processPoll(media));
|
||||||
}, [&](const MTPDmessageMediaToDo &media) -> Result {
|
}, [&](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 {
|
}, [&](const MTPDmessageMediaDice &media) -> Result {
|
||||||
return std::make_unique<Data::MediaDice>(
|
return std::make_unique<Data::MediaDice>(
|
||||||
item,
|
item,
|
||||||
@ -820,6 +823,10 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() {
|
|||||||
return same;
|
return same;
|
||||||
} else if (const auto results = Get<HistoryServiceGiveawayResults>()) {
|
} else if (const auto results = Get<HistoryServiceGiveawayResults>()) {
|
||||||
return results;
|
return results;
|
||||||
|
} else if (const auto done = Get<HistoryServiceTodoCompletions>()) {
|
||||||
|
return done;
|
||||||
|
} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {
|
||||||
|
return append;
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -877,6 +884,10 @@ void HistoryItem::updateDependentServiceText() {
|
|||||||
updateServiceText(prepareGameScoreText());
|
updateServiceText(prepareGameScoreText());
|
||||||
} else if (Has<HistoryServicePayment>()) {
|
} else if (Has<HistoryServicePayment>()) {
|
||||||
updateServiceText(preparePaymentSentText());
|
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());
|
refund->transactionId = qs(data.vcharge().data().vid());
|
||||||
const auto id = fullId();
|
const auto id = fullId();
|
||||||
refund->link = std::make_shared<LambdaClickHandler>([=](
|
refund->link = std::make_shared<LambdaClickHandler>([=](
|
||||||
ClickContext context) {
|
ClickContext context) {
|
||||||
const auto my = context.other.value<ClickHandlerContext>();
|
const auto my = context.other.value<ClickHandlerContext>();
|
||||||
if (const auto window = my.sessionWindow.get()) {
|
if (const auto window = my.sessionWindow.get()) {
|
||||||
Settings::ShowRefundInfoBox(window, id);
|
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()) {
|
if (const auto replyTo = message.vreply_to()) {
|
||||||
replyTo->match([&](const MTPDmessageReplyHeader &data) {
|
replyTo->match([&](const MTPDmessageReplyHeader &data) {
|
||||||
@ -5874,14 +5905,12 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &action) {
|
auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &) {
|
||||||
auto result = PreparedServiceText(); // #TODO todo
|
return prepareTodoCompletionsText();
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &action) {
|
auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &) {
|
||||||
auto result = PreparedServiceText(); // #TODO todo
|
return prepareTodoAppendTasksText();
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText {
|
auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText {
|
||||||
@ -6549,6 +6578,92 @@ PreparedServiceText HistoryItem::prepareCallScheduledText(
|
|||||||
return result;
|
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 {
|
TextWithEntities HistoryItem::fromLinkText() const {
|
||||||
return Ui::Text::Link(st::wrap_rtl(_from->name()), 1);
|
return Ui::Text::Link(st::wrap_rtl(_from->name()), 1);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ struct HistoryMessageReplyMarkup;
|
|||||||
struct HistoryMessageTranslation;
|
struct HistoryMessageTranslation;
|
||||||
struct HistoryMessageForwarded;
|
struct HistoryMessageForwarded;
|
||||||
struct HistoryServiceDependentData;
|
struct HistoryServiceDependentData;
|
||||||
|
struct HistoryServiceTodoCompletions;
|
||||||
enum class HistorySelfDestructType;
|
enum class HistorySelfDestructType;
|
||||||
struct PreparedServiceText;
|
struct PreparedServiceText;
|
||||||
struct MessageFactcheck;
|
struct MessageFactcheck;
|
||||||
@ -644,6 +645,13 @@ private:
|
|||||||
CallId linkCallId);
|
CallId linkCallId);
|
||||||
[[nodiscard]] PreparedServiceText prepareCallScheduledText(
|
[[nodiscard]] PreparedServiceText prepareCallScheduledText(
|
||||||
TimeId scheduleDate);
|
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(
|
[[nodiscard]] PreparedServiceText prepareServiceTextForMessage(
|
||||||
const MTPMessageMedia &media,
|
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_file_click_handler.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "data/data_stories.h"
|
#include "data/data_stories.h"
|
||||||
|
#include "data/data_todo_list.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "window/window_session_controller.h"
|
#include "window/window_session_controller.h"
|
||||||
#include "api/api_bot.h"
|
#include "api/api_bot.h"
|
||||||
@ -70,6 +71,38 @@ base::options::toggle FastButtonsModeOption({
|
|||||||
.description = "Trigger inline keyboard buttons by 1-9 keyboard keys.",
|
.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
|
} // namespace
|
||||||
|
|
||||||
const char kOptionFastButtonsMode[] = "fast-buttons-mode";
|
const char kOptionFastButtonsMode[] = "fast-buttons-mode";
|
||||||
@ -1225,6 +1258,38 @@ MessageFactcheck FromMTP(
|
|||||||
return result;
|
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()
|
HistoryDocumentCaptioned::HistoryDocumentCaptioned()
|
||||||
: caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) {
|
: 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"
|
#include "ui/chat/message_bubble.h"
|
||||||
|
|
||||||
struct WebPageData;
|
struct WebPageData;
|
||||||
|
struct TodoListItem;
|
||||||
class VoiceSeekClickHandler;
|
class VoiceSeekClickHandler;
|
||||||
class ReplyKeyboard;
|
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
|
struct HistoryServiceGameScore
|
||||||
: RuntimeComponent<HistoryServiceGameScore, HistoryItem>
|
: RuntimeComponent<HistoryServiceGameScore, HistoryItem>
|
||||||
, HistoryServiceDependentData {
|
, HistoryServiceDependentData {
|
||||||
|
@ -598,12 +598,15 @@ void MonoforumSenderBar::Paint(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServicePreMessage::init(PreparedServiceText string) {
|
void ServicePreMessage::init(
|
||||||
|
PreparedServiceText string,
|
||||||
|
ClickHandlerPtr fullClickHandler) {
|
||||||
text = Ui::Text::String(
|
text = Ui::Text::String(
|
||||||
st::serviceTextStyle,
|
st::serviceTextStyle,
|
||||||
string.text,
|
string.text,
|
||||||
kMarkupTextOptions,
|
kMarkupTextOptions,
|
||||||
st::msgMinWidth);
|
st::msgMinWidth);
|
||||||
|
handler = std::move(fullClickHandler);
|
||||||
for (auto i = 0; i != int(string.links.size()); ++i) {
|
for (auto i = 0; i != int(string.links.size()); ++i) {
|
||||||
text.setLink(i + 1, string.links[i]);
|
text.setLink(i + 1, string.links[i]);
|
||||||
}
|
}
|
||||||
@ -687,10 +690,16 @@ ClickHandlerPtr ServicePreMessage::textState(
|
|||||||
if (trect.contains(point)) {
|
if (trect.contains(point)) {
|
||||||
auto textRequest = request.forText();
|
auto textRequest = request.forText();
|
||||||
textRequest.align = style::al_center;
|
textRequest.align = style::al_center;
|
||||||
return text.getState(
|
const auto link = text.getState(
|
||||||
point - trect.topLeft(),
|
point - trect.topLeft(),
|
||||||
trect.width(),
|
trect.width(),
|
||||||
textRequest).link;
|
textRequest).link;
|
||||||
|
if (link) {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handler && rect.contains(point)) {
|
||||||
|
return handler;
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -1282,6 +1291,16 @@ void Element::validateText() {
|
|||||||
? _textItem->customTextLinks()
|
? _textItem->customTextLinks()
|
||||||
: contextDependentText.links;
|
: contextDependentText.links;
|
||||||
setTextWithLinks(markedText, customLinks);
|
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 {
|
} else {
|
||||||
const auto unavailable = item->computeUnavailableReason();
|
const auto unavailable = item->computeUnavailableReason();
|
||||||
if (!unavailable.isEmpty()) {
|
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()) {
|
if (!text.text.empty()) {
|
||||||
AddComponents(ServicePreMessage::Bit());
|
AddComponents(ServicePreMessage::Bit());
|
||||||
const auto service = Get<ServicePreMessage>();
|
const auto service = Get<ServicePreMessage>();
|
||||||
service->init(std::move(text));
|
service->init(std::move(text), std::move(fullClickHandler));
|
||||||
setPendingResize();
|
setPendingResize();
|
||||||
} else if (Has<ServicePreMessage>()) {
|
} else if (Has<ServicePreMessage>()) {
|
||||||
RemoveComponents(ServicePreMessage::Bit());
|
RemoveComponents(ServicePreMessage::Bit());
|
||||||
|
@ -309,7 +309,7 @@ private:
|
|||||||
// Any HistoryView::Element can have this Component for
|
// Any HistoryView::Element can have this Component for
|
||||||
// displaying some text in layout of a service message above the message.
|
// displaying some text in layout of a service message above the message.
|
||||||
struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> {
|
struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> {
|
||||||
void init(PreparedServiceText string);
|
void init(PreparedServiceText string, ClickHandlerPtr fullClickHandler);
|
||||||
|
|
||||||
int resizeToWidth(int newWidth, ElementChatMode mode);
|
int resizeToWidth(int newWidth, ElementChatMode mode);
|
||||||
|
|
||||||
@ -324,6 +324,7 @@ struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> {
|
|||||||
QRect g) const;
|
QRect g) const;
|
||||||
|
|
||||||
Ui::Text::String text;
|
Ui::Text::String text;
|
||||||
|
ClickHandlerPtr handler;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
|
|
||||||
@ -456,7 +457,9 @@ public:
|
|||||||
|
|
||||||
// For blocks context this should be called only from recountDisplayDate().
|
// For blocks context this should be called only from recountDisplayDate().
|
||||||
void setDisplayDate(bool displayDate);
|
void setDisplayDate(bool displayDate);
|
||||||
void setServicePreMessage(PreparedServiceText text);
|
void setServicePreMessage(
|
||||||
|
PreparedServiceText text,
|
||||||
|
ClickHandlerPtr fullClickHandler = nullptr);
|
||||||
|
|
||||||
bool computeIsAttachToPrevious(not_null<Element*> previous);
|
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_abstract_structure.h"
|
||||||
#include "data/data_chat.h"
|
#include "data/data_chat.h"
|
||||||
#include "data/data_channel.h"
|
#include "data/data_channel.h"
|
||||||
|
#include "data/data_todo_list.h"
|
||||||
#include "info/profile/info_profile_cover.h"
|
#include "info/profile/info_profile_cover.h"
|
||||||
#include "ui/chat/chat_style.h"
|
#include "ui/chat/chat_style.h"
|
||||||
#include "ui/effects/reaction_fly_animation.h"
|
#include "ui/effects/reaction_fly_animation.h"
|
||||||
@ -448,16 +449,14 @@ void Service::animateReaction(Ui::ReactionFlyAnimationArgs &&args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QSize Service::performCountCurrentSize(int newWidth) {
|
QSize Service::performCountCurrentSize(int newWidth) {
|
||||||
auto newHeight = displayedDateHeight();
|
auto newHeight = marginTop();
|
||||||
if (const auto bar = Get<UnreadBar>()) {
|
|
||||||
newHeight += bar->height();
|
|
||||||
}
|
|
||||||
if (const auto monoforumBar = Get<MonoforumSenderBar>()) {
|
|
||||||
newHeight += monoforumBar->height();
|
|
||||||
}
|
|
||||||
|
|
||||||
data()->resolveDependent();
|
data()->resolveDependent();
|
||||||
|
|
||||||
|
if (const auto service = Get<ServicePreMessage>()) {
|
||||||
|
service->resizeToWidth(newWidth, delegate()->elementChatMode());
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden()) {
|
if (isHidden()) {
|
||||||
return { newWidth, newHeight };
|
return { newWidth, newHeight };
|
||||||
}
|
}
|
||||||
@ -465,9 +464,7 @@ QSize Service::performCountCurrentSize(int newWidth) {
|
|||||||
const auto mediaDisplayed = media && media->isDisplayed();
|
const auto mediaDisplayed = media && media->isDisplayed();
|
||||||
auto contentWidth = newWidth;
|
auto contentWidth = newWidth;
|
||||||
if (mediaDisplayed && media->hideServiceText()) {
|
if (mediaDisplayed && media->hideServiceText()) {
|
||||||
newHeight += st::msgServiceMargin.top()
|
newHeight += media->resizeGetHeight(newWidth) + marginBottom();
|
||||||
+ media->resizeGetHeight(newWidth)
|
|
||||||
+ st::msgServiceMargin.bottom();
|
|
||||||
} else if (!text().isEmpty()) {
|
} else if (!text().isEmpty()) {
|
||||||
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
|
if (delegate()->elementChatMode() == ElementChatMode::Wide) {
|
||||||
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
|
accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
|
||||||
@ -481,12 +478,15 @@ QSize Service::performCountCurrentSize(int newWidth) {
|
|||||||
newHeight += (contentWidth >= maxWidth())
|
newHeight += (contentWidth >= maxWidth())
|
||||||
? minHeight()
|
? minHeight()
|
||||||
: textHeightFor(nwidth);
|
: textHeightFor(nwidth);
|
||||||
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom();
|
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom();
|
||||||
if (mediaDisplayed) {
|
if (mediaDisplayed) {
|
||||||
const auto mediaWidth = std::min(media->maxWidth(), nwidth);
|
const auto mediaWidth = std::min(media->maxWidth(), nwidth);
|
||||||
newHeight += st::msgServiceMargin.top()
|
newHeight += st::msgServiceMargin.top()
|
||||||
+ media->resizeGetHeight(mediaWidth);
|
+ media->resizeGetHeight(mediaWidth);
|
||||||
}
|
}
|
||||||
|
newHeight += marginBottom();
|
||||||
|
} else {
|
||||||
|
newHeight -= st::msgServiceMargin.top();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_reactions) {
|
if (_reactions) {
|
||||||
@ -523,7 +523,7 @@ bool Service::isHidden() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Service::marginTop() const {
|
int Service::marginTop() const {
|
||||||
auto result = st::msgServiceMargin.top();
|
auto result = isHidden() ? 0 : st::msgServiceMargin.top();
|
||||||
result += displayedDateHeight();
|
result += displayedDateHeight();
|
||||||
if (const auto bar = Get<UnreadBar>()) {
|
if (const auto bar = Get<UnreadBar>()) {
|
||||||
result += bar->height();
|
result += bar->height();
|
||||||
@ -531,6 +531,9 @@ int Service::marginTop() const {
|
|||||||
if (const auto monoforumBar = Get<MonoforumSenderBar>()) {
|
if (const auto monoforumBar = Get<MonoforumSenderBar>()) {
|
||||||
result += monoforumBar->height();
|
result += monoforumBar->height();
|
||||||
}
|
}
|
||||||
|
if (const auto service = Get<ServicePreMessage>()) {
|
||||||
|
result += service->height;
|
||||||
|
}
|
||||||
return result;
|
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()) {
|
if (isHidden()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -667,6 +674,13 @@ TextState Service::textState(QPoint point, StateRequest request) const {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (const auto service = Get<ServicePreMessage>()) {
|
||||||
|
result.link = service->textState(point, request, g);
|
||||||
|
if (result.link) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_reactions) {
|
if (_reactions) {
|
||||||
const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();
|
const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();
|
||||||
const auto reactionsLeft = 0;
|
const auto reactionsLeft = 0;
|
||||||
@ -724,6 +738,10 @@ TextState Service::textState(QPoint point, StateRequest request) const {
|
|||||||
result.link = custom->link;
|
result.link = custom->link;
|
||||||
} else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) {
|
} else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) {
|
||||||
result.link = payment->link;
|
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()) {
|
} else if (media && data()->showSimilarChannels()) {
|
||||||
result = media->textState(mediaPoint, request);
|
result = media->textState(mediaPoint, request);
|
||||||
}
|
}
|
||||||
|
@ -589,6 +589,10 @@ QImage Media::locationTakeImage() {
|
|||||||
return QImage();
|
return QImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Media::TodoTaskInfo> Media::takeTasksInfo() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
TextState Media::getStateGrouped(
|
TextState Media::getStateGrouped(
|
||||||
const QRect &geometry,
|
const QRect &geometry,
|
||||||
RectParts sides,
|
RectParts sides,
|
||||||
|
@ -209,6 +209,14 @@ public:
|
|||||||
not_null<DocumentData*> data,
|
not_null<DocumentData*> data,
|
||||||
const Lottie::ColorReplacements *replacements);
|
const Lottie::ColorReplacements *replacements);
|
||||||
virtual QImage locationTakeImage();
|
virtual QImage locationTakeImage();
|
||||||
|
|
||||||
|
struct TodoTaskInfo {
|
||||||
|
int id = 0;
|
||||||
|
PeerData *completedBy = nullptr;
|
||||||
|
TimeId completionDate = TimeId();
|
||||||
|
};
|
||||||
|
virtual std::vector<TodoTaskInfo> takeTasksInfo();
|
||||||
|
|
||||||
virtual void checkAnimation() {
|
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…
x
Reference in New Issue
Block a user