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)
|
||||
endif()
|
||||
|
||||
set(QML_IMPORT_PATH ${CMAKE_SOURCE_DIR}/qml ${CMAKE_SOURCE_DIR}/imports CACHE string "" FORCE)
|
||||
|
||||
if(WIN32)
|
||||
enable_language(RC)
|
||||
include(CMakeDetermineRCCompiler)
|
||||
@@ -174,6 +176,7 @@ QT5_ADD_RESOURCES(spectral_QRC_SRC ${spectral_QRC})
|
||||
set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON)
|
||||
|
||||
if(WIN32)
|
||||
set(spectral_WINRC spectral_win32.rc)
|
||||
set_property(SOURCE spectral_win32.rc APPEND PROPERTY
|
||||
OBJECT_DEPENDS ${PROJECT_SOURCE_DIR}/icons/icon.ico
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ ColumnLayout {
|
||||
readonly property bool avatarVisible: !sentByMe && showAuthor
|
||||
readonly property bool sentByMe: author === currentRoom.localUser
|
||||
readonly property bool darkBackground: !sentByMe
|
||||
readonly property bool replyVisible: replyEventId || false
|
||||
readonly property bool replyVisible: reply || false
|
||||
|
||||
signal saveFileAs()
|
||||
signal openExternally()
|
||||
@@ -66,6 +66,7 @@ ColumnLayout {
|
||||
|
||||
Control {
|
||||
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + messageRow.spacing : 0) - 48
|
||||
Layout.minimumHeight: 36
|
||||
|
||||
padding: 0
|
||||
|
||||
@@ -144,15 +145,15 @@ ColumnLayout {
|
||||
Layout.preferredHeight: 28
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
source: replyVisible ? replyAuthor.avatarMediaId : ""
|
||||
hint: replyVisible ? replyAuthor.displayName : "H"
|
||||
source: replyVisible ? reply.author.avatarMediaId : ""
|
||||
hint: replyVisible ? reply.author.displayName : "H"
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
textFormat: Label.RichText
|
||||
@@ -168,13 +169,13 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: replyAuthor && sentByMe ? replyAuthor.color : MPalette.background
|
||||
color: replyVisible && sentByMe ? reply.author.color : MPalette.background
|
||||
radius: 18
|
||||
|
||||
AutoMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: goToEvent(replyEventId)
|
||||
onClicked: goToEvent(reply.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +221,15 @@ ColumnLayout {
|
||||
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
|
||||
FileDelegate 2.0 FileDelegate.qml
|
||||
VideoDelegate 2.0 VideoDelegate.qml
|
||||
ReactionDelegate 2.0 ReactionDelegate.qml
|
||||
|
||||
@@ -44,6 +44,16 @@ Dialog {
|
||||
placeholderText: "Password"
|
||||
echoMode: TextInput.Password
|
||||
|
||||
onAccepted: accessTokenField.forceActiveFocus()
|
||||
}
|
||||
|
||||
AutoTextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: accessTokenField
|
||||
|
||||
placeholderText: "Access Token (Optional)"
|
||||
|
||||
onAccepted: deviceNameField.forceActiveFocus()
|
||||
}
|
||||
|
||||
@@ -52,14 +62,19 @@ Dialog {
|
||||
|
||||
id: deviceNameField
|
||||
|
||||
placeholderText: "Device Name"
|
||||
placeholderText: "Device Name (Optional)"
|
||||
|
||||
onAccepted: root.accept()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -12,6 +12,38 @@ Menu {
|
||||
|
||||
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 {
|
||||
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/Component/Timeline/VideoDelegate.qml</file>
|
||||
<file>imports/Spectral/Component/AutoRectangle.qml</file>
|
||||
<file>imports/Spectral/Component/Timeline/ReactionDelegate.qml</file>
|
||||
</qresource>
|
||||
</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) {
|
||||
if (!conn) {
|
||||
qCritical() << "Attempt to logout null connection";
|
||||
@@ -112,16 +145,24 @@ void Controller::logout(Connection* conn) {
|
||||
SettingsGroup("Accounts").remove(conn->userId());
|
||||
QFile(accessTokenFileName(AccountSettings(conn->userId()))).remove();
|
||||
|
||||
auto job = conn->callApi<LogoutJob>();
|
||||
connect(job, &LogoutJob::finished, conn, [=] {
|
||||
QKeychain::DeletePasswordJob job(qAppName());
|
||||
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();
|
||||
emit conn->stateChanged();
|
||||
emit conn->loggedOut();
|
||||
if (!m_connections.isEmpty())
|
||||
setConnection(m_connections[0]);
|
||||
});
|
||||
connect(job, &LogoutJob::failure, this, [=] {
|
||||
emit errorOccured("Server-side Logout Failed", job->errorString());
|
||||
connect(logoutJob, &LogoutJob::failure, this, [=] {
|
||||
emit errorOccured("Server-side Logout Failed", logoutJob->errorString());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class Controller : public QObject {
|
||||
~Controller();
|
||||
|
||||
Q_INVOKABLE void loginWithCredentials(QString, QString, QString, QString);
|
||||
Q_INVOKABLE void loginWithAccessToken(QString, QString, QString, QString);
|
||||
|
||||
QVector<Connection*> connections() { return m_connections; }
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <settings.h>
|
||||
#include <user.h>
|
||||
|
||||
#include <events/reactionevent.h>
|
||||
#include <events/redactionevent.h>
|
||||
#include <events/roomavatarevent.h>
|
||||
#include <events/roommemberevent.h>
|
||||
@@ -30,13 +31,12 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
||||
roles[LongOperationRole] = "progressInfo";
|
||||
roles[AnnotationRole] = "annotation";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[ReplyEventIdRole] = "replyEventId";
|
||||
roles[ReplyAuthorRole] = "replyAuthor";
|
||||
roles[ReplyDisplayRole] = "replyDisplay";
|
||||
roles[ReplyRole] = "reply";
|
||||
roles[UserMarkerRole] = "userMarker";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
roles[BubbleShapeRole] = "bubbleShape";
|
||||
roles[ReactionRole] = "reaction";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -136,6 +136,10 @@ void MessageEventModel::setRoom(SpectralRoom* room) {
|
||||
refreshLastUserEvents(refreshEvent(newEvent->id()) -
|
||||
timelineBaseIndex());
|
||||
});
|
||||
connect(m_currentRoom, &Room::updatedEvent, this,
|
||||
[this](const QString& eventId) {
|
||||
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::fileTransferProgress, this,
|
||||
&MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferCompleted, this,
|
||||
@@ -178,15 +182,24 @@ void MessageEventModel::refreshEventRoles(int row, const QVector<int>& roles) {
|
||||
emit dataChanged(idx, idx, roles);
|
||||
}
|
||||
|
||||
int MessageEventModel::refreshEventRoles(const QString& eventId,
|
||||
int MessageEventModel::refreshEventRoles(const QString& id,
|
||||
const QVector<int>& roles) {
|
||||
const auto it = m_currentRoom->findInTimeline(eventId);
|
||||
if (it == m_currentRoom->timelineEdge()) {
|
||||
qWarning() << "Trying to refresh inexistent event:" << eventId;
|
||||
return -1;
|
||||
// On 64-bit platforms, difference_type for std containers is long long
|
||||
// but Qt uses int throughout its interfaces; hence casting to int below.
|
||||
int row = -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);
|
||||
return row;
|
||||
}
|
||||
@@ -359,7 +372,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (is<RedactionEvent>(evt))
|
||||
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt))
|
||||
return EventStatus::Hidden;
|
||||
if (evt.isRedacted())
|
||||
return EventStatus::Hidden;
|
||||
@@ -368,6 +381,12 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
||||
static_cast<const StateEventBase&>(evt).repeatsState())
|
||||
return EventStatus::Hidden;
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (!e->replacedEvent().isEmpty()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_currentRoom->connection()->isIgnored(
|
||||
m_currentRoom->user(evt.senderId())))
|
||||
return EventStatus::Hidden;
|
||||
@@ -404,8 +423,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
||||
return variantList;
|
||||
}
|
||||
|
||||
if (role == ReplyEventIdRole || role == ReplyDisplayRole ||
|
||||
role == ReplyAuthorRole) {
|
||||
if (role == ReplyRole) {
|
||||
const QString& replyEventId = evt.contentJson()["m.relates_to"]
|
||||
.toObject()["m.in_reply_to"]
|
||||
.toObject()["event_id"]
|
||||
@@ -416,16 +434,13 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
||||
if (replyIt == m_currentRoom->timelineEdge())
|
||||
return {};
|
||||
const auto& replyEvt = **replyIt;
|
||||
switch (role) {
|
||||
case ReplyEventIdRole:
|
||||
return replyEventId;
|
||||
case ReplyDisplayRole:
|
||||
return utils::cleanHTML(utils::removeReply(
|
||||
m_currentRoom->eventToString(replyEvt, Qt::RichText)));
|
||||
case ReplyAuthorRole:
|
||||
return QVariant::fromValue(m_currentRoom->user(replyEvt.senderId()));
|
||||
}
|
||||
return {};
|
||||
|
||||
return QVariantMap{
|
||||
{"eventId", replyEventId},
|
||||
{"display", utils::cleanHTML(utils::removeReply(
|
||||
m_currentRoom->eventToString(replyEvt, Qt::RichText)))},
|
||||
{"author",
|
||||
QVariant::fromValue(m_currentRoom->user(replyEvt.senderId()))}};
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
@@ -484,6 +499,44 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
||||
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 {};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,16 @@ class MessageEventModel : public QAbstractListModel {
|
||||
LongOperationRole,
|
||||
AnnotationRole,
|
||||
UserMarkerRole,
|
||||
// For reply
|
||||
ReplyEventIdRole,
|
||||
ReplyAuthorRole,
|
||||
ReplyDisplayRole,
|
||||
|
||||
ReplyRole,
|
||||
|
||||
ShowAuthorRole,
|
||||
ShowSectionRole,
|
||||
|
||||
BubbleShapeRole,
|
||||
|
||||
ReactionRole,
|
||||
|
||||
// For debugging
|
||||
EventResolvedTypeRole,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "csapi/rooms.h"
|
||||
#include "csapi/typing.h"
|
||||
#include "events/accountdataevents.h"
|
||||
#include "events/reactionevent.h"
|
||||
#include "events/roommessageevent.h"
|
||||
#include "events/typingevent.h"
|
||||
#include "jobs/downloadfilejob.h"
|
||||
@@ -119,15 +120,21 @@ QString SpectralRoom::lastEvent() {
|
||||
for (auto i = messageEvents().rbegin(); i < messageEvents().rend(); i++) {
|
||||
const RoomEvent* evt = i->get();
|
||||
|
||||
if (is<RedactionEvent>(*evt))
|
||||
if (is<RedactionEvent>(*evt) || is<ReactionEvent>(*evt))
|
||||
continue;
|
||||
if (evt->isRedacted())
|
||||
continue;
|
||||
|
||||
if (evt->isStateEvent() &&
|
||||
static_cast<const StateEventBase*>(evt)->repeatsState())
|
||||
static_cast<const StateEventBase&>(*evt).repeatsState())
|
||||
continue;
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(evt)) {
|
||||
if (!e->replacedEvent().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (connection()->isIgnored(user(evt->senderId())))
|
||||
continue;
|
||||
|
||||
@@ -164,6 +171,13 @@ void SpectralRoom::onAddHistoricalTimelineEvents(rev_iter_t from) {
|
||||
[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() {
|
||||
if (displayed() && !hasUnreadMessages()) {
|
||||
resetNotificationCount();
|
||||
@@ -305,7 +319,7 @@ QString SpectralRoom::markdownToHTML(const QString& markdown) {
|
||||
|
||||
std::string html(tmp_buf);
|
||||
|
||||
free((char *)tmp_buf);
|
||||
free((char*)tmp_buf);
|
||||
|
||||
auto result = QString::fromStdString(html).trimmed();
|
||||
|
||||
@@ -419,3 +433,40 @@ void SpectralRoom::postHtmlMessage(const QString& text,
|
||||
|
||||
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 onAddHistoricalTimelineEvents(rev_iter_t from) override;
|
||||
void onRedaction(const RoomEvent& prevEvent, const RoomEvent& after) override;
|
||||
|
||||
static QString markdownToHTML(const QString& plaintext);
|
||||
|
||||
@@ -315,6 +316,7 @@ class SpectralRoom : public Room {
|
||||
void changeAvatar(QUrl localFile);
|
||||
void addLocalAlias(const QString& alias);
|
||||
void removeLocalAlias(const QString& alias);
|
||||
void toggleReaction(const QString& eventId, const QString& reaction);
|
||||
};
|
||||
|
||||
#endif // SpectralRoom_H
|
||||
|
||||
Reference in New Issue
Block a user