Improve adding tasks to todo lists.

This commit is contained in:
John Preston 2025-06-12 14:39:25 +04:00
parent 248fe1b53f
commit d83a80ec53
5 changed files with 93 additions and 37 deletions

View file

@ -72,7 +72,7 @@ public:
[[nodiscard]] std::vector<TodoListItem> toTodoListItems() const; [[nodiscard]] std::vector<TodoListItem> toTodoListItems() const;
void focusFirst(); void focusFirst();
[[nodiscard]] rpl::producer<int> usedCount() const; [[nodiscard]] rpl::producer<int> addedCount() const;
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const; [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
[[nodiscard]] rpl::producer<> backspaceInFront() const; [[nodiscard]] rpl::producer<> backspaceInFront() const;
[[nodiscard]] rpl::producer<> tabbed() const; [[nodiscard]] rpl::producer<> tabbed() const;
@ -162,7 +162,7 @@ private:
int _tasksLimit = 0; int _tasksLimit = 0;
std::vector<std::unique_ptr<Task>> _list; std::vector<std::unique_ptr<Task>> _list;
std::vector<std::unique_ptr<Task>> _destroyed; std::vector<std::unique_ptr<Task>> _destroyed;
rpl::variable<int> _usedCount = 0; rpl::variable<int> _addedCount = 0;
bool _hasTasks = false; bool _hasTasks = false;
bool _isValid = false; bool _isValid = false;
rpl::event_stream<not_null<QWidget*>> _scrollToWidget; rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
@ -224,6 +224,26 @@ void FocusAtEnd(not_null<Ui::InputField*> field) {
field->ensureCursorVisible(); field->ensureCursorVisible();
} }
[[nodiscard]] base::unique_qptr<ChatHelpers::TabbedPanel> MakeEmojiPanel(
not_null<QWidget*> outer,
not_null<Window::SessionController*> controller) {
auto result = base::make_unique_q<ChatHelpers::TabbedPanel>(
outer,
controller,
object_ptr<ChatHelpers::TabbedSelector>(
nullptr,
controller->uiShow(),
Window::GifPauseReason::Layer,
ChatHelpers::TabbedSelector::Mode::EmojiOnly));
result->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
result ->hide();
result->selector()->setCurrentPeer(controller->session().user());
return result;
}
Tasks::Task::Task( Tasks::Task::Task(
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
@ -482,8 +502,8 @@ bool Tasks::isValid() const {
return _isValid; return _isValid;
} }
rpl::producer<int> Tasks::usedCount() const { rpl::producer<int> Tasks::addedCount() const {
return _usedCount.value(); return _addedCount.value();
} }
rpl::producer<not_null<QWidget*>> Tasks::scrollToWidget() const { rpl::producer<not_null<QWidget*>> Tasks::scrollToWidget() const {
@ -747,7 +767,7 @@ void Tasks::validateState() {
_isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong); _isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong);
const auto lastEmpty = !_list.empty() && _list.back()->isEmpty(); const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
_usedCount = _list.size() _addedCount = _list.size()
- (lastEmpty ? 1 : 0) - (lastEmpty ? 1 : 0)
- (_existingLocked ? _existingCount : 0); - (_existingLocked ? _existingCount : 0);
} }
@ -817,37 +837,22 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle(
title->customTab(true); title->customTab(true);
if (isPremium) { if (isPremium) {
using Selector = ChatHelpers::TabbedSelector; _emojiPanel = MakeEmojiPanel(
const auto outer = getDelegate()->outerContainer(); getDelegate()->outerContainer(),
_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>( _controller);
outer,
_controller,
object_ptr<Selector>(
nullptr,
_controller->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::EmojiOnly));
const auto emojiPanel = _emojiPanel.get();
emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
emojiPanel->hide();
emojiPanel->selector()->setCurrentPeer(session->user());
const auto emojiToggle = Ui::AddEmojiToggleToField( const auto emojiToggle = Ui::AddEmojiToggleToField(
title, title,
this, this,
_controller, _controller,
emojiPanel, _emojiPanel.get(),
st::createPollOptionFieldPremiumEmojiPosition); st::createPollOptionFieldPremiumEmojiPosition);
emojiPanel->selector()->emojiChosen( _emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
if (title->hasFocus()) { if (title->hasFocus()) {
Ui::InsertEmojiAtCursor(title->textCursor(), data.emoji); Ui::InsertEmojiAtCursor(title->textCursor(), data.emoji);
} }
}, emojiToggle->lifetime()); }, emojiToggle->lifetime());
emojiPanel->selector()->customEmojiChosen( _emojiPanel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
if (title->hasFocus()) { if (title->hasFocus()) {
Data::InsertCustomEmoji(title, data.document); Data::InsertCustomEmoji(title, data.document);
@ -906,7 +911,7 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
container, container,
_controller, _controller,
_emojiPanel ? _emojiPanel.get() : nullptr); _emojiPanel ? _emojiPanel.get() : nullptr);
auto limit = tasks->usedCount() | rpl::after_next([=](int count) { auto limit = tasks->addedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count); setCloseByEscape(!count);
setCloseByOutsideClick(!count); setCloseByOutsideClick(!count);
}) | rpl::map([=](int count) { }) | rpl::map([=](int count) {
@ -1094,21 +1099,32 @@ object_ptr<Ui::RpWidget> AddTodoListTasksBox::setupContent() {
auto result = object_ptr<Ui::VerticalLayout>(this); auto result = object_ptr<Ui::VerticalLayout>(this);
const auto container = result.data(); const auto container = result.data();
if (_controller->session().premium()) {
_emojiPanel = MakeEmojiPanel(
getDelegate()->outerContainer(),
_controller);
}
const auto media = _item->media();
const auto todolist = media ? media->todolist() : nullptr;
Assert(todolist != nullptr);
const auto tasks = lifetime().make_state<Tasks>( const auto tasks = lifetime().make_state<Tasks>(
this, this,
container, container,
_controller, _controller,
_emojiPanel ? _emojiPanel.get() : nullptr, _emojiPanel ? _emojiPanel.get() : nullptr,
_item->media()->todolist()->items, todolist->items,
true); true);
auto limit = tasks->usedCount() | rpl::after_next([=](int count) { const auto already = int(todolist->items.size());
auto limit = tasks->addedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count); setCloseByEscape(!count);
setCloseByOutsideClick(!count); setCloseByOutsideClick(!count);
}) | rpl::map([=](int count) { }) | rpl::map([=](int count) {
const auto appConfig = &_controller->session().appConfig(); const auto appConfig = &_controller->session().appConfig();
const auto max = appConfig->todoListItemsLimit(); const auto max = appConfig->todoListItemsLimit();
return (count < max) const auto total = already + count;
? tr::lng_todo_create_limit(tr::now, lt_count, max - count) return (total < max)
? tr::lng_todo_create_limit(tr::now, lt_count, max - total)
: tr::lng_todo_create_maximum(tr::now); : tr::lng_todo_create_maximum(tr::now);
}) | rpl::after_next([=] { }) | rpl::after_next([=] {
container->resizeToWidth(container->widthNoMargins()); container->resizeToWidth(container->widthNoMargins());

View file

@ -2721,11 +2721,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}; };
const auto addTodoListAction = [&](HistoryItem *item) { const auto addTodoListAction = [&](HistoryItem *item) {
const auto media = item ? item->media() : nullptr; if (!item || !Window::PeerMenuShowAddTodoListTasks(item)) {
const auto todolist = media ? media->todolist() : nullptr;
if (!todolist
|| !item->isRegular()
|| (!item->out() && !todolist->othersCanAppend())) {
return; return;
} }
const auto itemId = item->fullId(); const auto itemId = item->fullId();

View file

@ -626,7 +626,9 @@ bool AddReplyToMessageAction(
const auto peer = item ? item->history()->peer.get() : nullptr; const auto peer = item ? item->history()->peer.get() : nullptr;
if (!item if (!item
|| !item->isRegular() || !item->isRegular()
|| (context != Context::History && context != Context::Replies)) { || (context != Context::History
&& context != Context::Replies
&& context != Context::Monoforum)) {
return false; return false;
} }
const auto canSendReply = topic const auto canSendReply = topic
@ -653,6 +655,30 @@ bool AddReplyToMessageAction(
return true; return true;
} }
bool AddTodoListAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
const auto context = list->elementContext();
const auto item = request.item;
if (!item
|| !Window::PeerMenuShowAddTodoListTasks(item)
|| (context != Context::History
&& context != Context::Replies
&& context != Context::Monoforum
&& context != Context::Pinned)) {
return false;
}
const auto itemId = item->fullId();
const auto controller = list->controller();
menu->addAction(tr::lng_todo_add_title(tr::now), [=] {
if (const auto item = controller->session().data().message(itemId)) {
Window::PeerMenuAddTodoListTasks(controller, item);
}
}, &st::menuIconCreateTodoList);
return true;
}
bool AddViewRepliesAction( bool AddViewRepliesAction(
not_null<Ui::PopupMenu*> menu, not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request, const ContextMenuRequest &request,
@ -1281,6 +1307,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
st::popupMenuWithIcons); st::popupMenuWithIcons);
AddReplyToMessageAction(result, request, list); AddReplyToMessageAction(result, request, list);
AddTodoListAction(result, request, list);
if (request.overSelection if (request.overSelection
&& !list->hasCopyRestrictionForSelected() && !list->hasCopyRestrictionForSelected()

View file

@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/delayed_activation.h" #include "ui/delayed_activation.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
#include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_session_settings.h" #include "main/main_session_settings.h"
#include "menu/menu_mute.h" #include "menu/menu_mute.h"
@ -2027,6 +2028,16 @@ void PeerMenuCreateTodoList(
controller->show(std::move(box), Ui::LayerOption::CloseOther); controller->show(std::move(box), Ui::LayerOption::CloseOther);
} }
bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item) {
const auto media = item ? item->media() : nullptr;
const auto todolist = media ? media->todolist() : nullptr;
const auto appConfig = &item->history()->session().appConfig();
return item->isRegular()
&& todolist
&& (todolist->items.size() < appConfig->todoListItemsLimit())
&& (item->out() || todolist->othersCanAppend());
}
void PeerMenuAddTodoListTasks( void PeerMenuAddTodoListTasks(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) { not_null<HistoryItem*> item) {
@ -2035,6 +2046,11 @@ void PeerMenuAddTodoListTasks(
PeerMenuTodoWantsPremium(TodoWantsPremium::Add); PeerMenuTodoWantsPremium(TodoWantsPremium::Add);
return; return;
} }
const auto media = item->media();
const auto todolist = media ? media->todolist() : nullptr;
if (!todolist) {
return;
}
auto box = Box<AddTodoListTasksBox>(controller, item); auto box = Box<AddTodoListTasksBox>(controller, item);
const auto raw = box.data(); const auto raw = box.data();
box->submitRequests( box->submitRequests(

View file

@ -123,6 +123,7 @@ void PeerMenuCreateTodoList(
FullReplyTo replyTo = FullReplyTo(), FullReplyTo replyTo = FullReplyTo(),
Api::SendType sendType = Api::SendType::Normal, Api::SendType sendType = Api::SendType::Normal,
SendMenu::Details sendMenuDetails = SendMenu::Details()); SendMenu::Details sendMenuDetails = SendMenu::Details());
[[nodiscard]] bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item);
void PeerMenuAddTodoListTasks( void PeerMenuAddTodoListTasks(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item); not_null<HistoryItem*> item);