diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 80b008c240..c1b0d964ef 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2636,6 +2636,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_effects" = "Add over 500 animated effects to private messages."; "lng_premium_summary_subtitle_filter_tags" = "Tag Your Chats"; "lng_premium_summary_about_filter_tags" = "Display folder names for each chat in the chat list."; +"lng_premium_summary_subtitle_todo_lists" = "To-Do Lists"; +"lng_premium_summary_about_todo_lists" = "Create To-Do Lists, I guess.."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; @@ -5860,6 +5862,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_todo_menu_item" = "To-Do List"; "lng_todo_create" = "Create To-Do List"; "lng_todo_create_title" = "New To-Do List"; "lng_todo_create_title_placeholder" = "Title"; @@ -5875,6 +5878,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_todo_choose_title" = "Please enter a title."; "lng_todo_choose_tasks" = "Please enter at least one task."; +"lng_todo_add_title" = "Add Tasks"; +"lng_todo_create_premium" = "Only subscribers of {link} can create To-Do Lists."; +"lng_todo_add_premium" = "Only subscribers of {link} can add tasks."; +"lng_todo_mark_premium" = "Only subscribers of {link} can mark tasks as done."; +"lng_todo_premium_link" = "Telegram Premium"; +"lng_todo_mark_restricted" = "{user} has restricted others from marking tasks as done."; + "lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM."; "lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM."; "lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}."; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 53c6a6744e..d938945e10 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -133,6 +133,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_subtitle_business(); case PremiumFeature::Effects: return tr::lng_premium_summary_subtitle_effects(); + case PremiumFeature::TodoLists: + return tr::lng_premium_summary_subtitle_todo_lists(); case PremiumFeature::BusinessLocation: return tr::lng_business_subtitle_location(); @@ -198,6 +200,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_about_business(); case PremiumFeature::Effects: return tr::lng_premium_summary_about_effects(); + case PremiumFeature::TodoLists: + return tr::lng_premium_summary_about_todo_lists(); case PremiumFeature::BusinessLocation: return tr::lng_business_about_location(); @@ -538,6 +542,7 @@ struct VideoPreviewDocument { case PremiumFeature::LastSeen: return "last_seen"; case PremiumFeature::MessagePrivacy: return "message_privacy"; case PremiumFeature::Effects: return "effects"; + case PremiumFeature::TodoLists: return "todo_lists"; AssertIsDebug() case PremiumFeature::BusinessLocation: return "business_location"; case PremiumFeature::BusinessHours: return "business_hours"; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index e631c97897..9ac7d04ac2 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -72,6 +72,7 @@ enum class PremiumFeature { Business, Effects, FilterTags, + TodoLists, // Business features. BusinessLocation, diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 38f10e8885..f54f5e6add 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -2332,7 +2332,10 @@ MediaTodoList::~MediaTodoList() { } std::unique_ptr MediaTodoList::clone(not_null parent) { - return std::make_unique(parent, _todolist); + const auto id = parent->fullId(); + return std::make_unique( + parent, + parent->history()->owner().duplicateTodoList(id, _todolist)); } TodoListData *MediaTodoList::todolist() const { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index b173fe8b18..490db7d8df 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4142,6 +4142,19 @@ not_null Session::processTodoList( return result; } +not_null Session::duplicateTodoList( + TodoListId id, + not_null existing) { + const auto result = todoList(id); + result->title = existing->title; + result->items = existing->items; + for (auto &item : result->items) { + item.completedBy = nullptr; + item.completionDate = TimeId(); + } + return result; +} + void Session::checkPollsClosings() { const auto now = base::unixtime::now(); auto closest = 0; diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 62083465bf..825e497ff6 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -698,6 +698,9 @@ public: not_null processTodoList( TodoListId id, const MTPDmessageMediaToDo &data); + [[nodiscard]] not_null duplicateTodoList( + TodoListId id, + not_null existing); [[nodiscard]] not_null location( const LocationPoint &point); diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index e061a65bb4..a41b10e1a9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_todo_list.h" #include "base/unixtime.h" +#include "core/application.h" +#include "core/click_handler_types.h" #include "core/ui_integration.h" // TextContext #include "lang/lang_keys.h" #include "history/history.h" @@ -35,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "apiwrap.h" #include "api/api_todo_lists.h" +#include "window/window_peer_menu.h" #include "styles/style_chat.h" #include "styles/style_widgets.h" #include "styles/style_window.h" @@ -324,6 +327,18 @@ void TodoList::startToggleAnimation(Task &task) { } void TodoList::toggleCompletion(int id) { + if (!canComplete()) { + _parent->delegate()->elementShowTooltip( + tr::lng_todo_mark_restricted( + tr::now, + lt_user, + Ui::Text::Bold(_parent->data()->from()->shortName()), + Ui::Text::RichLangValue), [] {}); + return; + } else if (!_parent->history()->session().premium()) { + Window::PeerMenuTodoWantsPremium(Window::TodoWantsPremium::Mark); + return; + } const auto i = ranges::find( _tasks, id, @@ -477,7 +492,11 @@ int TodoList::paintTask( p.setOpacity(1.); } - paintRadio(p, task, left, top, context); + if (canComplete()) { + paintRadio(p, task, left, top, context); + } else { + paintStatus(p, task, left, top, context); + } top += st::historyPollAnswerPadding.top(); p.setPen(stm->historyTextFg); @@ -584,6 +603,39 @@ void TodoList::paintRadio( p.setOpacity(o); } +void TodoList::paintStatus( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const { + top += st::historyPollAnswerPadding.top(); + + const auto stm = context.messageStyle(); + + const auto &radio = st::historyPollRadio; + const auto completed = (task.completionDate != 0); + + const auto rect = QRect(left, top, radio.diameter, radio.diameter); + if (completed) { + const auto &icon = stm->historyPollChosen; + icon.paint( + p, + left + (radio.diameter - icon.width()) / 2, + top + (radio.diameter - icon.height()) / 2, + width(), + stm->msgFileBg->c); + } else { + p.setPen(Qt::NoPen); + p.setBrush(stm->msgFileBg); + + PainterHighQualityEnabler hq(p); + p.drawEllipse(style::centerrect( + rect, + QRect(0, 0, st::mediaUnreadSize, st::mediaUnreadSize))); + } +} + TextSelection TodoList::adjustSelection( TextSelection selection, TextSelectType type) const { @@ -600,7 +652,6 @@ TextForMimeData TodoList::selectedText(TextSelection selection) const { 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; @@ -622,10 +673,9 @@ TextState TodoList::textState(QPoint point, StateRequest request) const { 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) { + _lastLinkPoint = point; + result.link = task.handler; + if (task.completionDate) { result.customTooltip = true; using Flag = Ui::Text::StateRequest::Flag; if (request.flags & Flag::LookupCustomTooltip) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h index bc6e5ee08a..37cb8ad2bd 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h @@ -103,6 +103,12 @@ private: int left, int top, const PaintContext &context) const; + void paintStatus( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const; void paintBottom( Painter &p, int left, diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 88d3a75fc9..d6ca306167 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -2628,7 +2628,7 @@ std::unique_ptr MakeAttachBotsMenu( } if (peer->canCreateTodoLists()) { ++minimal; - raw->addAction(tr::lng_todo_create(tr::now), [=] { + raw->addAction(tr::lng_todo_menu_item(tr::now), [=] { const auto action = actionFactory(); const auto source = action.options.scheduled ? Api::SendType::Scheduled diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 79535dfe8f..1a44c95ac2 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -386,6 +386,15 @@ using Order = std::vector; PremiumFeature::Effects, }, }, + { + u"todo_lists"_q,AssertIsDebug() + Entry{ + &st::settingsPremiumIconTranslations, + tr::lng_premium_summary_subtitle_todo_lists(), + tr::lng_premium_summary_about_todo_lists(), + PremiumFeature::TodoLists, + }, + }, }; } @@ -1608,6 +1617,8 @@ std::vector PremiumFeaturesOrder( return PremiumFeature::Wallpapers; } else if (s == u"effects"_q) { return PremiumFeature::Effects; + } else if (s == u"todo_lists"_q) {AssertIsDebug() + return PremiumFeature::TodoLists; } return PremiumFeature::kCount; }) | ranges::views::filter([](PremiumFeature type) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 61085178e6..f869102e19 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -98,6 +98,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "export/export_manager.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/premium_preview_box.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -1920,12 +1921,50 @@ void PeerMenuCreatePoll( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +void PeerMenuTodoWantsPremium(TodoWantsPremium type) { + const auto window = Core::App().activeWindow(); + if (!window) { + return; + } + const auto filter = [=](const auto &...) { + if (const auto controller = window->sessionController()) { + ShowPremiumPreviewBox(controller, PremiumFeature::TodoLists); + window->activate(); + } + return false; + }; + const auto link = Ui::Text::Link( + Ui::Text::Semibold(tr::lng_todo_premium_link(tr::now))); + const auto text = [&] { + switch (type) { + case TodoWantsPremium::Create: return tr::lng_todo_create_premium; + case TodoWantsPremium::Add: return tr::lng_todo_add_premium; + case TodoWantsPremium::Mark: return tr::lng_todo_mark_premium; + } + Unexpected("Type in PeerMenuTodoWantsPremium."); + }(); + constexpr auto kToastDuration = crl::time(4000); + window->uiShow()->showToast(Ui::Toast::Config{ + .text = text( + tr::now, + lt_link, + link, + Ui::Text::WithEntities), + .filter = filter, + .duration = kToastDuration, + }); +} + void PeerMenuCreateTodoList( not_null controller, not_null peer, FullReplyTo replyTo, Api::SendType sendType, SendMenu::Details sendMenuDetails) { + if (!peer->session().premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Create); + return; + } auto starsRequired = peer->session().changes().peerFlagsValue( peer, Data::PeerUpdate::Flag::FullInfo diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 59beef33b7..18da8845c6 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -111,6 +111,12 @@ void PeerMenuCreatePoll( PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +enum class TodoWantsPremium { + Create, + Add, + Mark, +}; +void PeerMenuTodoWantsPremium(TodoWantsPremium type); void PeerMenuCreateTodoList( not_null controller, not_null peer,