Merge branch 'relations' into 'develop'
Relations See merge request b0/spectral!62
This commit is contained in:
@@ -64,6 +64,8 @@ if (Qt5_POSITION_INDEPENDENT_CODE)
|
|||||||
SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set(QML_IMPORT_PATH ${CMAKE_SOURCE_DIR}/qml ${CMAKE_SOURCE_DIR}/imports CACHE string "" FORCE)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
enable_language(RC)
|
enable_language(RC)
|
||||||
include(CMakeDetermineRCCompiler)
|
include(CMakeDetermineRCCompiler)
|
||||||
@@ -174,6 +176,7 @@ QT5_ADD_RESOURCES(spectral_QRC_SRC ${spectral_QRC})
|
|||||||
set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON)
|
set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
set(spectral_WINRC spectral_win32.rc)
|
||||||
set_property(SOURCE spectral_win32.rc APPEND PROPERTY
|
set_property(SOURCE spectral_win32.rc APPEND PROPERTY
|
||||||
OBJECT_DEPENDS ${PROJECT_SOURCE_DIR}/icons/icon.ico
|
OBJECT_DEPENDS ${PROJECT_SOURCE_DIR}/icons/icon.ico
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ ColumnLayout {
|
|||||||
readonly property bool avatarVisible: !sentByMe && showAuthor
|
readonly property bool avatarVisible: !sentByMe && showAuthor
|
||||||
readonly property bool sentByMe: author === currentRoom.localUser
|
readonly property bool sentByMe: author === currentRoom.localUser
|
||||||
readonly property bool darkBackground: !sentByMe
|
readonly property bool darkBackground: !sentByMe
|
||||||
readonly property bool replyVisible: replyEventId || false
|
readonly property bool replyVisible: reply || false
|
||||||
|
|
||||||
signal saveFileAs()
|
signal saveFileAs()
|
||||||
signal openExternally()
|
signal openExternally()
|
||||||
@@ -66,6 +66,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
Control {
|
Control {
|
||||||
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + messageRow.spacing : 0) - 48
|
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + messageRow.spacing : 0) - 48
|
||||||
|
Layout.minimumHeight: 36
|
||||||
|
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
@@ -144,15 +145,15 @@ ColumnLayout {
|
|||||||
Layout.preferredHeight: 28
|
Layout.preferredHeight: 28
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
source: replyVisible ? replyAuthor.avatarMediaId : ""
|
source: replyVisible ? reply.author.avatarMediaId : ""
|
||||||
hint: replyVisible ? replyAuthor.displayName : "H"
|
hint: replyVisible ? reply.author.displayName : "H"
|
||||||
|
|
||||||
RippleEffect {
|
RippleEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
circular: true
|
circular: true
|
||||||
|
|
||||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": replyAuthor}).open()
|
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": reply.author}).open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ ColumnLayout {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
color: !sentByMe ? MPalette.foreground : "white"
|
color: !sentByMe ? MPalette.foreground : "white"
|
||||||
text: "<style>a{color: " + color + ";} .user-pill{}</style>" + (replyDisplay || "")
|
text: "<style>a{color: " + color + ";} .user-pill{}</style>" + (replyVisible ? reply.display : "")
|
||||||
|
|
||||||
wrapMode: Label.Wrap
|
wrapMode: Label.Wrap
|
||||||
textFormat: Label.RichText
|
textFormat: Label.RichText
|
||||||
@@ -168,13 +169,13 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: replyAuthor && sentByMe ? replyAuthor.color : MPalette.background
|
color: replyVisible && sentByMe ? reply.author.color : MPalette.background
|
||||||
radius: 18
|
radius: 18
|
||||||
|
|
||||||
AutoMouseArea {
|
AutoMouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClicked: goToEvent(replyEventId)
|
onClicked: goToEvent(reply.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,6 +221,15 @@ ColumnLayout {
|
|||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReactionDelegate {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Layout.topMargin: 0
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
imports/Spectral/Component/Timeline/ReactionDelegate.qml
Normal file
57
imports/Spectral/Component/Timeline/ReactionDelegate.qml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import Spectral.Setting 0.1
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
visible: (reaction && reaction.length > 0) || false
|
||||||
|
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: reaction
|
||||||
|
|
||||||
|
delegate: Control {
|
||||||
|
horizontalPadding: 6
|
||||||
|
verticalPadding: 0
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: height / 2
|
||||||
|
color: modelData.hasLocalUser ? (MSettings.darkTheme ? Qt.darker(MPalette.accent, 1.55) : Qt.lighter(MPalette.accent, 1.55)) : MPalette.banner
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
ToolTip.visible: containsMouse
|
||||||
|
ToolTip.text: {
|
||||||
|
var text = "";
|
||||||
|
|
||||||
|
for (var i = 0; i < modelData.authors.length; i++) {
|
||||||
|
if (i === modelData.authors.length - 1 && i !== 0) {
|
||||||
|
text += " and "
|
||||||
|
} else if (i !== 0) {
|
||||||
|
text += ", "
|
||||||
|
}
|
||||||
|
|
||||||
|
text += modelData.authors[i].displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
text += " reacted with " + modelData.reaction
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: currentRoom.toggleReaction(eventId, modelData.reaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Label {
|
||||||
|
text: modelData.reaction + " " + modelData.count
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,3 +5,4 @@ SectionDelegate 2.0 SectionDelegate.qml
|
|||||||
ImageDelegate 2.0 ImageDelegate.qml
|
ImageDelegate 2.0 ImageDelegate.qml
|
||||||
FileDelegate 2.0 FileDelegate.qml
|
FileDelegate 2.0 FileDelegate.qml
|
||||||
VideoDelegate 2.0 VideoDelegate.qml
|
VideoDelegate 2.0 VideoDelegate.qml
|
||||||
|
ReactionDelegate 2.0 ReactionDelegate.qml
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ Dialog {
|
|||||||
placeholderText: "Password"
|
placeholderText: "Password"
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
|
|
||||||
|
onAccepted: accessTokenField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoTextField {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
id: accessTokenField
|
||||||
|
|
||||||
|
placeholderText: "Access Token (Optional)"
|
||||||
|
|
||||||
onAccepted: deviceNameField.forceActiveFocus()
|
onAccepted: deviceNameField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,14 +62,19 @@ Dialog {
|
|||||||
|
|
||||||
id: deviceNameField
|
id: deviceNameField
|
||||||
|
|
||||||
placeholderText: "Device Name"
|
placeholderText: "Device Name (Optional)"
|
||||||
|
|
||||||
onAccepted: root.accept()
|
onAccepted: root.accept()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doLogin() {
|
function doLogin() {
|
||||||
spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text, deviceNameField.text)
|
if (accessTokenField.text !== "") {
|
||||||
|
console.log("Login using access token.")
|
||||||
|
spectralController.loginWithAccessToken(serverField.text, usernameField.text, accessTokenField.text, deviceNameField.text)
|
||||||
|
} else {
|
||||||
|
spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text, deviceNameField.text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClosed: destroy()
|
onClosed: destroy()
|
||||||
|
|||||||
@@ -12,6 +12,38 @@ Menu {
|
|||||||
|
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: Label {
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
font.pixelSize: 16
|
||||||
|
text: modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: currentRoom.toggleReaction(eventId, modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuSeparator {}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "View Source"
|
text: "View Source"
|
||||||
|
|
||||||
|
|||||||
Submodule include/libQuotient updated: 74caea2669...5b236dfe89
1
res.qrc
1
res.qrc
@@ -57,5 +57,6 @@
|
|||||||
<file>imports/Spectral/Dialog/OpenFolderDialog.qml</file>
|
<file>imports/Spectral/Dialog/OpenFolderDialog.qml</file>
|
||||||
<file>imports/Spectral/Component/Timeline/VideoDelegate.qml</file>
|
<file>imports/Spectral/Component/Timeline/VideoDelegate.qml</file>
|
||||||
<file>imports/Spectral/Component/AutoRectangle.qml</file>
|
<file>imports/Spectral/Component/AutoRectangle.qml</file>
|
||||||
|
<file>imports/Spectral/Component/Timeline/ReactionDelegate.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
1
spectral_win32.rc
Normal file
1
spectral_win32.rc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
IDI_ICON1 ICON DISCARDABLE "icons/icon.ico"
|
||||||
@@ -103,6 +103,39 @@ void Controller::loginWithCredentials(QString serverAddr,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Controller::loginWithAccessToken(QString serverAddr,
|
||||||
|
QString user,
|
||||||
|
QString token,
|
||||||
|
QString deviceName) {
|
||||||
|
if (!user.isEmpty() && !token.isEmpty()) {
|
||||||
|
QUrl serverUrl(serverAddr);
|
||||||
|
|
||||||
|
Connection* conn = new Connection(this);
|
||||||
|
if (serverUrl.isValid()) {
|
||||||
|
conn->setHomeserver(serverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(conn, &Connection::connected, [=] {
|
||||||
|
AccountSettings account(conn->userId());
|
||||||
|
account.setKeepLoggedIn(true);
|
||||||
|
account.clearAccessToken(); // Drop the legacy - just in case
|
||||||
|
account.setHomeserver(conn->homeserver());
|
||||||
|
account.setDeviceId(conn->deviceId());
|
||||||
|
account.setDeviceName(deviceName);
|
||||||
|
if (!saveAccessTokenToKeyChain(account, conn->accessToken()))
|
||||||
|
qWarning() << "Couldn't save access token";
|
||||||
|
account.sync();
|
||||||
|
addConnection(conn);
|
||||||
|
setConnection(conn);
|
||||||
|
});
|
||||||
|
connect(conn, &Connection::networkError,
|
||||||
|
[=](QString error, QString, int, int) {
|
||||||
|
emit errorOccured("Network Error", error);
|
||||||
|
});
|
||||||
|
conn->connectWithToken(user, token, deviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Controller::logout(Connection* conn) {
|
void Controller::logout(Connection* conn) {
|
||||||
if (!conn) {
|
if (!conn) {
|
||||||
qCritical() << "Attempt to logout null connection";
|
qCritical() << "Attempt to logout null connection";
|
||||||
@@ -112,16 +145,24 @@ void Controller::logout(Connection* conn) {
|
|||||||
SettingsGroup("Accounts").remove(conn->userId());
|
SettingsGroup("Accounts").remove(conn->userId());
|
||||||
QFile(accessTokenFileName(AccountSettings(conn->userId()))).remove();
|
QFile(accessTokenFileName(AccountSettings(conn->userId()))).remove();
|
||||||
|
|
||||||
auto job = conn->callApi<LogoutJob>();
|
QKeychain::DeletePasswordJob job(qAppName());
|
||||||
connect(job, &LogoutJob::finished, conn, [=] {
|
job.setAutoDelete(true);
|
||||||
|
job.setKey(conn->userId());
|
||||||
|
QEventLoop loop;
|
||||||
|
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||||
|
job.start();
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
auto logoutJob = conn->callApi<LogoutJob>();
|
||||||
|
connect(logoutJob, &LogoutJob::finished, conn, [=] {
|
||||||
conn->stopSync();
|
conn->stopSync();
|
||||||
emit conn->stateChanged();
|
emit conn->stateChanged();
|
||||||
emit conn->loggedOut();
|
emit conn->loggedOut();
|
||||||
if (!m_connections.isEmpty())
|
if (!m_connections.isEmpty())
|
||||||
setConnection(m_connections[0]);
|
setConnection(m_connections[0]);
|
||||||
});
|
});
|
||||||
connect(job, &LogoutJob::failure, this, [=] {
|
connect(logoutJob, &LogoutJob::failure, this, [=] {
|
||||||
emit errorOccured("Server-side Logout Failed", job->errorString());
|
emit errorOccured("Server-side Logout Failed", logoutJob->errorString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Controller : public QObject {
|
|||||||
~Controller();
|
~Controller();
|
||||||
|
|
||||||
Q_INVOKABLE void loginWithCredentials(QString, QString, QString, QString);
|
Q_INVOKABLE void loginWithCredentials(QString, QString, QString, QString);
|
||||||
|
Q_INVOKABLE void loginWithAccessToken(QString, QString, QString, QString);
|
||||||
|
|
||||||
QVector<Connection*> connections() { return m_connections; }
|
QVector<Connection*> connections() { return m_connections; }
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <settings.h>
|
#include <settings.h>
|
||||||
#include <user.h>
|
#include <user.h>
|
||||||
|
|
||||||
|
#include <events/reactionevent.h>
|
||||||
#include <events/redactionevent.h>
|
#include <events/redactionevent.h>
|
||||||
#include <events/roomavatarevent.h>
|
#include <events/roomavatarevent.h>
|
||||||
#include <events/roommemberevent.h>
|
#include <events/roommemberevent.h>
|
||||||
@@ -30,13 +31,12 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
|||||||
roles[LongOperationRole] = "progressInfo";
|
roles[LongOperationRole] = "progressInfo";
|
||||||
roles[AnnotationRole] = "annotation";
|
roles[AnnotationRole] = "annotation";
|
||||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||||
roles[ReplyEventIdRole] = "replyEventId";
|
roles[ReplyRole] = "reply";
|
||||||
roles[ReplyAuthorRole] = "replyAuthor";
|
|
||||||
roles[ReplyDisplayRole] = "replyDisplay";
|
|
||||||
roles[UserMarkerRole] = "userMarker";
|
roles[UserMarkerRole] = "userMarker";
|
||||||
roles[ShowAuthorRole] = "showAuthor";
|
roles[ShowAuthorRole] = "showAuthor";
|
||||||
roles[ShowSectionRole] = "showSection";
|
roles[ShowSectionRole] = "showSection";
|
||||||
roles[BubbleShapeRole] = "bubbleShape";
|
roles[BubbleShapeRole] = "bubbleShape";
|
||||||
|
roles[ReactionRole] = "reaction";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +136,10 @@ void MessageEventModel::setRoom(SpectralRoom* room) {
|
|||||||
refreshLastUserEvents(refreshEvent(newEvent->id()) -
|
refreshLastUserEvents(refreshEvent(newEvent->id()) -
|
||||||
timelineBaseIndex());
|
timelineBaseIndex());
|
||||||
});
|
});
|
||||||
|
connect(m_currentRoom, &Room::updatedEvent, this,
|
||||||
|
[this](const QString& eventId) {
|
||||||
|
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
||||||
|
});
|
||||||
connect(m_currentRoom, &Room::fileTransferProgress, this,
|
connect(m_currentRoom, &Room::fileTransferProgress, this,
|
||||||
&MessageEventModel::refreshEvent);
|
&MessageEventModel::refreshEvent);
|
||||||
connect(m_currentRoom, &Room::fileTransferCompleted, this,
|
connect(m_currentRoom, &Room::fileTransferCompleted, this,
|
||||||
@@ -178,15 +182,24 @@ void MessageEventModel::refreshEventRoles(int row, const QVector<int>& roles) {
|
|||||||
emit dataChanged(idx, idx, roles);
|
emit dataChanged(idx, idx, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
int MessageEventModel::refreshEventRoles(const QString& eventId,
|
int MessageEventModel::refreshEventRoles(const QString& id,
|
||||||
const QVector<int>& roles) {
|
const QVector<int>& roles) {
|
||||||
const auto it = m_currentRoom->findInTimeline(eventId);
|
// On 64-bit platforms, difference_type for std containers is long long
|
||||||
if (it == m_currentRoom->timelineEdge()) {
|
// but Qt uses int throughout its interfaces; hence casting to int below.
|
||||||
qWarning() << "Trying to refresh inexistent event:" << eventId;
|
int row = -1;
|
||||||
return -1;
|
// First try pendingEvents because it is almost always very short.
|
||||||
|
const auto pendingIt = m_currentRoom->findPendingEvent(id);
|
||||||
|
if (pendingIt != m_currentRoom->pendingEvents().end())
|
||||||
|
row = int(pendingIt - m_currentRoom->pendingEvents().begin());
|
||||||
|
else {
|
||||||
|
const auto timelineIt = m_currentRoom->findInTimeline(id);
|
||||||
|
if (timelineIt == m_currentRoom->timelineEdge()) {
|
||||||
|
qWarning() << "Trying to refresh inexistent event:" << id;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) +
|
||||||
|
timelineBaseIndex();
|
||||||
}
|
}
|
||||||
const auto row =
|
|
||||||
it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
|
||||||
refreshEventRoles(row, roles);
|
refreshEventRoles(row, roles);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
@@ -359,7 +372,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is<RedactionEvent>(evt))
|
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt))
|
||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
if (evt.isRedacted())
|
if (evt.isRedacted())
|
||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
@@ -368,6 +381,12 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||||||
static_cast<const StateEventBase&>(evt).repeatsState())
|
static_cast<const StateEventBase&>(evt).repeatsState())
|
||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
|
|
||||||
|
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||||
|
if (!e->replacedEvent().isEmpty()) {
|
||||||
|
return EventStatus::Hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_currentRoom->connection()->isIgnored(
|
if (m_currentRoom->connection()->isIgnored(
|
||||||
m_currentRoom->user(evt.senderId())))
|
m_currentRoom->user(evt.senderId())))
|
||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
@@ -404,8 +423,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||||||
return variantList;
|
return variantList;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReplyEventIdRole || role == ReplyDisplayRole ||
|
if (role == ReplyRole) {
|
||||||
role == ReplyAuthorRole) {
|
|
||||||
const QString& replyEventId = evt.contentJson()["m.relates_to"]
|
const QString& replyEventId = evt.contentJson()["m.relates_to"]
|
||||||
.toObject()["m.in_reply_to"]
|
.toObject()["m.in_reply_to"]
|
||||||
.toObject()["event_id"]
|
.toObject()["event_id"]
|
||||||
@@ -416,16 +434,13 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||||||
if (replyIt == m_currentRoom->timelineEdge())
|
if (replyIt == m_currentRoom->timelineEdge())
|
||||||
return {};
|
return {};
|
||||||
const auto& replyEvt = **replyIt;
|
const auto& replyEvt = **replyIt;
|
||||||
switch (role) {
|
|
||||||
case ReplyEventIdRole:
|
return QVariantMap{
|
||||||
return replyEventId;
|
{"eventId", replyEventId},
|
||||||
case ReplyDisplayRole:
|
{"display", utils::cleanHTML(utils::removeReply(
|
||||||
return utils::cleanHTML(utils::removeReply(
|
m_currentRoom->eventToString(replyEvt, Qt::RichText)))},
|
||||||
m_currentRoom->eventToString(replyEvt, Qt::RichText)));
|
{"author",
|
||||||
case ReplyAuthorRole:
|
QVariant::fromValue(m_currentRoom->user(replyEvt.senderId()))}};
|
||||||
return QVariant::fromValue(m_currentRoom->user(replyEvt.senderId()));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ShowAuthorRole) {
|
if (role == ShowAuthorRole) {
|
||||||
@@ -484,6 +499,44 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||||||
return BubbleShapes::MiddleShape;
|
return BubbleShapes::MiddleShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role == ReactionRole) {
|
||||||
|
const auto& annotations =
|
||||||
|
m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||||
|
if (annotations.isEmpty())
|
||||||
|
return {};
|
||||||
|
QMap<QString, QList<SpectralUser*>> reactions = {};
|
||||||
|
for (const auto& a : annotations) {
|
||||||
|
if (a->isRedacted()) // Just in case?
|
||||||
|
continue;
|
||||||
|
if (auto e = eventCast<const ReactionEvent>(a))
|
||||||
|
reactions[e->relation().key].append(
|
||||||
|
static_cast<SpectralUser*>(m_currentRoom->user(e->senderId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactions.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList res = {};
|
||||||
|
QMap<QString, QList<SpectralUser*>>::const_iterator i =
|
||||||
|
reactions.constBegin();
|
||||||
|
while (i != reactions.constEnd()) {
|
||||||
|
QVariantList authors;
|
||||||
|
for (auto author : i.value()) {
|
||||||
|
authors.append(QVariant::fromValue(author));
|
||||||
|
}
|
||||||
|
bool hasLocalUser = i.value().contains(
|
||||||
|
static_cast<SpectralUser*>(m_currentRoom->localUser()));
|
||||||
|
res.append(QVariantMap{{"reaction", i.key()},
|
||||||
|
{"count", i.value().count()},
|
||||||
|
{"authors", authors},
|
||||||
|
{"hasLocalUser", hasLocalUser}});
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ class MessageEventModel : public QAbstractListModel {
|
|||||||
LongOperationRole,
|
LongOperationRole,
|
||||||
AnnotationRole,
|
AnnotationRole,
|
||||||
UserMarkerRole,
|
UserMarkerRole,
|
||||||
// For reply
|
|
||||||
ReplyEventIdRole,
|
ReplyRole,
|
||||||
ReplyAuthorRole,
|
|
||||||
ReplyDisplayRole,
|
|
||||||
|
|
||||||
ShowAuthorRole,
|
ShowAuthorRole,
|
||||||
ShowSectionRole,
|
ShowSectionRole,
|
||||||
|
|
||||||
BubbleShapeRole,
|
BubbleShapeRole,
|
||||||
|
|
||||||
|
ReactionRole,
|
||||||
|
|
||||||
// For debugging
|
// For debugging
|
||||||
EventResolvedTypeRole,
|
EventResolvedTypeRole,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "csapi/rooms.h"
|
#include "csapi/rooms.h"
|
||||||
#include "csapi/typing.h"
|
#include "csapi/typing.h"
|
||||||
#include "events/accountdataevents.h"
|
#include "events/accountdataevents.h"
|
||||||
|
#include "events/reactionevent.h"
|
||||||
#include "events/roommessageevent.h"
|
#include "events/roommessageevent.h"
|
||||||
#include "events/typingevent.h"
|
#include "events/typingevent.h"
|
||||||
#include "jobs/downloadfilejob.h"
|
#include "jobs/downloadfilejob.h"
|
||||||
@@ -119,15 +120,21 @@ QString SpectralRoom::lastEvent() {
|
|||||||
for (auto i = messageEvents().rbegin(); i < messageEvents().rend(); i++) {
|
for (auto i = messageEvents().rbegin(); i < messageEvents().rend(); i++) {
|
||||||
const RoomEvent* evt = i->get();
|
const RoomEvent* evt = i->get();
|
||||||
|
|
||||||
if (is<RedactionEvent>(*evt))
|
if (is<RedactionEvent>(*evt) || is<ReactionEvent>(*evt))
|
||||||
continue;
|
continue;
|
||||||
if (evt->isRedacted())
|
if (evt->isRedacted())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (evt->isStateEvent() &&
|
if (evt->isStateEvent() &&
|
||||||
static_cast<const StateEventBase*>(evt)->repeatsState())
|
static_cast<const StateEventBase&>(*evt).repeatsState())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (auto e = eventCast<const RoomMessageEvent>(evt)) {
|
||||||
|
if (!e->replacedEvent().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (connection()->isIgnored(user(evt->senderId())))
|
if (connection()->isIgnored(user(evt->senderId())))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -164,6 +171,13 @@ void SpectralRoom::onAddHistoricalTimelineEvents(rev_iter_t from) {
|
|||||||
[this](const TimelineItem& ti) { checkForHighlights(ti); });
|
[this](const TimelineItem& ti) { checkForHighlights(ti); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpectralRoom::onRedaction(const RoomEvent& prevEvent,
|
||||||
|
const RoomEvent& /*after*/) {
|
||||||
|
if (const auto& e = eventCast<const ReactionEvent>(&prevEvent)) {
|
||||||
|
emit updatedEvent(e->relation().eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SpectralRoom::countChanged() {
|
void SpectralRoom::countChanged() {
|
||||||
if (displayed() && !hasUnreadMessages()) {
|
if (displayed() && !hasUnreadMessages()) {
|
||||||
resetNotificationCount();
|
resetNotificationCount();
|
||||||
@@ -305,7 +319,7 @@ QString SpectralRoom::markdownToHTML(const QString& markdown) {
|
|||||||
|
|
||||||
std::string html(tmp_buf);
|
std::string html(tmp_buf);
|
||||||
|
|
||||||
free((char *)tmp_buf);
|
free((char*)tmp_buf);
|
||||||
|
|
||||||
auto result = QString::fromStdString(html).trimmed();
|
auto result = QString::fromStdString(html).trimmed();
|
||||||
|
|
||||||
@@ -419,3 +433,40 @@ void SpectralRoom::postHtmlMessage(const QString& text,
|
|||||||
|
|
||||||
Room::postHtmlMessage(text, html, type);
|
Room::postHtmlMessage(text, html, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpectralRoom::toggleReaction(const QString& eventId,
|
||||||
|
const QString& reaction) {
|
||||||
|
if (eventId.isEmpty() || reaction.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto eventIt = findInTimeline(eventId);
|
||||||
|
if (eventIt == timelineEdge())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& evt = **eventIt;
|
||||||
|
|
||||||
|
QStringList redactEventIds; // What if there are multiple reaction events?
|
||||||
|
|
||||||
|
const auto& annotations = relatedEvents(evt, EventRelation::Annotation());
|
||||||
|
if (!annotations.isEmpty()) {
|
||||||
|
for (const auto& a : annotations) {
|
||||||
|
if (auto e = eventCast<const ReactionEvent>(a)) {
|
||||||
|
if (e->relation().key != reaction)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (e->senderId() == localUser()->id()) {
|
||||||
|
redactEventIds.push_back(e->id());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redactEventIds.isEmpty()) {
|
||||||
|
for (auto redactEventId : redactEventIds) {
|
||||||
|
redactEvent(redactEventId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
postReaction(eventId, reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ class SpectralRoom : public Room {
|
|||||||
|
|
||||||
void onAddNewTimelineEvents(timeline_iter_t from) override;
|
void onAddNewTimelineEvents(timeline_iter_t from) override;
|
||||||
void onAddHistoricalTimelineEvents(rev_iter_t from) override;
|
void onAddHistoricalTimelineEvents(rev_iter_t from) override;
|
||||||
|
void onRedaction(const RoomEvent& prevEvent, const RoomEvent& after) override;
|
||||||
|
|
||||||
static QString markdownToHTML(const QString& plaintext);
|
static QString markdownToHTML(const QString& plaintext);
|
||||||
|
|
||||||
@@ -315,6 +316,7 @@ class SpectralRoom : public Room {
|
|||||||
void changeAvatar(QUrl localFile);
|
void changeAvatar(QUrl localFile);
|
||||||
void addLocalAlias(const QString& alias);
|
void addLocalAlias(const QString& alias);
|
||||||
void removeLocalAlias(const QString& alias);
|
void removeLocalAlias(const QString& alias);
|
||||||
|
void toggleReaction(const QString& eventId, const QString& reaction);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SpectralRoom_H
|
#endif // SpectralRoom_H
|
||||||
|
|||||||
Reference in New Issue
Block a user