mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-07-28 00:13:04 +02:00
Add tasks to todo lists.
This commit is contained in:
parent
bf217bf7aa
commit
248fe1b53f
10 changed files with 325 additions and 58 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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();
|
||||||
|
if (locked) {
|
||||||
|
_field->setDisabled(true);
|
||||||
|
} else {
|
||||||
_field->customTab(true);
|
_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();
|
||||||
|
if (!locked) {
|
||||||
createRemove();
|
createRemove();
|
||||||
createWarning();
|
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,8 +392,10 @@ bool Tasks::Task::hasFocus() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tasks::Task::setFocus() const {
|
void Tasks::Task::setFocus() const {
|
||||||
|
if (!locked()) {
|
||||||
FocusAtEnd(field());
|
FocusAtEnd(field());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Tasks::Task::clearValue() {
|
void Tasks::Task::clearValue() {
|
||||||
field()->setText(QString());
|
field()->setText(QString());
|
||||||
|
@ -369,8 +406,10 @@ void Tasks::Task::setPlaceholder() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tasks::Task::toggleRemoveAlways(bool toggled) {
|
void Tasks::Task::toggleRemoveAlways(bool toggled) {
|
||||||
|
if (_removeAlways) {
|
||||||
*_removeAlways = toggled;
|
*_removeAlways = toggled;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Tasks::Task::updateFieldGeometry() {
|
void Tasks::Task::updateFieldGeometry() {
|
||||||
_field->resizeToWidth(_content->width());
|
_field->resizeToWidth(_content->width());
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Reference in a new issue