From 7a74b863402e5f67ce7fd0a99ab3ad64b7296344 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Oct 2020 23:24:07 +0100 Subject: Pasteable textinput --- src/timeline/InputBar.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/timeline/InputBar.cpp (limited to 'src/timeline/InputBar.cpp') diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp new file mode 100644 index 00000000..d128631d --- /dev/null +++ b/src/timeline/InputBar.cpp @@ -0,0 +1,46 @@ +#include "InputBar.h" + +#include +#include +#include + +#include "Logging.h" + +bool +InputBar::paste(bool fromMouse) +{ + const QMimeData *md = nullptr; + + if (fromMouse) { + if (QGuiApplication::clipboard()->supportsSelection()) { + md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection); + } + } else { + md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard); + } + + if (!md) + return false; + + if (md->hasImage()) { + return true; + } else { + nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString()); + return false; + } +} + +void +InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_) +{ + selectionStart = selectionStart_; + selectionEnd = selectionEnd_; + cursorPosition = cursorPosition_; + text = text_; +} + +void +InputBar::send() +{ + nhlog::ui()->debug("Send: {}", text.toStdString()); +} -- cgit 1.5.1 From 0bb488563288da75566e7da883fda31914ecf281 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Nov 2020 03:12:37 +0100 Subject: Basic text input in qml --- resources/qml/MessageInput.qml | 9 +- src/ChatPage.cpp | 48 ----------- src/ChatPage.h | 2 +- src/TextInputWidget.cpp | 65 +------------- src/TextInputWidget.h | 5 -- src/timeline/InputBar.cpp | 160 ++++++++++++++++++++++++++++++++++- src/timeline/InputBar.h | 15 +++- src/timeline/TimelineModel.cpp | 1 - src/timeline/TimelineViewManager.cpp | 75 ---------------- src/timeline/TimelineViewManager.h | 8 -- 10 files changed, 178 insertions(+), 210 deletions(-) (limited to 'src/timeline/InputBar.cpp') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 3b424bb3..b76a44f3 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -58,9 +58,14 @@ Rectangle { onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + Connections { + target: TimelineManager.timeline.input + function onInsertText(text_) { textArea.insert(textArea.cursorPosition, text_); } + } + Keys.onPressed: { if (event.matches(StandardKey.Paste)) { - TimelineManager.timeline.input.paste(false) || textArea.paste() + TimelineManager.timeline.input.paste(false) event.accepted = true } else if (event.matches(StandardKey.InsertParagraphSeparator)) { @@ -75,7 +80,7 @@ Rectangle { anchors.fill: parent acceptedButtons: Qt.MiddleButton cursorShape: Qt.IBeamCursor - onClicked: TimelineManager.timeline.input.paste(true) || textArea.paste() + onClicked: TimelineManager.timeline.input.paste(true) } background: Rectangle { diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 78987fb9..6e1ed8ca 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -160,15 +160,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) trySync(); }); - connect(text_input_, - &TextInputWidget::clearRoomTimeline, - view_manager_, - &TimelineViewManager::clearCurrentRoomTimeline); - - connect(text_input_, &TextInputWidget::rotateMegolmSession, this, [this]() { - cache::dropOutboundMegolmSession(current_room_.toStdString()); - }); - connect( new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { if (isVisible()) @@ -277,45 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, SIGNAL(unreadMessages(int))); - connect(text_input_, - &TextInputWidget::sendTextMessage, - view_manager_, - &TimelineViewManager::queueTextMessage); - - connect(text_input_, - &TextInputWidget::sendEmoteMessage, - view_manager_, - &TimelineViewManager::queueEmoteMessage); - - connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); - - // invites and bans via quick command - connect(text_input_, &TextInputWidget::sendInviteRoomRequest, this, &ChatPage::inviteUser); - connect(text_input_, &TextInputWidget::sendKickRoomRequest, this, &ChatPage::kickUser); - connect(text_input_, &TextInputWidget::sendBanRoomRequest, this, &ChatPage::banUser); - connect(text_input_, &TextInputWidget::sendUnbanRoomRequest, this, &ChatPage::unbanUser); - - connect( - text_input_, &TextInputWidget::changeRoomNick, this, [this](const QString &displayName) { - mtx::events::state::Member member; - member.display_name = displayName.toStdString(); - member.avatar_url = - cache::avatarUrl(currentRoom(), - QString::fromStdString(http::client()->user_id().to_string())) - .toStdString(); - member.membership = mtx::events::state::Membership::Join; - - http::client()->send_state_event( - currentRoom().toStdString(), - http::client()->user_id().to_string(), - member, - [](mtx::responses::EventId, mtx::http::RequestErr err) { - if (err) - nhlog::net()->error("Failed to set room displayname: {}", - err->matrix_error.error); - }); - }); - connect( text_input_, &TextInputWidget::uploadMedia, diff --git a/src/ChatPage.h b/src/ChatPage.h index 0c12d89f..9a38fd6e 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -109,6 +109,7 @@ public: public slots: void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); + void joinRoom(const QString &room); void inviteUser(QString userid, QString reason); void kickUser(QString userid, QString reason); @@ -200,7 +201,6 @@ private slots: void removeRoom(const QString &room_id); void dropToLoginPage(const QString &msg); - void joinRoom(const QString &room); void sendTypingNotifications(); void handleSyncResponse(const mtx::responses::Sync &res); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 454353fd..dec7a574 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -486,36 +486,7 @@ FilteredTextEdit::minimumSizeHint() const void FilteredTextEdit::submit() -{ - if (toPlainText().trimmed().isEmpty()) - return; - - if (true_history_.size() == INPUT_HISTORY_SIZE) - true_history_.pop_back(); - true_history_.push_front(toPlainText()); - working_history_ = true_history_; - working_history_.push_front(""); - history_index_ = 0; - - QString text = toPlainText(); - - if (text.startsWith('/')) { - int command_end = text.indexOf(' '); - if (command_end == -1) - command_end = text.size(); - auto name = text.mid(1, command_end - 1); - auto args = text.mid(command_end + 1); - if (name.isEmpty() || name == "/") { - message(args); - } else { - command(name, args); - } - } else { - message(std::move(text)); - } - - clear(); -} +{} void FilteredTextEdit::textChanged() @@ -653,8 +624,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) #endif connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); - connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), @@ -685,38 +654,6 @@ TextInputWidget::addSelectedEmoji(const QString &emoji) input_->show(); } -void -TextInputWidget::command(QString command, QString args) -{ - if (command == "me") { - emit sendEmoteMessage(args); - } else if (command == "join") { - emit sendJoinRoomRequest(args); - } else if (command == "invite") { - emit sendInviteRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); - } else if (command == "kick") { - emit sendKickRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); - } else if (command == "ban") { - emit sendBanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); - } else if (command == "unban") { - emit sendUnbanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); - } else if (command == "roomnick") { - emit changeRoomNick(args); - } else if (command == "shrug") { - emit sendTextMessage("¯\\_(ツ)_/¯" + (args.isEmpty() ? "" : " " + args)); - } else if (command == "fliptable") { - emit sendTextMessage("(╯°□°)╯︵ ┻━┻"); - } else if (command == "unfliptable") { - emit sendTextMessage(" ┯━┯╭( º _ º╭)"); - } else if (command == "sovietflip") { - emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\"); - } else if (command == "clear-timeline") { - emit clearRoomTimeline(); - } else if (command == "rotate-megolm-session") { - emit rotateMegolmSession(); - } -} - void TextInputWidget::openFileSelection() { diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 7cc73e98..f9d84871 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -170,9 +170,6 @@ private slots: void addSelectedEmoji(const QString &emoji); signals: - void sendTextMessage(const QString &msg); - void sendEmoteMessage(QString msg); - void clearRoomTimeline(); void heightChanged(int height); void uploadMedia(const QSharedPointer data, @@ -186,7 +183,6 @@ signals: void sendBanRoomRequest(const QString &userid, const QString &reason); void sendUnbanRoomRequest(const QString &userid, const QString &reason); void changeRoomNick(const QString &displayname); - void rotateMegolmSession(); void startedTyping(); void stoppedTyping(); @@ -197,7 +193,6 @@ protected: private: void showUploadSpinner(); - void command(QString name, QString args); QHBoxLayout *topLayout_; FilteredTextEdit *input_; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index d128631d..dcd4a106 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -4,9 +4,19 @@ #include #include +#include + +#include "Cache.h" +#include "ChatPage.h" #include "Logging.h" +#include "MatrixClient.h" +#include "TimelineModel.h" +#include "UserSettingsPage.h" +#include "Utils.h" + +static constexpr size_t INPUT_HISTORY_SIZE = 10; -bool +void InputBar::paste(bool fromMouse) { const QMimeData *md = nullptr; @@ -20,13 +30,13 @@ InputBar::paste(bool fromMouse) } if (!md) - return false; + return; if (md->hasImage()) { - return true; + } else if (md->hasText()) { + emit insertText(md->text()); } else { nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString()); - return false; } } @@ -42,5 +52,147 @@ InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition void InputBar::send() { + if (text.trimmed().isEmpty()) + return; + + if (history_.size() == INPUT_HISTORY_SIZE) + history_.pop_back(); + history_.push_front(text); + history_index_ = 0; + + if (text.startsWith('/')) { + int command_end = text.indexOf(' '); + if (command_end == -1) + command_end = text.size(); + auto name = text.mid(1, command_end - 1); + auto args = text.mid(command_end + 1); + if (name.isEmpty() || name == "/") { + message(args); + } else { + command(name, args); + } + } else { + message(text); + } + nhlog::ui()->debug("Send: {}", text.toStdString()); } + +void +InputBar::message(QString msg) +{ + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + + if (ChatPage::instance()->userSettings()->markdown()) { + text.formatted_body = utils::markdownToHtml(msg).toStdString(); + + // Don't send formatted_body, when we don't need to + if (text.formatted_body.find("<") == std::string::npos) + text.formatted_body = ""; + else + text.format = "org.matrix.custom.html"; + } + + if (!room->reply().isEmpty()) { + auto related = room->relatedInfo(room->reply()); + + QString body; + bool firstLine = true; + for (const auto &line : related.quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } + + text.body = QString("%1\n%2").arg(body).arg(msg).toStdString(); + + // NOTE(Nico): rich replies always need a formatted_body! + text.format = "org.matrix.custom.html"; + if (ChatPage::instance()->userSettings()->markdown()) + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg)) + .toStdString(); + else + text.formatted_body = + utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); + + text.relates_to.in_reply_to.event_id = related.related_event; + room->resetReply(); + } + + room->sendMessageEvent(text, mtx::events::EventType::RoomMessage); +} + +void +InputBar::emote(QString msg) +{ + auto html = utils::markdownToHtml(msg); + + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); + + if (html != msg.trimmed().toHtmlEscaped() && + ChatPage::instance()->userSettings()->markdown()) { + emote.formatted_body = html.toStdString(); + emote.format = "org.matrix.custom.html"; + } + + if (!room->reply().isEmpty()) { + emote.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); +} + +void +InputBar::command(QString command, QString args) +{ + if (command == "me") { + emote(args); + } else if (command == "join") { + ChatPage::instance()->joinRoom(args); + } else if (command == "invite") { + ChatPage::instance()->inviteUser(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "kick") { + ChatPage::instance()->kickUser(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "ban") { + ChatPage::instance()->banUser(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "unban") { + ChatPage::instance()->unbanUser(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "roomnick") { + mtx::events::state::Member member; + member.display_name = args.toStdString(); + member.avatar_url = + cache::avatarUrl(room->roomId(), + QString::fromStdString(http::client()->user_id().to_string())) + .toStdString(); + member.membership = mtx::events::state::Membership::Join; + + http::client()->send_state_event( + room->roomId().toStdString(), + http::client()->user_id().to_string(), + member, + [](mtx::responses::EventId, mtx::http::RequestErr err) { + if (err) + nhlog::net()->error("Failed to set room displayname: {}", + err->matrix_error.error); + }); + } else if (command == "shrug") { + message("¯\\_(ツ)_/¯" + (args.isEmpty() ? "" : " " + args)); + } else if (command == "fliptable") { + message("(╯°□°)╯︵ ┻━┻"); + } else if (command == "unfliptable") { + message(" ┯━┯╭( º _ º╭)"); + } else if (command == "sovietflip") { + message("ノ┬─┬ノ ︵ ( \\o°o)\\"); + } else if (command == "clear-timeline") { + room->clearTimeline(); + } else if (command == "rotate-megolm-session") { + cache::dropOutboundMegolmSession(room->roomId().toStdString()); + } +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 78b06960..f3a38c2e 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -1,10 +1,12 @@ #pragma once #include +#include class TimelineModel; -class InputBar : public QObject { +class InputBar : public QObject +{ Q_OBJECT public: @@ -15,11 +17,20 @@ public: public slots: void send(); - bool paste(bool fromMouse); + void paste(bool fromMouse); void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); +signals: + void insertText(QString text); + private: + void message(QString body); + void emote(QString body); + void command(QString name, QString args); + TimelineModel *room; QString text; + std::deque history_; + std::size_t history_index_ = 0; int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index aeb4e8f5..8b80ea51 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1567,4 +1567,3 @@ TimelineModel::roomTopic() const return utils::replaceEmoji(utils::linkifyMessage( utils::escapeBlacklistedHtml(QString::fromStdString(info[room_id_].topic)))); } - diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 3b80d020..f949498d 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -474,81 +474,6 @@ TimelineViewManager::initWithMessages(const std::vector &roomIds) addRoom(roomId); } -void -TimelineViewManager::queueTextMessage(const QString &msg) -{ - if (!timeline_) - return; - - mtx::events::msg::Text text = {}; - text.body = msg.trimmed().toStdString(); - - if (ChatPage::instance()->userSettings()->markdown()) { - text.formatted_body = utils::markdownToHtml(msg).toStdString(); - - // Don't send formatted_body, when we don't need to - if (text.formatted_body.find("<") == std::string::npos) - text.formatted_body = ""; - else - text.format = "org.matrix.custom.html"; - } - - if (!timeline_->reply().isEmpty()) { - auto related = timeline_->relatedInfo(timeline_->reply()); - - QString body; - bool firstLine = true; - for (const auto &line : related.quoted_body.split("\n")) { - if (firstLine) { - firstLine = false; - body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); - } else { - body = QString("%1\n> %2\n").arg(body).arg(line); - } - } - - text.body = QString("%1\n%2").arg(body).arg(msg).toStdString(); - - // NOTE(Nico): rich replies always need a formatted_body! - text.format = "org.matrix.custom.html"; - if (ChatPage::instance()->userSettings()->markdown()) - text.formatted_body = - utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg)) - .toStdString(); - else - text.formatted_body = - utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); - - text.relates_to.in_reply_to.event_id = related.related_event; - timeline_->resetReply(); - } - - timeline_->sendMessageEvent(text, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueEmoteMessage(const QString &msg) -{ - auto html = utils::markdownToHtml(msg); - - mtx::events::msg::Emote emote; - emote.body = msg.trimmed().toStdString(); - - if (html != msg.trimmed().toHtmlEscaped() && - ChatPage::instance()->userSettings()->markdown()) { - emote.formatted_body = html.toStdString(); - emote.format = "org.matrix.custom.html"; - } - - if (!timeline_->reply().isEmpty()) { - emote.relates_to.in_reply_to.event_id = timeline_->reply().toStdString(); - timeline_->resetReply(); - } - - if (timeline_) - timeline_->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); -} - void TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey) { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index f330d870..02e0e132 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -104,8 +104,6 @@ public slots: void setHistoryView(const QString &room_id); void updateColorPalette(); void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); - void queueTextMessage(const QString &msg); - void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, const std::optional &file, @@ -139,12 +137,6 @@ public slots: void updateEncryptedDescriptions(); - void clearCurrentRoomTimeline() - { - if (timeline_) - timeline_->clearTimeline(); - } - void enableBackButton() { if (isNarrowView_) -- cgit 1.5.1 From a31d3d08165646738d6ae624ac4eff6971207058 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 15 Nov 2020 04:52:49 +0100 Subject: Add file uploading --- resources/qml/ActiveCallBar.qml | 11 +- resources/qml/MessageInput.qml | 45 +++-- resources/qml/NhekoBusyIndicator.qml | 64 +++++++ resources/qml/TimelineView.qml | 5 +- resources/qml/VideoCall.qml | 3 +- resources/res.qrc | 1 + src/ChatPage.cpp | 120 ------------- src/ChatPage.h | 11 -- src/TextInputWidget.cpp | 146 ---------------- src/TextInputWidget.h | 14 -- src/Utils.cpp | 5 +- src/Utils.h | 2 +- src/dialogs/PreviewUploadOverlay.cpp | 5 +- src/dialogs/PreviewUploadOverlay.h | 1 + src/timeline/InputBar.cpp | 317 ++++++++++++++++++++++++++++++++++- src/timeline/InputBar.h | 41 +++++ src/timeline/TimelineModel.h | 2 +- 17 files changed, 475 insertions(+), 318 deletions(-) create mode 100644 resources/qml/NhekoBusyIndicator.qml (limited to 'src/timeline/InputBar.cpp') diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index 282cac81..cbe36e6d 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -12,8 +12,11 @@ Rectangle { MouseArea { anchors.fill: parent - onClicked: if (TimelineManager.onVideoCall) - stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1; + onClicked: { + if (TimelineManager.onVideoCall) + stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1; + + } } RowLayout { @@ -39,8 +42,7 @@ Rectangle { Image { Layout.preferredWidth: 24 Layout.preferredHeight: 24 - source: TimelineManager.onVideoCall ? - "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png" + source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png" } Label { @@ -69,6 +71,7 @@ Rectangle { callTimer.startTime = Math.floor(d.getTime() / 1000); if (TimelineManager.onVideoCall) stackLayout.currentIndex = 1; + break; case WebRTCState.DISCONNECTED: callStateLabel.text = ""; diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index b76a44f3..a1220599 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -2,7 +2,6 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 - import im.nheko 1.0 Rectangle { @@ -36,6 +35,20 @@ Rectangle { image: ":/icons/icons/ui/paper-clip-outline.png" Layout.topMargin: 8 Layout.bottomMargin: 8 + onClicked: TimelineManager.timeline.input.openFileSelection() + + Rectangle { + anchors.fill: parent + color: colors.window + visible: TimelineManager.timeline.input.uploading + + NhekoBusyIndicator { + anchors.fill: parent + running: parent.visible + } + + } + } ScrollView { @@ -52,27 +65,27 @@ Rectangle { placeholderTextColor: colors.buttonText color: colors.text wrapMode: TextEdit.Wrap - onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onCursorPositionChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - - Connections { - target: TimelineManager.timeline.input - function onInsertText(text_) { textArea.insert(textArea.cursorPosition, text_); } - } - Keys.onPressed: { if (event.matches(StandardKey.Paste)) { - TimelineManager.timeline.input.paste(false) - event.accepted = true + TimelineManager.timeline.input.paste(false); + event.accepted = true; + } else if (event.matches(StandardKey.InsertParagraphSeparator)) { + TimelineManager.timeline.input.send(); + textArea.clear(); + event.accepted = true; } - else if (event.matches(StandardKey.InsertParagraphSeparator)) { - TimelineManager.timeline.input.send() - textArea.clear() - event.accepted = true + } + + Connections { + function onInsertText(text_) { + textArea.insert(textArea.cursorPosition, text_); } + + target: TimelineManager.timeline.input } MouseArea { @@ -110,6 +123,10 @@ Rectangle { Layout.topMargin: 8 Layout.bottomMargin: 8 Layout.rightMargin: 16 + onClicked: { + TimelineManager.timeline.input.send(); + textArea.clear(); + } } } diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml new file mode 100644 index 00000000..8889989a --- /dev/null +++ b/resources/qml/NhekoBusyIndicator.qml @@ -0,0 +1,64 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 + +BusyIndicator { + id: control + + contentItem: Item { + implicitWidth: Math.min(parent.height, parent.width) + implicitHeight: implicitWidth + + Item { + id: item + + height: Math.min(parent.height, parent.width) + width: height + opacity: control.running ? 1 : 0 + + RotationAnimator { + target: item + running: control.visible && control.running + from: 0 + to: 360 + loops: Animation.Infinite + duration: 2000 + } + + Repeater { + id: repeater + + model: 6 + + Rectangle { + implicitWidth: radius * 2 + implicitHeight: radius * 2 + radius: item.height / 6 + color: colors.text + opacity: (index + 2) / (repeater.count + 2) + transform: [ + Translate { + y: -Math.min(item.width, item.height) * 0.5 + item.height / 6 + }, + Rotation { + angle: index / repeater.count * 360 + origin.x: item.height / 2 + origin.y: item.height / 2 + } + ] + } + + } + + Behavior on opacity { + OpacityAnimator { + duration: 250 + } + + } + + } + + } + +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d85167af..5fce0846 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -192,13 +192,15 @@ Page { StackLayout { id: stackLayout + currentIndex: 0 Connections { - target: TimelineManager function onActiveTimelineChanged() { stackLayout.currentIndex = 0; } + + target: TimelineManager } MessageView { @@ -210,6 +212,7 @@ Page { source: TimelineManager.onVideoCall ? "VideoCall.qml" : "" onLoaded: TimelineManager.setVideoCallItem() } + } TypingIndicator { diff --git a/resources/qml/VideoCall.qml b/resources/qml/VideoCall.qml index 69fc1a2b..14408b6e 100644 --- a/resources/qml/VideoCall.qml +++ b/resources/qml/VideoCall.qml @@ -1,7 +1,6 @@ import QtQuick 2.9 - import org.freedesktop.gstreamer.GLVideoItem 1.0 GstGLVideoItem { - objectName: "videoCallItem" + objectName: "videoCallItem" } diff --git a/resources/res.qrc b/resources/res.qrc index efb9c907..02f31498 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -132,6 +132,7 @@ qml/Avatar.qml qml/ImageButton.qml qml/MatrixText.qml + qml/NhekoBusyIndicator.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml qml/Reactions.qml diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 6e1ed8ca..e09041e7 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -268,126 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, SIGNAL(unreadMessages(int))); - connect( - text_input_, - &TextInputWidget::uploadMedia, - this, - [this](QSharedPointer dev, QString mimeClass, const QString &fn) { - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(bin); - - auto payload = std::string(bin.data(), bin.size()); - std::optional encryptedFile; - if (cache::isRoomEncrypted(current_room_.toStdString())) { - mtx::crypto::BinaryBuf buf; - std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); - payload = mtx::crypto::to_string(buf); - } - - QSize dimensions; - QString blurhash; - if (mimeClass == "image") { - QImage img = utils::readImage(&bin); - - dimensions = img.size(); - if (img.height() > 200 && img.width() > 360) - img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); - std::vector data; - for (int y = 0; y < img.height(); y++) { - for (int x = 0; x < img.width(); x++) { - auto p = img.pixel(x, y); - data.push_back(static_cast(qRed(p))); - data.push_back(static_cast(qGreen(p))); - data.push_back(static_cast(qBlue(p))); - } - } - blurhash = QString::fromStdString( - blurhash::encode(data.data(), img.width(), img.height(), 4, 3)); - } - - http::client()->upload( - payload, - encryptedFile ? "application/octet-stream" : mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - encryptedFile, - mimeClass, - mime = mime.name(), - size = payload.size(), - dimensions, - blurhash](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload media. Please try again.")); - nhlog::net()->warn("failed to upload media: {} {} ({})", - err->matrix_error.error, - to_string(err->matrix_error.errcode), - static_cast(err->status_code)); - return; - } - - emit mediaUploaded(room_id, - filename, - encryptedFile, - QString::fromStdString(res.content_uri), - mimeClass, - mime, - size, - dimensions, - blurhash); - }); - }); - - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { - text_input_->hideUploadSpinner(); - emit showNotification(msg); - }); - connect(this, - &ChatPage::mediaUploaded, - this, - [this](QString roomid, - QString filename, - std::optional encryptedFile, - QString url, - QString mimeClass, - QString mime, - qint64 dsize, - QSize dimensions, - QString blurhash) { - text_input_->hideUploadSpinner(); - - if (encryptedFile) - encryptedFile->url = url.toStdString(); - - if (mimeClass == "image") - view_manager_->queueImageMessage(roomid, - filename, - encryptedFile, - url, - mime, - dsize, - dimensions, - blurhash); - else if (mimeClass == "audio") - view_manager_->queueAudioMessage( - roomid, filename, encryptedFile, url, mime, dsize); - else if (mimeClass == "video") - view_manager_->queueVideoMessage( - roomid, filename, encryptedFile, url, mime, dsize); - else - view_manager_->queueFileMessage( - roomid, filename, encryptedFile, url, mime, dsize); - }); - connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() { if (callManager_->onActiveCall()) { callManager_->hangUp(); diff --git a/src/ChatPage.h b/src/ChatPage.h index 9a38fd6e..41c6f276 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -126,17 +126,6 @@ signals: void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos); - void uploadFailed(const QString &msg); - void mediaUploaded(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mimeClass, - const QString &mime, - qint64 dsize, - const QSize &dimensions, - const QString &blurhash); - void contentLoaded(); void closing(); void changeWindowTitle(const int); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index dec7a574..232c0cad 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -88,10 +88,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) typingTimer_->setSingleShot(true); connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); - connect(&previewDialog_, - &dialogs::PreviewUploadOverlay::confirmUpload, - this, - &FilteredTextEdit::uploadData); connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); connect( @@ -355,81 +351,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } -bool -FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const -{ - return (source->hasImage() || QTextEdit::canInsertFromMimeData(source)); -} - -void -FilteredTextEdit::insertFromMimeData(const QMimeData *source) -{ - qInfo() << "Got mime formats: \n" << source->formats(); - const auto formats = source->formats().filter("/"); - const auto image = formats.filter("image/", Qt::CaseInsensitive); - const auto audio = formats.filter("audio/", Qt::CaseInsensitive); - const auto video = formats.filter("video/", Qt::CaseInsensitive); - - if (!image.empty() && source->hasImage()) { - QImage img = qvariant_cast(source->imageData()); - previewDialog_.setPreview(img, image.front()); - } else if (!audio.empty()) { - showPreview(source, audio); - } else if (!video.empty()) { - showPreview(source, video); - } else if (source->hasUrls()) { - // Generic file path for any platform. - QString path; - for (auto &&u : source->urls()) { - if (u.isLocalFile()) { - path = u.toLocalFile(); - break; - } - } - - if (!path.isEmpty() && QFileInfo{path}.exists()) { - previewDialog_.setPreview(path); - } else { - qWarning() - << "Clipboard does not contain any valid file paths:" << source->urls(); - } - } else if (source->hasFormat("x-special/gnome-copied-files")) { - // Special case for X11 users. See "Notes for X11 Users" in source. - // Source: http://doc.qt.io/qt-5/qclipboard.html - - // This MIME type returns a string with multiple lines separated by '\n'. The first - // line is the command to perform with the clipboard (not useful to us). The - // following lines are the file URIs. - // - // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function - // nautilus_clipboard_get_uri_list_from_selection_data() - // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c - - auto data = source->data("x-special/gnome-copied-files").split('\n'); - if (data.size() < 2) { - qWarning() << "MIME format is malformed, cannot perform paste."; - return; - } - - QString path; - for (int i = 1; i < data.size(); ++i) { - QUrl url{data[i]}; - if (url.isLocalFile()) { - path = url.toLocalFile(); - break; - } - } - - if (!path.isEmpty()) { - previewDialog_.setPreview(path); - } else { - qWarning() << "Clipboard does not contain any valid file paths:" << data; - } - } else { - QTextEdit::insertFromMimeData(source); - } -} - void FilteredTextEdit::stopTyping() { @@ -494,28 +415,6 @@ FilteredTextEdit::textChanged() working_history_[history_index_] = toPlainText(); } -void -FilteredTextEdit::uploadData(const QByteArray data, - const QString &mediaType, - const QString &filename) -{ - QSharedPointer buffer{new QBuffer{this}}; - buffer->setData(data); - - emit startedUpload(); - - emit media(buffer, mediaType, filename); -} - -void -FilteredTextEdit::showPreview(const QMimeData *source, const QStringList &formats) -{ - // Retrieve data as MIME type. - auto const &mime = formats.first(); - QByteArray data = source->data(mime); - previewDialog_.setPreview(data, mime); -} - TextInputWidget::TextInputWidget(QWidget *parent) : QWidget(parent) { @@ -624,7 +523,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) #endif connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -633,9 +531,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); - - connect( - input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner); } void @@ -654,47 +549,6 @@ TextInputWidget::addSelectedEmoji(const QString &emoji) input_->show(); } -void -TextInputWidget::openFileSelection() -{ - const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select a file"), homeFolder, tr("All Files (*)")); - - if (fileName.isEmpty()) - return; - - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); - - const auto format = mime.name().split("/")[0]; - - QSharedPointer file{new QFile{fileName, this}}; - - emit uploadMedia(file, format, QFileInfo(fileName).fileName()); - - showUploadSpinner(); -} - -void -TextInputWidget::showUploadSpinner() -{ - topLayout_->removeWidget(sendFileBtn_); - sendFileBtn_->hide(); - - topLayout_->insertWidget(1, spinner_); - spinner_->start(); -} - -void -TextInputWidget::hideUploadSpinner() -{ - topLayout_->removeWidget(spinner_); - topLayout_->insertWidget(1, sendFileBtn_); - sendFileBtn_->show(); - spinner_->stop(); -} - void TextInputWidget::stopTyping() { diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index f9d84871..44419547 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -57,9 +57,6 @@ signals: void startedTyping(); void stoppedTyping(); void startedUpload(); - void message(QString msg); - void command(QString name, QString args); - void media(QSharedPointer data, QString mimeClass, const QString &filename); //! Trigger the suggestion popup. void showSuggestions(const QString &query); @@ -73,8 +70,6 @@ public slots: protected: void keyPressEvent(QKeyEvent *event) override; - bool canInsertFromMimeData(const QMimeData *source) const override; - void insertFromMimeData(const QMimeData *source) override; void focusOutEvent(QFocusEvent *event) override { suggestionsPopup_.hide(); @@ -131,9 +126,7 @@ private: void insertCompletion(QString completion); void textChanged(); - void uploadData(const QByteArray data, const QString &media, const QString &filename); void afterCompletion(int); - void showPreview(const QMimeData *source, const QStringList &formats); }; class TextInputWidget : public QWidget @@ -161,8 +154,6 @@ public: } public slots: - void openFileSelection(); - void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } void changeCallButtonState(webrtc::State); @@ -172,9 +163,6 @@ private slots: signals: void heightChanged(int height); - void uploadMedia(const QSharedPointer data, - QString mimeClass, - const QString &filename); void callButtonPress(); void sendJoinRoomRequest(const QString &room); @@ -192,8 +180,6 @@ protected: void paintEvent(QPaintEvent *) override; private: - void showUploadSpinner(); - QHBoxLayout *topLayout_; FilteredTextEdit *input_; diff --git a/src/Utils.cpp b/src/Utils.cpp index 38dbba22..2896e773 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -677,9 +677,10 @@ utils::restoreCombobox(QComboBox *combo, const QString &value) } QImage -utils::readImage(QByteArray *data) +utils::readImage(const QByteArray *data) { - QBuffer buf(data); + QBuffer buf; + buf.setData(*data); QImageReader reader(&buf); reader.setAutoTransform(true); return reader.read(); diff --git a/src/Utils.h b/src/Utils.h index 5e7fb601..f59e8673 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -303,5 +303,5 @@ restoreCombobox(QComboBox *combo, const QString &value); //! Read image respecting exif orientation QImage -readImage(QByteArray *data); +readImage(const QByteArray *data); } diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp index 20959b0a..e03993c7 100644 --- a/src/dialogs/PreviewUploadOverlay.cpp +++ b/src/dialogs/PreviewUploadOverlay.cpp @@ -60,7 +60,10 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent) emit confirmUpload(data_, mediaType_, fileName_.text()); close(); }); - connect(&cancel_, &QPushButton::clicked, this, &PreviewUploadOverlay::close); + connect(&cancel_, &QPushButton::clicked, this, [this]() { + emit aborted(); + close(); + }); } void diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h index 11cd49bc..5139e3f2 100644 --- a/src/dialogs/PreviewUploadOverlay.h +++ b/src/dialogs/PreviewUploadOverlay.h @@ -40,6 +40,7 @@ public: signals: void confirmUpload(const QByteArray data, const QString &media, const QString &filename); + void aborted(); private: void init(); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index dcd4a106..bd8f6414 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -1,18 +1,27 @@ #include "InputBar.h" #include +#include #include #include +#include +#include +#include #include +#include #include "Cache.h" #include "ChatPage.h" #include "Logging.h" #include "MatrixClient.h" +#include "Olm.h" #include "TimelineModel.h" #include "UserSettingsPage.h" #include "Utils.h" +#include "dialogs/PreviewUploadOverlay.h" + +#include "blurhash.hpp" static constexpr size_t INPUT_HISTORY_SIZE = 10; @@ -32,7 +41,66 @@ InputBar::paste(bool fromMouse) if (!md) return; - if (md->hasImage()) { + nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString()); + const auto formats = md->formats().filter("/"); + const auto image = formats.filter("image/", Qt::CaseInsensitive); + const auto audio = formats.filter("audio/", Qt::CaseInsensitive); + const auto video = formats.filter("video/", Qt::CaseInsensitive); + + if (!image.empty() && md->hasImage()) { + showPreview(*md, "", image); + } else if (!audio.empty()) { + showPreview(*md, "", audio); + } else if (!video.empty()) { + showPreview(*md, "", video); + } else if (md->hasUrls()) { + // Generic file path for any platform. + QString path; + for (auto &&u : md->urls()) { + if (u.isLocalFile()) { + path = u.toLocalFile(); + break; + } + } + + if (!path.isEmpty() && QFileInfo{path}.exists()) { + showPreview(*md, path, formats); + } else { + nhlog::ui()->warn("Clipboard does not contain any valid file paths."); + } + } else if (md->hasFormat("x-special/gnome-copied-files")) { + // Special case for X11 users. See "Notes for X11 Users" in md. + // Source: http://doc.qt.io/qt-5/qclipboard.html + + // This MIME type returns a string with multiple lines separated by '\n'. The first + // line is the command to perform with the clipboard (not useful to us). The + // following lines are the file URIs. + // + // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function + // nautilus_clipboard_get_uri_list_from_selection_data() + // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c + + auto data = md->data("x-special/gnome-copied-files").split('\n'); + if (data.size() < 2) { + nhlog::ui()->warn("MIME format is malformed, cannot perform paste."); + return; + } + + QString path; + for (int i = 1; i < data.size(); ++i) { + QUrl url{data[i]}; + if (url.isLocalFile()) { + path = url.toLocalFile(); + break; + } + } + + if (!path.isEmpty()) { + showPreview(*md, path, formats); + } else { + nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}", + data.join(", ").toStdString()); + } } else if (md->hasText()) { emit insertText(md->text()); } else { @@ -78,6 +146,37 @@ InputBar::send() nhlog::ui()->debug("Send: {}", text.toStdString()); } +void +InputBar::openFileSelection() +{ + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const auto fileName = QFileDialog::getOpenFileName( + ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)")); + + if (fileName.isEmpty()) + return; + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + + QFile file{fileName}; + + if (!file.open(QIODevice::ReadOnly)) { + emit ChatPage::instance()->showNotification( + QString("Error while reading media: %1").arg(file.errorString())); + return; + } + + setUploading(true); + + auto bin = file.readAll(); + + QMimeData data; + data.setData(mime.name(), bin); + + showPreview(data, fileName, QStringList{mime.name()}); +} + void InputBar::message(QString msg) { @@ -149,6 +248,112 @@ InputBar::emote(QString msg) room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); } +void +InputBar::image(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const QString &blurhash) +{ + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.info.blurhash = blurhash.toStdString(); + image.body = filename.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + + if (file) + image.file = file; + else + image.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + image.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(image, mtx::events::EventType::RoomMessage); +} + +void +InputBar::file(const QString &filename, + const std::optional &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + + if (encryptedFile) + file.file = encryptedFile; + else + file.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + file.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(file, mtx::events::EventType::RoomMessage); +} + +void +InputBar::audio(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + + if (file) + audio.file = file; + else + audio.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + audio.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); +} + +void +InputBar::video(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + + if (file) + video.file = file; + else + video.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + video.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); +} + void InputBar::command(QString command, QString args) { @@ -196,3 +401,113 @@ InputBar::command(QString command, QString args) cache::dropOutboundMegolmSession(room->roomId().toStdString()); } } + +void +InputBar::showPreview(const QMimeData &source, QString path, const QStringList &formats) +{ + dialogs::PreviewUploadOverlay *previewDialog_ = + new dialogs::PreviewUploadOverlay(ChatPage::instance()); + previewDialog_->setAttribute(Qt::WA_DeleteOnClose); + + if (source.hasImage()) + previewDialog_->setPreview(qvariant_cast(source.imageData()), + formats.front()); + else if (!path.isEmpty()) + previewDialog_->setPreview(path); + else if (!formats.isEmpty()) { + auto mime = formats.first(); + previewDialog_->setPreview(source.data(mime), mime); + } else { + setUploading(false); + previewDialog_->deleteLater(); + return; + } + + connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() { + setUploading(false); + }); + + connect( + previewDialog_, + &dialogs::PreviewUploadOverlay::confirmUpload, + this, + [this](const QByteArray data, const QString &mime, const QString &fn) { + setUploading(true); + + auto payload = std::string(data.data(), data.size()); + std::optional encryptedFile; + if (cache::isRoomEncrypted(room->roomId().toStdString())) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); + payload = mtx::crypto::to_string(buf); + } + + QSize dimensions; + QString blurhash; + auto mimeClass = mime.split("/")[0]; + if (mimeClass == "image") { + QImage img = utils::readImage(&data); + + dimensions = img.size(); + if (img.height() > 200 && img.width() > 360) + img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); + std::vector data; + for (int y = 0; y < img.height(); y++) { + for (int x = 0; x < img.width(); x++) { + auto p = img.pixel(x, y); + data.push_back(static_cast(qRed(p))); + data.push_back(static_cast(qGreen(p))); + data.push_back(static_cast(qBlue(p))); + } + } + blurhash = QString::fromStdString( + blurhash::encode(data.data(), img.width(), img.height(), 4, 3)); + } + + http::client()->upload( + payload, + encryptedFile ? "application/octet-stream" : mime.toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + filename = fn, + encryptedFile = std::move(encryptedFile), + mimeClass, + mime, + size = payload.size(), + dimensions, + blurhash](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) mutable { + if (err) { + emit ChatPage::instance()->showNotification( + tr("Failed to upload media. Please try again.")); + nhlog::net()->warn("failed to upload media: {} {} ({})", + err->matrix_error.error, + to_string(err->matrix_error.errcode), + static_cast(err->status_code)); + setUploading(false); + return; + } + + auto url = QString::fromStdString(res.content_uri); + if (encryptedFile) + encryptedFile->url = res.content_uri; + + if (mimeClass == "image") + image(filename, + encryptedFile, + url, + mime, + size, + dimensions, + blurhash); + else if (mimeClass == "audio") + audio(filename, encryptedFile, url, mime, size); + else if (mimeClass == "video") + video(filename, encryptedFile, url, mime, size); + else + file(filename, encryptedFile, url, mime, size); + + setUploading(false); + }); + }); +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index f3a38c2e..35e3f8a4 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -3,11 +3,17 @@ #include #include +#include +#include + class TimelineModel; +class QMimeData; +class QStringList; class InputBar : public QObject { Q_OBJECT + Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) public: InputBar(TimelineModel *parent) @@ -19,18 +25,53 @@ public slots: void send(); void paste(bool fromMouse); void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); + void openFileSelection(); + bool uploading() const { return uploading_; } signals: void insertText(QString text); + void uploadingChanged(bool value); private: void message(QString body); void emote(QString body); void command(QString name, QString args); + void image(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const QString &blurhash); + void file(const QString &filename, + const std::optional &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize); + void audio(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize); + void video(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize); + + void showPreview(const QMimeData &source, QString path, const QStringList &formats); + void setUploading(bool value) + { + if (value != uploading_) { + uploading_ = value; + emit uploadingChanged(value); + } + } TimelineModel *room; QString text; std::deque history_; std::size_t history_index_ = 0; int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; + bool uploading_ = false; }; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 58a1496c..16a2565e 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -150,7 +150,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) - Q_PROPERTY(InputBar *input READ input) + Q_PROPERTY(InputBar *input READ input CONSTANT) public: explicit TimelineModel(TimelineViewManager *manager, -- cgit 1.5.1 From d1af1a86691290c67ad9e2b6ae9440b1254a2c0b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 15 Nov 2020 23:14:47 +0100 Subject: Move calls to new input bar --- resources/qml/MessageInput.qml | 13 +++- resources/qml/NhekoBusyIndicator.qml | 4 +- src/ChatPage.cpp | 32 --------- src/ChatPage.h | 1 + src/TextInputWidget.cpp | 30 -------- src/TextInputWidget.h | 5 -- src/timeline/InputBar.cpp | 45 ++++++++++++ src/timeline/InputBar.h | 1 + src/timeline/TimelineViewManager.cpp | 136 ++++++----------------------------- src/timeline/TimelineViewManager.h | 31 ++------ 10 files changed, 86 insertions(+), 212 deletions(-) (limited to 'src/timeline/InputBar.cpp') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index a1220599..17e1198d 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -17,14 +17,18 @@ Rectangle { spacing: 16 ImageButton { + visible: TimelineManager.callsSupported Layout.alignment: Qt.AlignBottom hoverEnabled: true width: 22 height: 22 - image: ":/icons/icons/ui/place-call.png" + image: TimelineManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png" + ToolTip.visible: hovered + ToolTip.text: TimelineManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call") Layout.topMargin: 8 Layout.bottomMargin: 8 Layout.leftMargin: 16 + onClicked: TimelineManager.timeline.input.callButton() } ImageButton { @@ -35,7 +39,10 @@ Rectangle { image: ":/icons/icons/ui/paper-clip-outline.png" Layout.topMargin: 8 Layout.bottomMargin: 8 + Layout.leftMargin: TimelineManager.callsSupported ? 0 : 16 onClicked: TimelineManager.timeline.input.openFileSelection() + ToolTip.visible: hovered + ToolTip.text: qsTr("Send a file") Rectangle { anchors.fill: parent @@ -112,6 +119,8 @@ Rectangle { image: ":/icons/icons/ui/smile.png" Layout.topMargin: 8 Layout.bottomMargin: 8 + ToolTip.visible: hovered + ToolTip.text: qsTr("Emoji") } ImageButton { @@ -123,6 +132,8 @@ Rectangle { Layout.topMargin: 8 Layout.bottomMargin: 8 Layout.rightMargin: 16 + ToolTip.visible: hovered + ToolTip.text: qsTr("Send") onClicked: { TimelineManager.timeline.input.send(); textArea.clear(); diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml index 8889989a..89a40dd5 100644 --- a/resources/qml/NhekoBusyIndicator.qml +++ b/resources/qml/NhekoBusyIndicator.qml @@ -6,8 +6,8 @@ BusyIndicator { id: control contentItem: Item { - implicitWidth: Math.min(parent.height, parent.width) - implicitHeight: implicitWidth + implicitWidth: 64 + implicitHeight: 64 Item { id: item diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index e09041e7..c703d95f 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -268,38 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, SIGNAL(unreadMessages(int))); - connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() { - if (callManager_->onActiveCall()) { - callManager_->hangUp(); - } else { - if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString()); - roomInfo.member_count != 2) { - showNotification("Calls are limited to 1:1 rooms."); - } else { - std::vector members( - cache::getMembers(current_room_.toStdString())); - const RoomMember &callee = - members.front().user_id == utils::localUser() ? members.back() - : members.front(); - auto dialog = new dialogs::PlaceCall( - callee.user_id, - callee.display_name, - QString::fromStdString(roomInfo.name), - QString::fromStdString(roomInfo.avatar_url), - userSettings_, - MainWindow::instance()); - connect(dialog, &dialogs::PlaceCall::voice, this, [this]() { - callManager_->sendInvite(current_room_, false); - }); - connect(dialog, &dialogs::PlaceCall::video, this, [this]() { - callManager_->sendInvite(current_room_, true); - }); - utils::centerWidget(dialog, MainWindow::instance()); - dialog->show(); - } - } - }); - connect( this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); diff --git a/src/ChatPage.h b/src/ChatPage.h index 41c6f276..7eb37f04 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -88,6 +88,7 @@ public: static ChatPage *instance() { return instance_; } QSharedPointer userSettings() { return userSettings_; } + CallManager *callManager() { return callManager_; } void deleteConfigs(); CommunitiesList *communitiesList() { return communitiesList_; } diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 232c0cad..1a856abb 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -431,15 +431,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) topLayout_->setSpacing(0); topLayout_->setContentsMargins(13, 1, 13, 0); -#ifdef GSTREAMER_AVAILABLE - callBtn_ = new FlatButton(this); - changeCallButtonState(webrtc::State::DISCONNECTED); - connect(&WebRTCSession::instance(), - &WebRTCSession::stateChanged, - this, - &TextInputWidget::changeCallButtonState); -#endif - QIcon send_file_icon; send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png"); @@ -508,9 +499,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) emojiBtn_->setIcon(emoji_icon); emojiBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); -#ifdef GSTREAMER_AVAILABLE - topLayout_->addWidget(callBtn_); -#endif topLayout_->addWidget(sendFileBtn_); topLayout_->addWidget(input_); topLayout_->addWidget(emojiBtn_); @@ -518,9 +506,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) setLayout(topLayout_); -#ifdef GSTREAMER_AVAILABLE - connect(callBtn_, &FlatButton::clicked, this, &TextInputWidget::callButtonPress); -#endif connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); connect(emojiBtn_, @@ -570,18 +555,3 @@ TextInputWidget::paintEvent(QPaintEvent *) style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } - -void -TextInputWidget::changeCallButtonState(webrtc::State state) -{ - QIcon icon; - if (state == webrtc::State::ICEFAILED || state == webrtc::State::DISCONNECTED) { - callBtn_->setToolTip(tr("Place a call")); - icon.addFile(":/icons/icons/ui/place-call.png"); - } else { - callBtn_->setToolTip(tr("Hang up")); - icon.addFile(":/icons/icons/ui/end-call.png"); - } - callBtn_->setIcon(icon); - callBtn_->setIconSize(QSize(ButtonHeight * 1.1, ButtonHeight * 1.1)); -} diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 44419547..9613f209 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -26,7 +26,6 @@ #include #include -#include "WebRTCSession.h" #include "dialogs/PreviewUploadOverlay.h" #include "emoji/PickButton.h" #include "popups/SuggestionsPopup.h" @@ -155,7 +154,6 @@ public: public slots: void focusLineEdit() { input_->setFocus(); } - void changeCallButtonState(webrtc::State); private slots: void addSelectedEmoji(const QString &emoji); @@ -163,8 +161,6 @@ private slots: signals: void heightChanged(int height); - void callButtonPress(); - void sendJoinRoomRequest(const QString &room); void sendInviteRoomRequest(const QString &userid, const QString &reason); void sendKickRoomRequest(const QString &userid, const QString &reason); @@ -185,7 +181,6 @@ private: LoadingIndicator *spinner_; - FlatButton *callBtn_; FlatButton *sendFileBtn_; FlatButton *sendMessageBtn_; emoji::PickButton *emojiBtn_; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index bd8f6414..dc287f94 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -12,13 +12,16 @@ #include #include "Cache.h" +#include "CallManager.h" #include "ChatPage.h" #include "Logging.h" +#include "MainWindow.h" #include "MatrixClient.h" #include "Olm.h" #include "TimelineModel.h" #include "UserSettingsPage.h" #include "Utils.h" +#include "dialogs/PlaceCall.h" #include "dialogs/PreviewUploadOverlay.h" #include "blurhash.hpp" @@ -511,3 +514,45 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList & }); }); } + +void +InputBar::callButton() +{ + auto callManager_ = ChatPage::instance()->callManager(); + if (callManager_->onActiveCall()) { + callManager_->hangUp(); + } else { + auto current_room_ = room->roomId(); + if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString()); + roomInfo.member_count != 2) { + ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms."); + } else { + std::vector members( + cache::getMembers(current_room_.toStdString())); + const RoomMember &callee = members.front().user_id == utils::localUser() + ? members.back() + : members.front(); + auto dialog = + new dialogs::PlaceCall(callee.user_id, + callee.display_name, + QString::fromStdString(roomInfo.name), + QString::fromStdString(roomInfo.avatar_url), + ChatPage::instance()->userSettings(), + MainWindow::instance()); + connect(dialog, + &dialogs::PlaceCall::voice, + callManager_, + [callManager_, current_room_]() { + callManager_->sendInvite(current_room_, false); + }); + connect(dialog, + &dialogs::PlaceCall::video, + callManager_, + [callManager_, current_room_]() { + callManager_->sendInvite(current_room_, true); + }); + utils::centerWidget(dialog, MainWindow::instance()); + dialog->show(); + } + } +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 35e3f8a4..a52a3904 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -27,6 +27,7 @@ public slots: void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); void openFileSelection(); bool uploading() const { return uploading_; } + void callButton(); signals: void insertText(QString text); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index f949498d..1392c505 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -244,6 +244,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par &CallManager::newVideoCallState, this, &TimelineViewManager::videoCallChanged); + + connect(&WebRTCSession::instance(), + &WebRTCSession::stateChanged, + this, + &TimelineViewManager::onCallChanged); +} + +bool +TimelineViewManager::isOnCall() const +{ + return callManager_->onActiveCall(); +} +bool +TimelineViewManager::callsSupported() const +{ +#ifdef GSTREAMER_AVAILABLE + return true; +#else + return false; +#endif } void @@ -506,122 +526,6 @@ TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QSt timeline_->redactEvent(selfReactedEvent); } } - -void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions, - const QString &blurhash) -{ - mtx::events::msg::Image image; - image.info.mimetype = mime.toStdString(); - image.info.size = dsize; - image.info.blurhash = blurhash.toStdString(); - image.body = filename.toStdString(); - image.info.h = dimensions.height(); - image.info.w = dimensions.width(); - - if (file) - image.file = file; - else - image.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - image.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(image, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueFileMessage( - const QString &roomid, - const QString &filename, - const std::optional &encryptedFile, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::File file; - file.info.mimetype = mime.toStdString(); - file.info.size = dsize; - file.body = filename.toStdString(); - - if (encryptedFile) - file.file = encryptedFile; - else - file.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - file.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(file, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = mime.toStdString(); - audio.info.size = dsize; - audio.body = filename.toStdString(); - audio.url = url.toStdString(); - - if (file) - audio.file = file; - else - audio.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - audio.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Video video; - video.info.mimetype = mime.toStdString(); - video.info.size = dsize; - video.body = filename.toStdString(); - - if (file) - video.file = file; - else - video.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - video.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(video, mtx::events::EventType::RoomMessage); -} - void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &callInvite) diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 02e0e132..371e9af2 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -41,6 +41,8 @@ class TimelineViewManager : public QObject Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY callPartyChanged) Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY callPartyChanged) Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) + Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY onCallChanged) + Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT) public: TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr); @@ -95,6 +97,7 @@ signals: void videoCallChanged(); void callPartyChanged(); void micMuteChanged(); + void onCallChanged(); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); @@ -104,38 +107,14 @@ public slots: void setHistoryView(const QString &room_id); void updateColorPalette(); void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); - void queueImageMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions, - const QString &blurhash); - void queueFileMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueAudioMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueVideoMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); void updateEncryptedDescriptions(); + bool isOnCall() const; + bool callsSupported() const; void enableBackButton() { -- cgit 1.5.1 From 921379a4cc3bc6381c4562bfdb9ce0dcde6b1f2c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 17 Nov 2020 02:37:43 +0100 Subject: Send typing updates from QML --- src/ChatPage.cpp | 47 ------------------------------------------ src/ChatPage.h | 4 ---- src/TextInputWidget.cpp | 30 --------------------------- src/TextInputWidget.h | 10 --------- src/timeline/InputBar.cpp | 41 ++++++++++++++++++++++++++++++++++++ src/timeline/InputBar.h | 16 +++++++++++++- src/timeline/TimelineModel.cpp | 1 - 7 files changed, 56 insertions(+), 93 deletions(-) (limited to 'src/timeline/InputBar.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c703d95f..1b235a95 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -141,9 +141,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) text_input_ = new TextInputWidget(this); contentLayout_->addWidget(text_input_); - typingRefresher_ = new QTimer(this); - typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); - connect(this, &ChatPage::connectionLost, this, [this]() { nhlog::net()->info("connectivity lost"); isConnected_ = false; @@ -221,9 +218,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) { this->current_room_ = room_id; }); - connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); - connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit); connect( room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); @@ -237,27 +232,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) room_list_->removeRoom(room_id, currentRoom() == room_id); }); - connect( - text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications); - connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications); - connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() { - if (!userSettings_->typingNotifications()) - return; - - typingRefresher_->stop(); - - if (current_room_.isEmpty()) - return; - - http::client()->stop_typing( - current_room_.toStdString(), [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to stop typing notifications: {}", - err->matrix_error.error); - } - }); - }); - connect(view_manager_, &TimelineViewManager::updateRoomsLastMessage, room_list_, @@ -435,12 +409,6 @@ ChatPage::resetUI() emit unreadMessages(0); } -void -ChatPage::focusMessageInput() -{ - this->text_input_->focusLineEdit(); -} - void ChatPage::deleteConfigs() { @@ -1099,21 +1067,6 @@ ChatPage::receivedSessionKey(const std::string &room_id, const std::string &sess view_manager_->receivedSessionKey(room_id, session_id); } -void -ChatPage::sendTypingNotifications() -{ - if (!userSettings_->typingNotifications()) - return; - - http::client()->start_typing( - current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send typing notification: {}", - err->matrix_error.error); - } - }); -} - QString ChatPage::status() const { diff --git a/src/ChatPage.h b/src/ChatPage.h index 7eb37f04..37abafa0 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -100,7 +100,6 @@ public: //! Show the room/group list (if it was visible). void showSideBars(); void initiateLogout(); - void focusMessageInput(); QString status() const; void setStatus(const QString &status); @@ -191,7 +190,6 @@ private slots: void removeRoom(const QString &room_id); void dropToLoginPage(const QString &msg); - void sendTypingNotifications(); void handleSyncResponse(const mtx::responses::Sync &res); private: @@ -265,8 +263,6 @@ private: popups::UserMentions *user_mentions_popup_; - QTimer *typingRefresher_; - // Global user settings. QSharedPointer userSettings_; diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 92449231..30589b61 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -83,12 +83,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) insertCompletion(emoji); }); - typingTimer_ = new QTimer(this); - typingTimer_->setInterval(1000); - typingTimer_->setSingleShot(true); - - connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); - connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); connect( &suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { @@ -164,13 +158,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_U) QTextEdit::setText(""); - if (!isModifier) { - if (!typingTimer_->isActive()) - emit startedTyping(); - - typingTimer_->start(); - } - // calculate the new query if (textCursor().position() < atTriggerPosition_ || !isAnchorValid()) { resetAnchor(); @@ -264,7 +251,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } if (!(event->modifiers() & Qt::ShiftModifier)) { - stopTyping(); submit(); } else { QTextEdit::keyPressEvent(event); @@ -351,13 +337,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } -void -FilteredTextEdit::stopTyping() -{ - typingTimer_->stop(); - emit stoppedTyping(); -} - QRect FilteredTextEdit::completerRect() { @@ -494,15 +473,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); - - connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); -} - -void -TextInputWidget::stopTyping() -{ - input_->stopTyping(); } void diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index afd29439..c62a98be 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -43,8 +43,6 @@ class FilteredTextEdit : public QTextEdit public: explicit FilteredTextEdit(QWidget *parent = nullptr); - void stopTyping(); - QSize sizeHint() const override; QSize minimumSizeHint() const override; @@ -52,8 +50,6 @@ public: signals: void heightChanged(int height); - void startedTyping(); - void stoppedTyping(); void startedUpload(); //! Trigger the suggestion popup. @@ -81,7 +77,6 @@ private: int trigger_pos_; // Where emoji completer was triggered size_t history_index_; QCompleter *completer_; - QTimer *typingTimer_; SuggestionsPopup suggestionsPopup_; @@ -136,8 +131,6 @@ class TextInputWidget : public QWidget public: TextInputWidget(QWidget *parent = nullptr); - void stopTyping(); - QColor borderColor() const { return borderColor_; } void setBorderColor(QColor &color) { borderColor_ = color; } void disableInput() @@ -164,9 +157,6 @@ signals: void sendUnbanRoomRequest(const QString &userid, const QString &reason); void changeRoomNick(const QString &displayname); - void startedTyping(); - void stoppedTyping(); - protected: void focusInEvent(QFocusEvent *event) override; void paintEvent(QPaintEvent *) override; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index dc287f94..6603287b 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -114,6 +114,11 @@ InputBar::paste(bool fromMouse) void InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_) { + if (text_.isEmpty()) + stopTyping(); + else + startTyping(); + selectionStart = selectionStart_; selectionEnd = selectionEnd_; cursorPosition = cursorPosition_; @@ -556,3 +561,39 @@ InputBar::callButton() } } } + +void +InputBar::startTyping() +{ + if (!typingRefresh_.isActive()) { + typingRefresh_.start(); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + http::client()->start_typing( + room->roomId().toStdString(), 10'000, [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to send typing notification: {}", + err->matrix_error.error); + } + }); + } + } + typingTimeout_.start(); +} +void +InputBar::stopTyping() +{ + typingRefresh_.stop(); + typingTimeout_.stop(); + + if (!ChatPage::instance()->userSettings()->typingNotifications()) + return; + + http::client()->stop_typing(room->roomId().toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to stop typing notifications: {}", + err->matrix_error.error); + } + }); +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index a52a3904..0e9ef592 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -19,7 +20,14 @@ public: InputBar(TimelineModel *parent) : QObject() , room(parent) - {} + { + typingRefresh_.setInterval(10'000); + typingRefresh_.setSingleShot(true); + typingTimeout_.setInterval(5'000); + typingTimeout_.setSingleShot(true); + connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping); + connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); + } public slots: void send(); @@ -29,6 +37,10 @@ public slots: bool uploading() const { return uploading_; } void callButton(); +private slots: + void startTyping(); + void stopTyping(); + signals: void insertText(QString text); void uploadingChanged(bool value); @@ -69,6 +81,8 @@ private: } } + QTimer typingRefresh_; + QTimer typingTimeout_; TimelineModel *room; QString text; std::deque history_; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 8b80ea51..4cbd5777 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -800,7 +800,6 @@ void TimelineModel::replyAction(QString id) { setReply(id); - ChatPage::instance()->focusMessageInput(); } RelatedInfo -- cgit 1.5.1 From d14a5f80676a695b3a087bd1378e8057fc7ffc13 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 17 Nov 2020 13:25:16 +0100 Subject: Reimplement message history --- resources/qml/MessageInput.qml | 5 +++- src/timeline/InputBar.cpp | 66 +++++++++++++++++++++++++++++++++--------- src/timeline/InputBar.h | 5 +++- 3 files changed, 60 insertions(+), 16 deletions(-) (limited to 'src/timeline/InputBar.cpp') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index da76c6ac..236cc009 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -84,7 +84,10 @@ Rectangle { TimelineManager.timeline.input.send(); textArea.clear(); event.accepted = true; - } + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) + textArea.text = TimelineManager.timeline.input.previousText(); + else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) + textArea.text = TimelineManager.timeline.input.nextText(); } Connections { diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 6603287b..1eaaaa64 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -119,39 +119,77 @@ InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition else startTyping(); + if (text_ != text()) { + if (history_.empty()) + history_.push_front(text_); + else + history_.front() = text_; + history_index_ = 0; + } + selectionStart = selectionStart_; selectionEnd = selectionEnd_; cursorPosition = cursorPosition_; - text = text_; +} + +QString +InputBar::text() const +{ + if (history_index_ < history_.size()) + return history_.at(history_index_); + + return ""; +} + +QString +InputBar::previousText() +{ + history_index_++; + if (history_index_ >= INPUT_HISTORY_SIZE) + history_index_ = INPUT_HISTORY_SIZE; + else if (text().isEmpty()) + history_index_--; + + return text(); +} + +QString +InputBar::nextText() +{ + history_index_--; + if (history_index_ >= INPUT_HISTORY_SIZE) + history_index_ = 0; + + return text(); } void InputBar::send() { - if (text.trimmed().isEmpty()) + if (text().trimmed().isEmpty()) return; - if (history_.size() == INPUT_HISTORY_SIZE) - history_.pop_back(); - history_.push_front(text); - history_index_ = 0; - - if (text.startsWith('/')) { - int command_end = text.indexOf(' '); + if (text().startsWith('/')) { + int command_end = text().indexOf(' '); if (command_end == -1) - command_end = text.size(); - auto name = text.mid(1, command_end - 1); - auto args = text.mid(command_end + 1); + command_end = text().size(); + auto name = text().mid(1, command_end - 1); + auto args = text().mid(command_end + 1); if (name.isEmpty() || name == "/") { message(args); } else { command(name, args); } } else { - message(text); + message(text()); } - nhlog::ui()->debug("Send: {}", text.toStdString()); + nhlog::ui()->debug("Send: {}", text().toStdString()); + + if (history_.size() == INPUT_HISTORY_SIZE) + history_.pop_back(); + history_.push_front(""); + history_index_ = 0; } void diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 0e9ef592..5e66e86f 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -30,6 +30,10 @@ public: } public slots: + QString text() const; + QString previousText(); + QString nextText(); + void send(); void paste(bool fromMouse); void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); @@ -84,7 +88,6 @@ private: QTimer typingRefresh_; QTimer typingTimeout_; TimelineModel *room; - QString text; std::deque history_; std::size_t history_index_ = 0; int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; -- cgit 1.5.1 From cabeb1464c85d911eea427bd48e8188facde8e56 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 01:22:36 +0100 Subject: WIP Qml completer --- resources/qml/Completer.qml | 107 +++++++++++++++++++++++++++++++++++++++++ resources/qml/MessageInput.qml | 58 +++++++++++++++++++--- resources/res.qrc | 15 +++--- src/timeline/InputBar.cpp | 6 +++ src/timeline/InputBar.h | 2 + 5 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 resources/qml/Completer.qml (limited to 'src/timeline/InputBar.cpp') diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml new file mode 100644 index 00000000..d53eae62 --- /dev/null +++ b/resources/qml/Completer.qml @@ -0,0 +1,107 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import im.nheko 1.0 + +Popup { + id: popup + + property int currentIndex: -1 + property string completerName + property var completer + + function up() { + currentIndex = currentIndex - 1; + if (currentIndex == -2) + currentIndex = repeater.count - 1; + + } + + function down() { + currentIndex = currentIndex + 1; + if (currentIndex >= repeater.count) + currentIndex = -1; + + } + + function currentCompletion() { + if (currentIndex > -1 && currentIndex < repeater.count) + return completer.completionAt(currentIndex); + else + return null; + } + + onCompleterNameChanged: { + if (completerName) + completer = TimelineManager.timeline.input.completerFor(completerName); + + } + padding: 0 + onAboutToShow: currentIndex = -1 + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Repeater { + id: repeater + + model: completer + + delegate: Rectangle { + color: model.index == popup.currentIndex ? colors.window : colors.base + height: del.implicitHeight + 4 + width: del.implicitWidth + 4 + + RowLayout { + id: del + + anchors.centerIn: parent + + Avatar { + height: 24 + width: 24 + displayName: model.displayName + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + } + + Label { + text: model.displayName + color: colors.text + } + + } + + } + + } + + } + + enter: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 100 + } + + } + + exit: Transition { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 100 + } + + } + + background: Rectangle { + color: colors.base + implicitHeight: popup.contentHeight + implicitWidth: popup.contentWidth + } + +} diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index a5c84a17..a4a47a3e 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -68,28 +68,72 @@ Rectangle { TextArea { id: textArea + property int completerTriggeredAt: -1 + placeholderText: qsTr("Write a message...") placeholderTextColor: colors.buttonText color: colors.text wrapMode: TextEdit.Wrap onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - onCursorPositionChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onCursorPositionChanged: { + TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + if (cursorPosition < completerTriggeredAt) { + completerTriggeredAt = -1; + popup.close(); + } + } onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + // Ensure that we get escape key press events first. + Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onPressed: { if (event.matches(StandardKey.Paste)) { TimelineManager.timeline.input.paste(false); event.accepted = true; + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { + textArea.clear(); + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { + textArea.text = TimelineManager.timeline.input.previousText(); + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { + textArea.text = TimelineManager.timeline.input.nextText(); + } else if (event.key == Qt.Key_At) { + completerTriggeredAt = cursorPosition + 1; + popup.completerName = "user"; + popup.open(); + } else if (event.key == Qt.Key_Escape && popup.opened) { + completerTriggeredAt = -1; + event.accepted = true; + popup.close(); + } else if (event.matches(StandardKey.InsertParagraphSeparator) && popup.opened) { + var currentCompletion = popup.currentCompletion(); + popup.close(); + if (currentCompletion) { + textArea.remove(completerTriggeredAt - 1, cursorPosition); + textArea.insert(cursorPosition, currentCompletion); + event.accepted = true; + return ; + } + } else if (event.key == Qt.Key_Tab && popup.opened) { + event.accepted = true; + popup.down(); + } else if (event.key == Qt.Key_Up && popup.opened) { + event.accepted = true; + popup.up(); + } else if (event.key == Qt.Key_Down && popup.opened) { + event.accepted = true; + popup.down(); } else if (event.matches(StandardKey.InsertParagraphSeparator)) { TimelineManager.timeline.input.send(); textArea.clear(); event.accepted = true; - } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) - textArea.clear(); - else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) - textArea.text = TimelineManager.timeline.input.previousText(); - else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) - textArea.text = TimelineManager.timeline.input.nextText(); + } + } + + Completer { + id: popup + + x: textArea.positionToRectangle(textArea.completerTriggeredAt).x + y: textArea.positionToRectangle(textArea.completerTriggeredAt).y - height } Connections { diff --git a/resources/res.qrc b/resources/res.qrc index 02f31498..a01907ec 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -123,21 +123,22 @@ qtquickcontrols2.conf qml/TimelineView.qml - qml/TopBar.qml - qml/MessageView.qml - qml/MessageInput.qml - qml/TypingIndicator.qml - qml/ReplyPopup.qml qml/ActiveCallBar.qml qml/Avatar.qml + qml/Completer.qml + qml/EncryptionIndicator.qml qml/ImageButton.qml qml/MatrixText.qml + qml/MessageInput.qml + qml/MessageView.qml qml/NhekoBusyIndicator.qml - qml/StatusIndicator.qml - qml/EncryptionIndicator.qml qml/Reactions.qml + qml/ReplyPopup.qml qml/ScrollHelper.qml + qml/StatusIndicator.qml qml/TimelineRow.qml + qml/TopBar.qml + qml/TypingIndicator.qml qml/VideoCall.qml qml/emoji/EmojiButton.qml qml/emoji/EmojiPicker.qml diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 1eaaaa64..82649faa 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -163,6 +163,12 @@ InputBar::nextText() return text(); } +QObject * +InputBar::completerFor(QString completerName) +{ + return nullptr; +} + void InputBar::send() { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 5e66e86f..939e8dad 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -41,6 +41,8 @@ public slots: bool uploading() const { return uploading_; } void callButton(); + QObject *completerFor(QString completerName); + private slots: void startTyping(); void stopTyping(); -- cgit 1.5.1 From add5903fb0abc76d77ce4369c4679a95be03b433 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 02:38:08 +0100 Subject: Working User completer --- CMakeLists.txt | 2 ++ resources/qml/Completer.qml | 8 +++++++- resources/qml/MessageInput.qml | 20 ++++++++++++++++---- src/CompletionModel.h | 20 -------------------- src/CompletionModelRoles.h | 2 +- src/CompletionProxyModel.h | 21 ++++++++++++++++++++- src/UsersModel.cpp | 28 ++++++++++++++++++++++------ src/UsersModel.h | 12 +++++++----- src/timeline/InputBar.cpp | 8 ++++++++ 9 files changed, 83 insertions(+), 38 deletions(-) delete mode 100644 src/CompletionModel.h (limited to 'src/timeline/InputBar.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index aa81f285..7e68db77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -492,6 +492,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ChatPage.h src/CommunitiesList.h src/CommunitiesListItem.h + src/CompletionProxyModel.h src/DeviceVerificationFlow.h src/InviteeItem.h src/LoginPage.h @@ -508,6 +509,7 @@ qt5_wrap_cpp(MOC_HEADERS src/TrayIcon.h src/UserInfoWidget.h src/UserSettingsPage.h + src/UsersModel.h src/WebRTCSession.h src/WelcomePage.h src/popups/PopupItem.h diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index d53eae62..2c520dec 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -34,11 +34,17 @@ Popup { onCompleterNameChanged: { if (completerName) completer = TimelineManager.timeline.input.completerFor(completerName); - + else + completer = undefined; } padding: 0 onAboutToShow: currentIndex = -1 + Connections { + onTimelineChanged: completer = null + target: TimelineManager + } + ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index a4a47a3e..50ff7324 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -77,14 +77,13 @@ Rectangle { onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onCursorPositionChanged: { TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); - if (cursorPosition < completerTriggeredAt) { + if (cursorPosition <= completerTriggeredAt) { completerTriggeredAt = -1; popup.close(); } } onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - // Ensure that we get escape key press events first. Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onPressed: { if (event.matches(StandardKey.Paste)) { @@ -97,18 +96,20 @@ Rectangle { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { textArea.text = TimelineManager.timeline.input.nextText(); } else if (event.key == Qt.Key_At) { - completerTriggeredAt = cursorPosition + 1; + completerTriggeredAt = cursorPosition; popup.completerName = "user"; popup.open(); } else if (event.key == Qt.Key_Escape && popup.opened) { completerTriggeredAt = -1; + popup.completerName = ""; event.accepted = true; popup.close(); } else if (event.matches(StandardKey.InsertParagraphSeparator) && popup.opened) { var currentCompletion = popup.currentCompletion(); + popup.completerName = ""; popup.close(); if (currentCompletion) { - textArea.remove(completerTriggeredAt - 1, cursorPosition); + textArea.remove(completerTriggeredAt, cursorPosition); textArea.insert(cursorPosition, currentCompletion); event.accepted = true; return ; @@ -129,6 +130,17 @@ Rectangle { } } + Connections { + onTimelineChanged: { + textArea.clear(); + textArea.append(TimelineManager.timeline.input.text()); + textArea.completerTriggeredAt = -1; + popup.completerName = ""; + } + target: TimelineManager + } + // Ensure that we get escape key press events first. + Completer { id: popup diff --git a/src/CompletionModel.h b/src/CompletionModel.h deleted file mode 100644 index ed021051..00000000 --- a/src/CompletionModel.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -// Class for showing a limited amount of completions at a time - -#include - -class CompletionModel : public QSortFilterProxyModel -{ -public: - CompletionModel(QAbstractItemModel *model, QObject *parent = nullptr) - : QSortFilterProxyModel(parent) - { - setSourceModel(model); - } - int rowCount(const QModelIndex &parent) const override - { - auto row_count = QSortFilterProxyModel::rowCount(parent); - return (row_count < 7) ? row_count : 7; - } -}; diff --git a/src/CompletionModelRoles.h b/src/CompletionModelRoles.h index bd6c4114..7c7307d3 100644 --- a/src/CompletionModelRoles.h +++ b/src/CompletionModelRoles.h @@ -10,6 +10,6 @@ enum Roles { CompletionRole = Qt::UserRole * 2, // The string to replace the active completion SearchRole, // String completer uses for search + SearchRole2, // Secondary string completer uses for search }; - } diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index ee38075e..757aa990 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -4,17 +4,36 @@ #include +#include "CompletionModelRoles.h" + class CompletionProxyModel : public QSortFilterProxyModel { + Q_OBJECT + public: CompletionProxyModel(QAbstractItemModel *model, QObject *parent = nullptr) : QSortFilterProxyModel(parent) { setSourceModel(model); } - int rowCount(const QModelIndex &parent) const override + + QHash roleNames() const override + { + return this->sourceModel()->roleNames(); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override { auto row_count = QSortFilterProxyModel::rowCount(parent); return (row_count < 7) ? row_count : 7; } + +public slots: + QVariant completionAt(int i) const + { + if (i >= 0 && i < rowCount()) + return data(index(i, 0), CompletionModel::CompletionRole); + else + return {}; + } }; diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index c63292bb..f102cff1 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -3,12 +3,23 @@ #include "Cache.h" #include "CompletionModelRoles.h" -#include - UsersModel::UsersModel(const std::string &roomId, QObject *parent) : QAbstractListModel(parent) + , room_id(roomId) +{ + roomMembers_ = cache::roomMembers(roomId); +} + +QHash +UsersModel::roleNames() const { - roomMembers_ = cache::getMembers(roomId, 0, 9999); + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + }; } QVariant @@ -19,9 +30,14 @@ UsersModel::data(const QModelIndex &index, int role) const case CompletionModel::CompletionRole: case CompletionModel::SearchRole: case Qt::DisplayRole: - return roomMembers_[index.row()].display_name; - case Avatar: - return roomMembers_[index.row()].avatar; + case Roles::DisplayName: + return QString::fromStdString( + cache::displayName(room_id, roomMembers_[index.row()])); + case CompletionModel::SearchRole2: + return QString::fromStdString(roomMembers_[index.row()]); + case Roles::AvatarUrl: + return cache::avatarUrl(QString::fromStdString(room_id), + QString::fromStdString(roomMembers_[index.row()])); } } return {}; diff --git a/src/UsersModel.h b/src/UsersModel.h index 09ccbf25..6ee8261f 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -2,23 +2,25 @@ #include -class RoomMember; - class UsersModel : public QAbstractListModel { public: enum Roles { - Avatar = Qt::UserRole // QImage avatar + AvatarUrl = Qt::UserRole, + DisplayName, }; UsersModel(const std::string &roomId, QObject *parent = nullptr); + QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override { - return (parent == QModelIndex()) ? roomMembers_.size() : 0; + (void)parent; + return roomMembers_.size(); } QVariant data(const QModelIndex &index, int role) const override; private: - std::vector roomMembers_; + std::string room_id; + std::vector roomMembers_; }; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 82649faa..641d8379 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -14,12 +14,14 @@ #include "Cache.h" #include "CallManager.h" #include "ChatPage.h" +#include "CompletionProxyModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" #include "Olm.h" #include "TimelineModel.h" #include "UserSettingsPage.h" +#include "UsersModel.h" #include "Utils.h" #include "dialogs/PlaceCall.h" #include "dialogs/PreviewUploadOverlay.h" @@ -166,6 +168,12 @@ InputBar::nextText() QObject * InputBar::completerFor(QString completerName) { + if (completerName == "user") { + auto userModel = new UsersModel(room->roomId().toStdString()); + auto proxy = new CompletionProxyModel(userModel); + userModel->setParent(proxy); + return proxy; + } return nullptr; } -- cgit 1.5.1 From 094c0b09abba70e5e4cec5740f918ae56971f3e7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 04:33:11 +0100 Subject: Qml emoji completer --- resources/qml/Completer.qml | 60 +++++++++++++++++++++++++++++++++--------- resources/qml/MessageInput.qml | 4 +++ src/emoji/EmojiModel.cpp | 4 +++ src/timeline/InputBar.cpp | 6 +++++ 4 files changed, 62 insertions(+), 12 deletions(-) (limited to 'src/timeline/InputBar.cpp') diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 2c520dec..0557a2e7 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -56,24 +56,60 @@ Popup { delegate: Rectangle { color: model.index == popup.currentIndex ? colors.window : colors.base - height: del.implicitHeight + 4 - width: del.implicitWidth + 4 + height: chooser.childrenRect.height + 4 + width: chooser.childrenRect.width + 4 - RowLayout { - id: del + DelegateChooser { + id: chooser + roleValue: popup.completerName anchors.centerIn: parent - Avatar { - height: 24 - width: 24 - displayName: model.displayName - url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + DelegateChoice { + roleValue: "user" + + RowLayout { + id: del + + anchors.centerIn: parent + + Avatar { + height: 24 + width: 24 + displayName: model.displayName + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + } + + Label { + text: model.displayName + color: colors.text + } + + } + } - Label { - text: model.displayName - color: colors.text + DelegateChoice { + roleValue: "emoji" + + RowLayout { + id: del + + anchors.centerIn: parent + + Label { + text: model.unicode + color: colors.text + font: Settings.emojiFont + } + + Label { + text: model.shortName + color: colors.text + } + + } + } } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 1366689c..ac91e46c 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -99,6 +99,10 @@ Rectangle { completerTriggeredAt = cursorPosition; popup.completerName = "user"; popup.open(); + } else if (event.key == Qt.Key_Colon) { + completerTriggeredAt = cursorPosition; + popup.completerName = "emoji"; + popup.open(); } else if (event.key == Qt.Key_Escape && popup.opened) { completerTriggeredAt = -1; popup.completerName = ""; diff --git a/src/emoji/EmojiModel.cpp b/src/emoji/EmojiModel.cpp index b6a985b8..85c2dd34 100644 --- a/src/emoji/EmojiModel.cpp +++ b/src/emoji/EmojiModel.cpp @@ -3,6 +3,8 @@ #include #include +#include "CompletionModelRoles.h" + using namespace emoji; QHash @@ -35,10 +37,12 @@ EmojiModel::data(const QModelIndex &index, int role) const if (hasIndex(index.row(), index.column(), index.parent())) { switch (role) { case Qt::DisplayRole: + case CompletionModel::CompletionRole: case static_cast(EmojiModel::Roles::Unicode): return Provider::emoji[index.row()].unicode; case Qt::ToolTipRole: + case CompletionModel::SearchRole: case static_cast(EmojiModel::Roles::ShortName): return Provider::emoji[index.row()].shortName; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 641d8379..5c8f0f11 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -25,6 +25,7 @@ #include "Utils.h" #include "dialogs/PlaceCall.h" #include "dialogs/PreviewUploadOverlay.h" +#include "emoji/EmojiModel.h" #include "blurhash.hpp" @@ -173,6 +174,11 @@ InputBar::completerFor(QString completerName) auto proxy = new CompletionProxyModel(userModel); userModel->setParent(proxy); return proxy; + } else if (completerName == "emoji") { + auto emojiModel = new emoji::EmojiModel(); + auto proxy = new CompletionProxyModel(emojiModel); + emojiModel->setParent(proxy); + return proxy; } return nullptr; } -- cgit 1.5.1 From c2eea5cb5508ce0ec090b84dc9a2c9fd25b4dd88 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 23 Nov 2020 18:19:24 +0100 Subject: Fix mimetype of media messages --- src/dialogs/PreviewUploadOverlay.cpp | 8 ++++---- src/timeline/InputBar.cpp | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src/timeline/InputBar.cpp') diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp index e03993c7..bd207642 100644 --- a/src/dialogs/PreviewUploadOverlay.cpp +++ b/src/dialogs/PreviewUploadOverlay.cpp @@ -118,7 +118,7 @@ PreviewUploadOverlay::init() void PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64_t upload_size) { - if (mediaType_ == "image") { + if (mediaType_.split('/')[0] == "image") { if (!image_.loadFromData(data_)) { titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); } else { @@ -154,7 +154,7 @@ PreviewUploadOverlay::setPreview(const QImage &src, const QString &mime) else titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); - mediaType_ = split[0]; + mediaType_ = mime; filePath_ = "clipboard." + type; image_.convertFromImage(src); isImage_ = true; @@ -170,7 +170,7 @@ PreviewUploadOverlay::setPreview(const QByteArray data, const QString &mime) auto const &type = split[1]; data_ = data; - mediaType_ = split[0]; + mediaType_ = mime; filePath_ = "clipboard." + type; isImage_ = false; @@ -202,7 +202,7 @@ PreviewUploadOverlay::setPreview(const QString &path) auto const &split = mime.name().split('/'); - mediaType_ = split[0]; + mediaType_ = mime.name(); filePath_ = file.fileName(); isImage_ = false; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 5c8f0f11..b0d555b7 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -511,6 +511,7 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList & QSize dimensions; QString blurhash; auto mimeClass = mime.split("/")[0]; + nhlog::ui()->debug("Mime: {}", mime.toStdString()); if (mimeClass == "image") { QImage img = utils::readImage(&data); -- cgit 1.5.1 From c74077a41f5e89c0331d682b481408ad22d7ec78 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 25 Nov 2020 17:02:23 +0100 Subject: Implement Qml drag and drop --- CMakeLists.txt | 12 ++++++----- resources/qml/MessageInput.qml | 5 +++++ src/ChatPage.h | 1 + src/timeline/InputBar.cpp | 8 ++++++++ src/timeline/InputBar.h | 2 ++ src/timeline/TimelineViewManager.cpp | 3 +++ src/timeline/TimelineViewManager.h | 9 +++++++++ src/ui/NhekoDropArea.cpp | 39 ++++++++++++++++++++++++++++++++++++ src/ui/NhekoDropArea.h | 30 +++++++++++++++++++++++++++ 9 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/ui/NhekoDropArea.cpp create mode 100644 src/ui/NhekoDropArea.h (limited to 'src/timeline/InputBar.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index bd42938a..e8570c77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -257,22 +257,23 @@ set(SRC_FILES src/ui/Avatar.cpp src/ui/Badge.cpp src/ui/DropShadow.cpp - src/ui/LoadingIndicator.cpp - src/ui/InfoMessage.cpp src/ui/FlatButton.cpp src/ui/FloatingButton.cpp + src/ui/InfoMessage.cpp src/ui/Label.cpp + src/ui/LoadingIndicator.cpp + src/ui/NhekoDropArea.cpp src/ui/OverlayModal.cpp - src/ui/SnackBar.cpp + src/ui/OverlayWidget.cpp src/ui/RaisedButton.cpp src/ui/Ripple.cpp src/ui/RippleOverlay.cpp - src/ui/OverlayWidget.cpp + src/ui/SnackBar.cpp src/ui/TextField.cpp src/ui/TextLabel.cpp - src/ui/ToggleButton.cpp src/ui/Theme.cpp src/ui/ThemeManager.cpp + src/ui/ToggleButton.cpp src/ui/UserProfile.cpp src/AvatarProvider.cpp @@ -471,6 +472,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ui/Label.h src/ui/FloatingButton.h src/ui/Menu.h + src/ui/NhekoDropArea.h src/ui/OverlayWidget.h src/ui/SnackBar.h src/ui/RaisedButton.h diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 76eac5b3..812c450e 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -178,6 +178,11 @@ Rectangle { onClicked: TimelineManager.timeline.input.paste(true) } + NhekoDropArea { + anchors.fill: parent + roomid: TimelineManager.timeline.roomId() + } + background: Rectangle { color: colors.window } diff --git a/src/ChatPage.h b/src/ChatPage.h index 273ba4af..5b336cbb 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -88,6 +88,7 @@ public: QSharedPointer userSettings() { return userSettings_; } CallManager *callManager() { return callManager_; } + TimelineViewManager *timelineManager() { return view_manager_; } void deleteConfigs(); CommunitiesList *communitiesList() { return communitiesList_; } diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index b0d555b7..46bbd9a2 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -1,6 +1,7 @@ #include "InputBar.h" #include +#include #include #include #include @@ -44,6 +45,13 @@ InputBar::paste(bool fromMouse) md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard); } + if (md) + insertMimeData(md); +} + +void +InputBar::insertMimeData(const QMimeData *md) +{ if (!md) return; diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 939e8dad..27aa4bc3 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -9,6 +9,7 @@ class TimelineModel; class QMimeData; +class QDropEvent; class QStringList; class InputBar : public QObject @@ -36,6 +37,7 @@ public slots: void send(); void paste(bool fromMouse); + void insertMimeData(const QMimeData *data); void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); void openFileSelection(); bool uploading() const { return uploading_; } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 1392c505..866e7c51 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,6 +1,7 @@ #include "TimelineViewManager.h" #include +#include #include #include #include @@ -20,6 +21,7 @@ #include "dialogs/ImageOverlay.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include "ui/NhekoDropArea.h" #include //only for debugging @@ -115,6 +117,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qmlRegisterType("im.nheko", 1, 0, "DelegateChoice"); qmlRegisterType("im.nheko", 1, 0, "DelegateChooser"); + qmlRegisterType("im.nheko", 1, 0, "NhekoDropArea"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); qmlRegisterUncreatableType( diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 371e9af2..b9febf75 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -105,6 +105,15 @@ public slots: void initWithMessages(const std::vector &roomIds); void setHistoryView(const QString &room_id); + TimelineModel *getHistoryView(const QString &room_id) + { + auto room = models.find(room_id); + if (room != models.end()) + return room.value().data(); + else + return nullptr; + } + void updateColorPalette(); void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp new file mode 100644 index 00000000..14b71524 --- /dev/null +++ b/src/ui/NhekoDropArea.cpp @@ -0,0 +1,39 @@ +#include "NhekoDropArea.h" + +#include + +#include "ChatPage.h" +#include "timeline/InputBar.h" +#include "timeline/TimelineModel.h" +#include "timeline/TimelineViewManager.h" + +#include "Logging.h" + +NhekoDropArea::NhekoDropArea(QQuickItem *parent) + : QQuickItem(parent) +{ + setFlags(ItemAcceptsDrops); +} + +void +NhekoDropArea::dragEnterEvent(QDragEnterEvent *event) +{ + event->acceptProposedAction(); +} + +void +NhekoDropArea::dragMoveEvent(QDragMoveEvent *event) +{ + event->acceptProposedAction(); +} + +void +NhekoDropArea::dropEvent(QDropEvent *event) +{ + if (event) { + auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_); + if (model) { + model->input()->insertMimeData(event->mimeData()); + } + } +} diff --git a/src/ui/NhekoDropArea.h b/src/ui/NhekoDropArea.h new file mode 100644 index 00000000..b03620f2 --- /dev/null +++ b/src/ui/NhekoDropArea.h @@ -0,0 +1,30 @@ +#include + +class NhekoDropArea : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged) +public: + NhekoDropArea(QQuickItem *parent = nullptr); + +signals: + void roomidChanged(QString roomid); + +public slots: + void setRoomid(QString roomid) + { + if (roomid_ != roomid) { + roomid_ = roomid; + emit roomidChanged(roomid); + } + } + QString roomid() const { return roomid_; } + +protected: + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + +private: + QString roomid_; +}; -- cgit 1.5.1