diff --git a/main.cpp b/main.cpp index 5cccb32..7150ccf 100644 --- a/main.cpp +++ b/main.cpp @@ -31,6 +31,7 @@ int main(int argc, char *argv[]) qmlRegisterType("Matrique", 0, 1, "Controller"); qmlRegisterType("Matrique", 0, 1, "RoomListModel"); qmlRegisterType("Matrique", 0, 1, "MessageEventModel"); + qRegisterMetaType("User*"); QQmlApplicationEngine engine; diff --git a/matrix/controller.cpp b/matrix/controller.cpp index 08f3cee..0b86fb5 100644 --- a/matrix/controller.cpp +++ b/matrix/controller.cpp @@ -1,12 +1,13 @@ #include "controller.h" -#include "libqmatrixclient/connection.h" +#include "connection.h" Controller::Controller(QObject *parent) : QObject(parent) { connect(m_connection, &QMatrixClient::Connection::connected, this, &Controller::connected); connect(m_connection, &QMatrixClient::Connection::resolveError, this, &Controller::reconnect); connect(m_connection, &QMatrixClient::Connection::syncError, this, &Controller::reconnect); connect(m_connection, &QMatrixClient::Connection::syncDone, this, &Controller::resync); + connect(m_connection, &QMatrixClient::Connection::connected, this, &Controller::connectionChanged); } Controller::~Controller() { diff --git a/matrix/controller.h b/matrix/controller.h index 3258949..a34d3f9 100644 --- a/matrix/controller.h +++ b/matrix/controller.h @@ -2,7 +2,8 @@ #define CONTROLLER_H #include -#include "libqmatrixclient/connection.h" +#include "connection.h" +#include "user.h" #include "roomlistmodel.h" namespace QMatrixClient { diff --git a/matrix/imageprovider.h b/matrix/imageprovider.h index cb26560..dafb2b7 100644 --- a/matrix/imageprovider.h +++ b/matrix/imageprovider.h @@ -5,7 +5,7 @@ #include #include -#include "libqmatrixclient/connection.h" +#include "connection.h" #include "imageproviderconnection.h" class ImageProvider: public QQuickImageProvider diff --git a/matrix/imageproviderconnection.h b/matrix/imageproviderconnection.h index 752a048..7d153c6 100644 --- a/matrix/imageproviderconnection.h +++ b/matrix/imageproviderconnection.h @@ -3,7 +3,7 @@ #include -#include "libqmatrixclient/connection.h" +#include "connection.h" class ImageProviderConnection : public QObject { diff --git a/matrix/libqmatrixclient b/matrix/libqmatrixclient index b7c1ff1..c4acd8e 160000 --- a/matrix/libqmatrixclient +++ b/matrix/libqmatrixclient @@ -1 +1 @@ -Subproject commit b7c1ff183384738f170d53128c684681cb34f3b7 +Subproject commit c4acd8ece12622164caf396c06bd0f22ab3589f7 diff --git a/matrix/messageeventmodel.cpp b/matrix/messageeventmodel.cpp index a2f740c..cc758c9 100644 --- a/matrix/messageeventmodel.cpp +++ b/matrix/messageeventmodel.cpp @@ -7,37 +7,25 @@ #include "events/roommemberevent.h" #include "events/simplestateevents.h" #include "events/redactionevent.h" +#include "events/roomavatarevent.h" #include "connection.h" #include "user.h" #include "settings.h" -QHash MessageEventModel::roleNames() const -{ - QHash roles = QAbstractItemModel::roleNames(); - roles[EventTypeRole] = "eventType"; - roles[EventIdRole] = "eventId"; - roles[TimeRole] = "time"; - roles[SectionRole] = "section"; - roles[AboveSectionRole] = "aboveSection"; - roles[AuthorRole] = "author"; - roles[ContentRole] = "content"; - roles[ContentTypeRole] = "contentType"; - roles[ReadMarkerRole] = "readMarker"; - roles[SpecialMarksRole] = "marks"; - roles[LongOperationRole] = "progressInfo"; - return roles; -} - MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent) - , m_currentRoom(nullptr) { qmlRegisterType(); qRegisterMetaType(); } -void MessageEventModel::changeRoom(QMatrixClient::Room* room) +MessageEventModel::~MessageEventModel() +{ + +} + +void MessageEventModel::setRoom(QMatrixClient::Room* room) { if (room == m_currentRoom) return; @@ -90,6 +78,7 @@ void MessageEventModel::changeRoom(QMatrixClient::Room* room) } lastReadEventId = room ? room->readMarkerEventId() : ""; endResetModel(); + emit roomChanged(); } void MessageEventModel::refreshEvent(const QString& eventId) @@ -163,156 +152,146 @@ int MessageEventModel::rowCount(const QModelIndex& parent) const QVariant MessageEventModel::data(const QModelIndex& index, int role) const { - if(!m_currentRoom || + if( !m_currentRoom || index.row() < 0 || index.row() >= m_currentRoom->timelineSize()) return QVariant(); - const auto eventIt = m_currentRoom->messageEvents().rbegin() + index.row(); - auto* event = eventIt->event(); - // FIXME: Rewind to the name that was right before this event - QString senderName = m_currentRoom->roomMembername(event->senderId()); + const auto timelineIt = m_currentRoom->messageEvents().rbegin() + index.row(); + const auto& ti = *timelineIt; using namespace QMatrixClient; - if(role == Qt::DisplayRole) + if( role == Qt::DisplayRole ) { - if (event->isRedacted()) + if (ti->isRedacted()) { - auto reason = event->redactedBecause()->reason(); + auto reason = ti->redactedBecause()->reason(); if (reason.isEmpty()) return tr("Redacted"); - else - return tr("Redacted: %1") - .arg(event->redactedBecause()->reason()); + + return tr("Redacted: %1") + .arg(ti->redactedBecause()->reason()); } - if(event->type() == EventType::RoomMessage) - { - using namespace MessageEventContent; + return visit(*ti + , [this] (const RoomMessageEvent& e) -> QVariant { + using namespace MessageEventContent; - auto* e = static_cast(event); - if (e->hasTextContent() && e->mimeType().name() != "text/plain") - return static_cast(e->content())->body; - if (e->hasFileContent()) - { - auto fileCaption = e->content()->fileInfo()->originalName; - if (fileCaption.isEmpty()) - fileCaption = m_currentRoom->prettyPrint(e->plainBody()); - if (fileCaption.isEmpty()) - return tr("a file"); - } - return m_currentRoom->prettyPrint(e->plainBody()); - } - if(event->type() == EventType::RoomMember) - { - auto* e = static_cast(event); - // FIXME: Rewind to the name that was at the time of this event - QString subjectName = m_currentRoom->roomMembername(e->userId()); - // The below code assumes senderName output in AuthorRole - switch(e->membership()) - { - case MembershipType::Invite: - if (e->repeatsState()) - return tr("reinvited %1 to the room").arg(subjectName); - // [[fallthrough]] - case MembershipType::Join: + if (e.hasTextContent() && e.mimeType().name() != "text/plain") + return static_cast(e.content())->body; + if (e.hasFileContent()) { - if (e->repeatsState()) - return tr("joined the room (repeated)"); - if (!e->prev_content() || - e->membership() != e->prev_content()->membership) - { - return e->membership() == MembershipType::Invite - ? tr("invited %1 to the room").arg(subjectName) - : tr("joined the room"); - } - QString text {}; - if (e->displayName() != e->prev_content()->displayName) - { - if (e->displayName().isEmpty()) - text = tr("cleared the display name"); - else - text = tr("changed the display name to %1") - .arg(e->displayName()); - } - if (e->avatarUrl() != e->prev_content()->avatarUrl) - { - if (!text.isEmpty()) - text += " and "; - if (e->avatarUrl().isEmpty()) - text += tr("cleared the avatar"); - else - text += tr("updated the avatar"); - } - return text; + auto fileCaption = e.content()->fileInfo()->originalName; + if (fileCaption.isEmpty()) + fileCaption = m_currentRoom->prettyPrint(e.plainBody()); + if (fileCaption.isEmpty()) + return tr("a file"); } - case MembershipType::Leave: - if (e->prev_content() && - e->prev_content()->membership == MembershipType::Ban) - { - if (e->senderId() != e->userId()) - return tr("unbanned %1").arg(subjectName); - else - return tr("self-unbanned"); - } - if (e->senderId() != e->userId()) - return tr("has put %1 out of the room").arg(subjectName); - else - return tr("left the room"); - case MembershipType::Ban: - if (e->senderId() != e->userId()) - return tr("banned %1 from the room").arg(subjectName); - else - return tr("self-banned from the room"); - case MembershipType::Knock: - return tr("knocked"); -// case MembershipType::Unknown; -// return tr("made something unknown"); + return m_currentRoom->prettyPrint(e.plainBody()); } - } - if(event->type() == EventType::RoomAliases) - { - auto* e = static_cast(event); - return tr("set aliases to: %1").arg(e->aliases().join(", ")); - } - if(event->type() == EventType::RoomCanonicalAlias) - { - auto* e = static_cast(event); - return tr("set the room main alias to: %1").arg(e->alias()); - } - if(event->type() == EventType::RoomName) - { - auto* e = static_cast(event); - return tr("set the room name to: %1").arg(e->name()); - } - if(event->type() == EventType::RoomTopic) - { - auto* e = static_cast(event); - return tr("set the topic to: %1").arg(e->topic()); - } - if(event->type() == EventType::RoomAvatar) - { - return tr("changed the room avatar"); - } - if(event->type() == EventType::RoomEncryption) - { - return tr("activated End-to-End Encryption"); - } - return tr("Unknown Event"); + , [this] (const RoomMemberEvent& e) -> QVariant { + // FIXME: Rewind to the name that was at the time of this event + QString subjectName = m_currentRoom->roomMembername(e.userId()); + // The below code assumes senderName output in AuthorRole + switch( e.membership() ) + { + case MembershipType::Invite: + if (e.repeatsState()) + return tr("reinvited %1 to the room").arg(subjectName); + FALLTHROUGH; + case MembershipType::Join: + { + if (e.repeatsState()) + return tr("joined the room (repeated)"); + if (!e.prevContent() || + e.membership() != e.prevContent()->membership) + { + return e.membership() == MembershipType::Invite + ? tr("invited %1 to the room").arg(subjectName) + : tr("joined the room"); + } + QString text {}; + if (e.displayName() != e.prevContent()->displayName) + { + if (e.displayName().isEmpty()) + text = tr("cleared the display name"); + else + text = tr("changed the display name to %1") + .arg(e.displayName()); + } + if (e.avatarUrl() != e.prevContent()->avatarUrl) + { + if (!text.isEmpty()) + text += " and "; + if (e.avatarUrl().isEmpty()) + text += tr("cleared the avatar"); + else + text += tr("updated the avatar"); + } + return text; + } + case MembershipType::Leave: + if (e.prevContent() && + e.prevContent()->membership == MembershipType::Ban) + { + return (e.senderId() != e.userId()) + ? tr("unbanned %1").arg(subjectName) + : tr("self-unbanned"); + } + return (e.senderId() != e.userId()) + ? tr("has put %1 out of the room").arg(subjectName) + : tr("left the room"); + case MembershipType::Ban: + return (e.senderId() != e.userId()) + ? tr("banned %1 from the room").arg(subjectName) + : tr("self-banned from the room"); + case MembershipType::Knock: + return tr("knocked"); + default: + ; + } + return tr("made something unknown"); + } + , [] (const RoomAliasesEvent& e) -> QVariant { + return tr("set aliases to: %1").arg(e.aliases().join(", ")); + } + , [] (const RoomCanonicalAliasEvent& e) -> QVariant { + return (e.alias().isEmpty()) + ? tr("cleared the room main alias") + : tr("set the room main alias to: %1").arg(e.alias()); + } + , [] (const RoomNameEvent& e) -> QVariant { + return (e.name().isEmpty()) + ? tr("cleared the room name") + : tr("set the room name to: %1").arg(e.name()); + } + , [] (const RoomTopicEvent& e) -> QVariant { + return (e.topic().isEmpty()) + ? tr("cleared the topic") + : tr("set the topic to: %1").arg(e.topic()); + } + , [] (const RoomAvatarEvent&) -> QVariant { + return tr("changed the room avatar"); + } + , [] (const EncryptionEvent&) -> QVariant { + return tr("activated End-to-End Encryption"); + } + , tr("Unknown Event") + ); } - if(role == Qt::ToolTipRole) + if( role == Qt::ToolTipRole ) { - return event->originalJson(); + return ti->originalJson(); } - if(role == EventTypeRole) + if( role == EventTypeRole ) { - if (event->isStateEvent()) + if (ti->isStateEvent()) return "state"; - if (event->type() == EventType::RoomMessage) + if (auto e = ti.viewAs()) { - switch (static_cast(event)->msgtype()) + switch (e->msgtype()) { case MessageEventType::Emote: return "emote"; @@ -328,44 +307,31 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const return "message"; } } - return "other"; } - if(role == TimeRole) - return makeMessageTimestamp(eventIt); + if (role == EventResolvedTypeRole) + return EventTypeRegistry::getMatrixType(ti->type()); - if(role == SectionRole) - return makeDateString(eventIt); // FIXME: move date rendering to QML + if( role == TimeRole ) + return makeMessageTimestamp(timelineIt); - if(role == AboveSectionRole) // FIXME: shouldn't be here, because #312 + if( role == SectionRole ) + return makeDateString(timelineIt); // FIXME: move date rendering to QML + + if( role == AuthorRole ) { - auto aboveEventIt = eventIt + 1; - if (aboveEventIt != m_currentRoom->timelineEdge()) - return makeDateString(aboveEventIt); - } - - if(role == AuthorRole) - { - auto userId = event->senderId(); - // FIXME: This will go away after senderName is generated correctly - // (see the FIXME in the beginning of the method). -// if (event->type() == EventType::RoomMember) -// { -// const auto* e = static_cast(event); -// if (e->senderId() == e->userId() /*???*/ && e->prev_content() -// && !e->prev_content()->displayName.isEmpty()) -// userId = e->prevSenderId(); -// } - return QVariant::fromValue(m_currentRoom->connection()->user(userId)); + auto userId = ti->senderId(); + // FIXME: It shouldn't be User, it should be its state "as of event" + return QVariant::fromValue(m_currentRoom->user(userId)); } if (role == ContentTypeRole) { - if (event->type() == EventType::RoomMessage) + if (is(*ti)) { const auto& contentType = - static_cast(event)->mimeType().name(); + ti.viewAs()->mimeType().name(); return contentType == "text/plain" ? "text/html" : contentType; } return "text/plain"; @@ -373,21 +339,19 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const if (role == ContentRole) { - if (event->isRedacted()) + if (ti->isRedacted()) { - auto reason = event->redactedBecause()->reason(); - if (reason.isEmpty()) - return tr("Redacted"); - else - return tr("Redacted: %1") - .arg(event->redactedBecause()->reason()); + auto reason = ti->redactedBecause()->reason(); + return (reason.isEmpty()) + ? tr("Redacted") + : tr("Redacted: %1").arg(ti->redactedBecause()->reason()); } - if(event->type() == EventType::RoomMessage) + if (is(*ti)) { using namespace MessageEventContent; - auto* e = static_cast(event); + auto* e = ti.viewAs(); switch (e->msgtype()) { case MessageEventType::Image: @@ -401,29 +365,64 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const } } - if(role == ReadMarkerRole) - return event->id() == lastReadEventId; + if( role == HighlightRole ) + return QVariant(); - if(role == SpecialMarksRole) + if( role == ReadMarkerRole ) + return ti->id() == lastReadEventId; + + if( role == SpecialMarksRole ) { - if (event->isStateEvent() && - static_cast(event)->repeatsState()) - return "hidden"; - return event->isRedacted() ? "redacted" : ""; + if (auto e = ti.viewAs()) + if (e->repeatsState()) + return "hidden"; + return ti->isRedacted() ? "redacted" : ""; } - if(role == EventIdRole) - return event->id(); + if( role == EventIdRole ) + return ti->id(); - if(role == LongOperationRole) + if( role == LongOperationRole ) { - if (event->type() == EventType::RoomMessage && - static_cast(event)->hasFileContent()) + if (is(*ti) && + ti.viewAs()->hasFileContent()) { - auto info = m_currentRoom->fileTransferInfo(event->id()); + auto info = m_currentRoom->fileTransferInfo(ti->id()); return QVariant::fromValue(info); } } + auto aboveEventIt = timelineIt + 1; // FIXME: shouldn't be here, because #312 + if (aboveEventIt != m_currentRoom->timelineEdge()) + { + if( role == AboveSectionRole ) + return makeDateString(aboveEventIt); + + if( role == AboveAuthorRole ) + return QVariant::fromValue( + m_currentRoom->user((*aboveEventIt)->senderId())); + } + return QVariant(); } + + +QHash MessageEventModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles[EventTypeRole] = "eventType"; + roles[EventIdRole] = "eventId"; + roles[TimeRole] = "time"; + roles[SectionRole] = "section"; + roles[AboveSectionRole] = "aboveSection"; + roles[AuthorRole] = "author"; + roles[AboveAuthorRole] = "aboveAuthor"; + roles[ContentRole] = "content"; + roles[ContentTypeRole] = "contentType"; + roles[HighlightRole] = "highlight"; + roles[ReadMarkerRole] = "readMarker"; + roles[SpecialMarksRole] = "marks"; + roles[LongOperationRole] = "progressInfo"; + roles[EventResolvedTypeRole] = "eventResolvedType"; +return roles; +} diff --git a/matrix/messageeventmodel.h b/matrix/messageeventmodel.h index 81f2584..8fce89b 100644 --- a/matrix/messageeventmodel.h +++ b/matrix/messageeventmodel.h @@ -7,12 +7,7 @@ class MessageEventModel: public QAbstractListModel { Q_OBJECT - // The below property is marked constant because it only changes - // when the whole model is reset (so anything that depends on the model - // has to be re-calculated anyway). - // XXX: A better way would be to make [Room::]Timeline a list model - // itself, leaving only representation of the model to a client. - Q_PROPERTY(QMatrixClient::Room* room MEMBER m_currentRoom CONSTANT) + Q_PROPERTY(QMatrixClient::Room* room READ getRoom WRITE setRoom NOTIFY roomChanged) public: enum EventRoles { @@ -22,31 +17,40 @@ class MessageEventModel: public QAbstractListModel SectionRole, AboveSectionRole, AuthorRole, + AboveAuthorRole, ContentRole, ContentTypeRole, + HighlightRole, ReadMarkerRole, SpecialMarksRole, LongOperationRole, + // For debugging + EventResolvedTypeRole, }; explicit MessageEventModel(QObject* parent = nullptr); + ~MessageEventModel(); - void changeRoom(QMatrixClient::Room* room); + QMatrixClient::Room* getRoom() { return m_currentRoom; } + void setRoom(QMatrixClient::Room* room); - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; + Q_INVOKABLE int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const; private slots: void refreshEvent(const QString& eventId); private: - QMatrixClient::Room* m_currentRoom; + QMatrixClient::Room* m_currentRoom = nullptr; QString lastReadEventId; QDateTime makeMessageTimestamp(QMatrixClient::Room::rev_iter_t baseIt) const; QString makeDateString(QMatrixClient::Room::rev_iter_t baseIt) const; void refreshEventRoles(const QString& eventId, const QVector roles); + + signals: + void roomChanged(); }; #endif // MESSAGEEVENTMODEL_H diff --git a/matrix/roomlistmodel.cpp b/matrix/roomlistmodel.cpp index d1dbbb3..84f4ccc 100644 --- a/matrix/roomlistmodel.cpp +++ b/matrix/roomlistmodel.cpp @@ -7,7 +7,7 @@ RoomListModel::RoomListModel(QObject* parent) : QAbstractListModel(parent) { - m_connection = 0; + } RoomListModel::~RoomListModel() diff --git a/matrix/roomlistmodel.h b/matrix/roomlistmodel.h index 2bcd417..d378558 100644 --- a/matrix/roomlistmodel.h +++ b/matrix/roomlistmodel.h @@ -30,7 +30,7 @@ class RoomListModel: public QAbstractListModel void addRoom(QMatrixClient::Room* room); private: - QMatrixClient::Connection* m_connection; + QMatrixClient::Connection* m_connection = nullptr; QList m_rooms; signals: diff --git a/qml/form/ListForm.qml b/qml/form/ListForm.qml index f031a4e..0ad0f0f 100644 --- a/qml/form/ListForm.qml +++ b/qml/form/ListForm.qml @@ -80,14 +80,7 @@ Item { function applyFilter(filterName){ var roomCount = listModel.rowCount(); for (var i = 0; i < roomCount; i++){ - var roomName = ""; - if (listModel.roomAt(i).name !== "") { - roomName = listModel.roomAt(i).name; - } else if (model.alias !== "") { - roomName = listModel.roomAt(i).alias; - } else { - roomName = listModel.roomAt(i).id; - } + var roomName = listModel.roomAt(i).displayName; if (roomName.toLowerCase().indexOf(filterName.toLowerCase()) !== -1) { items.addGroups(i, 1, "filterGroup"); } else {items.removeGroups(i, 1, "filterGroup");} diff --git a/qml/form/RoomForm.qml b/qml/form/RoomForm.qml index a0aa79d..fd64f14 100644 --- a/qml/form/RoomForm.qml +++ b/qml/form/RoomForm.qml @@ -52,7 +52,7 @@ Item { ImageStatus { Layout.preferredWidth: parent.height Layout.fillHeight: true - source: "qrc:/asset/img/avatar.png" + source: currentRoom != null && currentRoom.avatarUrl != "" ? "image://mxc/" + currentRoom.avatarUrl : "qrc:/asset/img/avatar.png" } ColumnLayout { @@ -88,34 +88,35 @@ Item { displayMarginEnd: 40 verticalLayoutDirection: ListView.BottomToTop spacing: 12 -// model: MessageEventModel{ currentRoom: item.currentRoom } - model: 10 + model: MessageEventModel{ + id: messageEventModel + room: currentRoom + } delegate: Row { - readonly property bool sentByMe: index % 2 == 0 + readonly property bool sentByMe: author === currentRoom.localUser id: messageRow - height: 40 anchors.right: sentByMe ? parent.right : undefined spacing: 6 - Rectangle { + Image { id: avatar width: height - height: parent.height - color: "grey" + height: 40 visible: !sentByMe + source: author.avatarUrl != "" ? "image://mxc/" + author.avatarUrl : "qrc:/asset/img/avatar.png" } Rectangle { width: Math.min(messageText.implicitWidth + 24, messageListView.width - (!sentByMe ? avatar.width + messageRow.spacing : 0)) - height: parent.height + height: messageText.implicitHeight + 24 color: sentByMe ? "lightgrey" : Material.accent Label { id: messageText - text: index + text: display color: sentByMe ? "black" : "white" anchors.fill: parent anchors.margins: 12 @@ -146,6 +147,7 @@ Item { } TextField { + id: inputField Layout.fillWidth: true Layout.fillHeight: true placeholderText: "Send a Message" @@ -157,6 +159,11 @@ Item { background: Rectangle { color: Material.theme == Material.Light ? "#eaeaea" : "#242424" } + + Keys.onReturnPressed: { + currentRoom.postMessage("text", inputField.text) + inputField.text = "" + } } ItemDelegate { diff --git a/qml/main.qml b/qml/main.qml index 3d4b8eb..1e7eeb0 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -118,7 +118,7 @@ ApplicationWindow { anchors.fill: parent anchors.margins: 15 - source: "qrc:/asset/img/avatar.png" + source: matriqueController.connection.localUser != null ? "image://mxc/" + matriqueController.connection.localUser.avatarUrl : "qrc:/asset/img/avatar.png" opaqueBackground: false }