diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 812bf59d5..80d097493 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -308,6 +308,9 @@ TextWithEntities GenerateParticipantChangeText(not_null channel, c } // namespace +OwnedItem::OwnedItem(std::nullptr_t) { +} + OwnedItem::OwnedItem( not_null delegate, not_null data) diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.h index b29ea016b..a7455ce46 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.h @@ -27,6 +27,7 @@ void GenerateItems( // Smart pointer wrapper for HistoryItem* that destroys the owned item. class OwnedItem { public: + OwnedItem(std::nullptr_t = nullptr); OwnedItem( not_null delegate, not_null data); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index a320874b0..b68945761 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -528,12 +528,7 @@ HistoryWidget::HistoryWidget( connect(_fieldAutocomplete, SIGNAL(stickerChosen(not_null,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerOrGifSend(not_null))); connect(_fieldAutocomplete, SIGNAL(moderateKeyActivate(int,bool*)), this, SLOT(onModerateKeyActivate(int,bool*))); if (_supportAutocomplete) { - _supportAutocomplete->hide(); - _supportAutocomplete->insertRequests( - ) | rpl::start_with_next([=](const QString &text) { - _field->setFocus(); - _field->textCursor().insertText(text); - }, lifetime()); + supportInitAutocomplete(); } _fieldLinksParser = std::make_unique(_field); _fieldLinksParser->list().changes( @@ -772,6 +767,56 @@ HistoryWidget::HistoryWidget( orderWidgets(); } +void HistoryWidget::supportInitAutocomplete() { + _supportAutocomplete->hide(); + + _supportAutocomplete->insertRequests( + ) | rpl::start_with_next([=](const QString &text) { + supportInsertText(text); + }, _supportAutocomplete->lifetime()); + + _supportAutocomplete->shareContactRequests( + ) | rpl::start_with_next([=](const Support::Contact &contact) { + supportShareContact(contact); + }, _supportAutocomplete->lifetime()); +} + +void HistoryWidget::supportInsertText(const QString &text) { + _field->setFocus(); + _field->textCursor().insertText(text); +} + +void HistoryWidget::supportShareContact(Support::Contact contact) { + if (!_history) { + return; + } + const auto commented = !contact.comment.isEmpty(); + if (commented) { + supportInsertText(contact.comment); + } + contact.comment = _field->getLastText(); + + const auto submit = [=] { + if (!_history) { + return; + } + send(); + Auth().api().shareContact( + contact.phone, + contact.firstName, + contact.lastName, + ApiWrap::SendOptions(_history)); + }; + const auto box = Ui::show(Box( + _history, + contact, + crl::guard(this, submit))); + box->boxClosing( + ) | rpl::start_with_next([=] { + _field->document()->undo(); + }, lifetime()); +} + void HistoryWidget::scrollToCurrentVoiceMessage(FullMsgId fromId, FullMsgId toId) { if (getms() <= _lastUserScrolled + kScrollToVoiceAfterScrolledMs) { return; @@ -1538,33 +1583,50 @@ bool HistoryWidget::cmd_next_chat() { Dialogs::RowDescriptor( _history, FullMsgId(_history->channelId(), std::max(_showAtMsgId, 0)))); - if (const auto history = next.key.history()) { - Ui::showPeerHistory(history, next.fullId.msg); - return true; - } else if (const auto feed = next.key.feed()) { - if (const auto item = App::histItemById(next.fullId)) { - controller()->showSection(HistoryFeed::Memento(feed, item->position())); - } else { - controller()->showSection(HistoryFeed::Memento(feed)); + const auto to = [&] { + auto result = next; + if (Auth().supportMode()) { + while (result.key + && !result.key.entry()->chatListUnreadCount() + && !result.key.entry()->chatListUnreadMark()) { + result = App::main()->chatListEntryAfter(result); + } } - } - return false; + return result; + }(); + return jumpToDialogRow(to); } bool HistoryWidget::cmd_previous_chat() { if (!_history) { return false; } - const auto next = App::main()->chatListEntryBefore( + const auto previous = App::main()->chatListEntryBefore( Dialogs::RowDescriptor( _history, FullMsgId(_history->channelId(), std::max(_showAtMsgId, 0)))); - if (const auto history = next.key.history()) { - Ui::showPeerHistory(history, next.fullId.msg); + const auto to = [&] { + auto result = previous; + if (Auth().supportMode()) { + while (result.key + && !result.key.entry()->chatListUnreadCount() + && !result.key.entry()->chatListUnreadMark()) { + result = App::main()->chatListEntryBefore(result); + } + } + return result; + }(); + return jumpToDialogRow(to); +} + +bool HistoryWidget::jumpToDialogRow(const Dialogs::RowDescriptor &to) { + if (const auto history = to.key.history()) { + Ui::showPeerHistory(history, to.fullId.msg); return true; - } else if (const auto feed = next.key.feed()) { - if (const auto item = App::histItemById(next.fullId)) { - controller()->showSection(HistoryFeed::Memento(feed, item->position())); + } else if (const auto feed = to.key.feed()) { + if (const auto item = App::histItemById(to.fullId)) { + controller()->showSection( + HistoryFeed::Memento(feed, item->position())); } else { controller()->showSection(HistoryFeed::Memento(feed)); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 49b2133c9..b93c91be6 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -38,6 +38,7 @@ struct Draft; namespace Support { class Autocomplete; +struct Contact; } // namespace Support namespace Ui { @@ -454,6 +455,10 @@ private: void refreshAboutProxyPromotion(); void unreadCountUpdated(); + void supportInitAutocomplete(); + void supportInsertText(const QString &text); + void supportShareContact(Support::Contact contact); + void highlightMessage(MsgId universalMessageId); void adjustHighlightedMessageToMigrated(); void checkNextHighlight(); @@ -565,6 +570,7 @@ private: bool editingMessage() const { return _editMsgId != 0; } + bool jumpToDialogRow(const Dialogs::RowDescriptor &to); MsgId _replyToId = 0; Text _replyToName; diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 6d61d768a..9f9ba0989 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -28,7 +28,8 @@ struct TextState; enum class Context : char { History, Feed, - AdminLog + AdminLog, + ContactPreview }; class Element; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 500d09bb6..0f12ce8b5 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -691,6 +691,8 @@ bool Message::hasFromPhoto() const { } return !item->out() && !item->history()->peer->isUser(); } break; + case Context::ContactPreview: + return false; } Unexpected("Context in Message::hasFromPhoto."); } @@ -1283,6 +1285,8 @@ bool Message::hasFromName() const { && (!item->history()->peer->isUser() || item->history()->peer->isSelf()); } break; + case Context::ContactPreview: + return false; } Unexpected("Context in Message::hasFromPhoto."); } diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index 476b3a4fe..e99cbc9a1 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -11,9 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "ui/wrap/padding_wrap.h" #include "support/support_templates.h" +#include "history/view/history_view_message.h" +#include "history/view/history_view_service_message.h" +#include "history/history_message.h" +#include "lang/lang_keys.h" #include "auth_session.h" +#include "apiwrap.h" #include "styles/style_chat_helpers.h" #include "styles/style_window.h" +#include "styles/style_boxes.h" namespace Support { namespace { @@ -241,6 +247,66 @@ void Inner::mouseReleaseEvent(QMouseEvent *e) { } } +AdminLog::OwnedItem GenerateCommentItem( + not_null delegate, + not_null history, + const Contact &data) { + if (data.comment.isEmpty()) { + return nullptr; + } + using Flag = MTPDmessage::Flag; + const auto id = ServerMaxMsgId + (ServerMaxMsgId / 2); + const auto flags = Flag::f_entities | Flag::f_from_id | Flag::f_out; + const auto replyTo = 0; + const auto viaBotId = 0; + const auto item = new HistoryMessage( + history, + id, + flags, + replyTo, + viaBotId, + unixtime(), + Auth().userId(), + QString(), + TextWithEntities{ TextUtilities::Clean(data.comment) }); + return AdminLog::OwnedItem(delegate, item); +} + +AdminLog::OwnedItem GenerateContactItem( + not_null delegate, + not_null history, + const Contact &data) { + using Flag = MTPDmessage::Flag; + const auto id = ServerMaxMsgId + (ServerMaxMsgId / 2) + 1; + const auto flags = Flag::f_from_id | Flag::f_media | Flag::f_out; + const auto replyTo = 0; + const auto viaBotId = 0; + const auto message = MTP_message( + MTP_flags(flags), + MTP_int(id), + MTP_int(Auth().userId()), + peerToMTP(history->peer->id), + MTPMessageFwdHeader(), + MTP_int(viaBotId), + MTP_int(replyTo), + MTP_int(unixtime()), + MTP_string(QString()), + MTP_messageMediaContact( + MTP_string(data.phone), + MTP_string(data.firstName), + MTP_string(data.lastName), + MTP_string(QString()), + MTP_int(0)), + MTPReplyMarkup(), + MTPVector(), + MTP_int(0), + MTP_int(0), + MTP_string(QString()), + MTP_long(0)); + const auto item = new HistoryMessage(history, message.c_message()); + return AdminLog::OwnedItem(delegate, item); +} + } // namespace Autocomplete::Autocomplete(QWidget *parent, not_null session) @@ -267,10 +333,14 @@ void Autocomplete::setBoundings(QRect rect) { height); } -rpl::producer Autocomplete::insertRequests() { +rpl::producer Autocomplete::insertRequests() const { return _insertRequests.events(); } +rpl::producer Autocomplete::shareContactRequests() const { + return _shareContactRequests.events(); +} + void Autocomplete::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Up) { _moveSelection(-1); @@ -296,7 +366,7 @@ void Autocomplete::setupContent() { const auto submit = [=] { if (const auto question = inner->selected()) { - _insertRequests.fire_copy(question->value); + submitValue(question->value); } }; @@ -358,4 +428,121 @@ void Autocomplete::setupContent() { }, lifetime()); } +void Autocomplete::submitValue(const QString &value) { + const auto prefix = qstr("contact:"); + if (value.startsWith(prefix)) { + const auto line = value.indexOf('\n'); + const auto text = (line > 0) ? value.mid(line + 1) : QString(); + const auto commented = !text.isEmpty(); + const auto contact = value.mid( + prefix.size(), + (line > 0) ? (line - prefix.size()) : -1); + const auto parts = contact.split(' ', QString::SkipEmptyParts); + if (parts.size() > 1) { + const auto phone = parts[0]; + const auto firstName = parts[1]; + const auto lastName = (parts.size() > 2) + ? QStringList(parts.mid(2)).join(' ') + : QString(); + _shareContactRequests.fire(Contact{ + text, + phone, + firstName, + lastName }); + } + } else { + _insertRequests.fire_copy(value); + } +} + +ConfirmContactBox::ConfirmContactBox( + QWidget*, + not_null history, + const Contact &data, + Fn submit) +: _comment(GenerateCommentItem(this, history, data)) +, _contact(GenerateContactItem(this, history, data)) +, _submit(submit) { +} + +void ConfirmContactBox::prepare() { + setTitle([] { return "Confirmation"; }); + + auto maxWidth = 0; + if (_comment) { + _comment->setAttachToNext(true); + _contact->setAttachToPrevious(true); + _comment->initDimensions(); + accumulate_max(maxWidth, _comment->maxWidth()); + } + _contact->initDimensions(); + accumulate_max(maxWidth, _contact->maxWidth()); + maxWidth += st::boxPadding.left() + st::boxPadding.right(); + const auto width = snap(maxWidth, st::boxWidth, st::boxWideWidth); + const auto available = width + - st::boxPadding.left() + - st::boxPadding.right(); + auto height = 0; + if (_comment) { + height += _comment->resizeGetHeight(available); + } + height += _contact->resizeGetHeight(available); + setDimensions(width, height); + _contact->initDimensions(); + + addButton(langFactory(lng_send_button), [=] { + const auto weak = make_weak(this); + _submit(); + if (weak) { + closeBox(); + } + }); + addButton(langFactory(lng_cancel), [=] { closeBox(); }); +} + +void ConfirmContactBox::paintEvent(QPaintEvent *e) { + Painter p(this); + + p.fillRect(e->rect(), st::boxBg); + + const auto ms = getms(); + p.translate(st::boxPadding.left(), 0); + if (_comment) { + _comment->draw(p, rect(), TextSelection(), ms); + p.translate(0, _comment->height()); + } + _contact->draw(p, rect(), TextSelection(), ms); +} + +HistoryView::Context ConfirmContactBox::elementContext() { + return HistoryView::Context::ContactPreview; +} + +std::unique_ptr ConfirmContactBox::elementCreate( + not_null message) { + return std::make_unique(this, message); +} + +std::unique_ptr ConfirmContactBox::elementCreate( + not_null message) { + return std::make_unique(this, message); +} + +bool ConfirmContactBox::elementUnderCursor(not_null view) { + return false; +} + +void ConfirmContactBox::elementAnimationAutoplayAsync( + not_null element) { +} + +TimeMs ConfirmContactBox::elementHighlightTime( + not_null element) { + return TimeMs(); +} + +bool ConfirmContactBox::elementInSelectionMode() { + return false; +} + } // namespace Support diff --git a/Telegram/SourceFiles/support/support_autocomplete.h b/Telegram/SourceFiles/support/support_autocomplete.h index 79d202c6f..9b5a85007 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.h +++ b/Telegram/SourceFiles/support/support_autocomplete.h @@ -8,6 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "ui/rp_widget.h" +#include "boxes/abstract_box.h" +#include "history/admin_log/history_admin_log_item.h" +#include "history/view/history_view_element.h" +#include "history/history.h" class AuthSession; @@ -17,6 +21,13 @@ class ScrollArea; namespace Support { +struct Contact { + QString comment; + QString phone; + QString firstName; + QString lastName; +}; + class Autocomplete : public Ui::RpWidget { public: Autocomplete(QWidget *parent, not_null session); @@ -25,13 +36,15 @@ public: void deactivate(); void setBoundings(QRect rect); - rpl::producer insertRequests(); + rpl::producer insertRequests() const; + rpl::producer shareContactRequests() const; protected: void keyPressEvent(QKeyEvent *e) override; private: void setupContent(); + void submitValue(const QString &value); not_null _session; Fn _activate; @@ -39,6 +52,41 @@ private: Fn _moveSelection; rpl::event_stream _insertRequests; + rpl::event_stream _shareContactRequests; + +}; + +class ConfirmContactBox + : public BoxContent + , public HistoryView::ElementDelegate { +public: + ConfirmContactBox( + QWidget*, + not_null history, + const Contact &data, + Fn submit); + + using Element = HistoryView::Element; + HistoryView::Context elementContext() override; + std::unique_ptr elementCreate( + not_null message) override; + std::unique_ptr elementCreate( + not_null message) override; + bool elementUnderCursor(not_null view) override; + void elementAnimationAutoplayAsync( + not_null element) override; + TimeMs elementHighlightTime( + not_null element) override; + bool elementInSelectionMode() override; + +protected: + void prepare() override; + void paintEvent(QPaintEvent *e) override; + +private: + AdminLog::OwnedItem _comment; + AdminLog::OwnedItem _contact; + Fn _submit; };