Merge pull request #192 from Nheko-Reborn/reactions

Reactions
This commit is contained in:
DeepBlueV7.X 2020-05-08 00:13:24 +02:00 committed by GitHub
commit 7beaf868ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 296 additions and 23 deletions

View file

@ -249,6 +249,7 @@ set(SRC_FILES
src/emoji/Provider.cpp
# Timeline
src/timeline/ReactionsModel.cpp
src/timeline/TimelineViewManager.cpp
src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp
@ -335,7 +336,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG v0.3.0
GIT_TAG 1893cd6171c40c250ca64d388c082789452340a8
)
FetchContent_MakeAvailable(MatrixClient)
else()
@ -451,6 +452,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/emoji/PickButton.h
# Timeline
src/timeline/ReactionsModel.h
src/timeline/TimelineViewManager.h
src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h

View file

@ -146,9 +146,9 @@
"name": "mtxclient",
"sources": [
{
"sha256": "0c2930b5861d93bab9a6515adca74ebaa78984119705d9b4372a9deb275dd30c",
"sha256": "a8c0239b7157fe8eadae8b06cd6c4e3531dcc61fc5a7f52dbb3c85106f70e3a5",
"type": "archive",
"url": "https://github.com/Nheko-Reborn/mtxclient/archive/v0.3.0.tar.gz"
"url": "https://github.com/Nheko-Reborn/mtxclient/archive/1893cd6171c40c250ca64d388c082789452340a8.tar.gz"
}
]
},

View file

@ -19,7 +19,7 @@ Rectangle {
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
visible: img.status != Image.Ready
color: colors.brightText
color: colors.text
}
Image {
@ -43,5 +43,5 @@ Rectangle {
}
}
}
color: colors.dark
color: colors.base
}

View file

@ -1,17 +1,11 @@
import QtQuick 2.3
import QtQuick.Controls 2.3
Button {
AbstractButton {
property string image: undefined
id: button
flat: true
// disable background, because we don't want a border on hover
background: Item {
}
Image {
id: buttonImg
// Workaround, can't get icon.source working for now...

View file

@ -0,0 +1,76 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
Flow {
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
property alias reactions: repeater.model
Repeater {
id: repeater
AbstractButton {
id: reaction
text: model.key
hoverEnabled: true
implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding*2
implicitHeight: contentItem.childrenRect.height
ToolTip.visible: hovered
ToolTip.text: model.users
contentItem: Row {
anchors.centerIn: parent
spacing: reactionText.implicitHeight/4
leftPadding: reactionText.implicitHeight / 2
rightPadding: reactionText.implicitHeight / 2
TextMetrics {
id: textMetrics
font.family: settings.emoji_font_family
elide: Text.ElideRight
elideWidth: 150
text: reaction.text
}
Text {
anchors.baseline: reactionCounter.baseline
id: reactionText
text: textMetrics.elidedText + (textMetrics.elidedText == textMetrics.text ? "" : "…")
font.family: settings.emoji_font_family
color: reaction.hovered ? colors.highlight : colors.text
maximumLineCount: 1
}
Rectangle {
id: divider
height: reactionCounter.implicitHeight * 1.4
width: 1
color: reaction.hovered ? colors.highlight : colors.text
}
Text {
anchors.verticalCenter: divider.verticalCenter
id: reactionCounter
text: model.counter
font: reaction.font
color: reaction.hovered ? colors.highlight : colors.text
}
}
background: Rectangle {
anchors.centerIn: parent
implicitWidth: reaction.implicitWidth
implicitHeight: reaction.implicitHeight
border.color: (reaction.hovered || model.selfReacted )? colors.highlight : colors.text
color: colors.base
border.width: 1
radius: reaction.height / 2.0
}
}
}
}

View file

@ -52,6 +52,10 @@ MouseArea {
modelData: model
}
Reactions {
reactions: model.reactions
}
}
StatusIndicator {

View file

@ -25,6 +25,7 @@ Page {
id: settings
category: "user"
property bool avatar_circles: true
property string emoji_font_family: "default"
}
Settings {
@ -133,6 +134,21 @@ Page {
sequence: StandardKey.MoveToNextPage
onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); }
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: chat.model.reply = undefined
}
Shortcut {
sequence: "Alt+Up"
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply? chat.model.idToIndex(chat.model.reply) + 1 : 0)
}
Shortcut {
sequence: "Alt+Down"
onActivated: {
var idx = chat.model.reply? chat.model.idToIndex(chat.model.reply) - 1 : -1
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined
}
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
@ -210,7 +226,7 @@ Page {
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: section.includes(" ")
text: chat.model.formatDateSeparator(modelData.timestamp)
color: colors.brightText
color: colors.text
height: fontMetrics.height * 1.4
width: contentWidth * 1.2
@ -218,7 +234,7 @@ Page {
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
radius: parent.height / 2
color: colors.dark
color: colors.base
}
}
Row {

View file

@ -15,7 +15,7 @@ Item {
MouseArea {
anchors.fill: parent
preventStealing: true
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(timelineManager.replyingEvent), ListView.Contain)
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
cursorShape: Qt.PointingHandCursor
}

View file

@ -117,8 +117,9 @@
<file>qml/MatrixText.qml</file>
<file>qml/StatusIndicator.qml</file>
<file>qml/EncryptionIndicator.qml</file>
<file>qml/TimelineRow.qml</file>
<file>qml/Reactions.qml</file>
<file>qml/ScrollHelper.qml</file>
<file>qml/TimelineRow.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file>

View file

@ -98,15 +98,15 @@ UserMentionsWidget {
qproperty-highlightedTitleColor: palette(highlighted-text);
qproperty-highlightedSubtitleColor: palette(highlighted-text);
qproperty-hoverTitleColor: palette(highlightedtext);
qproperty-hoverSubtitleColor: palette(highlightedtext);
qproperty-hoverTitleColor: palette(dark);
qproperty-hoverSubtitleColor: palette(dark);
qproperty-btnColor: palette(dark);
qproperty-btnTextColor: palette(bright-text);
qproperty-timestampColor: palette(text);
qproperty-highlightedTimestampColor: palette(highlighted-text);
qproperty-hoverTimestampColor: palette(highlighted-text);
qproperty-hoverTimestampColor: palette(dark);
qproperty-bubbleBgColor: palette(base);
qproperty-bubbleFgColor: palette(text);

View file

@ -164,8 +164,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
using namespace mtx::events;
// relations shouldn't be encrypted...
mtx::common::RelatesTo relation;
if (body["content"].count("m.relates_to") != 0) {
mtx::common::ReplyRelatesTo relation;
if (body["content"]["m.relates_to"].contains("m.in_reply_to")) {
relation = body["content"]["m.relates_to"];
body["content"].erase("m.relates_to");
}

View file

@ -115,7 +115,7 @@ UserSettings::applyTheme()
/*mid*/ QColor(110, 110, 110),
/*text*/ QColor("#333"),
/*bright_text*/ QColor("#333"),
/*base*/ QColor("white"),
/*base*/ QColor("#eee"),
/*window*/ QColor("white"));
lightActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());

View file

@ -0,0 +1,98 @@
#include "ReactionsModel.h"
#include <Cache.h>
#include <MatrixClient.h>
QHash<int, QByteArray>
ReactionsModel::roleNames() const
{
return {
{Key, "key"},
{Count, "counter"},
{Users, "users"},
{SelfReacted, "selfReacted"},
};
}
int
ReactionsModel::rowCount(const QModelIndex &) const
{
return static_cast<int>(reactions.size());
}
QVariant
ReactionsModel::data(const QModelIndex &index, int role) const
{
const int i = index.row();
if (i < 0 || i >= static_cast<int>(reactions.size()))
return {};
switch (role) {
case Key:
return QString::fromStdString(reactions[i].key);
case Count:
return static_cast<int>(reactions[i].reactions.size());
case Users: {
QString users;
bool first = true;
for (const auto &reaction : reactions[i].reactions) {
if (!first)
users += ", ";
else
first = false;
users += QString::fromStdString(
cache::displayName(room_id_, reaction.second.sender));
}
return users;
}
case SelfReacted:
for (const auto &reaction : reactions[i].reactions)
if (reaction.second.sender == http::client()->user_id().to_string())
return true;
return false;
default:
return {};
}
}
void
ReactionsModel::addReaction(const std::string &room_id,
const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction)
{
room_id_ = room_id;
int idx = 0;
for (auto &storedReactions : reactions) {
if (storedReactions.key == reaction.content.relates_to.key) {
storedReactions.reactions[reaction.event_id] = reaction;
emit dataChanged(index(idx, 0), index(idx, 0));
return;
}
idx++;
}
beginInsertRows(QModelIndex(), idx, idx);
reactions.push_back(
KeyReaction{reaction.content.relates_to.key, {{reaction.event_id, reaction}}});
endInsertRows();
}
void
ReactionsModel::removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction)
{
int idx = 0;
for (auto &storedReactions : reactions) {
if (storedReactions.key == reaction.content.relates_to.key) {
storedReactions.reactions.erase(reaction.event_id);
if (storedReactions.reactions.size() == 0) {
beginRemoveRows(QModelIndex(), idx, idx);
reactions.erase(reactions.begin() + idx);
endRemoveRows();
} else
emit dataChanged(index(idx, 0), index(idx, 0));
return;
}
idx++;
}
}

View file

@ -0,0 +1,41 @@
#pragma once
#include <QAbstractListModel>
#include <QHash>
#include <utility>
#include <vector>
#include <mtx/events/collections.hpp>
class ReactionsModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit ReactionsModel(QObject *parent = nullptr) { Q_UNUSED(parent); }
enum Roles
{
Key,
Count,
Users,
SelfReacted,
};
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void addReaction(const std::string &room_id,
const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction);
void removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction);
private:
struct KeyReaction
{
std::string key;
std::map<std::string, mtx::events::RoomEvent<mtx::events::msg::Reaction>> reactions;
};
std::string room_id_;
std::vector<KeyReaction> reactions;
};

View file

@ -42,6 +42,8 @@ struct RoomEventType
switch (e.type) {
case EventType::RoomKeyRequest:
return qml_mtx_events::EventType::KeyRequest;
case EventType::Reaction:
return qml_mtx_events::EventType::Reaction;
case EventType::RoomAliases:
return qml_mtx_events::EventType::Aliases;
case EventType::RoomAvatar:
@ -223,6 +225,7 @@ TimelineModel::roleNames() const
{State, "state"},
{IsEncrypted, "isEncrypted"},
{ReplyTo, "replyTo"},
{Reactions, "reactions"},
{RoomId, "roomId"},
{RoomName, "roomName"},
{RoomTopic, "roomTopic"},
@ -345,6 +348,11 @@ TimelineModel::data(const QString &id, int role) const
}
case ReplyTo:
return QVariant(QString::fromStdString(in_reply_to_event(event)));
case Reactions:
if (reactions.count(id))
return QVariant::fromValue((QObject *)&reactions.at(id));
else
return {};
case RoomId:
return QVariant(QString::fromStdString(room_id(event)));
case RoomName:
@ -471,7 +479,6 @@ TimelineModel::fetchMore(const QModelIndex &)
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error,
err->parse_error);
emit oldMessagesRetrieved(std::move(res));
setPaginationInProgress(false);
return;
}
@ -609,6 +616,18 @@ TimelineModel::internalAddEvents(
QString redacts = QString::fromStdString(redaction->redacts);
auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts);
auto event = events.value(redacts);
if (auto reaction =
std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
&event)) {
QString reactedTo =
QString::fromStdString(reaction->content.relates_to.event_id);
reactions[reactedTo].removeReaction(*reaction);
int idx = idToIndex(reactedTo);
if (idx >= 0)
emit dataChanged(index(idx, 0), index(idx, 0));
}
if (redacted != eventOrder.end()) {
auto redactedEvent = std::visit(
[](const auto &ev)
@ -632,6 +651,18 @@ TimelineModel::internalAddEvents(
continue; // don't insert redaction into timeline
}
if (auto reaction =
std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&e)) {
QString reactedTo =
QString::fromStdString(reaction->content.relates_to.event_id);
events.insert(id, e);
reactions[reactedTo].addReaction(room_id_.toStdString(), *reaction);
int idx = idToIndex(reactedTo);
if (idx >= 0)
emit dataChanged(index(idx, 0), index(idx, 0));
continue; // don't insert reaction into timeline
}
if (auto event =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
auto e_ = decryptEvent(*event).event;
@ -707,6 +738,11 @@ TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
}
prev_batch_token_ = QString::fromStdString(msgs.end);
if (ids.empty() && !msgs.chunk.empty()) {
// no visible events fetched, prevent loading from stopping
fetchMore(QModelIndex());
}
}
QString

View file

@ -9,6 +9,7 @@
#include <mtxclient/http/errors.hpp>
#include "CacheCryptoStructs.h"
#include "ReactionsModel.h"
namespace mtx::http {
using RequestErr = const std::optional<mtx::http::ClientError> &;
@ -29,6 +30,8 @@ enum EventType
Unsupported,
/// m.room_key_request
KeyRequest,
/// m.reaction,
Reaction,
/// m.room.aliases
Aliases,
/// m.room.avatar
@ -155,6 +158,7 @@ public:
State,
IsEncrypted,
ReplyTo,
Reactions,
RoomId,
RoomName,
RoomTopic,
@ -271,6 +275,7 @@ private:
QSet<QString> read;
QList<QString> pending;
std::vector<QString> eventOrder;
std::map<QString, ReactionsModel> reactions;
QString room_id_;
QString prev_batch_token_;