Merge branch 'dev' into 'master'
Merge Dev -> Master Closes #14 See merge request b0/matrique!11
This commit is contained in:
commit
bca88c17b5
|
@ -1 +1 @@
|
||||||
Subproject commit 98751495f1990dccf285e3b4739f86de7b7f68fd
|
Subproject commit c8dc0c075497ca8f174b738ee4253ca282cbec8c
|
|
@ -31,6 +31,8 @@ Page {
|
||||||
Layout.maximumWidth: 360
|
Layout.maximumWidth: 360
|
||||||
|
|
||||||
listModel: roomListModel
|
listModel: roomListModel
|
||||||
|
|
||||||
|
onEnterRoom: roomForm.currentRoom = currentRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomForm {
|
RoomForm {
|
||||||
|
@ -38,8 +40,6 @@ Page {
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
currentRoom: roomListForm.currentRoom
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Controls.Material 2.2
|
import QtQuick.Controls.Material 2.2
|
||||||
|
import Matrique 0.1
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: messageDelegate
|
id: messageDelegate
|
||||||
|
@ -13,7 +14,7 @@ Item {
|
||||||
readonly property bool isMessage: eventType === "message" || eventType === "notice"
|
readonly property bool isMessage: eventType === "message" || eventType === "notice"
|
||||||
readonly property bool isFile: eventType === "video" || eventType === "audio" || eventType === "file" || eventType === "image"
|
readonly property bool isFile: eventType === "video" || eventType === "audio" || eventType === "file" || eventType === "image"
|
||||||
|
|
||||||
visible: eventType != "redaction"
|
visible: marks !== EventStatus.Hidden
|
||||||
|
|
||||||
z: -5
|
z: -5
|
||||||
width: delegateLoader.width
|
width: delegateLoader.width
|
||||||
|
|
|
@ -10,7 +10,7 @@ import "qrc:/js/md.js" as Markdown
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: item
|
id: item
|
||||||
property var currentRoom
|
property var currentRoom: null
|
||||||
|
|
||||||
Pane {
|
Pane {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -269,9 +269,9 @@ Item {
|
||||||
text = text.substr(PREFIX_RAINBOW.length)
|
text = text.substr(PREFIX_RAINBOW.length)
|
||||||
|
|
||||||
var parsedText = ""
|
var parsedText = ""
|
||||||
var rainbowColor = ["#ee0000", "#ff7700", "#eeee00", "#00bb00", "#0000ee", "#dd00dd", "#880088"]
|
var rainbowColor = ["#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"]
|
||||||
for (var i = 0; i < text.length; i++) {
|
for (var i = 0; i < text.length; i++) {
|
||||||
parsedText = parsedText + "<font color='" + rainbowColor[i % 7] + "'>" + text.charAt(i) + "</font>"
|
parsedText = parsedText + "<font color='" + rainbowColor[i % rainbowColor.length] + "'>" + text.charAt(i) + "</font>"
|
||||||
}
|
}
|
||||||
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
|
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
|
||||||
return
|
return
|
||||||
|
|
|
@ -14,6 +14,7 @@ Item {
|
||||||
readonly property int currentIndex: roomListProxyModel.mapToSource(listView.currentIndex)
|
readonly property int currentIndex: roomListProxyModel.mapToSource(listView.currentIndex)
|
||||||
readonly property var currentRoom: currentIndex != -1 ? listModel.roomAt(currentIndex) : null
|
readonly property var currentRoom: currentIndex != -1 ? listModel.roomAt(currentIndex) : null
|
||||||
readonly property bool mini: setting.miniMode // Used as an indicator of whether the listform should be displayed as "Mini mode".
|
readonly property bool mini: setting.miniMode // Used as an indicator of whether the listform should be displayed as "Mini mode".
|
||||||
|
signal enterRoom()
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -137,11 +138,19 @@ Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 80
|
||||||
onPressed: listView.currentIndex = index
|
onPressed: listView.currentIndex = index
|
||||||
|
onClicked: enterRoom()
|
||||||
onPressAndHold: menuComponent.createObject(this)
|
onPressAndHold: menuComponent.createObject(this)
|
||||||
|
|
||||||
ToolTip.visible: mini && hovered
|
ToolTip.visible: mini && hovered
|
||||||
ToolTip.text: name
|
ToolTip.text: name
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 4
|
||||||
|
height: parent.height
|
||||||
|
color: Qt.tint(Material.accent, "#20FFFFFF")
|
||||||
|
visible: unreadCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
contentItem: RowLayout {
|
contentItem: RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 16
|
anchors.margins: 16
|
||||||
|
@ -201,6 +210,10 @@ Item {
|
||||||
onTriggered: currentRoom.isLowPriority ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", "1")
|
onTriggered: currentRoom.isLowPriority ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", "1")
|
||||||
}
|
}
|
||||||
MenuSeparator {}
|
MenuSeparator {}
|
||||||
|
MenuItem {
|
||||||
|
text: "Mark as Read"
|
||||||
|
onTriggered: currentRoom.markAllMessagesAsRead()
|
||||||
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Leave Room"
|
text: "Leave Room"
|
||||||
onTriggered: matriqueController.forgetRoom(currentRoom.id)
|
onTriggered: matriqueController.forgetRoom(currentRoom.id)
|
||||||
|
|
21
qml/main.qml
21
qml/main.qml
|
@ -91,27 +91,6 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog {
|
|
||||||
property alias text: errorLabel.text
|
|
||||||
|
|
||||||
id: errorDialog
|
|
||||||
width: 360
|
|
||||||
modal: true
|
|
||||||
title: "ERROR"
|
|
||||||
|
|
||||||
x: (window.width - width) / 2
|
|
||||||
y: (window.height - height) / 2
|
|
||||||
|
|
||||||
standardButtons: Dialog.Ok
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: errorLabel
|
|
||||||
width: parent.width
|
|
||||||
text: "Label"
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: loginPage
|
id: loginPage
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
|
|
||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
#include <QtCore/QSettings>
|
|
||||||
#include <QtQml> // for qmlRegisterType()
|
#include <QtQml> // for qmlRegisterType()
|
||||||
|
|
||||||
#include <connection.h>
|
#include <connection.h>
|
||||||
|
@ -24,22 +23,27 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
||||||
roles[AboveAuthorRole] = "aboveAuthor";
|
roles[AboveAuthorRole] = "aboveAuthor";
|
||||||
roles[ContentRole] = "content";
|
roles[ContentRole] = "content";
|
||||||
roles[ContentTypeRole] = "contentType";
|
roles[ContentTypeRole] = "contentType";
|
||||||
|
roles[HighlightRole] = "highlight";
|
||||||
roles[ReadMarkerRole] = "readMarker";
|
roles[ReadMarkerRole] = "readMarker";
|
||||||
roles[SpecialMarksRole] = "marks";
|
roles[SpecialMarksRole] = "marks";
|
||||||
roles[LongOperationRole] = "progressInfo";
|
roles[LongOperationRole] = "progressInfo";
|
||||||
|
roles[AnnotationRole] = "annotation";
|
||||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||||
roles[PlainTextRole] = "plainText";
|
roles[PlainTextRole] = "plainText";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageEventModel::~MessageEventModel() {}
|
||||||
|
|
||||||
MessageEventModel::MessageEventModel(QObject* parent)
|
MessageEventModel::MessageEventModel(QObject* parent)
|
||||||
: QAbstractListModel(parent), m_currentRoom(nullptr) {
|
: QAbstractListModel(parent), m_currentRoom(nullptr) {
|
||||||
qmlRegisterType<QMatrixClient::FileTransferInfo>();
|
using namespace QMatrixClient;
|
||||||
qRegisterMetaType<QMatrixClient::FileTransferInfo>();
|
qmlRegisterType<FileTransferInfo>();
|
||||||
|
qRegisterMetaType<FileTransferInfo>();
|
||||||
|
qmlRegisterUncreatableType<EventStatus>(
|
||||||
|
"Matrique", 0, 1, "EventStatus", "EventStatus is not an creatable type");
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageEventModel::~MessageEventModel() {}
|
|
||||||
|
|
||||||
void MessageEventModel::setRoom(QMatrixClient::Room* room) {
|
void MessageEventModel::setRoom(QMatrixClient::Room* room) {
|
||||||
if (room == m_currentRoom) return;
|
if (room == m_currentRoom) return;
|
||||||
|
|
||||||
|
@ -52,26 +56,32 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) {
|
||||||
m_currentRoom = room;
|
m_currentRoom = room;
|
||||||
if (room) {
|
if (room) {
|
||||||
lastReadEventId = room->readMarkerEventId();
|
lastReadEventId = room->readMarkerEventId();
|
||||||
|
|
||||||
using namespace QMatrixClient;
|
using namespace QMatrixClient;
|
||||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this,
|
connect(m_currentRoom, &Room::aboutToAddNewMessages, this,
|
||||||
[=](RoomEventsRange events) {
|
[=](RoomEventsRange events) {
|
||||||
const auto pos = m_currentRoom->pendingEvents().size();
|
beginInsertRows({}, timelineBaseIndex(),
|
||||||
beginInsertRows(QModelIndex(), int(pos),
|
timelineBaseIndex() + int(events.size()) - 1);
|
||||||
int(pos + events.size() - 1));
|
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this,
|
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this,
|
||||||
[=](RoomEventsRange events) {
|
[=](RoomEventsRange events) {
|
||||||
if (rowCount() > 0) nextNewerRow = rowCount() - 1; // See #312
|
if (rowCount() > 0)
|
||||||
beginInsertRows(QModelIndex(), rowCount(),
|
rowBelowInserted = rowCount() - 1; // See #312
|
||||||
|
beginInsertRows({}, rowCount(),
|
||||||
rowCount() + int(events.size()) - 1);
|
rowCount() + int(events.size()) - 1);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::addedMessages, this, [=] {
|
connect(m_currentRoom, &Room::addedMessages, this,
|
||||||
if (nextNewerRow > -1) {
|
[=](int lowest, int biggest) {
|
||||||
const auto idx = index(nextNewerRow);
|
|
||||||
emit dataChanged(idx, idx);
|
|
||||||
nextNewerRow = -1;
|
|
||||||
}
|
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
||||||
|
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() -
|
||||||
|
biggest + timelineBaseIndex() - 1;
|
||||||
|
refreshEventRoles(rowBelowInserted,
|
||||||
|
{AboveAuthorRole, AboveSectionRole});
|
||||||
|
}
|
||||||
|
for (auto i = m_currentRoom->maxTimelineIndex() - biggest;
|
||||||
|
i <= m_currentRoom->maxTimelineIndex() - lowest; ++i)
|
||||||
|
refreshLastUserEvents(i);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this,
|
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this,
|
||||||
[this] { beginInsertRows({}, 0, 0); });
|
[this] { beginInsertRows({}, 0, 0); });
|
||||||
|
@ -79,31 +89,43 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) {
|
||||||
&MessageEventModel::endInsertRows);
|
&MessageEventModel::endInsertRows);
|
||||||
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this,
|
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this,
|
||||||
[this](RoomEvent*, int i) {
|
[this](RoomEvent*, int i) {
|
||||||
const auto timelineBaseIdx =
|
if (i == 0) return; // No need to move anything, just refresh
|
||||||
int(m_currentRoom->pendingEvents().size());
|
|
||||||
if (i + 1 == timelineBaseIdx) return; // No need to move anything
|
movingEvent = true;
|
||||||
mergingEcho = true;
|
// Reverse i because row 0 is bottommost in the model
|
||||||
Q_ASSERT(beginMoveRows({}, i, i, {}, timelineBaseIdx));
|
const auto row = timelineBaseIndex() - i - 1;
|
||||||
|
Q_ASSERT(beginMoveRows({}, row, row, {}, timelineBaseIndex()));
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventMerged, this, [this] {
|
connect(m_currentRoom, &Room::pendingEventMerged, this, [this] {
|
||||||
if (mergingEcho) {
|
if (movingEvent) {
|
||||||
endMoveRows();
|
endMoveRows();
|
||||||
mergingEcho = false;
|
movingEvent = false;
|
||||||
}
|
}
|
||||||
refreshEventRoles(int(m_currentRoom->pendingEvents().size()),
|
refreshRow(timelineBaseIndex()); // Refresh the looks
|
||||||
{SpecialMarksRole});
|
refreshLastUserEvents(0);
|
||||||
|
if (m_currentRoom->timelineSize() > 1) // Refresh above
|
||||||
|
refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole});
|
||||||
|
if (timelineBaseIndex() > 0) // Refresh below, see #312
|
||||||
|
refreshEventRoles(timelineBaseIndex() - 1,
|
||||||
|
{AboveAuthorRole, AboveSectionRole});
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventChanged, this,
|
connect(m_currentRoom, &Room::pendingEventChanged, this,
|
||||||
[this](int i) { refreshEventRoles(i, {SpecialMarksRole}); });
|
&MessageEventModel::refreshRow);
|
||||||
|
connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this,
|
||||||
|
[this](int i) { beginRemoveRows({}, i, i); });
|
||||||
|
connect(m_currentRoom, &Room::pendingEventDiscarded, this,
|
||||||
|
&MessageEventModel::endRemoveRows);
|
||||||
connect(m_currentRoom, &Room::readMarkerMoved, this, [this] {
|
connect(m_currentRoom, &Room::readMarkerMoved, this, [this] {
|
||||||
refreshEventRoles(
|
refreshEventRoles(
|
||||||
std::exchange(lastReadEventId, m_currentRoom->readMarkerEventId()),
|
std::exchange(lastReadEventId, m_currentRoom->readMarkerEventId()),
|
||||||
{ReadMarkerRole});
|
{ReadMarkerRole});
|
||||||
refreshEventRoles(lastReadEventId, {ReadMarkerRole});
|
refreshEventRoles(lastReadEventId, {ReadMarkerRole});
|
||||||
});
|
});
|
||||||
connect(
|
connect(m_currentRoom, &Room::replacedEvent, this,
|
||||||
m_currentRoom, &Room::replacedEvent, this,
|
[this](const RoomEvent* newEvent) {
|
||||||
[this](const RoomEvent* newEvent) { refreshEvent(newEvent->id()); });
|
refreshLastUserEvents(refreshEvent(newEvent->id()) -
|
||||||
|
timelineBaseIndex());
|
||||||
|
});
|
||||||
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,
|
||||||
|
@ -119,21 +141,32 @@ void MessageEventModel::setRoom(QMatrixClient::Room* room) {
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageEventModel::refreshEvent(const QString& eventId) {
|
int MessageEventModel::refreshEvent(const QString& eventId) {
|
||||||
refreshEventRoles(eventId, {});
|
return refreshEventRoles(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageEventModel::refreshEventRoles(const int row,
|
void MessageEventModel::refreshRow(int row) { refreshEventRoles(row); }
|
||||||
const QVector<int>& roles) {
|
|
||||||
|
int MessageEventModel::timelineBaseIndex() const {
|
||||||
|
return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MessageEventModel::refreshEventRoles(int row, const QVector<int>& roles) {
|
||||||
const auto idx = index(row);
|
const auto idx = index(row);
|
||||||
emit dataChanged(idx, idx, roles);
|
emit dataChanged(idx, idx, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageEventModel::refreshEventRoles(const QString& eventId,
|
int MessageEventModel::refreshEventRoles(const QString& eventId,
|
||||||
const QVector<int>& roles) {
|
const QVector<int>& roles) {
|
||||||
const auto it = m_currentRoom->findInTimeline(eventId);
|
const auto it = m_currentRoom->findInTimeline(eventId);
|
||||||
if (it != m_currentRoom->timelineEdge())
|
if (it == m_currentRoom->timelineEdge()) {
|
||||||
refreshEventRoles(it - m_currentRoom->messageEvents().rbegin(), roles);
|
qWarning() << "Trying to refresh inexistent event:" << eventId;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const auto row =
|
||||||
|
it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
||||||
|
refreshEventRoles(row, roles);
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) {
|
inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) {
|
||||||
|
@ -161,9 +194,8 @@ QDateTime MessageEventModel::makeMessageTimestamp(
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MessageEventModel::makeDateString(
|
QString MessageEventModel::renderDate(QDateTime timestamp) const {
|
||||||
const QMatrixClient::Room::rev_iter_t& baseIt) const {
|
auto date = timestamp.toLocalTime().date();
|
||||||
auto date = makeMessageTimestamp(baseIt).toLocalTime().date();
|
|
||||||
if (QMatrixClient::SettingsGroup("UI")
|
if (QMatrixClient::SettingsGroup("UI")
|
||||||
.value("banner_human_friendly_date", true)
|
.value("banner_human_friendly_date", true)
|
||||||
.toBool()) {
|
.toBool()) {
|
||||||
|
@ -176,25 +208,101 @@ QString MessageEventModel::makeDateString(
|
||||||
return date.toString(Qt::DefaultLocaleShortDate);
|
return date.toString(Qt::DefaultLocaleShortDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MessageEventModel::isUserActivityNotable(
|
||||||
|
const QMatrixClient::Room::rev_iter_t& baseIt) const {
|
||||||
|
const auto& senderId = (*baseIt)->senderId();
|
||||||
|
// TODO: Go up and down the timeline (limit to 100 events for
|
||||||
|
// the sake of performance) and collect all messages of
|
||||||
|
// this author; find out if there's anything besides joins, leaves
|
||||||
|
// and redactions; if not, double-check whether the current event is
|
||||||
|
// a part of a re-join without following redactions.
|
||||||
|
using namespace QMatrixClient;
|
||||||
|
bool joinFound = false, redactionsFound = false;
|
||||||
|
// Find the nearest join of this user above, or a no-nonsense event.
|
||||||
|
for (auto it = baseIt,
|
||||||
|
limit = baseIt +
|
||||||
|
std::min(int(m_currentRoom->timelineEdge() - baseIt), 100);
|
||||||
|
it != limit; ++it) {
|
||||||
|
const auto& e = **it;
|
||||||
|
if (e.senderId() != senderId) continue;
|
||||||
|
if (e.isRedacted()) {
|
||||||
|
redactionsFound = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (auto* me = it->viewAs<QMatrixClient::RoomMemberEvent>()) {
|
||||||
|
if (me->isJoin()) {
|
||||||
|
joinFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true; // Consider all other events notable
|
||||||
|
}
|
||||||
|
// Find the nearest leave of this user below, or a no-nonsense event
|
||||||
|
bool leaveFound = false;
|
||||||
|
for (auto it = baseIt.base() - 1,
|
||||||
|
limit = baseIt.base() +
|
||||||
|
std::min(int(m_currentRoom->messageEvents().end() -
|
||||||
|
baseIt.base()),
|
||||||
|
100);
|
||||||
|
it != limit; ++it) {
|
||||||
|
const auto& e = **it;
|
||||||
|
if (e.senderId() != senderId) continue;
|
||||||
|
if (e.isRedacted()) {
|
||||||
|
redactionsFound = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (auto* me = it->viewAs<RoomMemberEvent>()) {
|
||||||
|
if (me->isLeave() || me->membership() == MembershipType::Ban) {
|
||||||
|
leaveFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If we are here, it means that no notable events have been found in
|
||||||
|
// the timeline vicinity, and probably redactions are there. Doesn't look
|
||||||
|
// notable but let's give some benefit of doubt.
|
||||||
|
if (redactionsFound) return false; // Join + redactions or redactions + leave
|
||||||
|
return !(joinFound && leaveFound); // Join + (maybe profile changes) + leave
|
||||||
|
}
|
||||||
|
|
||||||
|
void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) {
|
||||||
|
if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow)
|
||||||
|
return;
|
||||||
|
const auto& timelineBottom = m_currentRoom->messageEvents().rbegin();
|
||||||
|
const auto& lastSender = (*(timelineBottom + baseTimelineRow))->senderId();
|
||||||
|
const auto limit = timelineBottom + std::min(baseTimelineRow + 100,
|
||||||
|
m_currentRoom->timelineSize());
|
||||||
|
for (auto it = timelineBottom + std::max(baseTimelineRow - 100, 0);
|
||||||
|
it != limit; ++it) {
|
||||||
|
if ((*it)->senderId() == lastSender) {
|
||||||
|
auto idx = index(it - timelineBottom);
|
||||||
|
emit dataChanged(idx, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int MessageEventModel::rowCount(const QModelIndex& parent) const {
|
int MessageEventModel::rowCount(const QModelIndex& parent) const {
|
||||||
if (!m_currentRoom || parent.isValid()) return 0;
|
if (!m_currentRoom || parent.isValid()) return 0;
|
||||||
return m_currentRoom->timelineSize();
|
return m_currentRoom->timelineSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
||||||
const auto row = index.row();
|
const auto row = idx.row();
|
||||||
|
|
||||||
if (!m_currentRoom || row < 0 ||
|
if (!m_currentRoom || row < 0 ||
|
||||||
row >= int(m_currentRoom->pendingEvents().size()) +
|
row >= int(m_currentRoom->pendingEvents().size()) +
|
||||||
m_currentRoom->timelineSize())
|
m_currentRoom->timelineSize())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
const auto timelineBaseIdx = int(m_currentRoom->pendingEvents().size());
|
bool isPending = row < timelineBaseIndex();
|
||||||
const auto timelineIt = m_currentRoom->messageEvents().crbegin() +
|
const auto timelineIt = m_currentRoom->messageEvents().crbegin() +
|
||||||
std::max(-1, row - timelineBaseIdx);
|
std::max(0, row - timelineBaseIndex());
|
||||||
const auto& evt = row < timelineBaseIdx
|
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() +
|
||||||
? *m_currentRoom->pendingEvents()[size_t(row)]
|
std::min(row, timelineBaseIndex());
|
||||||
: *timelineIt->event();
|
const auto& evt = isPending ? **pendingIt : **timelineIt;
|
||||||
|
|
||||||
using namespace QMatrixClient;
|
using namespace QMatrixClient;
|
||||||
if (role == Qt::DisplayRole) {
|
if (role == Qt::DisplayRole) {
|
||||||
|
@ -238,14 +346,14 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
||||||
: tr("joined the room");
|
: tr("joined the room");
|
||||||
}
|
}
|
||||||
QString text{};
|
QString text{};
|
||||||
if (e.displayName() != e.prevContent()->displayName) {
|
if (e.isRename()) {
|
||||||
if (e.displayName().isEmpty())
|
if (e.displayName().isEmpty())
|
||||||
text = tr("cleared the display name");
|
text = tr("cleared the display name");
|
||||||
else
|
else
|
||||||
text =
|
text =
|
||||||
tr("changed the display name to %1").arg(e.displayName());
|
tr("changed the display name to %1").arg(e.displayName());
|
||||||
}
|
}
|
||||||
if (e.avatarUrl() != e.prevContent()->avatarUrl) {
|
if (e.isAvatarUpdate()) {
|
||||||
if (!text.isEmpty()) text += " and ";
|
if (!text.isEmpty()) text += " and ";
|
||||||
if (e.avatarUrl().isEmpty())
|
if (e.avatarUrl().isEmpty())
|
||||||
text += tr("cleared the avatar");
|
text += tr("cleared the avatar");
|
||||||
|
@ -314,8 +422,7 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
||||||
|
|
||||||
if (e.hasFileContent()) {
|
if (e.hasFileContent()) {
|
||||||
auto fileCaption = e.content()->fileInfo()->originalName;
|
auto fileCaption = e.content()->fileInfo()->originalName;
|
||||||
if (fileCaption.isEmpty())
|
if (fileCaption.isEmpty()) fileCaption = e.plainBody();
|
||||||
fileCaption = e.plainBody();
|
|
||||||
if (fileCaption.isEmpty()) return tr("a file");
|
if (fileCaption.isEmpty()) return tr("a file");
|
||||||
}
|
}
|
||||||
return e.plainBody();
|
return e.plainBody();
|
||||||
|
@ -412,15 +519,10 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
||||||
return "notice";
|
return "notice";
|
||||||
case MessageEventType::Image:
|
case MessageEventType::Image:
|
||||||
return "image";
|
return "image";
|
||||||
case MessageEventType::File:
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
case MessageEventType::Video:
|
|
||||||
return "file";
|
|
||||||
default:
|
default:
|
||||||
return "message";
|
return e->hasFileContent() ? "file" : "message";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is<RedactionEvent>(evt)) return "redaction";
|
|
||||||
if (evt.isStateEvent()) return "state";
|
if (evt.isStateEvent()) return "state";
|
||||||
|
|
||||||
return "other";
|
return "other";
|
||||||
|
@ -431,8 +533,7 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
||||||
|
|
||||||
if (role == AuthorRole) {
|
if (role == AuthorRole) {
|
||||||
// FIXME: It shouldn't be User, it should be its state "as of event"
|
// FIXME: It shouldn't be User, it should be its state "as of event"
|
||||||
return QVariant::fromValue(row < timelineBaseIdx
|
return QVariant::fromValue(isPending ? m_currentRoom->localUser()
|
||||||
? m_currentRoom->localUser()
|
|
||||||
: m_currentRoom->user(evt.senderId()));
|
: m_currentRoom->user(evt.senderId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,16 +567,39 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
||||||
if (role == ReadMarkerRole) return evt.id() == lastReadEventId;
|
if (role == ReadMarkerRole) return evt.id() == lastReadEventId;
|
||||||
|
|
||||||
if (role == SpecialMarksRole) {
|
if (role == SpecialMarksRole) {
|
||||||
if (row < timelineBaseIdx)
|
if (isPending) return pendingIt->deliveryStatus();
|
||||||
return evt.id().isEmpty() ? "unsent" : "unsynced";
|
|
||||||
|
if (is<RedactionEvent>(evt)) return EventStatus::Hidden;
|
||||||
|
auto* memberEvent = timelineIt->viewAs<RoomMemberEvent>();
|
||||||
|
if (memberEvent) {
|
||||||
|
if ((memberEvent->isJoin() || memberEvent->isLeave()) &&
|
||||||
|
!Settings().value("UI/show_joinleave", true).toBool())
|
||||||
|
return EventStatus::Hidden;
|
||||||
|
}
|
||||||
|
if (memberEvent || evt.isRedacted()) {
|
||||||
|
if (evt.senderId() == m_currentRoom->localUser()->id() ||
|
||||||
|
Settings().value("UI/show_spammy").toBool()) {
|
||||||
|
// QElapsedTimer et; et.start();
|
||||||
|
auto hide = !isUserActivityNotable(timelineIt);
|
||||||
|
// qDebug() << "Checked user activity for" << evt.id() <<
|
||||||
|
// "in" << et;
|
||||||
|
if (hide) return EventStatus::Hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evt.isRedacted())
|
||||||
|
return Settings().value("UI/show_redacted").toBool()
|
||||||
|
? EventStatus::Redacted : EventStatus::Hidden;
|
||||||
|
|
||||||
if (evt.isStateEvent() &&
|
if (evt.isStateEvent() &&
|
||||||
static_cast<const StateEventBase&>(evt).repeatsState())
|
static_cast<const StateEventBase&>(evt).repeatsState() &&
|
||||||
return "noop";
|
!Settings().value("UI/show_noop_events").toBool())
|
||||||
return evt.isRedacted() ? "redacted" : "";
|
return EventStatus::Hidden;
|
||||||
|
|
||||||
|
return EventStatus::Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == EventIdRole) return evt.id();
|
if (role == EventIdRole)
|
||||||
|
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
|
||||||
|
|
||||||
if (role == LongOperationRole) {
|
if (role == LongOperationRole) {
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt))
|
if (auto e = eventCast<const RoomMessageEvent>(&evt))
|
||||||
|
@ -483,30 +607,24 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const {
|
||||||
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
|
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row >= timelineBaseIdx - 1) // The timeline and the topmost unsynced
|
if (role == AnnotationRole)
|
||||||
{
|
if (isPending) return pendingIt->annotation();
|
||||||
if (role == TimeRole)
|
|
||||||
return row < timelineBaseIdx ? QDateTime::currentDateTimeUtc()
|
|
||||||
: makeMessageTimestamp(timelineIt);
|
|
||||||
|
|
||||||
if (role == SectionRole)
|
if (role == TimeRole || role == SectionRole) {
|
||||||
return row < timelineBaseIdx
|
auto ts =
|
||||||
? tr("Today")
|
isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
|
||||||
: makeDateString(
|
return role == TimeRole ? QVariant(ts) : renderDate(ts);
|
||||||
timelineIt); // FIXME: move date rendering to QML
|
|
||||||
|
|
||||||
// FIXME: shouldn't be here, because #312
|
|
||||||
auto aboveEventIt = timelineIt + 1;
|
|
||||||
if (aboveEventIt != m_currentRoom->timelineEdge()) {
|
|
||||||
if (role == AboveSectionRole) return makeDateString(aboveEventIt);
|
|
||||||
|
|
||||||
if (role == AboveAuthorRole)
|
|
||||||
return QVariant::fromValue(
|
|
||||||
m_currentRoom->user((*aboveEventIt)->senderId()));
|
|
||||||
|
|
||||||
if (role == AboveTimeRole) return makeMessageTimestamp(aboveEventIt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return QVariant();
|
if (role == AboveSectionRole || role == AboveAuthorRole ||
|
||||||
|
role == AboveTimeRole)
|
||||||
|
for (auto r = row + 1; r < rowCount(); ++r) {
|
||||||
|
auto i = index(r);
|
||||||
|
if (data(i, SpecialMarksRole) != EventStatus::Hidden)
|
||||||
|
return data(i, role == AboveSectionRole
|
||||||
|
? SectionRole
|
||||||
|
: role == AboveAuthorRole ? AuthorRole : TimeRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ class MessageEventModel : public QAbstractListModel {
|
||||||
ReadMarkerRole,
|
ReadMarkerRole,
|
||||||
SpecialMarksRole,
|
SpecialMarksRole,
|
||||||
LongOperationRole,
|
LongOperationRole,
|
||||||
|
AnnotationRole,
|
||||||
PlainTextRole,
|
PlainTextRole,
|
||||||
// For debugging
|
// For debugging
|
||||||
EventResolvedTypeRole,
|
EventResolvedTypeRole,
|
||||||
|
@ -36,25 +37,31 @@ class MessageEventModel : public QAbstractListModel {
|
||||||
QMatrixClient::Room* getRoom() { return m_currentRoom; }
|
QMatrixClient::Room* getRoom() { return m_currentRoom; }
|
||||||
void setRoom(QMatrixClient::Room* room);
|
void setRoom(QMatrixClient::Room* room);
|
||||||
|
|
||||||
Q_INVOKABLE int rowCount(
|
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||||
const QModelIndex& parent = QModelIndex()) const override;
|
QVariant data(const QModelIndex& index,
|
||||||
QVariant data(const QModelIndex& index, int role) const override;
|
int role = Qt::DisplayRole) const override;
|
||||||
QHash<int, QByteArray> roleNames() const;
|
QHash<int, QByteArray> roleNames() const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void refreshEvent(const QString& eventId);
|
int refreshEvent(const QString& eventId);
|
||||||
|
void refreshRow(int row);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QMatrixClient::Room* m_currentRoom = nullptr;
|
QMatrixClient::Room* m_currentRoom = nullptr;
|
||||||
QString lastReadEventId;
|
QString lastReadEventId;
|
||||||
bool mergingEcho = 0;
|
int rowBelowInserted = -1;
|
||||||
int nextNewerRow = -1;
|
bool movingEvent = 0;
|
||||||
|
|
||||||
|
int timelineBaseIndex() const;
|
||||||
QDateTime makeMessageTimestamp(
|
QDateTime makeMessageTimestamp(
|
||||||
const QMatrixClient::Room::rev_iter_t& baseIt) const;
|
const QMatrixClient::Room::rev_iter_t& baseIt) const;
|
||||||
QString makeDateString(const QMatrixClient::Room::rev_iter_t& baseIt) const;
|
QString renderDate(QDateTime timestamp) const;
|
||||||
void refreshEventRoles(const int row, const QVector<int>& roles);
|
bool isUserActivityNotable(
|
||||||
void refreshEventRoles(const QString& eventId, const QVector<int>& roles);
|
const QMatrixClient::Room::rev_iter_t& baseIt) const;
|
||||||
|
|
||||||
|
void refreshLastUserEvents(int baseRow);
|
||||||
|
void refreshEventRoles(int row, const QVector<int>& roles = {});
|
||||||
|
int refreshEventRoles(const QString& eventId, const QVector<int>& roles = {});
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
|
|
|
@ -66,7 +66,8 @@ void RoomListModel::connectRoomSignals(Room* room) {
|
||||||
// connect(
|
// connect(
|
||||||
// room, &QMatrixClient::Room::aboutToAddNewMessages, this,
|
// room, &QMatrixClient::Room::aboutToAddNewMessages, this,
|
||||||
// [=](QMatrixClient::RoomEventsRange eventsRange) {
|
// [=](QMatrixClient::RoomEventsRange eventsRange) {
|
||||||
// for (QMatrixClient::RoomEvents events : eventsRange.const_iterator) {
|
// for (QMatrixClient::RoomEvents events : eventsRange.const_iterator)
|
||||||
|
// {
|
||||||
// for (QMatrixClient::RoomEvent event : events) {
|
// for (QMatrixClient::RoomEvent event : events) {
|
||||||
// qDebug() << event.fullJson();
|
// qDebug() << event.fullJson();
|
||||||
// }
|
// }
|
||||||
|
@ -159,6 +160,9 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const {
|
||||||
if (room->highlightCount() > 0) return QBrush(QColor("orange"));
|
if (room->highlightCount() > 0) return QBrush(QColor("orange"));
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
if (role == UnreadCountRole) {
|
||||||
|
return room->unreadCount();
|
||||||
|
}
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,5 +193,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const {
|
||||||
roles[TopicRole] = "topic";
|
roles[TopicRole] = "topic";
|
||||||
roles[CategoryRole] = "category";
|
roles[CategoryRole] = "category";
|
||||||
roles[HighlightRole] = "highlight";
|
roles[HighlightRole] = "highlight";
|
||||||
|
roles[UnreadCountRole] = "unreadCount";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ class RoomListModel : public QAbstractListModel {
|
||||||
TopicRole,
|
TopicRole,
|
||||||
CategoryRole,
|
CategoryRole,
|
||||||
HighlightRole,
|
HighlightRole,
|
||||||
|
UnreadCountRole
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomListModel(QObject* parent = 0);
|
RoomListModel(QObject* parent = 0);
|
||||||
|
|
Loading…
Reference in New Issue