Add tasks to todo lists.

This commit is contained in:
John Preston 2025-06-12 12:22:06 +04:00
parent bf217bf7aa
commit 248fe1b53f
10 changed files with 325 additions and 58 deletions

View file

@ -42,7 +42,7 @@ void TodoLists::create(
const TodoListData &data, const TodoListData &data,
SendAction action, SendAction action,
Fn<void()> done, Fn<void()> done,
Fn<void()> fail) { Fn<void(QString)> fail) {
_session->api().sendAction(action); _session->api().sendAction(action);
const auto history = action.history; const auto history = action.history;
@ -118,7 +118,9 @@ void TodoLists::create(
(action.options.scheduled (action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent ? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent)); : Data::HistoryUpdate::Flag::MessageSent));
done(); if (const auto onstack = done) {
onstack();
}
}, [=](const MTP::Error &error, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) {
if (clearCloudDraft) { if (clearCloudDraft) {
history->finishSavingCloudDraft( history->finishSavingCloudDraft(
@ -126,10 +128,37 @@ void TodoLists::create(
monoforumPeerId, monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId)); UnixtimeFromMsgId(response.outerMsgId));
} }
fail(); if (const auto onstack = fail) {
onstack(error.type());
}
}); });
} }
void TodoLists::add(
not_null<HistoryItem*> item,
const std::vector<TodoListItem> &items,
Fn<void()> done,
Fn<void(QString)> fail) {
if (items.empty()) {
return;
}
const auto session = _session;
_session->api().request(MTPmessages_AppendTodoList(
item->history()->peer->input,
MTP_int(item->id.bare),
TodoListItemsToMTP(&item->history()->session(), items)
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
if (const auto onstack = done) {
onstack();
}
}).fail([=](const MTP::Error &error) {
if (const auto onstack = fail) {
onstack(error.type());
}
}).send();
}
void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) { void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) {
auto &entry = _toggles[itemId]; auto &entry = _toggles[itemId];
if (completed) { if (completed) {

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class ApiWrap; class ApiWrap;
class HistoryItem; class HistoryItem;
struct TodoListItem;
struct TodoListData; struct TodoListData;
namespace Main { namespace Main {
@ -30,7 +31,12 @@ public:
const TodoListData &data, const TodoListData &data,
SendAction action, SendAction action,
Fn<void()> done, Fn<void()> done,
Fn<void()> fail); Fn<void(QString)> fail);
void add(
not_null<HistoryItem*> item,
const std::vector<TodoListItem> &items,
Fn<void()> done,
Fn<void(QString)> fail);
void toggleCompletion(FullMsgId itemId, int id, bool completed); void toggleCompletion(FullMsgId itemId, int id, bool completed);
private: private:

View file

@ -17,11 +17,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "data/data_changes.h"
#include "data/data_media_types.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_todo_list.h" #include "data/data_todo_list.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "history/view/history_view_schedule_box.h" #include "history/view/history_view_schedule_box.h"
#include "history/history_item.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_app_config.h" #include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -60,7 +63,9 @@ public:
not_null<Ui::BoxContent*> box, not_null<Ui::BoxContent*> box,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
ChatHelpers::TabbedPanel *emojiPanel); ChatHelpers::TabbedPanel *emojiPanel,
std::vector<TodoListItem> existing = {},
bool existingLocked = false);
[[nodiscard]] bool hasTasks() const; [[nodiscard]] bool hasTasks() const;
[[nodiscard]] bool isValid() const; [[nodiscard]] bool isValid() const;
@ -79,7 +84,10 @@ private:
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session, not_null<Main::Session*> session,
int position); int id,
TextWithEntities text,
int position,
bool locked);
Task(const Task &other) = delete; Task(const Task &other) = delete;
Task &operator=(const Task &other) = delete; Task &operator=(const Task &other) = delete;
@ -93,6 +101,8 @@ private:
void createShadow(); void createShadow();
void destroyShadow(); void destroyShadow();
[[nodiscard]] int id() const;
[[nodiscard]] bool locked() const;
[[nodiscard]] bool isEmpty() const; [[nodiscard]] bool isEmpty() const;
[[nodiscard]] bool isGood() const; [[nodiscard]] bool isGood() const;
[[nodiscard]] bool isTooLong() const; [[nodiscard]] bool isTooLong() const;
@ -105,7 +115,7 @@ private:
[[nodiscard]] not_null<Ui::InputField*> field() const; [[nodiscard]] not_null<Ui::InputField*> field() const;
[[nodiscard]] TodoListItem toTodoListItem(int index) const; [[nodiscard]] TodoListItem toTodoListItem(int nextId) const;
[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const; [[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
@ -114,6 +124,7 @@ private:
void createWarning(); void createWarning();
void updateFieldGeometry(); void updateFieldGeometry();
int _id = 0;
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap; base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
not_null<Ui::RpWidget*> _content; not_null<Ui::RpWidget*> _content;
Ui::InputField *_field = nullptr; Ui::InputField *_field = nullptr;
@ -129,6 +140,11 @@ private:
void fixShadows(); void fixShadows();
void removeEmptyTail(); void removeEmptyTail();
void addEmptyTask(); void addEmptyTask();
void addTask(
int id,
TextWithEntities text,
anim::type animated);
void initTaskField(not_null<Task*> task);
void checkLastTask(); void checkLastTask();
void validateState(); void validateState();
void fixAfterErase(); void fixAfterErase();
@ -139,6 +155,8 @@ private:
not_null<Ui::BoxContent*> _box; not_null<Ui::BoxContent*> _box;
not_null<Ui::VerticalLayout*> _container; not_null<Ui::VerticalLayout*> _container;
const not_null<Window::SessionController*> _controller; const not_null<Window::SessionController*> _controller;
const int _existingCount = 0;
const bool _existingLocked = false;
ChatHelpers::TabbedPanel * const _emojiPanel; ChatHelpers::TabbedPanel * const _emojiPanel;
int _position = 0; int _position = 0;
int _tasksLimit = 0; int _tasksLimit = 0;
@ -210,8 +228,12 @@ Tasks::Task::Task(
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session, not_null<Main::Session*> session,
int position) int id,
: _wrap(container->insert( TextWithEntities text,
int position,
bool locked)
: _id(id)
, _wrap(container->insert(
position, position,
object_ptr<Ui::SlideWrap<Ui::RpWidget>>( object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
container, container,
@ -228,8 +250,17 @@ Tasks::Task::Task(
, _limit(session->appConfig().todoListItemTextLimit()) { , _limit(session->appConfig().todoListItemTextLimit()) {
InitField(outer, _field, session); InitField(outer, _field, session);
_field->setMaxLength(_limit + kErrorLimit); _field->setMaxLength(_limit + kErrorLimit);
_field->setTextWithTags({
text.text,
TextUtilities::ConvertEntitiesToTextTags(text.entities)
});
_field->finishAnimating();
_field->show(); _field->show();
_field->customTab(true); if (locked) {
_field->setDisabled(true);
} else {
_field->customTab(true);
}
_wrap->hide(anim::type::instant); _wrap->hide(anim::type::instant);
@ -244,8 +275,10 @@ Tasks::Task::Task(
}, _field->lifetime()); }, _field->lifetime());
createShadow(); createShadow();
createRemove(); if (!locked) {
createWarning(); createRemove();
createWarning();
}
updateFieldGeometry(); updateFieldGeometry();
} }
@ -345,7 +378,9 @@ bool Tasks::Task::isEmpty() const {
} }
bool Tasks::Task::isGood() const { bool Tasks::Task::isGood() const {
return !field()->getLastText().trimmed().isEmpty() && !isTooLong(); return !locked()
&& !field()->getLastText().trimmed().isEmpty()
&& !isTooLong();
} }
bool Tasks::Task::isTooLong() const { bool Tasks::Task::isTooLong() const {
@ -357,7 +392,9 @@ bool Tasks::Task::hasFocus() const {
} }
void Tasks::Task::setFocus() const { void Tasks::Task::setFocus() const {
FocusAtEnd(field()); if (!locked()) {
FocusAtEnd(field());
}
} }
void Tasks::Task::clearValue() { void Tasks::Task::clearValue() {
@ -369,7 +406,9 @@ void Tasks::Task::setPlaceholder() const {
} }
void Tasks::Task::toggleRemoveAlways(bool toggled) { void Tasks::Task::toggleRemoveAlways(bool toggled) {
*_removeAlways = toggled; if (_removeAlways) {
*_removeAlways = toggled;
}
} }
void Tasks::Task::updateFieldGeometry() { void Tasks::Task::updateFieldGeometry() {
@ -385,37 +424,49 @@ void Tasks::Task::removePlaceholder() const {
field()->setPlaceholder(rpl::single(QString())); field()->setPlaceholder(rpl::single(QString()));
} }
TodoListItem Tasks::Task::toTodoListItem(int index) const { int Tasks::Task::id() const {
Expects(index >= 0 && index < kMaxOptionsCount); return _id;
}
bool Tasks::Task::locked() const {
return !_remove;
}
TodoListItem Tasks::Task::toTodoListItem(int nextId) const {
const auto text = field()->getTextWithTags(); const auto text = field()->getTextWithTags();
auto result = TodoListItem{ auto result = TodoListItem{
.text = TextWithEntities{ .text = TextWithEntities{
.text = text.text, .text = text.text,
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags), .entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
}, },
.id = (index + 1) .id = _id ? _id : nextId,
}; };
TextUtilities::Trim(result.text); TextUtilities::Trim(result.text);
return result; return result;
} }
rpl::producer<Qt::MouseButton> Tasks::Task::removeClicks() const { rpl::producer<Qt::MouseButton> Tasks::Task::removeClicks() const {
return _remove->clicks(); return _remove ? _remove->clicks() : rpl::never<Qt::MouseButton>();
} }
Tasks::Tasks( Tasks::Tasks(
not_null<Ui::BoxContent*> box, not_null<Ui::BoxContent*> box,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
ChatHelpers::TabbedPanel *emojiPanel) ChatHelpers::TabbedPanel *emojiPanel,
std::vector<TodoListItem> existing,
bool existingLocked)
: _box(box) : _box(box)
, _container(container) , _container(container)
, _controller(controller) , _controller(controller)
, _existingCount(existing.size())
, _existingLocked(existingLocked)
, _emojiPanel(emojiPanel) , _emojiPanel(emojiPanel)
, _position(_container->count()) , _position(_container->count())
, _tasksLimit(controller->session().appConfig().todoListItemsLimit()) { , _tasksLimit(controller->session().appConfig().todoListItemsLimit()) {
for (const auto &task : existing) {
addTask(task.id, task.text, anim::type::instant);
}
checkLastTask(); checkLastTask();
} }
@ -466,22 +517,21 @@ void Tasks::Task::destroy(FnMut<void()> done) {
std::vector<TodoListItem> Tasks::toTodoListItems() const { std::vector<TodoListItem> Tasks::toTodoListItems() const {
auto result = std::vector<TodoListItem>(); auto result = std::vector<TodoListItem>();
result.reserve(_list.size()); result.reserve(_list.size());
auto counter = int(0); auto usedId = 0;
const auto makeTask = [&](const std::unique_ptr<Task> &task) { for (const auto &task : _list) {
return task->toTodoListItem(counter++); if (task->isGood()) {
}; result.push_back(task->toTodoListItem(++usedId));
ranges::copy( } else if (const auto id = task->id()) {
_list usedId = std::max(usedId, id);
| ranges::views::filter(&Task::isGood) }
| ranges::views::transform(makeTask), }
ranges::back_inserter(result));
return result; return result;
} }
void Tasks::focusFirst() { void Tasks::focusFirst() {
Expects(!_list.empty()); const auto locked = _existingLocked ? _existingCount : 0;
Assert(locked < _list.size());
_list.front()->setFocus(); FocusAtEnd((_list.begin() + locked)->get()->field());
} }
bool Tasks::correctShadows() const { bool Tasks::correctShadows() const {
@ -549,21 +599,45 @@ void Tasks::fixAfterErase() {
} }
void Tasks::addEmptyTask() { void Tasks::addEmptyTask() {
if (full()) { if (!_list.empty() && _list.back()->isEmpty()) {
return; return;
} else if (!_list.empty() && _list.back()->isEmpty()) { }
const auto locked = _existingLocked ? _existingCount : 0;
addTask(
0, // id
TextWithEntities(),
(locked < _list.size()) ? anim::type::normal : anim::type::instant);
}
void Tasks::addTask(
int id,
TextWithEntities text,
anim::type animated) {
if (full()) {
return; return;
} }
if (_list.size() > 1) { if (_list.size() > 1) {
(*(_list.end() - 2))->removePlaceholder(); (*(_list.end() - 2))->removePlaceholder();
(*(_list.end() - 2))->toggleRemoveAlways(true); (*(_list.end() - 2))->toggleRemoveAlways(true);
} }
const auto locked = id && _existingLocked;
_list.push_back(std::make_unique<Task>( _list.push_back(std::make_unique<Task>(
_box, _box,
_container, _container,
&_controller->session(), &_controller->session(),
_position + _list.size() + _destroyed.size())); id,
const auto field = _list.back()->field(); std::move(text),
_position + _list.size() + _destroyed.size(),
locked));
if (!locked) {
initTaskField(_list.back().get());
}
_list.back()->show(animated);
fixShadows();
}
void Tasks::initTaskField(not_null<Task*> task) {
const auto field = task->field();
if (const auto emojiPanel = _emojiPanel) { if (const auto emojiPanel = _emojiPanel) {
const auto emojiToggle = Ui::AddEmojiToggleToField( const auto emojiToggle = Ui::AddEmojiToggleToField(
field, field,
@ -637,7 +711,7 @@ void Tasks::addEmptyTask() {
return base::EventFilterResult::Cancel; return base::EventFilterResult::Cancel;
}); });
_list.back()->removeClicks( task->removeClicks(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
Ui::PostponeCall(crl::guard(field, [=] { Ui::PostponeCall(crl::guard(field, [=] {
Expects(!_list.empty()); Expects(!_list.empty());
@ -656,11 +730,6 @@ void Tasks::addEmptyTask() {
validateState(); validateState();
})); }));
}, field->lifetime()); }, field->lifetime());
_list.back()->show((_list.size() == 1)
? anim::type::instant
: anim::type::normal);
fixShadows();
} }
void Tasks::removeDestroyed(not_null<Task*> task) { void Tasks::removeDestroyed(not_null<Task*> task) {
@ -678,7 +747,9 @@ 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() - (lastEmpty ? 1 : 0); _usedCount = _list.size()
- (lastEmpty ? 1 : 0)
- (_existingLocked ? _existingCount : 0);
} }
int Tasks::findField(not_null<Ui::InputField*> field) const { int Tasks::findField(not_null<Ui::InputField*> field) const {
@ -728,7 +799,7 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle(
using namespace Settings; using namespace Settings;
const auto session = &_controller->session(); const auto session = &_controller->session();
const auto isPremium = session->user()->isPremium(); const auto isPremium = session->premium();
const auto title = container->add( const auto title = container->add(
object_ptr<Ui::InputField>( object_ptr<Ui::InputField>(
@ -993,3 +1064,85 @@ void CreateTodoListBox::prepare() {
setDimensionsToContent(st::boxWideWidth, inner); setDimensionsToContent(st::boxWideWidth, inner);
} }
AddTodoListTasksBox::AddTodoListTasksBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item)
: _controller(controller)
, _item(item) {
_controller->session().changes().messageUpdates(
Data::MessageUpdate::Flag::Destroyed
) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
if (update.item == item) {
closeBox();
}
}, lifetime());
}
void AddTodoListTasksBox::prepare() {
setTitle(tr::lng_todo_add_title());
const auto inner = setInnerWidget(setupContent());
setDimensionsToContent(st::boxWideWidth, inner);
scrollToY(ScrollMax);
}
object_ptr<Ui::RpWidget> AddTodoListTasksBox::setupContent() {
auto result = object_ptr<Ui::VerticalLayout>(this);
const auto container = result.data();
const auto tasks = lifetime().make_state<Tasks>(
this,
container,
_controller,
_emojiPanel ? _emojiPanel.get() : nullptr,
_item->media()->todolist()->items,
true);
auto limit = tasks->usedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count);
setCloseByOutsideClick(!count);
}) | rpl::map([=](int count) {
const auto appConfig = &_controller->session().appConfig();
const auto max = appConfig->todoListItemsLimit();
return (count < max)
? tr::lng_todo_create_limit(tr::now, lt_count, max - count)
: tr::lng_todo_create_maximum(tr::now);
}) | rpl::after_next([=] {
container->resizeToWidth(container->widthNoMargins());
});
container->add(
object_ptr<Ui::DividerLabel>(
container,
object_ptr<Ui::FlatLabel>(
container,
std::move(limit),
st::boxDividerLabel),
st::createPollLimitPadding));
_setInnerFocus = [=] {
tasks->focusFirst();
};
tasks->scrollToWidget(
) | rpl::start_with_next([=](not_null<QWidget*> widget) {
scrollToWidget(widget);
}, lifetime());
const auto submit = addButton(tr::lng_settings_save(), [=] {
_submitRequests.fire({ tasks->toTodoListItems() });
});
addButton(tr::lng_cancel(), [=] { closeBox(); });
return result;
}
auto AddTodoListTasksBox::submitRequests() const -> rpl::producer<Result> {
return _submitRequests.events();
}
void AddTodoListTasksBox::setInnerFocus() {
_setInnerFocus();
}

View file

@ -76,3 +76,32 @@ private:
int _titleLimit = 0; int _titleLimit = 0;
}; };
class AddTodoListTasksBox : public Ui::BoxContent {
public:
struct Result {
std::vector<TodoListItem> items;
};
AddTodoListTasksBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
[[nodiscard]] rpl::producer<Result> submitRequests() const;
void setInnerFocus() override;
protected:
void prepare() override;
private:
[[nodiscard]] object_ptr<Ui::RpWidget> setupContent();
const not_null<Window::SessionController*> _controller;
const not_null<HistoryItem*> _item;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus;
rpl::event_stream<Result> _submitRequests;
};

View file

@ -177,22 +177,23 @@ bool TodoListData::othersCanComplete() const {
return (_flags & Flag::OthersCanComplete); return (_flags & Flag::OthersCanComplete);
} }
MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) { MTPVector<MTPTodoItem> TodoListItemsToMTP(
not_null<Main::Session*> session,
const std::vector<TodoListItem> &tasks) {
const auto convert = [&](const TodoListItem &item) { const auto convert = [&](const TodoListItem &item) {
return MTP_todoItem( return MTP_todoItem(
MTP_int(item.id), MTP_int(item.id),
MTP_textWithEntities( MTP_textWithEntities(
MTP_string(item.text.text), MTP_string(item.text.text),
Api::EntitiesToMTP( Api::EntitiesToMTP(session, item.text.entities)));
&todolist->session(),
item.text.entities)));
}; };
auto items = QVector<MTPTodoItem>(); auto items = QVector<MTPTodoItem>();
items.reserve(todolist->items.size()); items.reserve(tasks.size());
ranges::transform( ranges::transform(tasks, ranges::back_inserter(items), convert);
todolist->items, return MTP_vector<MTPTodoItem>(items);
ranges::back_inserter(items), }
convert);
MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) {
using Flag = MTPDtodoList::Flag; using Flag = MTPDtodoList::Flag;
const auto flags = Flag() const auto flags = Flag()
| (todolist->othersCanAppend() | (todolist->othersCanAppend()
@ -208,7 +209,7 @@ MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) {
Api::EntitiesToMTP( Api::EntitiesToMTP(
&todolist->session(), &todolist->session(),
todolist->title.entities)), todolist->title.entities)),
MTP_vector<MTPTodoItem>(items)); TodoListItemsToMTP(&todolist->session(), todolist->items));
} }
MTPInputMedia TodoListDataToInputMedia( MTPInputMedia TodoListDataToInputMedia(

View file

@ -70,6 +70,9 @@ private:
}; };
[[nodiscard]] MTPVector<MTPTodoItem> TodoListItemsToMTP(
not_null<Main::Session*> session,
const std::vector<TodoListItem> &tasks);
[[nodiscard]] MTPTodoList TodoListDataToMTP( [[nodiscard]] MTPTodoList TodoListDataToMTP(
not_null<const TodoListData*> todolist); not_null<const TodoListData*> todolist);
[[nodiscard]] MTPInputMedia TodoListDataToInputMedia( [[nodiscard]] MTPInputMedia TodoListDataToInputMedia(

View file

@ -94,6 +94,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_histories.h" #include "data/data_histories.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_todo_list.h"
#include "dialogs/ui/dialogs_video_userpic.h" #include "dialogs/ui/dialogs_video_userpic.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
@ -2719,6 +2720,24 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
} }
}; };
const auto addTodoListAction = [&](HistoryItem *item) {
const auto media = item ? item->media() : nullptr;
const auto todolist = media ? media->todolist() : nullptr;
if (!todolist
|| !item->isRegular()
|| (!item->out() && !todolist->othersCanAppend())) {
return;
}
const auto itemId = item->fullId();
_menu->addAction(
tr::lng_todo_add_title(tr::now),
crl::guard(this, [=] {
if (const auto item = session->data().message(itemId)) {
Window::PeerMenuAddTodoListTasks(_controller, item);
}
}),
&st::menuIconCreateTodoList);
};
const auto lnkPhoto = link const auto lnkPhoto = link
? reinterpret_cast<PhotoData*>( ? reinterpret_cast<PhotoData*>(
link->property(kPhotoLinkMediaProperty).toULongLong()) link->property(kPhotoLinkMediaProperty).toULongLong())
@ -2889,6 +2908,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
addItemActions(item, item); addItemActions(item, item);
} else { } else {
addReplyAction(partItemOrLeader); addReplyAction(partItemOrLeader);
addTodoListAction(partItemOrLeader);
addItemActions(item, albumPartItem); addItemActions(item, albumPartItem);
if (item && !isUponSelected) { if (item && !isUponSelected) {
const auto media = (view ? view->media() : nullptr); const auto media = (view ? view->media() : nullptr);

View file

@ -620,7 +620,7 @@ int ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) {
st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left());
} }
auto contentWidth = width; auto contentWidth = width;
contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.right();
if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) {
contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1;
} }

View file

@ -2017,9 +2017,9 @@ void PeerMenuCreateTodoList(
api->todoLists().create(result.todolist, action, crl::guard(weak, [=] { api->todoLists().create(result.todolist, action, crl::guard(weak, [=] {
state->create = nullptr; state->create = nullptr;
weak->closeBox(); weak->closeBox();
}), crl::guard(weak, [=] { }), crl::guard(weak, [=](const QString &error) {
state->lock = false; state->lock = false;
weak->submitFailed(tr::lng_attach_failed(tr::now)); weak->submitFailed(error);
})); }));
}; };
box->submitRequests( box->submitRequests(
@ -2027,6 +2027,29 @@ void PeerMenuCreateTodoList(
controller->show(std::move(box), Ui::LayerOption::CloseOther); controller->show(std::move(box), Ui::LayerOption::CloseOther);
} }
void PeerMenuAddTodoListTasks(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
if (!session->premium()) {
PeerMenuTodoWantsPremium(TodoWantsPremium::Add);
return;
}
auto box = Box<AddTodoListTasksBox>(controller, item);
const auto raw = box.data();
box->submitRequests(
) | rpl::start_with_next([=](const AddTodoListTasksBox::Result &result) {
const auto show = raw->uiShow();
raw->closeBox();
session->api().todoLists().add(
item,
result.items,
[] {},
[=](const QString &error) { show->showToast(error); });
}, box->lifetime());
controller->show(std::move(box), Ui::LayerOption::CloseOther);
}
void PeerMenuBlockUserBox( void PeerMenuBlockUserBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window, not_null<Window::Controller*> window,

View file

@ -123,6 +123,9 @@ 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());
void PeerMenuAddTodoListTasks(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
void PeerMenuDeleteTopicWithConfirmation( void PeerMenuDeleteTopicWithConfirmation(
not_null<Window::SessionNavigation*> navigation, not_null<Window::SessionNavigation*> navigation,
not_null<Data::ForumTopic*> topic); not_null<Data::ForumTopic*> topic);