mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-09-06 12:03:41 +02:00
Add local message editor and manager for custom messages
Co-authored-by: g6582566 <g6582566@morris.umn.edu>
This commit is contained in:
parent
683aef44f9
commit
00840d76e1
9 changed files with 1526 additions and 79 deletions
|
@ -6909,3 +6909,64 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"ayu_AddLocalMessageTitle" = "Add Local Message";
|
||||
"ayu_AddLocalMessagePlaceholder" = "Message text...";
|
||||
"ayu_AddLocalMessageFromPlaceholder" = "From user (optional)...";
|
||||
|
||||
// Local Message Editor
|
||||
"ayu_LocalMessageEditorTitle" = "Local Message Editor";
|
||||
"ayu_LocalMessageText" = "Message Text";
|
||||
"ayu_LocalMessageTextPlaceholder" = "Enter message text...";
|
||||
"ayu_LocalMessageType" = "Message Type";
|
||||
"ayu_LocalMessageIsService" = "Service message";
|
||||
"ayu_LocalMessageServiceText" = "Service text";
|
||||
"ayu_LocalMessageAuthor" = "Author";
|
||||
"ayu_LocalMessageFrom" = "From user";
|
||||
"ayu_LocalMessagePostAuthor" = "Post author (for channels)";
|
||||
"ayu_LocalMessageViaBot" = "Via bot";
|
||||
"ayu_LocalMessageTiming" = "Date & Time";
|
||||
"ayu_LocalMessageDate" = "Date (DD.MM.YYYY)";
|
||||
"ayu_LocalMessageTime" = "Time (HH:MM:SS)";
|
||||
"ayu_LocalMessageWasEdited" = "Show as edited";
|
||||
"ayu_LocalMessageEditDate" = "Edit date (DD.MM.YYYY)";
|
||||
"ayu_LocalMessageEditTime" = "Edit time (HH:MM:SS)";
|
||||
"ayu_LocalMessageFlags" = "Message Flags";
|
||||
"ayu_LocalMessageSilent" = "Silent message";
|
||||
"ayu_LocalMessagePinned" = "Pinned message";
|
||||
"ayu_LocalMessageNoForwards" = "Restrict forwarding";
|
||||
"ayu_LocalMessageInvertMedia" = "Invert media";
|
||||
"ayu_LocalMessageStats" = "Statistics";
|
||||
"ayu_LocalMessageHasViews" = "Show views";
|
||||
"ayu_LocalMessageViews" = "Views count";
|
||||
"ayu_LocalMessageHasShares" = "Show shares";
|
||||
"ayu_LocalMessageShares" = "Shares count";
|
||||
"ayu_LocalMessageReply" = "Reply";
|
||||
"ayu_LocalMessageReplyTo" = "Reply to message ID";
|
||||
"ayu_LocalMessageReplyQuote" = "Reply quote text";
|
||||
"ayu_LocalMessageForward" = "Forward";
|
||||
"ayu_LocalMessageIsForwarded" = "Forwarded message";
|
||||
"ayu_LocalMessageForwardFrom" = "Forward from user";
|
||||
"ayu_LocalMessageForwardFromName" = "Forward from name";
|
||||
"ayu_LocalMessageForwardDate" = "Forward date (DD.MM.YYYY)";
|
||||
"ayu_LocalMessageForwardTime" = "Forward time (HH:MM:SS)";
|
||||
"ayu_LocalMessageForwardPostAuthor" = "Forward post author";
|
||||
"ayu_LocalMessageMedia" = "Media";
|
||||
"ayu_LocalMessageHasMedia" = "Has media";
|
||||
"ayu_LocalMessageMediaPath" = "Media file path";
|
||||
"ayu_LocalMessageMediaCaption" = "Media caption";
|
||||
"ayu_LocalMessageAdvanced" = "Advanced";
|
||||
"ayu_LocalMessageStarsPaid" = "Stars paid";
|
||||
"ayu_LocalMessageEffectId" = "Effect ID";
|
||||
"ayu_LocalMessageGroupedId" = "Grouped ID";
|
||||
|
||||
// Local Messages Manager
|
||||
"ayu_LocalMessagesManager" = "Manage Local Messages";
|
||||
"ayu_LocalMessagesManagerTitle" = "Local Messages Manager";
|
||||
"ayu_LocalMessagesSearch" = "Search";
|
||||
"ayu_LocalMessagesSearchPlaceholder" = "Search messages...";
|
||||
"ayu_LocalMessagesStats" = "{count} local messages";
|
||||
"ayu_LocalMessageCreateNew" = "Create New Message";
|
||||
"ayu_LocalMessagesEmpty" = "No local messages in this chat";
|
||||
"ayu_LocalMessagesNoResults" = "No messages found";
|
||||
"ayu_LocalMessageEmptyText" = "(empty message)";
|
||||
"ayu_LocalMessageUnknownSender" = "Unknown sender";
|
||||
"ayu_LocalMessageEdit" = "Edit";
|
||||
"ayu_LocalMessageDelete" = "Delete";
|
||||
"ayu_LocalMessageDeleteConfirm" = "Delete local message: \"{message}\"?";
|
||||
|
|
803
Telegram/SourceFiles/ayu/ui/boxes/local_message_editor.cpp
Normal file
803
Telegram/SourceFiles/ayu/ui/boxes/local_message_editor.cpp
Normal file
|
@ -0,0 +1,803 @@
|
|||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "ayu/ui/boxes/local_message_editor.h"
|
||||
|
||||
#include "ayu/data/messages_storage.h"
|
||||
#include "ayu/ui/context_menu/context_menu.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "history/history.h"
|
||||
#include "lang_auto.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/fields/masked_input_field.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
|
||||
namespace AyuUi {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxMessageLength = 4096;
|
||||
constexpr auto kMaxCaptionLength = 1024;
|
||||
constexpr auto kMaxPostAuthorLength = 255;
|
||||
|
||||
class LocalMessageEditor {
|
||||
public:
|
||||
LocalMessageEditor(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const LocalMessageData &initialData);
|
||||
|
||||
private:
|
||||
void setupContent();
|
||||
void setupBasicFields();
|
||||
void setupAdvancedFields();
|
||||
void setupMessageTypeSection();
|
||||
void setupAuthorSection();
|
||||
void setupTimingSection();
|
||||
void setupFlagsSection();
|
||||
void setupStatsSection();
|
||||
void setupReplySection();
|
||||
void setupForwardSection();
|
||||
void setupMediaSection();
|
||||
void setupButtons();
|
||||
|
||||
void updateFromUser();
|
||||
void updateDateTime();
|
||||
void updatePreview();
|
||||
void saveMessage();
|
||||
|
||||
[[nodiscard]] MessageFlags buildFlags() const;
|
||||
[[nodiscard]] HistoryItemCommonFields buildFields() const;
|
||||
|
||||
const not_null<Ui::GenericBox*> _box;
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<History*> _history;
|
||||
LocalMessageData _data;
|
||||
|
||||
// UI Elements
|
||||
not_null<Ui::ScrollArea*> _scroll;
|
||||
not_null<Ui::VerticalLayout*> _content;
|
||||
|
||||
// Basic fields
|
||||
Ui::InputField* _textField = nullptr;
|
||||
Ui::InputField* _fromField = nullptr;
|
||||
Ui::UserpicButton* _fromUserpic = nullptr;
|
||||
|
||||
// Message type
|
||||
Ui::Checkbox* _isServiceMessage = nullptr;
|
||||
Ui::SlideWrap<Ui::InputField>* _serviceTextWrap = nullptr;
|
||||
|
||||
// Author section
|
||||
Ui::InputField* _postAuthorField = nullptr;
|
||||
Ui::InputField* _viaBotField = nullptr;
|
||||
|
||||
// Timing section
|
||||
Ui::MaskedInputField* _dateField = nullptr;
|
||||
Ui::MaskedInputField* _timeField = nullptr;
|
||||
Ui::Checkbox* _wasEditedCheck = nullptr;
|
||||
Ui::MaskedInputField* _editDateField = nullptr;
|
||||
Ui::MaskedInputField* _editTimeField = nullptr;
|
||||
|
||||
// Flags section
|
||||
Ui::Checkbox* _silentCheck = nullptr;
|
||||
Ui::Checkbox* _pinnedCheck = nullptr;
|
||||
Ui::Checkbox* _noForwardsCheck = nullptr;
|
||||
Ui::Checkbox* _invertMediaCheck = nullptr;
|
||||
|
||||
// Stats section
|
||||
Ui::Checkbox* _hasViewsCheck = nullptr;
|
||||
Ui::MaskedInputField* _viewsField = nullptr;
|
||||
Ui::Checkbox* _hasSharesCheck = nullptr;
|
||||
Ui::MaskedInputField* _sharesField = nullptr;
|
||||
|
||||
// Reply section
|
||||
Ui::InputField* _replyToField = nullptr;
|
||||
Ui::InputField* _replyQuoteField = nullptr;
|
||||
|
||||
// Forward section
|
||||
Ui::Checkbox* _isForwardedCheck = nullptr;
|
||||
Ui::SlideWrap<Ui::VerticalLayout>* _forwardWrap = nullptr;
|
||||
Ui::InputField* _forwardFromField = nullptr;
|
||||
Ui::InputField* _forwardFromNameField = nullptr;
|
||||
Ui::MaskedInputField* _forwardDateField = nullptr;
|
||||
Ui::MaskedInputField* _forwardTimeField = nullptr;
|
||||
Ui::InputField* _forwardPostAuthorField = nullptr;
|
||||
|
||||
// Media section
|
||||
Ui::Checkbox* _hasMediaCheck = nullptr;
|
||||
Ui::SlideWrap<Ui::VerticalLayout>* _mediaWrap = nullptr;
|
||||
Ui::InputField* _mediaPathField = nullptr;
|
||||
Ui::InputField* _mediaCaptionField = nullptr;
|
||||
|
||||
// Business section
|
||||
Ui::MaskedInputField* _starsPaidField = nullptr;
|
||||
Ui::MaskedInputField* _effectIdField = nullptr;
|
||||
Ui::MaskedInputField* _groupedIdField = nullptr;
|
||||
};
|
||||
|
||||
LocalMessageEditor::LocalMessageEditor(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const LocalMessageData &initialData)
|
||||
: _box(box)
|
||||
, _controller(controller)
|
||||
, _history(history)
|
||||
, _data(initialData)
|
||||
, _scroll(_box->addRow(object_ptr<Ui::ScrollArea>(_box)))
|
||||
, _content(_scroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(_scroll))) {
|
||||
|
||||
// Initialize default values
|
||||
if (_data.date == 0) {
|
||||
_data.date = base::unixtime::now();
|
||||
}
|
||||
if (_data.fromId == 0) {
|
||||
_data.fromId = _history->peer->id;
|
||||
}
|
||||
|
||||
setupContent();
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupContent() {
|
||||
_box->setTitle(tr::ayu_LocalMessageEditorTitle());
|
||||
_box->setWidth(st::boxWideWidth);
|
||||
|
||||
setupBasicFields();
|
||||
setupMessageTypeSection();
|
||||
setupAuthorSection();
|
||||
setupTimingSection();
|
||||
setupFlagsSection();
|
||||
setupStatsSection();
|
||||
setupReplySection();
|
||||
setupForwardSection();
|
||||
setupMediaSection();
|
||||
setupButtons();
|
||||
|
||||
_scroll->setMaxHeight(st::boxMaxListHeight);
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupBasicFields() {
|
||||
// Text field
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageText(),
|
||||
st::boxLabel));
|
||||
|
||||
_textField = _content->add(object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageTextPlaceholder(),
|
||||
_data.text));
|
||||
_textField->setMaxLength(kMaxMessageLength);
|
||||
_textField->heightValue() | rpl::start_with_next([=](int height) {
|
||||
if (height > st::defaultInputField.heightMin * 3) {
|
||||
_textField->setMaxHeight(st::defaultInputField.heightMin * 3);
|
||||
}
|
||||
}, _textField->lifetime());
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupMessageTypeSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageType(),
|
||||
st::boxLabel));
|
||||
|
||||
_isServiceMessage = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageIsService(),
|
||||
_data.isService,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_serviceTextWrap = _content->add(object_ptr<Ui::SlideWrap<Ui::InputField>>(
|
||||
_content,
|
||||
object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageServiceText(),
|
||||
_data.serviceText)));
|
||||
|
||||
_serviceTextWrap->toggle(_data.isService, anim::type::instant);
|
||||
|
||||
_isServiceMessage->checkedChanges() | rpl::start_with_next([=](bool checked) {
|
||||
_data.isService = checked;
|
||||
_serviceTextWrap->toggle(checked, anim::type::normal);
|
||||
}, _isServiceMessage->lifetime());
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupAuthorSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageAuthor(),
|
||||
st::boxLabel));
|
||||
|
||||
// From user field with userpic
|
||||
const auto fromContainer = _content->add(object_ptr<Ui::RpWidget>(_content));
|
||||
fromContainer->resize(fromContainer->width(), st::defaultInputField.height);
|
||||
|
||||
_fromUserpic = Ui::CreateChild<Ui::UserpicButton>(
|
||||
fromContainer,
|
||||
st::defaultUserpicButton);
|
||||
_fromUserpic->move(0, 0);
|
||||
|
||||
_fromField = Ui::CreateChild<Ui::InputField>(
|
||||
fromContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageFrom(),
|
||||
QString());
|
||||
_fromField->move(st::defaultUserpicButton.size.width() + st::boxMediumSkip, 0);
|
||||
|
||||
fromContainer->widthValue() | rpl::start_with_next([=](int width) {
|
||||
_fromField->resize(
|
||||
width - st::defaultUserpicButton.size.width() - st::boxMediumSkip,
|
||||
st::defaultInputField.height);
|
||||
}, fromContainer->lifetime());
|
||||
|
||||
// Post author field (for channels)
|
||||
_postAuthorField = _content->add(object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessagePostAuthor(),
|
||||
_data.postAuthor));
|
||||
_postAuthorField->setMaxLength(kMaxPostAuthorLength);
|
||||
|
||||
// Via bot field
|
||||
_viaBotField = _content->add(object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageViaBot(),
|
||||
QString()));
|
||||
|
||||
updateFromUser();
|
||||
|
||||
_fromField->changes() | rpl::start_with_next([=] {
|
||||
updateFromUser();
|
||||
}, _fromField->lifetime());
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupTimingSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageTiming(),
|
||||
st::boxLabel));
|
||||
|
||||
// Date and time fields
|
||||
const auto dateTimeContainer = _content->add(object_ptr<Ui::RpWidget>(_content));
|
||||
dateTimeContainer->resize(dateTimeContainer->width(), st::defaultInputField.height);
|
||||
|
||||
_dateField = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
dateTimeContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageDate(),
|
||||
QString());
|
||||
_dateField->setMask("99.99.9999");
|
||||
_dateField->move(0, 0);
|
||||
|
||||
_timeField = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
dateTimeContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageTime(),
|
||||
QString());
|
||||
_timeField->setMask("99:99:99");
|
||||
|
||||
dateTimeContainer->widthValue() | rpl::start_with_next([=](int width) {
|
||||
const auto fieldWidth = (width - st::boxMediumSkip) / 2;
|
||||
_dateField->resize(fieldWidth, st::defaultInputField.height);
|
||||
_timeField->resize(fieldWidth, st::defaultInputField.height);
|
||||
_timeField->move(fieldWidth + st::boxMediumSkip, 0);
|
||||
}, dateTimeContainer->lifetime());
|
||||
|
||||
// Edit date section
|
||||
_wasEditedCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageWasEdited(),
|
||||
_data.wasEdited,
|
||||
st::defaultCheckbox));
|
||||
|
||||
const auto editDateContainer = _content->add(object_ptr<Ui::RpWidget>(_content));
|
||||
editDateContainer->resize(editDateContainer->width(), st::defaultInputField.height);
|
||||
|
||||
_editDateField = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
editDateContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageEditDate(),
|
||||
QString());
|
||||
_editDateField->setMask("99.99.9999");
|
||||
_editDateField->move(0, 0);
|
||||
|
||||
_editTimeField = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
editDateContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageEditTime(),
|
||||
QString());
|
||||
_editTimeField->setMask("99:99:99");
|
||||
|
||||
editDateContainer->widthValue() | rpl::start_with_next([=](int width) {
|
||||
const auto fieldWidth = (width - st::boxMediumSkip) / 2;
|
||||
_editDateField->resize(fieldWidth, st::defaultInputField.height);
|
||||
_editTimeField->resize(fieldWidth, st::defaultInputField.height);
|
||||
_editTimeField->move(fieldWidth + st::boxMediumSkip, 0);
|
||||
}, editDateContainer->lifetime());
|
||||
|
||||
editDateContainer->setVisible(_data.wasEdited);
|
||||
|
||||
_wasEditedCheck->checkedChanges() | rpl::start_with_next([=](bool checked) {
|
||||
_data.wasEdited = checked;
|
||||
editDateContainer->setVisible(checked);
|
||||
}, _wasEditedCheck->lifetime());
|
||||
|
||||
updateDateTime();
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupFlagsSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageFlags(),
|
||||
st::boxLabel));
|
||||
|
||||
_silentCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageSilent(),
|
||||
_data.silent,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_pinnedCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessagePinned(),
|
||||
_data.pinned,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_noForwardsCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageNoForwards(),
|
||||
_data.noForwards,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_invertMediaCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageInvertMedia(),
|
||||
_data.invertMedia,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupStatsSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageStats(),
|
||||
st::boxLabel));
|
||||
|
||||
// Views
|
||||
_hasViewsCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageHasViews(),
|
||||
_data.hasViews,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_viewsField = _content->add(object_ptr<Ui::MaskedInputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageViews(),
|
||||
QString::number(_data.views)));
|
||||
_viewsField->setMask("999999999");
|
||||
_viewsField->setVisible(_data.hasViews);
|
||||
|
||||
_hasViewsCheck->checkedChanges() | rpl::start_with_next([=](bool checked) {
|
||||
_data.hasViews = checked;
|
||||
_viewsField->setVisible(checked);
|
||||
}, _hasViewsCheck->lifetime());
|
||||
|
||||
// Shares
|
||||
_hasSharesCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageHasShares(),
|
||||
_data.hasShares,
|
||||
st::defaultCheckbox));
|
||||
|
||||
_sharesField = _content->add(object_ptr<Ui::MaskedInputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageShares(),
|
||||
QString::number(_data.shares)));
|
||||
_sharesField->setMask("999999999");
|
||||
_sharesField->setVisible(_data.hasShares);
|
||||
|
||||
_hasSharesCheck->checkedChanges() | rpl::start_with_next([=](bool checked) {
|
||||
_data.hasShares = checked;
|
||||
_sharesField->setVisible(checked);
|
||||
}, _hasSharesCheck->lifetime());
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupReplySection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageReply(),
|
||||
st::boxLabel));
|
||||
|
||||
_replyToField = _content->add(object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageReplyTo(),
|
||||
QString()));
|
||||
|
||||
_replyQuoteField = _content->add(object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageReplyQuote(),
|
||||
QString()));
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupForwardSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageForward(),
|
||||
st::boxLabel));
|
||||
|
||||
_isForwardedCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageIsForwarded(),
|
||||
_data.isForwarded,
|
||||
st::defaultCheckbox));
|
||||
|
||||
const auto forwardContent = _content->add(object_ptr<Ui::VerticalLayout>(_content));
|
||||
_forwardWrap = _content->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_content,
|
||||
object_ptr<Ui::VerticalLayout>(_content)));
|
||||
|
||||
auto forwardFields = _forwardWrap->entity();
|
||||
|
||||
_forwardFromField = forwardFields->add(object_ptr<Ui::InputField>(
|
||||
forwardFields,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageForwardFrom(),
|
||||
QString()));
|
||||
|
||||
_forwardFromNameField = forwardFields->add(object_ptr<Ui::InputField>(
|
||||
forwardFields,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageForwardFromName(),
|
||||
_data.forwardFromName));
|
||||
|
||||
// Forward date and time
|
||||
const auto forwardDateContainer = forwardFields->add(object_ptr<Ui::RpWidget>(forwardFields));
|
||||
forwardDateContainer->resize(forwardDateContainer->width(), st::defaultInputField.height);
|
||||
|
||||
_forwardDateField = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
forwardDateContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageForwardDate(),
|
||||
QString());
|
||||
_forwardDateField->setMask("99.99.9999");
|
||||
_forwardDateField->move(0, 0);
|
||||
|
||||
_forwardTimeField = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
forwardDateContainer,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageForwardTime(),
|
||||
QString());
|
||||
_forwardTimeField->setMask("99:99:99");
|
||||
|
||||
forwardDateContainer->widthValue() | rpl::start_with_next([=](int width) {
|
||||
const auto fieldWidth = (width - st::boxMediumSkip) / 2;
|
||||
_forwardDateField->resize(fieldWidth, st::defaultInputField.height);
|
||||
_forwardTimeField->resize(fieldWidth, st::defaultInputField.height);
|
||||
_forwardTimeField->move(fieldWidth + st::boxMediumSkip, 0);
|
||||
}, forwardDateContainer->lifetime());
|
||||
|
||||
_forwardPostAuthorField = forwardFields->add(object_ptr<Ui::InputField>(
|
||||
forwardFields,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageForwardPostAuthor(),
|
||||
_data.forwardPostAuthor));
|
||||
|
||||
_forwardWrap->toggle(_data.isForwarded, anim::type::instant);
|
||||
|
||||
_isForwardedCheck->checkedChanges() | rpl::start_with_next([=](bool checked) {
|
||||
_data.isForwarded = checked;
|
||||
_forwardWrap->toggle(checked, anim::type::normal);
|
||||
}, _isForwardedCheck->lifetime());
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupMediaSection() {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageMedia(),
|
||||
st::boxLabel));
|
||||
|
||||
_hasMediaCheck = _content->add(object_ptr<Ui::Checkbox>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageHasMedia(),
|
||||
_data.hasMedia,
|
||||
st::defaultCheckbox));
|
||||
|
||||
const auto mediaContent = _content->add(object_ptr<Ui::VerticalLayout>(_content));
|
||||
_mediaWrap = _content->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_content,
|
||||
object_ptr<Ui::VerticalLayout>(_content)));
|
||||
|
||||
auto mediaFields = _mediaWrap->entity();
|
||||
|
||||
_mediaPathField = mediaFields->add(object_ptr<Ui::InputField>(
|
||||
mediaFields,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageMediaPath(),
|
||||
_data.mediaPath));
|
||||
|
||||
_mediaCaptionField = mediaFields->add(object_ptr<Ui::InputField>(
|
||||
mediaFields,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageMediaCaption(),
|
||||
_data.mediaCaption));
|
||||
_mediaCaptionField->setMaxLength(kMaxCaptionLength);
|
||||
|
||||
_mediaWrap->toggle(_data.hasMedia, anim::type::instant);
|
||||
|
||||
_hasMediaCheck->checkedChanges() | rpl::start_with_next([=](bool checked) {
|
||||
_data.hasMedia = checked;
|
||||
_mediaWrap->toggle(checked, anim::type::normal);
|
||||
}, _hasMediaCheck->lifetime());
|
||||
|
||||
// Advanced fields
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageAdvanced(),
|
||||
st::boxLabel));
|
||||
|
||||
_starsPaidField = _content->add(object_ptr<Ui::MaskedInputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageStarsPaid(),
|
||||
QString::number(_data.starsPaid)));
|
||||
_starsPaidField->setMask("999999999");
|
||||
|
||||
_effectIdField = _content->add(object_ptr<Ui::MaskedInputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageEffectId(),
|
||||
QString::number(_data.effectId)));
|
||||
_effectIdField->setMask("999999999999999999");
|
||||
|
||||
_groupedIdField = _content->add(object_ptr<Ui::MaskedInputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessageGroupedId(),
|
||||
QString::number(_data.groupedId)));
|
||||
_groupedIdField->setMask("999999999999999999");
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
}
|
||||
|
||||
void LocalMessageEditor::setupButtons() {
|
||||
_box->addButton(tr::lng_box_ok(), [=] {
|
||||
saveMessage();
|
||||
_box->closeBox();
|
||||
});
|
||||
|
||||
_box->addButton(tr::lng_cancel(), [=] {
|
||||
_box->closeBox();
|
||||
});
|
||||
|
||||
_box->setFocusCallback([=] {
|
||||
_textField->setFocusFast();
|
||||
});
|
||||
}
|
||||
|
||||
void LocalMessageEditor::updateFromUser() {
|
||||
const auto text = _fromField->getLastText().trimmed();
|
||||
if (text.isEmpty()) {
|
||||
_data.fromId = _history->peer->id;
|
||||
if (const auto user = _history->peer->asUser()) {
|
||||
_fromUserpic->showUser(user);
|
||||
} else {
|
||||
_fromUserpic->showPeer(_history->peer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find user by username or name
|
||||
const auto &owner = _controller->session().data();
|
||||
if (const auto user = owner.userByUsername(text)) {
|
||||
_data.fromId = user->id;
|
||||
_fromUserpic->showUser(user);
|
||||
} else {
|
||||
// Search by name in chat participants
|
||||
if (const auto chat = _history->peer->asChat()) {
|
||||
for (const auto &participant : chat->participants) {
|
||||
if (const auto user = owner.user(participant.userId())) {
|
||||
if (user->name().toLower().contains(text.toLower())) {
|
||||
_data.fromId = user->id;
|
||||
_fromUserpic->showUser(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If not found, keep current peer
|
||||
_fromUserpic->showPeer(_history->peer);
|
||||
}
|
||||
}
|
||||
|
||||
void LocalMessageEditor::updateDateTime() {
|
||||
const auto dateTime = QDateTime::fromSecsSinceEpoch(_data.date);
|
||||
_dateField->setText(dateTime.date().toString("dd.MM.yyyy"));
|
||||
_timeField->setText(dateTime.time().toString("hh:mm:ss"));
|
||||
|
||||
if (_data.wasEdited && _data.editDate > 0) {
|
||||
const auto editDateTime = QDateTime::fromSecsSinceEpoch(_data.editDate);
|
||||
_editDateField->setText(editDateTime.date().toString("dd.MM.yyyy"));
|
||||
_editTimeField->setText(editDateTime.time().toString("hh:mm:ss"));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalMessageEditor::updatePreview() {
|
||||
// TODO: Add preview functionality
|
||||
}
|
||||
|
||||
void LocalMessageEditor::saveMessage() {
|
||||
// Collect data from fields
|
||||
_data.text = _textField->getLastText();
|
||||
_data.postAuthor = _postAuthorField->getLastText();
|
||||
_data.serviceText = _serviceTextWrap->entity()->getLastText();
|
||||
|
||||
// Parse date and time
|
||||
const auto dateText = _dateField->getLastText();
|
||||
const auto timeText = _timeField->getLastText();
|
||||
if (!dateText.isEmpty() && !timeText.isEmpty()) {
|
||||
const auto dateTime = QDateTime::fromString(
|
||||
dateText + " " + timeText,
|
||||
"dd.MM.yyyy hh:mm:ss");
|
||||
if (dateTime.isValid()) {
|
||||
_data.date = dateTime.toSecsSinceEpoch();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse edit date if applicable
|
||||
if (_data.wasEdited) {
|
||||
const auto editDateText = _editDateField->getLastText();
|
||||
const auto editTimeText = _editTimeField->getLastText();
|
||||
if (!editDateText.isEmpty() && !editTimeText.isEmpty()) {
|
||||
const auto editDateTime = QDateTime::fromString(
|
||||
editDateText + " " + editTimeText,
|
||||
"dd.MM.yyyy hh:mm:ss");
|
||||
if (editDateTime.isValid()) {
|
||||
_data.editDate = editDateTime.toSecsSinceEpoch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect flags
|
||||
_data.silent = _silentCheck->checked();
|
||||
_data.pinned = _pinnedCheck->checked();
|
||||
_data.noForwards = _noForwardsCheck->checked();
|
||||
_data.invertMedia = _invertMediaCheck->checked();
|
||||
|
||||
// Collect stats
|
||||
if (_data.hasViews) {
|
||||
_data.views = _viewsField->getLastText().toInt();
|
||||
}
|
||||
if (_data.hasShares) {
|
||||
_data.shares = _sharesField->getLastText().toInt();
|
||||
}
|
||||
|
||||
// Collect advanced fields
|
||||
_data.starsPaid = _starsPaidField->getLastText().toInt();
|
||||
_data.effectId = _effectIdField->getLastText().toULongLong();
|
||||
_data.groupedId = _groupedIdField->getLastText().toULongLong();
|
||||
|
||||
// Create the message
|
||||
const auto localId = _controller->session().data().nextLocalMessageId();
|
||||
const auto fields = buildFields();
|
||||
|
||||
HistoryItem* localItem = nullptr;
|
||||
|
||||
if (_data.isService) {
|
||||
// Create service message
|
||||
// TODO: Implement service message creation
|
||||
} else {
|
||||
// Create regular message
|
||||
localItem = _history->addNewLocalMessage(
|
||||
HistoryItemCommonFields{
|
||||
.id = localId,
|
||||
.flags = buildFlags(),
|
||||
.from = _data.fromId,
|
||||
.date = _data.date,
|
||||
.postAuthor = _data.postAuthor,
|
||||
.groupedId = _data.groupedId,
|
||||
.effectId = _data.effectId,
|
||||
},
|
||||
TextWithEntities{ _data.text },
|
||||
MTP_messageMediaEmpty());
|
||||
}
|
||||
|
||||
if (localItem) {
|
||||
// Add to local messages database
|
||||
AyuMessages::addLocalMessage(localItem);
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlags LocalMessageEditor::buildFlags() const {
|
||||
MessageFlags flags = MessageFlag::HasFromId | MessageFlag::Local;
|
||||
|
||||
if (_data.silent) flags |= MessageFlag::Silent;
|
||||
if (_data.pinned) flags |= MessageFlag::Pinned;
|
||||
if (_data.noForwards) flags |= MessageFlag::NoForwards;
|
||||
if (_data.invertMedia) flags |= MessageFlag::InvertMedia;
|
||||
if (_data.hasViews) flags |= MessageFlag::HasViews;
|
||||
if (!_data.postAuthor.isEmpty()) flags |= MessageFlag::HasPostAuthor;
|
||||
if (_data.wasEdited) flags |= MessageFlag::HideEdited; // Will show as edited
|
||||
if (_data.isForwarded) {
|
||||
// TODO: Add forward flags
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
HistoryItemCommonFields LocalMessageEditor::buildFields() const {
|
||||
return HistoryItemCommonFields{
|
||||
.id = 0, // Will be set by caller
|
||||
.flags = buildFlags(),
|
||||
.from = _data.fromId,
|
||||
.date = _data.date,
|
||||
.starsPaid = _data.starsPaid,
|
||||
.viaBotId = UserId(_data.viaBotId),
|
||||
.postAuthor = _data.postAuthor,
|
||||
.groupedId = _data.groupedId,
|
||||
.effectId = _data.effectId,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LocalMessageEditorBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const LocalMessageData &initialData) {
|
||||
const auto editor = box->lifetime().make_state<LocalMessageEditor>(
|
||||
box,
|
||||
controller,
|
||||
history,
|
||||
initialData);
|
||||
}
|
||||
|
||||
} // namespace AyuUi
|
80
Telegram/SourceFiles/ayu/ui/boxes/local_message_editor.h
Normal file
80
Telegram/SourceFiles/ayu/ui/boxes/local_message_editor.h
Normal file
|
@ -0,0 +1,80 @@
|
|||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "history/history_item.h"
|
||||
#include "data/data_msg_id.h"
|
||||
|
||||
namespace Ui {
|
||||
class InputField;
|
||||
class MaskedInputField;
|
||||
class Checkbox;
|
||||
class Dropdown;
|
||||
class DateTimeEdit;
|
||||
class UserpicButton;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
class History;
|
||||
class HistoryItem;
|
||||
class PeerData;
|
||||
|
||||
namespace AyuUi {
|
||||
|
||||
struct LocalMessageData {
|
||||
QString text;
|
||||
PeerId fromId = 0;
|
||||
QString postAuthor;
|
||||
TimeId date = 0;
|
||||
bool silent = false;
|
||||
bool pinned = false;
|
||||
bool hasViews = false;
|
||||
int views = 0;
|
||||
bool hasShares = false;
|
||||
int shares = 0;
|
||||
bool noForwards = false;
|
||||
bool invertMedia = false;
|
||||
FullReplyTo replyTo;
|
||||
uint64 groupedId = 0;
|
||||
UserId viaBotId = 0;
|
||||
EffectId effectId = 0;
|
||||
bool isService = false;
|
||||
QString serviceText;
|
||||
|
||||
// Media parameters
|
||||
bool hasMedia = false;
|
||||
QString mediaPath;
|
||||
QString mediaCaption;
|
||||
|
||||
// Forward parameters
|
||||
bool isForwarded = false;
|
||||
PeerId forwardFromId = 0;
|
||||
QString forwardFromName;
|
||||
TimeId forwardDate = 0;
|
||||
QString forwardPostAuthor;
|
||||
bool forwardSavedFromPeer = false;
|
||||
|
||||
// Edit parameters
|
||||
bool wasEdited = false;
|
||||
TimeId editDate = 0;
|
||||
|
||||
// Business parameters
|
||||
BusinessShortcutId shortcutId = 0;
|
||||
int starsPaid = 0;
|
||||
};
|
||||
|
||||
void LocalMessageEditorBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const LocalMessageData &initialData = {});
|
||||
|
||||
} // namespace AyuUi
|
439
Telegram/SourceFiles/ayu/ui/boxes/local_messages_manager.cpp
Normal file
439
Telegram/SourceFiles/ayu/ui/boxes/local_messages_manager.cpp
Normal file
|
@ -0,0 +1,439 @@
|
|||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "ayu/ui/boxes/local_messages_manager.h"
|
||||
|
||||
#include "ayu/ui/boxes/local_message_editor.h"
|
||||
#include "ayu/data/messages_storage.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "lang_auto.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
|
||||
namespace AyuUi {
|
||||
|
||||
namespace {
|
||||
|
||||
class LocalMessageRow : public Ui::RpWidget {
|
||||
public:
|
||||
LocalMessageRow(
|
||||
QWidget *parent,
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<Window::SessionController*> controller);
|
||||
|
||||
[[nodiscard]] not_null<HistoryItem*> item() const {
|
||||
return _item;
|
||||
}
|
||||
|
||||
rpl::producer<> editRequests() const {
|
||||
return _editRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> deleteRequests() const {
|
||||
return _deleteRequests.events();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
private:
|
||||
void setupContent();
|
||||
void showContextMenu(QPoint position);
|
||||
|
||||
const not_null<HistoryItem*> _item;
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
|
||||
Ui::UserpicButton* _userpic = nullptr;
|
||||
QString _dateText;
|
||||
QString _previewText;
|
||||
|
||||
rpl::event_stream<> _editRequests;
|
||||
rpl::event_stream<> _deleteRequests;
|
||||
};
|
||||
|
||||
LocalMessageRow::LocalMessageRow(
|
||||
QWidget *parent,
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _item(item)
|
||||
, _controller(controller) {
|
||||
setupContent();
|
||||
}
|
||||
|
||||
void LocalMessageRow::setupContent() {
|
||||
const auto height = st::defaultPeerListItem.height;
|
||||
resize(width(), height);
|
||||
|
||||
_userpic = Ui::CreateChild<Ui::UserpicButton>(
|
||||
this,
|
||||
st::defaultUserpicButton);
|
||||
_userpic->move(st::defaultPeerListItem.photoPosition);
|
||||
|
||||
if (const auto from = _item->from()) {
|
||||
if (const auto user = _controller->session().data().user(from)) {
|
||||
_userpic->showUser(user);
|
||||
} else {
|
||||
_userpic->showPeer(_item->history()->peer);
|
||||
}
|
||||
} else {
|
||||
_userpic->showPeer(_item->history()->peer);
|
||||
}
|
||||
|
||||
// Format date
|
||||
const auto dateTime = QDateTime::fromSecsSinceEpoch(_item->date());
|
||||
_dateText = dateTime.toString("dd.MM.yyyy hh:mm");
|
||||
|
||||
// Format preview text
|
||||
const auto text = _item->originalText().text;
|
||||
_previewText = text.left(100);
|
||||
if (text.length() > 100) {
|
||||
_previewText += "...";
|
||||
}
|
||||
if (_previewText.isEmpty()) {
|
||||
_previewText = tr::ayu_LocalMessageEmptyText(tr::now);
|
||||
}
|
||||
}
|
||||
|
||||
void LocalMessageRow::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
p.fillRect(e->rect(), st::windowBg);
|
||||
|
||||
const auto left = st::defaultPeerListItem.namePosition.x();
|
||||
const auto top = st::defaultPeerListItem.namePosition.y();
|
||||
const auto available = width() - left - st::boxLayerScroll.width;
|
||||
|
||||
// Draw sender name
|
||||
p.setPen(st::contactsNameFg);
|
||||
p.setFont(st::defaultPeerListItem.nameStyle.font);
|
||||
|
||||
QString fromName;
|
||||
if (const auto from = _item->from()) {
|
||||
if (const auto user = _controller->session().data().user(from)) {
|
||||
fromName = user->name();
|
||||
} else {
|
||||
fromName = tr::ayu_LocalMessageUnknownSender(tr::now);
|
||||
}
|
||||
} else {
|
||||
fromName = _item->history()->peer->name();
|
||||
}
|
||||
|
||||
const auto nameWidth = available / 2;
|
||||
p.drawText(
|
||||
QRect(left, top, nameWidth, st::defaultPeerListItem.nameStyle.font->height),
|
||||
fromName,
|
||||
QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
|
||||
|
||||
// Draw date
|
||||
p.setPen(st::contactsStatusFg);
|
||||
p.setFont(st::defaultPeerListItem.statusFont);
|
||||
const auto dateWidth = available / 4;
|
||||
p.drawText(
|
||||
QRect(left + nameWidth, top, dateWidth, st::defaultPeerListItem.statusFont->height),
|
||||
_dateText,
|
||||
QTextOption(Qt::AlignRight | Qt::AlignVCenter));
|
||||
|
||||
// Draw preview text
|
||||
const auto previewTop = top + st::defaultPeerListItem.nameStyle.font->height + st::lineWidth;
|
||||
p.drawText(
|
||||
QRect(left, previewTop, available, st::defaultPeerListItem.statusFont->height),
|
||||
_previewText,
|
||||
QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
|
||||
|
||||
// Draw flags indicators
|
||||
const auto flagsTop = previewTop + st::defaultPeerListItem.statusFont->height + st::lineWidth;
|
||||
QString flagsText;
|
||||
if (_item->out()) flagsText += "📤 ";
|
||||
if (_item->isSilent()) flagsText += "🔇 ";
|
||||
if (_item->isPinned()) flagsText += "📌 ";
|
||||
if (_item->hasViews()) flagsText += QString("👁 %1 ").arg(_item->viewsCount());
|
||||
|
||||
if (!flagsText.isEmpty()) {
|
||||
p.setPen(st::contactsStatusFgOnline);
|
||||
p.setFont(st::normalFont);
|
||||
p.drawText(
|
||||
QRect(left, flagsTop, available, st::normalFont->height),
|
||||
flagsText.trimmed(),
|
||||
QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalMessageRow::mousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
_editRequests.fire({});
|
||||
}
|
||||
RpWidget::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void LocalMessageRow::contextMenuEvent(QContextMenuEvent *e) {
|
||||
showContextMenu(e->globalPos());
|
||||
}
|
||||
|
||||
void LocalMessageRow::showContextMenu(QPoint position) {
|
||||
auto menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::popupMenuWithIcons);
|
||||
|
||||
menu->addAction(
|
||||
tr::ayu_LocalMessageEdit(tr::now),
|
||||
[=] { _editRequests.fire({}); },
|
||||
&st::menuIconEdit);
|
||||
|
||||
menu->addAction(
|
||||
tr::ayu_LocalMessageDelete(tr::now),
|
||||
[=] { _deleteRequests.fire({}); },
|
||||
&st::menuIconDelete);
|
||||
|
||||
menu->popup(position);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LocalMessagesManager::LocalMessagesManager(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history)
|
||||
: _box(box)
|
||||
, _controller(controller)
|
||||
, _history(history)
|
||||
, _scroll(_box->addRow(object_ptr<Ui::ScrollArea>(_box)))
|
||||
, _content(_scroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(_scroll))) {
|
||||
setupContent();
|
||||
}
|
||||
|
||||
void LocalMessagesManager::setupContent() {
|
||||
_box->setTitle(tr::ayu_LocalMessagesManagerTitle());
|
||||
_box->setWidth(st::boxWideWidth);
|
||||
|
||||
setupHeader();
|
||||
setupMessagesList();
|
||||
setupButtons();
|
||||
|
||||
_scroll->setMaxHeight(st::boxMaxListHeight);
|
||||
refreshMessagesList();
|
||||
}
|
||||
|
||||
void LocalMessagesManager::setupHeader() {
|
||||
// Search field
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessagesSearch(),
|
||||
st::boxLabel));
|
||||
|
||||
_searchField = _content->add(object_ptr<Ui::InputField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::ayu_LocalMessagesSearchPlaceholder(),
|
||||
QString()));
|
||||
|
||||
_searchField->changes() | rpl::start_with_next([=] {
|
||||
refreshMessagesList();
|
||||
}, _searchField->lifetime());
|
||||
|
||||
// Stats
|
||||
const auto statsLabel = _content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
QString(),
|
||||
st::boxDividerLabel));
|
||||
|
||||
const auto updateStats = [=] {
|
||||
const auto total = _localMessages.size();
|
||||
statsLabel->setText(tr::ayu_LocalMessagesStats(tr::now, lt_count, total));
|
||||
};
|
||||
|
||||
// Add separator
|
||||
_content->add(object_ptr<Ui::BoxContentDivider>(_content));
|
||||
|
||||
// Create new message button
|
||||
const auto createButton = _content->add(object_ptr<Ui::RoundButton>(
|
||||
_content,
|
||||
tr::ayu_LocalMessageCreateNew(),
|
||||
st::defaultActiveButton));
|
||||
|
||||
createButton->setClickedCallback([=] {
|
||||
createNewMessage();
|
||||
});
|
||||
|
||||
_content->add(object_ptr<Ui::FixedHeightWidget>(_content, st::boxMediumSkip));
|
||||
|
||||
// Update stats initially
|
||||
rpl::combine(
|
||||
rpl::single(rpl::empty_value()),
|
||||
_searchField->changes()
|
||||
) | rpl::start_with_next([=] {
|
||||
updateStats();
|
||||
}, _content->lifetime());
|
||||
}
|
||||
|
||||
void LocalMessagesManager::setupMessagesList() {
|
||||
// Messages will be added dynamically in refreshMessagesList()
|
||||
}
|
||||
|
||||
void LocalMessagesManager::setupButtons() {
|
||||
_box->addButton(tr::lng_close(), [=] {
|
||||
_box->closeBox();
|
||||
});
|
||||
}
|
||||
|
||||
void LocalMessagesManager::refreshMessagesList() {
|
||||
// Clear existing rows
|
||||
while (_content->count() > 5) { // Keep header elements
|
||||
delete _content->widgetAt(_content->count() - 1);
|
||||
}
|
||||
|
||||
_localMessages.clear();
|
||||
|
||||
// Get local messages for this peer
|
||||
if (!AyuMessages::hasLocalMessages(_history->peer, 0)) {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
tr::ayu_LocalMessagesEmpty(),
|
||||
st::boxDividerLabel));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto messages = AyuMessages::getLocalMessages(_history->peer, 0, 0, 0, 1000);
|
||||
const auto searchText = _searchField->getLastText().toLower();
|
||||
|
||||
// Convert to HistoryItem and filter
|
||||
for (const auto &messageData : messages) {
|
||||
// Find the actual HistoryItem by ID
|
||||
const auto item = _history->owner().message(_history->peer->id, messageData.messageId);
|
||||
if (!item || !item->isLocal()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (!searchText.isEmpty()) {
|
||||
const auto text = item->originalText().text.toLower();
|
||||
const auto fromName = item->from()
|
||||
? _controller->session().data().user(item->from())->name().toLower()
|
||||
: _history->peer->name().toLower();
|
||||
|
||||
if (!text.contains(searchText) && !fromName.contains(searchText)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_localMessages.push_back(item);
|
||||
}
|
||||
|
||||
// Sort by date (newest first)
|
||||
std::sort(_localMessages.begin(), _localMessages.end(), [](
|
||||
not_null<HistoryItem*> a,
|
||||
not_null<HistoryItem*> b) {
|
||||
return a->date() > b->date();
|
||||
});
|
||||
|
||||
// Add message rows
|
||||
for (const auto &item : _localMessages) {
|
||||
addMessageRow(item);
|
||||
}
|
||||
|
||||
if (_localMessages.empty()) {
|
||||
_content->add(object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
searchText.isEmpty()
|
||||
? tr::ayu_LocalMessagesEmpty()
|
||||
: tr::ayu_LocalMessagesNoResults(),
|
||||
st::boxDividerLabel));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalMessagesManager::addMessageRow(not_null<HistoryItem*> item) {
|
||||
const auto row = _content->add(object_ptr<LocalMessageRow>(
|
||||
_content,
|
||||
item,
|
||||
_controller));
|
||||
|
||||
row->editRequests() | rpl::start_with_next([=] {
|
||||
editMessage(item);
|
||||
}, row->lifetime());
|
||||
|
||||
row->deleteRequests() | rpl::start_with_next([=] {
|
||||
deleteMessage(item);
|
||||
}, row->lifetime());
|
||||
}
|
||||
|
||||
void LocalMessagesManager::editMessage(not_null<HistoryItem*> item) {
|
||||
// Convert HistoryItem to LocalMessageData
|
||||
LocalMessageData data;
|
||||
data.text = item->originalText().text;
|
||||
data.fromId = item->from();
|
||||
data.postAuthor = item->postAuthor();
|
||||
data.date = item->date();
|
||||
data.silent = item->isSilent();
|
||||
data.pinned = item->isPinned();
|
||||
data.hasViews = item->hasViews();
|
||||
data.views = item->viewsCount();
|
||||
data.noForwards = item->forbidsForward();
|
||||
// ... populate other fields as needed
|
||||
|
||||
_controller->show(Box(
|
||||
LocalMessageEditorBox,
|
||||
_controller,
|
||||
_history,
|
||||
data));
|
||||
}
|
||||
|
||||
void LocalMessagesManager::deleteMessage(not_null<HistoryItem*> item) {
|
||||
const auto text = tr::ayu_LocalMessageDeleteConfirm(
|
||||
tr::now,
|
||||
lt_message,
|
||||
item->originalText().text.left(50));
|
||||
|
||||
_controller->show(Ui::MakeConfirmBox({
|
||||
.text = text,
|
||||
.confirmed = [=] {
|
||||
// Remove from history
|
||||
_history->destroyMessage(item);
|
||||
|
||||
// Refresh the list
|
||||
refreshMessagesList();
|
||||
},
|
||||
.confirmText = tr::lng_box_delete(),
|
||||
}));
|
||||
}
|
||||
|
||||
void LocalMessagesManager::createNewMessage() {
|
||||
_controller->show(Box(
|
||||
LocalMessageEditorBox,
|
||||
_controller,
|
||||
_history,
|
||||
LocalMessageData{}));
|
||||
}
|
||||
|
||||
void LocalMessagesManagerBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history) {
|
||||
const auto manager = box->lifetime().make_state<LocalMessagesManager>(
|
||||
box,
|
||||
controller,
|
||||
history);
|
||||
}
|
||||
|
||||
} // namespace AyuUi
|
65
Telegram/SourceFiles/ayu/ui/boxes/local_messages_manager.h
Normal file
65
Telegram/SourceFiles/ayu/ui/boxes/local_messages_manager.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
|
||||
namespace Ui {
|
||||
class RoundButton;
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
class History;
|
||||
class HistoryItem;
|
||||
class PeerData;
|
||||
|
||||
namespace AyuUi {
|
||||
|
||||
struct LocalMessageData;
|
||||
|
||||
class LocalMessagesManager {
|
||||
public:
|
||||
LocalMessagesManager(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history);
|
||||
|
||||
private:
|
||||
void setupContent();
|
||||
void setupHeader();
|
||||
void setupMessagesList();
|
||||
void setupButtons();
|
||||
|
||||
void refreshMessagesList();
|
||||
void addMessageRow(not_null<HistoryItem*> item);
|
||||
void editMessage(not_null<HistoryItem*> item);
|
||||
void deleteMessage(not_null<HistoryItem*> item);
|
||||
void createNewMessage();
|
||||
|
||||
const not_null<Ui::GenericBox*> _box;
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<History*> _history;
|
||||
|
||||
not_null<Ui::ScrollArea*> _scroll;
|
||||
not_null<Ui::VerticalLayout*> _content;
|
||||
Ui::InputField* _searchField = nullptr;
|
||||
|
||||
std::vector<not_null<HistoryItem*>> _localMessages;
|
||||
};
|
||||
|
||||
void LocalMessagesManagerBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history);
|
||||
|
||||
} // namespace AyuUi
|
|
@ -24,6 +24,8 @@
|
|||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/boxes/generic_box.h"
|
||||
#include "ayu/ui/boxes/local_message_editor.h"
|
||||
#include "ayu/ui/boxes/local_messages_manager.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
|
||||
#include "ayu/ui/message_history/history_section.h"
|
||||
|
@ -565,87 +567,76 @@ void AddLocalMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
|
|||
menu->addAction(
|
||||
tr::ayu_AddLocalMessage(tr::now),
|
||||
[=]() {
|
||||
const auto showBox = [=](const QString &text, PeerId fromId) {
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем локальное сообщение
|
||||
const auto localId = history->session().data().nextLocalMessageId();
|
||||
const auto localItem = history->addNewLocalMessage({
|
||||
.id = localId,
|
||||
.flags = MessageFlag::HasFromId,
|
||||
.from = fromId,
|
||||
.date = base::unixtime::now(),
|
||||
}, TextWithEntities{ text }, MTP_messageMediaEmpty());
|
||||
|
||||
// Добавляем в базу данных локальных сообщений
|
||||
AyuMessages::addLocalMessage(localItem);
|
||||
};
|
||||
|
||||
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->setTitle(tr::ayu_AddLocalMessageTitle());
|
||||
|
||||
// Поле для ввода текста сообщения
|
||||
const auto textField = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::defaultInputField,
|
||||
tr::ayu_AddLocalMessagePlaceholder(),
|
||||
QString()));
|
||||
textField->setMaxLength(4096);
|
||||
|
||||
// Поле для выбора отправителя
|
||||
const auto fromField = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::defaultInputField,
|
||||
tr::ayu_AddLocalMessageFromPlaceholder(),
|
||||
QString()));
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
textField->setFocusFast();
|
||||
});
|
||||
|
||||
box->addButton(tr::lng_box_ok(), [=] {
|
||||
const auto text = textField->getLastText().trimmed();
|
||||
const auto fromText = fromField->getLastText().trimmed();
|
||||
|
||||
PeerId fromId = history->peer->id;
|
||||
if (!fromText.isEmpty()) {
|
||||
// Пытаемся найти пользователя по имени или username
|
||||
const auto session = &history->session();
|
||||
const auto &owner = session->data();
|
||||
|
||||
// Сначала пытаемся найти по username
|
||||
if (const auto user = owner.userByUsername(fromText)) {
|
||||
fromId = user->id;
|
||||
} else {
|
||||
// Затем ищем по имени среди участников чата
|
||||
if (const auto chat = history->peer->asChat()) {
|
||||
for (const auto &participant : chat->participants) {
|
||||
if (const auto user = owner.user(participant.userId())) {
|
||||
if (user->name().toLower().contains(fromText.toLower())) {
|
||||
fromId = user->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (const auto channel = history->peer->asChannel()) {
|
||||
// Для каналов используем текущего пользователя, если не найден
|
||||
// В реальном приложении можно было бы реализовать поиск по участникам
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showBox(text, fromId);
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
}));
|
||||
controller->show(Box(
|
||||
LocalMessageEditorBox,
|
||||
controller,
|
||||
history,
|
||||
LocalMessageData{}));
|
||||
},
|
||||
&st::menuIconEdit);
|
||||
}
|
||||
|
||||
void AddLocalMessagesManagerAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& settings = AyuSettings::getInstance();
|
||||
if (!needToShowItem(settings.showAddLocalMessageInContextMenu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto history = item->history();
|
||||
const auto controller = history->session().tryResolveWindow();
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show if there are local messages in this chat
|
||||
if (!AyuMessages::hasLocalMessages(history->peer, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu->addAction(
|
||||
tr::ayu_LocalMessagesManager(tr::now),
|
||||
[=]() {
|
||||
controller->show(Box(
|
||||
LocalMessagesManagerBox,
|
||||
controller,
|
||||
history));
|
||||
},
|
||||
&st::menuIconInfo);
|
||||
}
|
||||
|
||||
void AddLocalMessagesManagerToChatMenu(PeerData *peerData,
|
||||
Data::Thread *thread,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
const Window::PeerMenuCallback &addCallback) {
|
||||
if (!peerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& settings = AyuSettings::getInstance();
|
||||
if (!needToShowItem(settings.showAddLocalMessageInContextMenu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto history = sessionController->session().data().history(peerData);
|
||||
|
||||
// Only show if there are local messages in this chat
|
||||
if (!AyuMessages::hasLocalMessages(peerData, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
addCallback(
|
||||
tr::ayu_LocalMessagesManager(tr::now),
|
||||
[=]() {
|
||||
sessionController->show(Box(
|
||||
LocalMessagesManagerBox,
|
||||
sessionController,
|
||||
history));
|
||||
},
|
||||
&st::menuIconInfo);
|
||||
}
|
||||
|
||||
} // namespace AyuUi
|
||||
|
|
|
@ -36,5 +36,11 @@ void AddMessageDetailsAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
|||
void AddReadUntilAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddLocalMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddLocalMessagesManagerAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
|
||||
void AddLocalMessagesManagerToChatMenu(PeerData *peerData,
|
||||
Data::Thread *thread,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
const Window::PeerMenuCallback &addCallback);
|
||||
|
||||
}
|
||||
|
|
|
@ -1032,6 +1032,7 @@ void AddMessageActions(
|
|||
AyuUi::AddUserMessagesAction(menu, request.item);
|
||||
AyuUi::AddMessageDetailsAction(menu, request.item);
|
||||
AyuUi::AddLocalMessageAction(menu, request.item);
|
||||
AyuUi::AddLocalMessagesManagerAction(menu, request.item);
|
||||
}
|
||||
|
||||
AddPostLinkAction(menu, request);
|
||||
|
|
|
@ -1447,6 +1447,7 @@ void Filler::fillHistoryActions() {
|
|||
addTranslate();
|
||||
addReport();
|
||||
AyuUi::AddDeletedMessagesActions(_peer, _thread, _controller, _addAction);
|
||||
AyuUi::AddLocalMessagesManagerToChatMenu(_peer, _thread, _controller, _addAction);
|
||||
addClearHistory();
|
||||
addDeleteChat();
|
||||
addLeaveChat();
|
||||
|
|
Loading…
Add table
Reference in a new issue