Move currentRoom/timeline handling to roomlist

This commit is contained in:
Nicolas Werner 2021-05-28 22:14:59 +02:00
parent e2765212fb
commit 298822baea
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
30 changed files with 349 additions and 345 deletions

View file

@ -31,6 +31,7 @@ Rectangle {
TimelineView { TimelineView {
id: timeline id: timeline
room: Rooms.currentRoom
SplitView.fillWidth: true SplitView.fillWidth: true
SplitView.minimumWidth: 400 SplitView.minimumWidth: 400

View file

@ -70,7 +70,7 @@ Popup {
onCompleterNameChanged: { onCompleterNameChanged: {
if (completerName) { if (completerName) {
if (completerName == "user") if (completerName == "user")
completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId()); completer = TimelineManager.completerFor(completerName, room.roomId());
else else
completer = TimelineManager.completerFor(completerName); completer = TimelineManager.completerFor(completerName);
completer.setSearchString(""); completer.setSearchString("");
@ -83,8 +83,8 @@ Popup {
height: listView.contentHeight + 2 // + 2 for the padding on top and bottom height: listView.contentHeight + 2 // + 2 for the padding on top and bottom
Connections { Connections {
onTimelineChanged: completer = null onRoomChanged: completer = null
target: TimelineManager target: timelineView
} }
ListView { ListView {

View file

@ -50,7 +50,7 @@ Popup {
Reply { Reply {
id: replyPreview id: replyPreview
modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : { modelData: room ? room.getDump(mid, "") : {
} }
userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
} }
@ -95,7 +95,7 @@ Popup {
Connections { Connections {
onCompletionSelected: { onCompletionSelected: {
TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id); room.forwardMessage(messageContextMenu.eventId, id);
forwardMessagePopup.close(); forwardMessagePopup.close();
} }
onCountChanged: { onCountChanged: {

View file

@ -28,7 +28,7 @@ Rectangle {
RowLayout { RowLayout {
id: row id: row
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) || messageContextMenu.isSender visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
anchors.fill: parent anchors.fill: parent
ImageButton { ImageButton {
@ -43,7 +43,7 @@ Rectangle {
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call") ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
Layout.margins: 8 Layout.margins: 8
onClicked: { onClicked: {
if (TimelineManager.timeline) { if (room) {
if (CallManager.haveCallInvite) { if (CallManager.haveCallInvite) {
return ; return ;
} else if (CallManager.isOnCall) { } else if (CallManager.isOnCall) {
@ -63,14 +63,14 @@ Rectangle {
height: 22 height: 22
image: ":/icons/icons/ui/paper-clip-outline.png" image: ":/icons/icons/ui/paper-clip-outline.png"
Layout.margins: 8 Layout.margins: 8
onClicked: TimelineManager.timeline.input.openFileSelection() onClicked: room.input.openFileSelection()
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Send a file") ToolTip.text: qsTr("Send a file")
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Nheko.colors.window color: Nheko.colors.window
visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading visible: room && room.input.uploading
NhekoBusyIndicator { NhekoBusyIndicator {
anchors.fill: parent anchors.fill: parent
@ -123,16 +123,16 @@ Rectangle {
padding: 8 padding: 8
focus: true focus: true
onTextChanged: { onTextChanged: {
if (TimelineManager.timeline) if (room)
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
forceActiveFocus(); forceActiveFocus();
} }
onCursorPositionChanged: { onCursorPositionChanged: {
if (!TimelineManager.timeline) if (!room)
return ; return ;
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
if (cursorPosition <= completerTriggeredAt) { if (cursorPosition <= completerTriggeredAt) {
completerTriggeredAt = -1; completerTriggeredAt = -1;
popup.close(); popup.close();
@ -141,13 +141,13 @@ Rectangle {
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)); popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
} }
onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
// Ensure that we get escape key press events first. // Ensure that we get escape key press events first.
Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter))
Keys.onPressed: { Keys.onPressed: {
if (event.matches(StandardKey.Paste)) { if (event.matches(StandardKey.Paste)) {
TimelineManager.timeline.input.paste(false); room.input.paste(false);
event.accepted = true; event.accepted = true;
} else if (event.key == Qt.Key_Space) { } else if (event.key == Qt.Key_Space) {
// close popup if user enters space after colon // close popup if user enters space after colon
@ -160,9 +160,9 @@ Rectangle {
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
messageInput.clear(); messageInput.clear();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
messageInput.text = TimelineManager.timeline.input.previousText(); messageInput.text = room.input.previousText();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
messageInput.text = TimelineManager.timeline.input.nextText(); messageInput.text = room.input.nextText();
} else if (event.key == Qt.Key_At) { } else if (event.key == Qt.Key_At) {
messageInput.openCompleter(cursorPosition, "user"); messageInput.openCompleter(cursorPosition, "user");
popup.open(); popup.open();
@ -188,7 +188,7 @@ Rectangle {
return ; return ;
} }
} }
TimelineManager.timeline.input.send(); room.input.send();
event.accepted = true; event.accepted = true;
} else if (event.key == Qt.Key_Tab) { } else if (event.key == Qt.Key_Tab) {
event.accepted = true; event.accepted = true;
@ -223,11 +223,11 @@ Rectangle {
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) { } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
if (cursorPosition == 0) { if (cursorPosition == 0) {
event.accepted = true; event.accepted = true;
var idx = TimelineManager.timeline.edit ? TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) + 1 : 0; var idx = room.edit ? room.idToIndex(room.edit) + 1 : 0;
while (true) { while (true) {
var id = TimelineManager.timeline.indexToId(idx); var id = room.indexToId(idx);
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { if (!id || room.getDump(id, "").isEditable) {
TimelineManager.timeline.edit = id; room.edit = id;
cursorPosition = 0; cursorPosition = 0;
Qt.callLater(positionCursorAtEnd); Qt.callLater(positionCursorAtEnd);
break; break;
@ -239,13 +239,13 @@ Rectangle {
positionCursorAtStart(); positionCursorAtStart();
} }
} else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) { } else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) {
if (cursorPosition == messageInput.length && TimelineManager.timeline.edit) { if (cursorPosition == messageInput.length && room.edit) {
event.accepted = true; event.accepted = true;
var idx = TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) - 1; var idx = room.idToIndex(room.edit) - 1;
while (true) { while (true) {
var id = TimelineManager.timeline.indexToId(idx); var id = room.indexToId(idx);
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { if (!id || room.getDump(id, "").isEditable) {
TimelineManager.timeline.edit = id; room.edit = id;
Qt.callLater(positionCursorAtStart); Qt.callLater(positionCursorAtStart);
break; break;
} }
@ -260,14 +260,14 @@ Rectangle {
background: null background: null
Connections { Connections {
onActiveTimelineChanged: { onRoomChanged: {
messageInput.clear(); messageInput.clear();
messageInput.append(TimelineManager.timeline.input.text()); messageInput.append(room.input.text());
messageInput.completerTriggeredAt = -1; messageInput.completerTriggeredAt = -1;
popup.completerName = ""; popup.completerName = "";
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
} }
target: TimelineManager target: timelineView
} }
Connections { Connections {
@ -292,14 +292,14 @@ Rectangle {
messageInput.text = newText; messageInput.text = newText;
messageInput.cursorPosition = newText.length; messageInput.cursorPosition = newText.length;
} }
target: TimelineManager.timeline ? TimelineManager.timeline.input : null target: room ? room.input : null
} }
Connections { Connections {
ignoreUnknownSignals: true ignoreUnknownSignals: true
onReplyChanged: messageInput.forceActiveFocus() onReplyChanged: messageInput.forceActiveFocus()
onEditChanged: messageInput.forceActiveFocus() onEditChanged: messageInput.forceActiveFocus()
target: TimelineManager.timeline target: room
} }
Connections { Connections {
@ -312,7 +312,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.MiddleButton acceptedButtons: Qt.MiddleButton
cursorShape: Qt.IBeamCursor cursorShape: Qt.IBeamCursor
onClicked: TimelineManager.timeline.input.paste(true) onClicked: room.input.paste(true)
} }
} }
@ -347,7 +347,7 @@ Rectangle {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Send") ToolTip.text: qsTr("Send")
onClicked: { onClicked: {
TimelineManager.timeline.input.send(); room.input.send();
} }
} }
@ -355,7 +355,7 @@ Rectangle {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: TimelineManager.timeline ? (!TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage)) : false visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
text: qsTr("You don't have permission to send messages in this room") text: qsTr("You don't have permission to send messages in this room")
color: Nheko.colors.text color: Nheko.colors.text
} }

View file

@ -4,6 +4,7 @@
import "./delegates" import "./delegates"
import "./emoji" import "./emoji"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
@ -22,7 +23,7 @@ ScrollView {
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2 property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
model: TimelineManager.timeline model: room
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
pixelAligned: true pixelAligned: true
spacing: 4 spacing: 4
@ -413,4 +414,141 @@ ScrollView {
} }
Platform.Menu {
id: messageContextMenu
property string eventId
property string link
property string text
property int eventType
property bool isEncrypted
property bool isEditable
property bool isSender
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
text = text_;
else
text = "";
if (link_)
link = link_;
else
link = "";
if (showAt_)
open(showAt_);
else
open();
}
Platform.MenuItem {
visible: messageContextMenu.text
enabled: visible
text: qsTr("Copy")
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
visible: messageContextMenu.link
enabled: visible
text: qsTr("Copy link location")
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
text: qsTr("React")
onTriggered: emojiPopup.show(null, function(emoji) {
room.input.reaction(messageContextMenu.eventId, emoji);
})
}
Platform.MenuItem {
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
text: qsTr("Reply")
onTriggered: room.replyAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
text: qsTr("Edit")
onTriggered: room.editAction(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("Read receipts")
onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
text: qsTr("Forward")
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
}
}
Platform.MenuItem {
text: qsTr("Mark as read")
}
Platform.MenuItem {
text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
enabled: visible
text: qsTr("View decrypted raw message")
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
text: qsTr("Remove message")
onTriggered: room.redactEvent(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("Save as")
onTriggered: room.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("Open in external program")
onTriggered: room.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventId
enabled: visible
text: qsTr("Copy link to event")
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
} }

View file

@ -72,8 +72,7 @@ Popup {
Connections { Connections {
onCompletionSelected: { onCompletionSelected: {
TimelineManager.setHistoryView(id); Rooms.setCurrentRoom(id);
TimelineManager.highlightRoom(id);
quickSwitcher.close(); quickSwitcher.close();
} }
onCountChanged: { onCountChanged: {

View file

@ -35,7 +35,7 @@ Flow {
ToolTip.text: modelData.users ToolTip.text: modelData.users
onClicked: { onClicked: {
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent);
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); room.input.reaction(reactionFlow.eventId, modelData.key);
} }
contentItem: Row { contentItem: Row {

View file

@ -11,8 +11,6 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: replyPopup id: replyPopup
property var room: TimelineManager.timeline
Layout.fillWidth: true Layout.fillWidth: true
visible: room && (room.reply || room.edit) visible: room && (room.reply || room.edit)
// Height of child, plus margins, plus border // Height of child, plus margins, plus border

View file

@ -149,7 +149,7 @@ Page {
}, },
State { State {
name: "selected" name: "selected"
when: TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId() when: Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()
PropertyChanges { PropertyChanges {
target: roomItem target: roomItem
@ -165,18 +165,27 @@ Page {
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onSingleTapped: roomContextMenu.show(model.roomId, model.tags) onSingleTapped: {
if (!TimelineManager.isInvite) {
roomContextMenu.show(model.roomId, model.tags);
}
}
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }
TapHandler {
onSingleTapped: Rooms.setCurrentRoom(model.roomId)
onLongPressed: {
if (!TimelineManager.isInvite) {
roomContextMenu.show(model.roomId, model.tags);
}
}
}
HoverHandler { HoverHandler {
id: hovered id: hovered
} }
TapHandler {
onSingleTapped: TimelineManager.setHistoryView(model.roomId)
}
RowLayout { RowLayout {
spacing: Nheko.paddingMedium spacing: Nheko.paddingMedium
anchors.fill: parent anchors.fill: parent

View file

@ -63,14 +63,6 @@ Page {
} }
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
Shortcut { Shortcut {
sequence: "Ctrl+K" sequence: "Ctrl+K"
onActivated: { onActivated: {
@ -80,135 +72,6 @@ Page {
} }
} }
Platform.Menu {
id: messageContextMenu
property string eventId
property string link
property string text
property int eventType
property bool isEncrypted
property bool isEditable
property bool isSender
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
text = text_;
else
text = "";
if (link_)
link = link_;
else
link = "";
if (showAt_)
open(showAt_);
else
open();
}
Platform.MenuItem {
visible: messageContextMenu.text
enabled: visible
text: qsTr("Copy")
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
visible: messageContextMenu.link
enabled: visible
text: qsTr("Copy link location")
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false
text: qsTr("React")
onTriggered: emojiPopup.show(null, function(emoji) {
TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji);
})
}
Platform.MenuItem {
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false
text: qsTr("Reply")
onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
text: qsTr("Edit")
onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("Read receipts")
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
text: qsTr("Forward")
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
}
}
Platform.MenuItem {
text: qsTr("Mark as read")
}
Platform.MenuItem {
text: qsTr("View raw message")
onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
enabled: visible
text: qsTr("View decrypted raw message")
onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender
text: qsTr("Remove message")
onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("Save as")
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("Open in external program")
onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventId
enabled: visible
text: qsTr("Copy link to event")
onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
}
}
Component { Component {
id: deviceVerificationDialog id: deviceVerificationDialog
@ -233,16 +96,6 @@ Page {
} }
} }
Connections {
target: TimelineManager.timeline
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
}
}
Connections { Connections {
target: CallManager target: CallManager
onNewInviteState: { onNewInviteState: {

View file

@ -31,7 +31,7 @@ ImageButton {
} }
onClicked: { onClicked: {
if (model.state == MtxEvent.Read) if (model.state == MtxEvent.Read)
TimelineManager.timeline.readReceiptsAction(model.id); room.readReceiptsAction(model.id);
} }
image: { image: {

View file

@ -18,8 +18,10 @@ import im.nheko.EmojiModel 1.0
Item { Item {
id: timelineView id: timelineView
property var room: null
Label { Label {
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync visible: !room && !TimelineManager.isInitialSync
anchors.centerIn: parent anchors.centerIn: parent
text: qsTr("No room open") text: qsTr("No room open")
font.pointSize: 24 font.pointSize: 24
@ -38,7 +40,7 @@ Item {
ColumnLayout { ColumnLayout {
id: timelineLayout id: timelineLayout
visible: TimelineManager.timeline != null visible: room != null
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -69,11 +71,11 @@ Item {
currentIndex: 0 currentIndex: 0
Connections { Connections {
function onActiveTimelineChanged() { function onRoomChanged() {
stackLayout.currentIndex = 0; stackLayout.currentIndex = 0;
} }
target: TimelineManager target: timelineView
} }
MessageView { MessageView {
@ -125,7 +127,17 @@ Item {
NhekoDropArea { NhekoDropArea {
anchors.fill: parent anchors.fill: parent
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" roomid: room ? room.roomId() : ""
}
Connections {
target: room
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
}
} }
} }

View file

@ -11,8 +11,6 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: topBar id: topBar
property var room: TimelineManager.timeline
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2 implicitHeight: topLayout.height + Nheko.paddingMedium * 2
z: 3 z: 3
@ -20,7 +18,7 @@ Rectangle {
TapHandler { TapHandler {
onSingleTapped: { onSingleTapped: {
TimelineManager.timeline.openRoomSettings(); room.openRoomSettings();
eventPoint.accepted = true; eventPoint.accepted = true;
} }
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
@ -61,7 +59,7 @@ Rectangle {
height: Nheko.avatarSize height: Nheko.avatarSize
url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
displayName: room ? room.roomName : qsTr("No room selected") displayName: room ? room.roomName : qsTr("No room selected")
onClicked: TimelineManager.timeline.openRoomSettings() onClicked: room.openRoomSettings()
} }
Label { Label {
@ -101,24 +99,24 @@ Rectangle {
id: roomOptionsMenu id: roomOptionsMenu
Platform.MenuItem { Platform.MenuItem {
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false visible: room ? room.permissions.canInvite() : false
text: qsTr("Invite users") text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsersDialog() onTriggered: TimelineManager.openInviteUsersDialog()
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Members") text: qsTr("Members")
onTriggered: TimelineManager.openMemberListDialog() onTriggered: TimelineManager.openMemberListDialog(room.roomId())
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Leave room") text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog() onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId())
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Settings") text: qsTr("Settings")
onTriggered: TimelineManager.timeline.openRoomSettings() onTriggered: room.openRoomSettings()
} }
} }

View file

@ -8,8 +8,6 @@ import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
property var room: TimelineManager.timeline
implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -34,7 +34,7 @@ Item {
} }
TapHandler { TapHandler {
onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id) onSingleTapped: room.saveMedia(model.data.id)
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }

View file

@ -207,7 +207,7 @@ Item {
roleValue: MtxEvent.PowerLevels roleValue: MtxEvent.PowerLevels
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) text: room.formatPowerLevelEvent(model.data.id)
} }
} }
@ -216,7 +216,7 @@ Item {
roleValue: MtxEvent.RoomJoinRules roleValue: MtxEvent.RoomJoinRules
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) text: room.formatJoinRuleEvent(model.data.id)
} }
} }
@ -225,7 +225,7 @@ Item {
roleValue: MtxEvent.RoomHistoryVisibility roleValue: MtxEvent.RoomHistoryVisibility
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) text: room.formatHistoryVisibilityEvent(model.data.id)
} }
} }
@ -234,7 +234,7 @@ Item {
roleValue: MtxEvent.RoomGuestAccess roleValue: MtxEvent.RoomGuestAccess
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) text: room.formatGuestAccessEvent(model.data.id)
} }
} }
@ -243,7 +243,7 @@ Item {
roleValue: MtxEvent.Member roleValue: MtxEvent.Member
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatMemberEvent(model.data.id) text: room.formatMemberEvent(model.data.id)
} }
} }

View file

@ -121,7 +121,7 @@ Rectangle {
onClicked: { onClicked: {
switch (button.state) { switch (button.state) {
case "": case "":
TimelineManager.timeline.cacheMedia(model.data.id); room.cacheMedia(model.data.id);
break; break;
case "stopped": case "stopped":
media.play(); media.play();
@ -174,7 +174,7 @@ Rectangle {
} }
Connections { Connections {
target: TimelineManager.timeline target: room
onMediaCached: { onMediaCached: {
if (mxcUrl == model.data.url) { if (mxcUrl == model.data.url) {
media.source = cacheUrl; media.source = cacheUrl;

View file

@ -17,7 +17,7 @@ ImageButton {
image: ":/icons/icons/ui/smile.png" image: ":/icons/icons/ui/smile.png"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
TimelineManager.queueReactionMessage(event_id, emoji); room.input.reaction(event_id, emoji);
TimelineManager.focusMessageInput(); TimelineManager.focusMessageInput();
}) })
} }

View file

@ -35,7 +35,7 @@ Rectangle {
height: Nheko.avatarSize height: Nheko.avatarSize
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty displayName: CallManager.callParty
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
} }
Label { Label {

View file

@ -42,7 +42,7 @@ Rectangle {
height: Nheko.avatarSize height: Nheko.avatarSize
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty displayName: CallManager.callParty
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
} }
Label { Label {

View file

@ -45,7 +45,7 @@ Popup {
Layout.leftMargin: 8 Layout.leftMargin: 8
Label { Label {
text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName) text: qsTr("Place a call to %1?").arg(room.roomName)
color: Nheko.colors.windowText color: Nheko.colors.windowText
} }
@ -77,9 +77,9 @@ Popup {
Layout.rightMargin: cameraCombo.visible ? 16 : 64 Layout.rightMargin: cameraCombo.visible ? 16 : 64
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/") url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: TimelineManager.timeline.roomName displayName: room.roomName
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
} }
Button { Button {
@ -88,7 +88,7 @@ Popup {
onClicked: { onClicked: {
if (buttonLayout.validateMic()) { if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText; Settings.microphone = micCombo.currentText;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE); CallManager.sendInvite(room.roomId(), CallType.VOICE);
close(); close();
} }
} }
@ -102,7 +102,7 @@ Popup {
if (buttonLayout.validateMic()) { if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText; Settings.microphone = micCombo.currentText;
Settings.camera = cameraCombo.currentText; Settings.camera = cameraCombo.currentText;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO); CallManager.sendInvite(room.roomId(), CallType.VIDEO);
close(); close();
} }
} }

View file

@ -27,7 +27,7 @@ Popup {
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName) text: qsTr("Share desktop with %1?").arg(room.roomName)
color: Nheko.colors.windowText color: Nheko.colors.windowText
} }
@ -136,7 +136,7 @@ Popup {
Settings.screenSharePiP = pipCheckBox.checked; Settings.screenSharePiP = pipCheckBox.checked;
Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked; Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
Settings.screenShareHideCursor = hideCursorCheckBox.checked; Settings.screenShareHideCursor = hideCursorCheckBox.checked;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex); CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex);
close(); close();
} }
} }

View file

@ -215,8 +215,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
this->current_room_ = room_id; this->current_room_ = room_id;
}); });
connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
connect(
room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView);
connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
joinRoom(room_id); joinRoom(room_id);
@ -982,7 +980,7 @@ ChatPage::leaveRoom(const QString &room_id)
void void
ChatPage::changeRoom(const QString &room_id) ChatPage::changeRoom(const QString &room_id)
{ {
view_manager_->setHistoryView(room_id); view_manager_->rooms()->setCurrentRoom(room_id);
room_list_->highlightSelectedRoom(room_id); room_list_->highlightSelectedRoom(room_id);
} }
@ -1397,7 +1395,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
if (sigil1 == "u") { if (sigil1 == "u") {
if (action.isEmpty()) { if (action.isEmpty()) {
view_manager_->activeTimeline()->openUserProfile(mxid1); if (auto t = view_manager_->rooms()->currentRoom())
t->openUserProfile(mxid1);
} else if (action == "chat") { } else if (action == "chat") {
this->startChat(mxid1); this->startChat(mxid1);
} }

View file

@ -508,8 +508,7 @@ InputBar::command(QString command, QString args)
} else if (command == "react") { } else if (command == "react") {
auto eventId = room->reply(); auto eventId = room->reply();
if (!eventId.isEmpty()) if (!eventId.isEmpty())
ChatPage::instance()->timelineManager()->queueReactionMessage( reaction(eventId, args.trimmed());
eventId, args.trimmed());
} else if (command == "join") { } else if (command == "join") {
ChatPage::instance()->joinRoom(args); ChatPage::instance()->joinRoom(args);
} else if (command == "part" || command == "leave") { } else if (command == "part" || command == "leave") {
@ -715,3 +714,35 @@ InputBar::stopTyping()
} }
}); });
} }
void
InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
{
auto reactions = room->reactions(reactedEvent.toStdString());
QString selfReactedEvent;
for (const auto &reaction : reactions) {
if (reactionKey == reaction.key_) {
selfReactedEvent = reaction.selfReactedEvent_;
break;
}
}
if (selfReactedEvent.startsWith("m"))
return;
// If selfReactedEvent is empty, that means we haven't previously reacted
if (selfReactedEvent.isEmpty()) {
mtx::events::msg::Reaction reaction;
mtx::common::Relation rel;
rel.rel_type = mtx::common::RelationType::Annotation;
rel.event_id = reactedEvent.toStdString();
rel.key = reactionKey.toStdString();
reaction.relations.relations.push_back(rel);
room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
// Otherwise, we have previously reacted and the reaction should be redacted
} else {
room->redactEvent(selfReactedEvent);
}
}

View file

@ -56,6 +56,7 @@ public slots:
void message(QString body, void message(QString body,
MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
bool rainbowify = false); bool rainbowify = false);
void reaction(const QString &reactedEvent, const QString &reactionKey);
private slots: private slots:
void startTyping(); void startTyping();

View file

@ -341,6 +341,8 @@ RoomlistModel::clear()
models.clear(); models.clear();
invites.clear(); invites.clear();
roomids.clear(); roomids.clear();
currentRoom_ = nullptr;
emit currentRoomChanged();
endResetModel(); endResetModel();
} }
@ -390,6 +392,17 @@ RoomlistModel::leave(QString roomid)
} }
} }
void
RoomlistModel::setCurrentRoom(QString roomid)
{
nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
if (models.contains(roomid)) {
currentRoom_ = models.value(roomid);
emit currentRoomChanged();
nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
}
}
namespace { namespace {
enum NotificationImportance : short enum NotificationImportance : short
{ {
@ -463,6 +476,11 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
invalidate(); invalidate();
}); });
connect(roomlistmodel,
&RoomlistModel::currentRoomChanged,
this,
&FilteredRoomlistModel::currentRoomChanged);
sort(0); sort(0);
} }

View file

@ -14,12 +14,14 @@
#include <mtx/responses/sync.hpp> #include <mtx/responses/sync.hpp>
class TimelineModel; #include "TimelineModel.h"
class TimelineViewManager; class TimelineViewManager;
class RoomlistModel : public QAbstractListModel class RoomlistModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
public: public:
enum Roles enum Roles
{ {
@ -69,12 +71,15 @@ public slots:
void acceptInvite(QString roomid); void acceptInvite(QString roomid);
void declineInvite(QString roomid); void declineInvite(QString roomid);
void leave(QString roomid); void leave(QString roomid);
TimelineModel *currentRoom() const { return currentRoom_.get(); }
void setCurrentRoom(QString roomid);
private slots: private slots:
void updateReadStatus(const std::map<QString, bool> roomReadStatus_); void updateReadStatus(const std::map<QString, bool> roomReadStatus_);
signals: signals:
void totalUnreadMessageCountUpdated(int unreadMessages); void totalUnreadMessageCountUpdated(int unreadMessages);
void currentRoomChanged();
private: private:
void addRoom(const QString &room_id, bool suppressInsertNotification = false); void addRoom(const QString &room_id, bool suppressInsertNotification = false);
@ -85,12 +90,15 @@ private:
QHash<QString, QSharedPointer<TimelineModel>> models; QHash<QString, QSharedPointer<TimelineModel>> models;
std::map<QString, bool> roomReadStatus; std::map<QString, bool> roomReadStatus;
QSharedPointer<TimelineModel> currentRoom_;
friend class FilteredRoomlistModel; friend class FilteredRoomlistModel;
}; };
class FilteredRoomlistModel : public QSortFilterProxyModel class FilteredRoomlistModel : public QSortFilterProxyModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
public: public:
FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
@ -107,6 +115,12 @@ public slots:
QStringList tags(); QStringList tags();
void toggleTag(QString roomid, QString tag, bool on); void toggleTag(QString roomid, QString tag, bool on);
TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
signals:
void currentRoomChanged();
private: private:
short int calculateImportance(const QModelIndex &idx) const; short int calculateImportance(const QModelIndex &idx) const;
RoomlistModel *roomlistmodel; RoomlistModel *roomlistmodel;

View file

@ -133,7 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
, colorImgProvider(new ColorImageProvider()) , colorImgProvider(new ColorImageProvider())
, blurhashProvider(new BlurhashProvider()) , blurhashProvider(new BlurhashProvider())
, callManager_(callManager) , callManager_(callManager)
, rooms(new RoomlistModel(this)) , rooms_(new RoomlistModel(this))
{ {
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
@ -193,7 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
}); });
qmlRegisterSingletonType<RoomlistModel>( qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new FilteredRoomlistModel(self->rooms); return new FilteredRoomlistModel(self->rooms_);
}); });
qmlRegisterSingletonType<UserSettings>( qmlRegisterSingletonType<UserSettings>(
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
@ -320,9 +320,9 @@ TimelineViewManager::setVideoCallItem()
} }
void void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms_) TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
{ {
this->rooms->sync(rooms_); this->rooms_->sync(rooms_res);
if (isInitialSync_) { if (isInitialSync_) {
this->isInitialSync_ = false; this->isInitialSync_ = false;
@ -330,37 +330,17 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
} }
} }
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
if (auto room = rooms->getRoomById(room_id)) {
timeline_ = room.get();
emit activeTimelineChanged(timeline_);
container->setFocus();
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
}
void
TimelineViewManager::highlightRoom(const QString &room_id)
{
ChatPage::instance()->highlightRoom(room_id);
}
void void
TimelineViewManager::showEvent(const QString &room_id, const QString &event_id) TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
{ {
if (auto room = rooms->getRoomById(room_id)) { if (auto room = rooms_->getRoomById(room_id)) {
if (timeline_ != room) { if (rooms_->currentRoom() != room) {
timeline_ = room.get(); rooms_->setCurrentRoom(room_id);
emit activeTimelineChanged(timeline_);
container->setFocus(); container->setFocus();
nhlog::ui()->info("Activated room {}", room_id.toStdString()); nhlog::ui()->info("Activated room {}", room_id.toStdString());
} }
timeline_->showEvent(event_id); room->showEvent(event_id);
} }
} }
@ -395,17 +375,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
auto imgDialog = new dialogs::ImageOverlay(pixmap); auto imgDialog = new dialogs::ImageOverlay(pixmap);
imgDialog->showFullScreen(); imgDialog->showFullScreen();
connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() {
// hide the overlay while presenting the save dialog for better
// cross platform support.
imgDialog->hide();
if (!timeline_->saveMedia(eventId)) { auto room = rooms_->currentRoom();
imgDialog->show(); connect(
} else { imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() {
imgDialog->close(); // hide the overlay while presenting the save dialog for better
} // cross platform support.
}); imgDialog->hide();
if (!room->saveMedia(eventId)) {
imgDialog->show();
} else {
imgDialog->close();
}
});
} }
void void
@ -415,14 +398,14 @@ TimelineViewManager::openInviteUsersDialog()
[this](const QStringList &invitees) { emit inviteUsers(invitees); }); [this](const QStringList &invitees) { emit inviteUsers(invitees); });
} }
void void
TimelineViewManager::openMemberListDialog() const TimelineViewManager::openMemberListDialog(QString roomid) const
{ {
MainWindow::instance()->openMemberListDialog(timeline_->roomId()); MainWindow::instance()->openMemberListDialog(roomid);
} }
void void
TimelineViewManager::openLeaveRoomDialog() const TimelineViewManager::openLeaveRoomDialog(QString roomid) const
{ {
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId()); MainWindow::instance()->openLeaveRoomDialog(roomid);
} }
void void
@ -439,7 +422,7 @@ TimelineViewManager::verifyUser(QString userid)
room_members.end(), room_members.end(),
(userid).toStdString()) != room_members.end()) { (userid).toStdString()) != room_members.end()) {
if (auto model = if (auto model =
rooms->getRoomById(QString::fromStdString(room_id))) { rooms_->getRoomById(QString::fromStdString(room_id))) {
auto flow = auto flow =
DeviceVerificationFlow::InitiateUserVerification( DeviceVerificationFlow::InitiateUserVerification(
this, model.data(), userid); this, model.data(), userid);
@ -485,7 +468,7 @@ void
TimelineViewManager::updateReadReceipts(const QString &room_id, TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids) const std::vector<QString> &event_ids)
{ {
if (auto room = rooms->getRoomById(room_id)) { if (auto room = rooms_->getRoomById(room_id)) {
room->markEventsAsRead(event_ids); room->markEventsAsRead(event_ids);
} }
} }
@ -493,7 +476,7 @@ TimelineViewManager::updateReadReceipts(const QString &room_id,
void void
TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
{ {
if (auto room = rooms->getRoomById(QString::fromStdString(room_id))) { if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
room->receivedSessionKey(session_id); room->receivedSessionKey(session_id);
} }
} }
@ -501,7 +484,7 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s
void void
TimelineViewManager::initializeRoomlist() TimelineViewManager::initializeRoomlist()
{ {
rooms->initializeRooms(); rooms_->initializeRooms();
} }
void void
@ -509,51 +492,17 @@ TimelineViewManager::queueReply(const QString &roomid,
const QString &repliedToEvent, const QString &repliedToEvent,
const QString &replyBody) const QString &replyBody)
{ {
if (auto room = rooms->getRoomById(roomid)) { if (auto room = rooms_->getRoomById(roomid)) {
room->setReply(repliedToEvent); room->setReply(repliedToEvent);
room->input()->message(replyBody); room->input()->message(replyBody);
} }
} }
void
TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
{
if (!timeline_)
return;
auto reactions = timeline_->reactions(reactedEvent.toStdString());
QString selfReactedEvent;
for (const auto &reaction : reactions) {
if (reactionKey == reaction.key_) {
selfReactedEvent = reaction.selfReactedEvent_;
break;
}
}
if (selfReactedEvent.startsWith("m"))
return;
// If selfReactedEvent is empty, that means we haven't previously reacted
if (selfReactedEvent.isEmpty()) {
mtx::events::msg::Reaction reaction;
mtx::common::Relation rel;
rel.rel_type = mtx::common::RelationType::Annotation;
rel.event_id = reactedEvent.toStdString();
rel.key = reactionKey.toStdString();
reaction.relations.relations.push_back(rel);
timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
// Otherwise, we have previously reacted and the reaction should be redacted
} else {
timeline_->redactEvent(selfReactedEvent);
}
}
void void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallInvite &callInvite) const mtx::events::msg::CallInvite &callInvite)
{ {
if (auto room = rooms->getRoomById(roomid)) if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
} }
@ -561,7 +510,7 @@ void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallCandidates &callCandidates) const mtx::events::msg::CallCandidates &callCandidates)
{ {
if (auto room = rooms->getRoomById(roomid)) if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates); room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
} }
@ -569,7 +518,7 @@ void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallAnswer &callAnswer) const mtx::events::msg::CallAnswer &callAnswer)
{ {
if (auto room = rooms->getRoomById(roomid)) if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
} }
@ -577,7 +526,7 @@ void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallHangUp &callHangUp) const mtx::events::msg::CallHangUp &callHangUp)
{ {
if (auto room = rooms->getRoomById(roomid)) if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
} }
@ -629,7 +578,7 @@ void
TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
QString roomId) QString roomId)
{ {
auto room = rooms->getRoomById(roomId); auto room = rooms_->getRoomById(roomId);
auto content = mtx::accessors::url(*e); auto content = mtx::accessors::url(*e);
std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e);
@ -672,7 +621,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
ev.content.url = url; ev.content.url = url;
} }
if (auto room = rooms->getRoomById(roomId)) { if (auto room = rooms_->getRoomById(roomId)) {
removeReplyFallback(ev); removeReplyFallback(ev);
ev.content.relations.relations ev.content.relations.relations
.clear(); .clear();

View file

@ -35,8 +35,6 @@ class TimelineViewManager : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(
TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
Q_PROPERTY( Q_PROPERTY(
bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
Q_PROPERTY( Q_PROPERTY(
@ -53,14 +51,8 @@ public:
MxcImageProvider *imageProvider() { return imgProvider; } MxcImageProvider *imageProvider() { return imgProvider; }
CallManager *callManager() { return callManager_; } CallManager *callManager() { return callManager_; }
void clearAll() void clearAll() { rooms_->clear(); }
{
timeline_ = nullptr;
emit activeTimelineChanged(nullptr);
rooms->clear();
}
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
bool isNarrowView() const { return isNarrowView_; } bool isNarrowView() const { return isNarrowView_; }
bool isWindowFocused() const { return isWindowFocused_; } bool isWindowFocused() const { return isWindowFocused_; }
@ -74,8 +66,8 @@ public:
Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void focusMessageInput();
Q_INVOKABLE void openInviteUsersDialog(); Q_INVOKABLE void openInviteUsersDialog();
Q_INVOKABLE void openMemberListDialog() const; Q_INVOKABLE void openMemberListDialog(QString roomid) const;
Q_INVOKABLE void openLeaveRoomDialog() const; Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
void verifyUser(QString userid); void verifyUser(QString userid);
@ -107,20 +99,13 @@ public slots:
emit focusChanged(); emit focusChanged();
} }
void setHistoryView(const QString &room_id);
void highlightRoom(const QString &room_id);
void showEvent(const QString &room_id, const QString &event_id); void showEvent(const QString &room_id, const QString &event_id);
void focusTimeline(); void focusTimeline();
TimelineModel *getHistoryView(const QString &room_id)
{
return rooms->getRoomById(room_id).get();
}
void updateColorPalette(); void updateColorPalette();
void queueReply(const QString &roomid, void queueReply(const QString &roomid,
const QString &repliedToEvent, const QString &repliedToEvent,
const QString &replyBody); const QString &replyBody);
void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey);
void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
@ -147,6 +132,8 @@ public slots:
QObject *completerFor(QString completerName, QString roomId = ""); QObject *completerFor(QString completerName, QString roomId = "");
void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
RoomlistModel *rooms() { return rooms_; }
private slots: private slots:
void openImageOverlayInternal(QString eventId, QImage img); void openImageOverlayInternal(QString eventId, QImage img);
@ -162,14 +149,13 @@ private:
ColorImageProvider *colorImgProvider; ColorImageProvider *colorImgProvider;
BlurhashProvider *blurhashProvider; BlurhashProvider *blurhashProvider;
TimelineModel *timeline_ = nullptr;
CallManager *callManager_ = nullptr; CallManager *callManager_ = nullptr;
bool isInitialSync_ = true; bool isInitialSync_ = true;
bool isNarrowView_ = false; bool isNarrowView_ = false;
bool isWindowFocused_ = false; bool isWindowFocused_ = false;
RoomlistModel *rooms = nullptr; RoomlistModel *rooms_ = nullptr;
QHash<QString, QColor> userColors; QHash<QString, QColor> userColors;

View file

@ -35,7 +35,7 @@ void
NhekoDropArea::dropEvent(QDropEvent *event) NhekoDropArea::dropEvent(QDropEvent *event)
{ {
if (event) { if (event) {
auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_); auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_);
if (model) { if (model) {
model->input()->insertMimeData(event->mimeData()); model->input()->insertMimeData(event->mimeData());
} }