Allow fully editing todo lists.

This commit is contained in:
John Preston 2025-06-12 18:41:01 +04:00
parent d83a80ec53
commit 9290c90bdc
17 changed files with 204 additions and 42 deletions

View file

@ -273,8 +273,6 @@ PRIVATE
boxes/connection_box.h
boxes/create_poll_box.cpp
boxes/create_poll_box.h
boxes/create_todo_list_box.cpp
boxes/create_todo_list_box.h
boxes/delete_messages_box.cpp
boxes/delete_messages_box.h
boxes/dictionaries_manager.cpp
@ -285,6 +283,8 @@ PRIVATE
boxes/edit_caption_box.h
boxes/edit_privacy_box.cpp
boxes/edit_privacy_box.h
boxes/edit_todo_list_box.cpp
boxes/edit_todo_list_box.h
boxes/gift_credits_box.cpp
boxes/gift_credits_box.h
boxes/gift_premium_box.cpp

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_origin.h"
#include "data/data_histories.h"
#include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_web_page.h"
#include "history/view/controls/history_view_compose_media_edit_manager.h"
#include "history/history.h"
@ -358,4 +359,22 @@ mtpRequestId EditTextMessage(
std::nullopt);
}
void EditTodoList(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail) {
const auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) {
applyUpdates();
done(id);
};
EditMessage(
item,
options,
callback,
fail,
MTP_inputMediaTodo(TodoListDataToMTP(&data)));
}
} // namespace Api

View file

@ -58,4 +58,11 @@ mtpRequestId EditTextMessage(
Fn<void(const QString &error, mtpRequestId requestId)> fail,
bool spoilered);
void EditTodoList(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail);
} // namespace Api

View file

@ -7,8 +7,7 @@ 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 "api/api_editing.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/business/data_shortcut_messages.h" // ShortcutIdToMTP
@ -134,6 +133,23 @@ void TodoLists::create(
});
}
void TodoLists::edit(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void()> done,
Fn<void(QString)> fail) {
EditTodoList(item, data, options, [=](mtpRequestId) {
if (const auto onstack = done) {
onstack();
}
}, [=](const QString &error, mtpRequestId) {
if (const auto onstack = fail) {
onstack(error);
}
});
}
void TodoLists::add(
not_null<HistoryItem*> item,
const std::vector<TodoListItem> &items,

View file

@ -22,6 +22,7 @@ class Session;
namespace Api {
struct SendAction;
struct SendOptions;
class TodoLists final {
public:
@ -32,6 +33,12 @@ public:
SendAction action,
Fn<void()> done,
Fn<void(QString)> fail);
void edit(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void()> done,
Fn<void(QString)> fail);
void add(
not_null<HistoryItem*> item,
const std::vector<TodoListItem> &items,

View file

@ -5,7 +5,7 @@ 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 "boxes/create_todo_list_box.h"
#include "boxes/edit_todo_list_box.h"
#include "base/call_delayed.h"
#include "base/event_filter.h"
@ -85,7 +85,6 @@ private:
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
int id,
TextWithEntities text,
int position,
bool locked);
@ -144,7 +143,7 @@ private:
int id,
TextWithEntities text,
anim::type animated);
void initTaskField(not_null<Task*> task);
void initTaskField(not_null<Task*> task, TextWithEntities text);
void checkLastTask();
void validateState();
void fixAfterErase();
@ -249,7 +248,6 @@ Tasks::Task::Task(
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
int id,
TextWithEntities text,
int position,
bool locked)
: _id(id)
@ -270,11 +268,6 @@ Tasks::Task::Task(
, _limit(session->appConfig().todoListItemTextLimit()) {
InitField(outer, _field, session);
_field->setMaxLength(_limit + kErrorLimit);
_field->setTextWithTags({
text.text,
TextUtilities::ConvertEntitiesToTextTags(text.entities)
});
_field->finishAnimating();
_field->show();
if (locked) {
_field->setDisabled(true);
@ -487,7 +480,7 @@ Tasks::Tasks(
for (const auto &task : existing) {
addTask(task.id, task.text, anim::type::instant);
}
checkLastTask();
validateState();
}
bool Tasks::full() const {
@ -539,10 +532,13 @@ std::vector<TodoListItem> Tasks::toTodoListItems() const {
result.reserve(_list.size());
auto usedId = 0;
for (const auto &task : _list) {
if (const auto id = task->id()) {
usedId = id;
} else if (task->isGood()) {
++usedId;
}
if (task->isGood()) {
result.push_back(task->toTodoListItem(++usedId));
} else if (const auto id = task->id()) {
usedId = std::max(usedId, id);
result.push_back(task->toTodoListItem(usedId));
}
}
return result;
@ -646,17 +642,28 @@ void Tasks::addTask(
_container,
&_controller->session(),
id,
std::move(text),
_position + _list.size() + _destroyed.size(),
locked));
const auto field = _list.back()->field();
if (!locked) {
initTaskField(_list.back().get());
initTaskField(_list.back().get(), std::move(text));
} else {
InitMessageFieldHandlers(
_controller,
field,
Window::GifPauseReason::Layer,
[](not_null<DocumentData*>) { return true; });
field->setTextWithTags({
text.text,
TextUtilities::ConvertEntitiesToTextTags(text.entities)
});
}
field->finishAnimating();
_list.back()->show(animated);
fixShadows();
}
void Tasks::initTaskField(not_null<Task*> task) {
void Tasks::initTaskField(not_null<Task*> task, TextWithEntities text) {
const auto field = task->field();
if (const auto emojiPanel = _emojiPanel) {
const auto emojiToggle = Ui::AddEmojiToggleToField(
@ -686,6 +693,10 @@ void Tasks::initTaskField(not_null<Task*> task) {
}, _emojiPanelLifetime);
}, emojiToggle->lifetime());
}
field->setTextWithTags({
text.text,
TextUtilities::ConvertEntitiesToTextTags(text.entities)
});
field->submits(
) | rpl::start_with_next([=] {
const auto index = findField(field);
@ -789,7 +800,7 @@ void Tasks::checkLastTask() {
} // namespace
CreateTodoListBox::CreateTodoListBox(
EditTodoListBox::EditTodoListBox(
QWidget*,
not_null<Window::SessionController*> controller,
rpl::producer<int> starsRequired,
@ -802,25 +813,41 @@ CreateTodoListBox::CreateTodoListBox(
, _titleLimit(controller->session().appConfig().todoListTitleLimit()) {
}
auto CreateTodoListBox::submitRequests() const -> rpl::producer<Result> {
EditTodoListBox::EditTodoListBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item)
: _controller(controller)
, _sendMenuDetails([] { return SendMenu::Details(); })
, _editingItem(item)
, _titleLimit(controller->session().appConfig().todoListTitleLimit()) {
_controller->session().changes().messageUpdates(
Data::MessageUpdate::Flag::Destroyed
) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
if (update.item == item) {
closeBox();
}
}, lifetime());
}
auto EditTodoListBox::submitRequests() const -> rpl::producer<Result> {
return _submitRequests.events();
}
void CreateTodoListBox::setInnerFocus() {
void EditTodoListBox::setInnerFocus() {
_setInnerFocus();
}
void CreateTodoListBox::submitFailed(const QString &error) {
void EditTodoListBox::submitFailed(const QString &error) {
showToast(error);
}
not_null<Ui::InputField*> CreateTodoListBox::setupTitle(
not_null<Ui::InputField*> EditTodoListBox::setupTitle(
not_null<Ui::VerticalLayout*> container) {
using namespace Settings;
const auto session = &_controller->session();
const auto isPremium = session->premium();
const auto title = container->add(
object_ptr<Ui::InputField>(
container,
@ -860,6 +887,15 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle(
}, emojiToggle->lifetime());
}
const auto media = _editingItem ? _editingItem->media() : nullptr;
if (const auto todolist = media ? media->todolist() : nullptr) {
const auto &text = todolist->title;
title->setTextWithTags({
text.text,
TextUtilities::ConvertEntitiesToTextTags(text.entities)
});
}
const auto warning = CreateWarningLabel(
container,
title,
@ -885,7 +921,7 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle(
return title;
}
object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
object_ptr<Ui::RpWidget> EditTodoListBox::setupContent() {
using namespace Settings;
const auto id = FullMsgId{
@ -906,11 +942,14 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
tr::lng_todo_create_list(),
st::defaultSubsectionTitle),
st::createPollFieldTitlePadding);
const auto media = _editingItem ? _editingItem->media() : nullptr;
const auto todolist = media ? media->todolist() : nullptr;
const auto tasks = lifetime().make_state<Tasks>(
this,
container,
_controller,
_emojiPanel ? _emojiPanel.get() : nullptr);
_emojiPanel ? _emojiPanel.get() : nullptr,
todolist ? todolist->items : std::vector<TodoListItem>());
auto limit = tasks->addedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count);
setCloseByOutsideClick(!count);
@ -944,14 +983,14 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
object_ptr<Ui::Checkbox>(
container,
tr::lng_todo_create_allow_add(tr::now),
true,
!todolist || todolist->othersCanAppend(),
st::defaultCheckbox),
st::createPollCheckboxMargin);
const auto allowMark = container->add(
object_ptr<Ui::Checkbox>(
container,
tr::lng_todo_create_allow_mark(tr::now),
true,
!todolist || todolist->othersCanComplete(),
st::defaultCheckbox),
st::createPollCheckboxMargin);
@ -1019,6 +1058,14 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
showError(tr::lng_todo_choose_tasks);
tasks->focusFirst();
} else if (!*error) {
if (_editingItem) {
sendOptions = {
.scheduled = (_editingItem->isScheduled()
? _editingItem->date()
: TimeId()),
.shortcutId = _editingItem->shortcutId(),
};
}
_submitRequests.fire({ collectResult(), sendOptions });
}
};
@ -1043,9 +1090,13 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
_sendMenuDetails());
};
const auto submit = addButton(
tr::lng_todo_create_button(),
(_editingItem
? tr::lng_settings_save()
: tr::lng_todo_create_button()),
[=] { isNormal ? send({}) : schedule(); });
submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal
submit->setText(PaidSendButtonText(_starsRequired.value(), _editingItem
? tr::lng_settings_save()
: isNormal
? tr::lng_todo_create_button()
: tr::lng_schedule_button()));
const auto sendMenuDetails = [=] {
@ -1062,7 +1113,7 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() {
return result;
}
void CreateTodoListBox::prepare() {
void EditTodoListBox::prepare() {
setTitle(tr::lng_todo_create_title());
const auto inner = setInnerWidget(setupContent());

View file

@ -30,19 +30,23 @@ namespace SendMenu {
struct Details;
} // namespace SendMenu
class CreateTodoListBox : public Ui::BoxContent {
class EditTodoListBox : public Ui::BoxContent {
public:
struct Result {
TodoListData todolist;
Api::SendOptions options;
};
CreateTodoListBox(
EditTodoListBox(
QWidget*,
not_null<Window::SessionController*> controller,
rpl::producer<int> starsRequired,
Api::SendType sendType,
SendMenu::Details sendMenuDetails);
EditTodoListBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
[[nodiscard]] rpl::producer<Result> submitRequests() const;
void submitFailed(const QString &error);
@ -68,6 +72,7 @@ private:
const not_null<Window::SessionController*> _controller;
const Api::SendType _sendType = Api::SendType();
const Fn<SendMenu::Details()> _sendMenuDetails;
HistoryItem *_editingItem = nullptr;
rpl::variable<int> _starsRequired;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus;

View file

@ -2367,6 +2367,10 @@ TextForMimeData MediaTodoList::clipboardText() const {
return TextForMimeData::Rich(std::move(result));
}
bool MediaTodoList::allowsEdit() const {
return parent()->out();
}
bool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) {
return false;
}

View file

@ -625,6 +625,7 @@ public:
TextWithEntities notificationText() const override;
QString pinnedTextSubstring() const override;
TextForMimeData clipboardText() const override;
bool allowsEdit() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;

View file

@ -685,7 +685,8 @@ bool PeerData::canCreatePolls() const {
}
bool PeerData::canCreateTodoLists() const {
return Data::CanSend(this, ChatRestriction::SendPolls) || isUser();
return session().premium()
&& (Data::CanSend(this, ChatRestriction::SendPolls) || isUser());
}
bool PeerData::canCreateTopics() const {

View file

@ -8559,6 +8559,11 @@ void HistoryWidget::editMessage(
} else if (_voiceRecordBar->isActive()) {
controller()->showToast(tr::lng_edit_caption_voice(tr::now));
return;
} else if (const auto media = item->media()) {
if (const auto todolist = media->todolist()) {
Window::PeerMenuEditTodoList(controller(), item);
return;
}
} else if (_composeSearch) {
_composeSearch->hideAnimated();
}

View file

@ -84,6 +84,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/spoiler_mess.h"
#include "webrtc/webrtc_environment.h"
#include "window/window_adaptive.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "mainwindow.h"
#include "styles/style_chat.h"
@ -2952,6 +2953,12 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) {
if (_voiceRecordBar->isActive()) {
_show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice()));
return;
} else if (const auto media = item->media()) {
if (const auto todolist = media->todolist()) {
Assert(_regularWindow != nullptr);
Window::PeerMenuEditTodoList(_regularWindow, item);
return;
}
}
if (!isEditingMessage()) {

View file

@ -352,6 +352,8 @@ ChatWidget::ChatWidget(
_composeControls->editMessage(
fullId,
_inner->getSelectedTextRange(item));
} else if (media->todolist()) {
Window::PeerMenuEditTodoList(controller, item);
}
}
}, _inner->lifetime());

View file

@ -231,6 +231,8 @@ ScheduledWidget::ScheduledWidget(
_composeControls->editMessage(
fullId,
_inner->getSelectedTextRange(item));
} else if (media->todolist()) {
Window::PeerMenuEditTodoList(controller, item);
}
}
}, _inner->lifetime());

View file

@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
@ -399,6 +400,8 @@ ShortcutMessages::ShortcutMessages(
_composeControls->editMessage(
fullId,
_inner->getSelectedTextRange(item));
} else if (media->todolist()) {
Window::PeerMenuEditTodoList(_controller, item);
}
}
}, _inner->lifetime());

View file

@ -29,7 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/moderate_messages_box.h"
#include "boxes/choose_filter_box.h"
#include "boxes/create_poll_box.h"
#include "boxes/create_todo_list_box.h"
#include "boxes/edit_todo_list_box.h"
#include "boxes/pin_messages_box.h"
#include "boxes/premium_limits_box.h"
#include "boxes/report_messages_box.h"
@ -1244,7 +1244,8 @@ void Filler::addCreateTodoList() {
return;
}
const auto can = _topic
? Data::CanSend(_topic, ChatRestriction::SendPolls)
? (_peer->session().premium()
&& Data::CanSend(_topic, ChatRestriction::SendPolls))
: _peer->canCreateTodoLists();
if (!can) {
return;
@ -1973,19 +1974,19 @@ void PeerMenuCreateTodoList(
) | rpl::map([=] {
return peer->starsPerMessageChecked();
});
auto box = Box<CreateTodoListBox>(
auto box = Box<EditTodoListBox>(
controller,
std::move(starsRequired),
sendType,
sendMenuDetails);
struct State {
Fn<void(const CreateTodoListBox::Result &)> create;
Fn<void(const EditTodoListBox::Result &)> create;
SendPaymentHelper sendPayment;
bool lock = false;
};
const auto weak = QPointer<CreateTodoListBox>(box);
const auto weak = QPointer<EditTodoListBox>(box);
const auto state = box->lifetime().make_state<State>();
state->create = [=](const CreateTodoListBox::Result &result) {
state->create = [=](const EditTodoListBox::Result &result) {
const auto withPaymentApproved = crl::guard(weak, [=](int stars) {
if (const auto onstack = state->create) {
auto copy = result;
@ -2028,6 +2029,34 @@ void PeerMenuCreateTodoList(
controller->show(std::move(box), Ui::LayerOption::CloseOther);
}
void PeerMenuEditTodoList(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto media = item->media();
const auto todolist = media ? media->todolist() : nullptr;
if (!todolist) {
return;
} else if (!item->history()->session().premium()) {
PeerMenuTodoWantsPremium(TodoWantsPremium::Add);
return;
}
auto box = Box<EditTodoListBox>(controller, item);
const auto weak = QPointer<EditTodoListBox>(box);
box->submitRequests(
) | rpl::start_with_next([=](const EditTodoListBox::Result &result) {
const auto api = &item->history()->session().api();
api->todoLists().edit(
item,
result.todolist,
result.options,
crl::guard(weak, [=] { weak->closeBox(); }),
crl::guard(weak, [=](const QString &error) {
weak->submitFailed(error);
}));
}, box->lifetime());
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;

View file

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