// 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 namespace AyuUi { namespace { constexpr auto kMaxMessageLength = 4096; constexpr auto kMaxCaptionLength = 1024; constexpr auto kMaxPostAuthorLength = 255; class LocalMessageEditor { public: LocalMessageEditor( not_null box, not_null controller, not_null 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 _box; const not_null _controller; const not_null _history; LocalMessageData _data; // UI Elements not_null _scroll; not_null _content; // Basic fields Ui::InputField* _textField = nullptr; Ui::InputField* _fromField = nullptr; Ui::UserpicButton* _fromUserpic = nullptr; // Message type Ui::Checkbox* _isServiceMessage = nullptr; Ui::SlideWrap* _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* _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* _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 box, not_null controller, not_null history, const LocalMessageData &initialData) : _box(box) , _controller(controller) , _history(history) , _data(initialData) , _scroll(_box->addRow(object_ptr(_box))) , _content(_scroll->setOwnedWidget(object_ptr(_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( _content, tr::ayu_LocalMessageText(), st::boxLabel)); _textField = _content->add(object_ptr( _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(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupMessageTypeSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageType(), st::boxLabel)); _isServiceMessage = _content->add(object_ptr( _content, tr::ayu_LocalMessageIsService(), _data.isService, st::defaultCheckbox)); _serviceTextWrap = _content->add(object_ptr>( _content, object_ptr( _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(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupAuthorSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageAuthor(), st::boxLabel)); // From user field with userpic const auto fromContainer = _content->add(object_ptr(_content)); fromContainer->resize(fromContainer->width(), st::defaultInputField.height); _fromUserpic = Ui::CreateChild( fromContainer, st::defaultUserpicButton); _fromUserpic->move(0, 0); _fromField = Ui::CreateChild( 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( _content, st::defaultInputField, tr::ayu_LocalMessagePostAuthor(), _data.postAuthor)); _postAuthorField->setMaxLength(kMaxPostAuthorLength); // Via bot field _viaBotField = _content->add(object_ptr( _content, st::defaultInputField, tr::ayu_LocalMessageViaBot(), QString())); updateFromUser(); _fromField->changes() | rpl::start_with_next([=] { updateFromUser(); }, _fromField->lifetime()); _content->add(object_ptr(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupTimingSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageTiming(), st::boxLabel)); // Date and time fields const auto dateTimeContainer = _content->add(object_ptr(_content)); dateTimeContainer->resize(dateTimeContainer->width(), st::defaultInputField.height); _dateField = Ui::CreateChild( dateTimeContainer, st::defaultInputField, tr::ayu_LocalMessageDate(), QString()); _dateField->setMask("99.99.9999"); _dateField->move(0, 0); _timeField = Ui::CreateChild( 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( _content, tr::ayu_LocalMessageWasEdited(), _data.wasEdited, st::defaultCheckbox)); const auto editDateContainer = _content->add(object_ptr(_content)); editDateContainer->resize(editDateContainer->width(), st::defaultInputField.height); _editDateField = Ui::CreateChild( editDateContainer, st::defaultInputField, tr::ayu_LocalMessageEditDate(), QString()); _editDateField->setMask("99.99.9999"); _editDateField->move(0, 0); _editTimeField = Ui::CreateChild( 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(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupFlagsSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageFlags(), st::boxLabel)); _silentCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessageSilent(), _data.silent, st::defaultCheckbox)); _pinnedCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessagePinned(), _data.pinned, st::defaultCheckbox)); _noForwardsCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessageNoForwards(), _data.noForwards, st::defaultCheckbox)); _invertMediaCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessageInvertMedia(), _data.invertMedia, st::defaultCheckbox)); _content->add(object_ptr(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupStatsSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageStats(), st::boxLabel)); // Views _hasViewsCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessageHasViews(), _data.hasViews, st::defaultCheckbox)); _viewsField = _content->add(object_ptr( _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( _content, tr::ayu_LocalMessageHasShares(), _data.hasShares, st::defaultCheckbox)); _sharesField = _content->add(object_ptr( _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(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupReplySection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageReply(), st::boxLabel)); _replyToField = _content->add(object_ptr( _content, st::defaultInputField, tr::ayu_LocalMessageReplyTo(), QString())); _replyQuoteField = _content->add(object_ptr( _content, st::defaultInputField, tr::ayu_LocalMessageReplyQuote(), QString())); _content->add(object_ptr(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupForwardSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageForward(), st::boxLabel)); _isForwardedCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessageIsForwarded(), _data.isForwarded, st::defaultCheckbox)); const auto forwardContent = _content->add(object_ptr(_content)); _forwardWrap = _content->add(object_ptr>( _content, object_ptr(_content))); auto forwardFields = _forwardWrap->entity(); _forwardFromField = forwardFields->add(object_ptr( forwardFields, st::defaultInputField, tr::ayu_LocalMessageForwardFrom(), QString())); _forwardFromNameField = forwardFields->add(object_ptr( forwardFields, st::defaultInputField, tr::ayu_LocalMessageForwardFromName(), _data.forwardFromName)); // Forward date and time const auto forwardDateContainer = forwardFields->add(object_ptr(forwardFields)); forwardDateContainer->resize(forwardDateContainer->width(), st::defaultInputField.height); _forwardDateField = Ui::CreateChild( forwardDateContainer, st::defaultInputField, tr::ayu_LocalMessageForwardDate(), QString()); _forwardDateField->setMask("99.99.9999"); _forwardDateField->move(0, 0); _forwardTimeField = Ui::CreateChild( 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( 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(_content, st::boxMediumSkip)); } void LocalMessageEditor::setupMediaSection() { _content->add(object_ptr( _content, tr::ayu_LocalMessageMedia(), st::boxLabel)); _hasMediaCheck = _content->add(object_ptr( _content, tr::ayu_LocalMessageHasMedia(), _data.hasMedia, st::defaultCheckbox)); const auto mediaContent = _content->add(object_ptr(_content)); _mediaWrap = _content->add(object_ptr>( _content, object_ptr(_content))); auto mediaFields = _mediaWrap->entity(); _mediaPathField = mediaFields->add(object_ptr( mediaFields, st::defaultInputField, tr::ayu_LocalMessageMediaPath(), _data.mediaPath)); _mediaCaptionField = mediaFields->add(object_ptr( 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( _content, tr::ayu_LocalMessageAdvanced(), st::boxLabel)); _starsPaidField = _content->add(object_ptr( _content, st::defaultInputField, tr::ayu_LocalMessageStarsPaid(), QString::number(_data.starsPaid))); _starsPaidField->setMask("999999999"); _effectIdField = _content->add(object_ptr( _content, st::defaultInputField, tr::ayu_LocalMessageEffectId(), QString::number(_data.effectId))); _effectIdField->setMask("999999999999999999"); _groupedIdField = _content->add(object_ptr( _content, st::defaultInputField, tr::ayu_LocalMessageGroupedId(), QString::number(_data.groupedId))); _groupedIdField->setMask("999999999999999999"); _content->add(object_ptr(_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 box, not_null controller, not_null history, const LocalMessageData &initialData) { const auto editor = box->lifetime().make_state( box, controller, history, initialData); } } // namespace AyuUi