Port RoomManager to C++
This also makes it possible to handle the Matrix URI
This commit is contained in:
@@ -5,6 +5,7 @@ import QtQuick 2.15
|
|||||||
import QtQuick.Controls 2.15 as QQC2
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
@@ -47,7 +48,7 @@ a{
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
onLinkActivated: applicationWindow().handleLink(link, currentRoom)
|
onLinkActivated: RoomManager.openResource(link)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ Loader {
|
|||||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 24
|
Layout.maximumWidth: Kirigami.Units.gridUnit * 24
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
onLinkActivated: {
|
onLinkActivated: RoomManager.openResource(link);
|
||||||
applicationWindow().handleLink(link, currentRoom)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +187,7 @@ Loader {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
onLinkActivated: {
|
onLinkActivated: RoomManager.openResource(link);
|
||||||
applicationWindow().handleLink(link, currentRoom)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,16 +116,15 @@ Kirigami.ScrollablePage {
|
|||||||
action: Kirigami.Action {
|
action: Kirigami.Action {
|
||||||
id: enterRoomAction
|
id: enterRoomAction
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
var roomItem = roomManager.enterRoom(currentRoom)
|
RoomManager.enterRoom(currentRoom);
|
||||||
roomListItem.KeyNavigation.right = roomItem
|
itemSelection.setCurrentIndex(sortFilterRoomListModel.mapToSource(
|
||||||
roomItem.focus = true;
|
sortFilterRoomListModel.index(index, 0)), ItemSelectionModel.SelectCurrent)
|
||||||
itemSelection.setCurrentIndex(sortFilterRoomListModel.mapToSource(sortFilterRoomListModel.index(index, 0)), ItemSelectionModel.SelectCurrent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bold: unreadCount > 0
|
bold: unreadCount > 0
|
||||||
label: name ?? ""
|
label: name ?? ""
|
||||||
subtitle: {
|
subtitle: {
|
||||||
let txt = (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
|
let txt = (lastEvent.length === 0 ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm, " ")
|
||||||
if (txt.length) {
|
if (txt.length) {
|
||||||
return txt
|
return txt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import NeoChat.Menu.Timeline 1.0
|
|||||||
Kirigami.ScrollablePage {
|
Kirigami.ScrollablePage {
|
||||||
id: page
|
id: page
|
||||||
|
|
||||||
required property var currentRoom
|
/// It's not readonly because of the seperate window view.
|
||||||
|
property var currentRoom: RoomManager.currentRoom
|
||||||
|
|
||||||
title: currentRoom.displayName
|
title: currentRoom.displayName
|
||||||
|
|
||||||
@@ -30,19 +31,17 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
onCurrentRoomChanged: ChatBoxHelper.clearEditReply()
|
onCurrentRoomChanged: ChatBoxHelper.clearEditReply()
|
||||||
|
|
||||||
|
|
||||||
ActionsHandler {
|
ActionsHandler {
|
||||||
id: actionsHandler
|
id: actionsHandler
|
||||||
room: page.currentRoom
|
room: page.currentRoom
|
||||||
connection: Controller.activeConnection
|
connection: Controller.activeConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Controller.activeConnection
|
target: Controller.activeConnection
|
||||||
function onJoinedRoom(room) {
|
function onJoinedRoom(room) {
|
||||||
if(room.id === invitation.id) {
|
if(room.id === invitation.id) {
|
||||||
roomManager.enterRoom(room);
|
RoomManager.enterRoom(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +80,7 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
page.currentRoom.forget()
|
page.currentRoom.forget()
|
||||||
roomManager.getBack();
|
RoomManager.getBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,6 +666,11 @@ Kirigami.ScrollablePage {
|
|||||||
FullScreenImage {}
|
FullScreenImage {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: userDetailDialog
|
||||||
|
|
||||||
|
UserDetailDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
header: TypingPane {
|
header: TypingPane {
|
||||||
id: typingPane
|
id: typingPane
|
||||||
@@ -746,6 +750,20 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function warning(title, message) {
|
||||||
|
page.header.contentItem.text = `${title}<br />${message}`;
|
||||||
|
page.header.contentItem.type = Kirigami.MessageType.Warning;
|
||||||
|
page.header.contentItem.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUserDetail(user) {
|
||||||
|
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
|
room: currentRoom,
|
||||||
|
user: user,
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
|
||||||
function goToLastMessage() {
|
function goToLastMessage() {
|
||||||
currentRoom.markAllMessagesAsRead()
|
currentRoom.markAllMessagesAsRead()
|
||||||
// scroll to the very end, i.e to messageListView.YEnd
|
// scroll to the very end, i.e to messageListView.YEnd
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import NeoChat.Dialog 1.0
|
|||||||
|
|
||||||
Kirigami.OverlayDrawer {
|
Kirigami.OverlayDrawer {
|
||||||
id: roomDrawer
|
id: roomDrawer
|
||||||
property var room
|
readonly property var room: RoomManager.currentRoom
|
||||||
|
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ Comment[sv]=Klient för protokollet Matrix
|
|||||||
Comment[uk]=Клієнт протоколу Matrix
|
Comment[uk]=Клієнт протоколу Matrix
|
||||||
Comment[x-test]=xxClient for the Matrix protocolxx
|
Comment[x-test]=xxClient for the Matrix protocolxx
|
||||||
Comment[zh_CN]=为 Matrix 协议打造的客户端
|
Comment[zh_CN]=为 Matrix 协议打造的客户端
|
||||||
Exec=neochat
|
MimeType=x-scheme-handler/matrix;
|
||||||
|
Exec=neochat %u
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Icon=org.kde.neochat
|
Icon=org.kde.neochat
|
||||||
Type=Application
|
Type=Application
|
||||||
|
|||||||
232
qml/main.qml
232
qml/main.qml
@@ -28,6 +28,8 @@ Kirigami.ApplicationWindow {
|
|||||||
|
|
||||||
pageStack.initialPage: LoadingPage {}
|
pageStack.initialPage: LoadingPage {}
|
||||||
|
|
||||||
|
property bool roomListLoaded: false
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.quitAction
|
target: root.quitAction
|
||||||
function onTriggered() {
|
function onTriggered() {
|
||||||
@@ -50,78 +52,85 @@ Kirigami.ApplicationWindow {
|
|||||||
onXChanged: saveWindowGeometryTimer.restart()
|
onXChanged: saveWindowGeometryTimer.restart()
|
||||||
onYChanged: saveWindowGeometryTimer.restart()
|
onYChanged: saveWindowGeometryTimer.restart()
|
||||||
|
|
||||||
/**
|
|
||||||
* Manage opening and close rooms
|
|
||||||
* TODO this should probably be moved to C++
|
|
||||||
*/
|
|
||||||
QtObject {
|
|
||||||
id: roomManager
|
|
||||||
|
|
||||||
property var currentRoom: null
|
/// Setup keyboard navigation to the room page.
|
||||||
property alias pageStack: root.pageStack
|
function connectRoomToSignal(item) {
|
||||||
property var roomList: null
|
if (!roomListLoaded) {
|
||||||
property Item roomItem: null
|
console.log("Should not happen: no room list page but room page");
|
||||||
|
|
||||||
readonly property bool hasOpenRoom: currentRoom !== null
|
|
||||||
|
|
||||||
signal leaveRoom(string room);
|
|
||||||
signal openRoom(string room);
|
|
||||||
|
|
||||||
function roomByAliasOrId(aliasOrId) {
|
|
||||||
return Controller.activeConnection.room(aliasOrId)
|
|
||||||
}
|
}
|
||||||
|
const roomList = pageStack.get(0);
|
||||||
|
item.switchRoomUp.connect(function() {
|
||||||
|
roomList.goToNextRoom();
|
||||||
|
});
|
||||||
|
|
||||||
function openRoomAndEvent(room, event) {
|
item.switchRoomDown.connect(function() {
|
||||||
enterRoom(room)
|
roomList.goToPreviousRoom();
|
||||||
roomItem.goToEvent(event)
|
});
|
||||||
}
|
item.forceActiveFocus();
|
||||||
|
item.KeyNavigation.left = pageStack.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
function loadInitialRoom() {
|
Connections {
|
||||||
if (Config.openRoom) {
|
target: RoomManager
|
||||||
const room = Controller.activeConnection.room(Config.openRoom);
|
|
||||||
currentRoom = room;
|
|
||||||
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
|
|
||||||
connectRoomToSignal(roomItem);
|
|
||||||
} else {
|
|
||||||
// TODO create welcome page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enterRoom(room) {
|
function onPushRoom(room, event) {
|
||||||
if (currentRoom != null) {
|
const roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml");
|
||||||
roomItem.currentRoom = room;
|
|
||||||
pageStack.currentIndex = pageStack.depth - 1;
|
|
||||||
} else {
|
|
||||||
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
|
|
||||||
}
|
|
||||||
currentRoom = room;
|
|
||||||
Config.openRoom = room.id;
|
|
||||||
Config.save();
|
|
||||||
connectRoomToSignal(roomItem);
|
connectRoomToSignal(roomItem);
|
||||||
return roomItem;
|
if (event.length > 0) {
|
||||||
|
roomItem.goToEvent(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBack() {
|
function onReplaceRoom(room, event) {
|
||||||
pageStack.replace("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': currentRoom, });
|
const roomItem = pageStack.get(pageStack.depth - 1);
|
||||||
|
pageStack.currentIndex = pageStack.depth - 1;
|
||||||
|
connectRoomToSignal(roomItem);
|
||||||
|
if (event.length > 0) {
|
||||||
|
roomItem.goToEvent(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openWindow(room) {
|
function goToEvent(event) {
|
||||||
|
if (event.length > 0) {
|
||||||
|
roomItem.goToEvent(event);
|
||||||
|
}
|
||||||
|
roomItem.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPushWelcomePage() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenRoomInNewWindow(room) {
|
||||||
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room});
|
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room});
|
||||||
secondayWindow.width = root.width - roomList.width;
|
secondayWindow.width = root.width - roomList.width;
|
||||||
secondayWindow.show();
|
secondayWindow.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectRoomToSignal(item) {
|
function onShowUserDetail(user) {
|
||||||
if (!roomList) {
|
const roomItem = pageStack.get(pageStack.depth - 1);
|
||||||
console.log("Should not happen: no room list page but room page");
|
roomItem.showUserDetail(user);
|
||||||
}
|
}
|
||||||
item.switchRoomUp.connect(function() {
|
|
||||||
roomList.goToNextRoom();
|
|
||||||
});
|
|
||||||
|
|
||||||
item.switchRoomDown.connect(function() {
|
function onAskDirectChatConfirmation(user) {
|
||||||
roomList.goToPreviousRoom();
|
askDirectChatConfirmationComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
});
|
user: user,
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWarning(title, message) {
|
||||||
|
if (RoomManager.currentRoom) {
|
||||||
|
const roomItem = pageStack.get(pageStack.depth - 1);
|
||||||
|
roomItem.warning(title, message);
|
||||||
|
} else {
|
||||||
|
showPassiveNotification(i18n("Warning: %1", message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenLink(url) {
|
||||||
|
openLinkConfirmationComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
|
url: url,
|
||||||
|
}).open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +155,7 @@ Kirigami.ApplicationWindow {
|
|||||||
modal: !root.wideScreen || !enabled
|
modal: !root.wideScreen || !enabled
|
||||||
onEnabledChanged: drawerOpen = enabled && !modal
|
onEnabledChanged: drawerOpen = enabled && !modal
|
||||||
onModalChanged: drawerOpen = !modal
|
onModalChanged: drawerOpen = !modal
|
||||||
enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
|
enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||||
room: roomManager.currentRoom
|
|
||||||
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3
|
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +246,7 @@ Kirigami.ApplicationWindow {
|
|||||||
Connections {
|
Connections {
|
||||||
target: LoginHelper
|
target: LoginHelper
|
||||||
function onInitialSyncFinished() {
|
function onInitialSyncFinished() {
|
||||||
roomManager.roomList = pageStack.replace(roomListComponent);
|
RoomManager.roomList = pageStack.replace(roomListComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,32 +254,38 @@ Kirigami.ApplicationWindow {
|
|||||||
target: Controller
|
target: Controller
|
||||||
|
|
||||||
function onInitiated() {
|
function onInitiated() {
|
||||||
if (roomManager.hasOpenRoom) {
|
if (RoomManager.hasOpenRoom) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Controller.accountCount === 0) {
|
if (Controller.accountCount === 0) {
|
||||||
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
|
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
|
||||||
} else {
|
} else {
|
||||||
roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection});
|
pageStack.replace(roomListComponent, {
|
||||||
roomManager.loadInitialRoom();
|
activeConnection: Controller.activeConnection
|
||||||
|
});
|
||||||
|
roomListLoaded = true;
|
||||||
|
RoomManager.loadInitialRoom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBusyChanged() {
|
function onBusyChanged() {
|
||||||
if(!Controller.busy && roomManager.roomList === null) {
|
if(!Controller.busy && roomListLoaded === false) {
|
||||||
roomManager.roomList = pageStack.replace(roomListComponent);
|
pageStack.replace(roomListComponent);
|
||||||
|
roomListLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConnectionDropped() {
|
function onConnectionDropped() {
|
||||||
if (Controller.accountCount === 0) {
|
if (Controller.accountCount === 0) {
|
||||||
|
RoomManager.reset();
|
||||||
pageStack.clear();
|
pageStack.clear();
|
||||||
|
roomListLoaded = false;
|
||||||
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
|
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGlobalErrorOccured(error, detail) {
|
function onGlobalErrorOccured(error, detail) {
|
||||||
showPassiveNotification(error + ": " + detail)
|
showPassiveNotification(i18nc("%1: %2", error, detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onShowWindow() {
|
function onShowWindow() {
|
||||||
@@ -279,7 +293,7 @@ Kirigami.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onOpenRoom(room) {
|
function onOpenRoom(room) {
|
||||||
roomManager.enterRoom(room)
|
RoomManager.enterRoom(room)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserConsentRequired(url) {
|
function onUserConsentRequired(url) {
|
||||||
@@ -288,14 +302,14 @@ Kirigami.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onRoomJoined(roomName) {
|
function onRoomJoined(roomName) {
|
||||||
roomManager.enterRoom(Controller.activeConnection.room(roomName))
|
RoomManager.enterRoom(Controller.activeConnection.room(roomName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Controller.activeConnection
|
target: Controller.activeConnection
|
||||||
onDirectChatAvailable: {
|
onDirectChatAvailable: {
|
||||||
roomManager.enterRoom(Controller.activeConnection.room(directChat.id));
|
RoomManager.enterRoom(Controller.activeConnection.room(directChat.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,36 +346,68 @@ Kirigami.ApplicationWindow {
|
|||||||
RoomWindow {}
|
RoomWindow {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLink(link, currentRoom) {
|
Component {
|
||||||
if (link.startsWith("https://matrix.to/")) {
|
id: userDialog
|
||||||
var content = link.replace("https://matrix.to/#/", "").replace(/\?.*/, "")
|
UserDetailDialog {}
|
||||||
if(content.match("^[#!]")) {
|
}
|
||||||
if(content.includes("/")) {
|
|
||||||
var result = content.match("([!#].*:.*)/(\\$.*)")
|
Component {
|
||||||
if(!result) {
|
id: askDirectChatConfirmationComponent
|
||||||
return
|
|
||||||
}
|
Kirigami.OverlaySheet {
|
||||||
if(result[1] == currentRoom.id) {
|
id: askDirectChatConfirmation
|
||||||
roomManager.roomItem.goToEvent(result[2])
|
required property var user;
|
||||||
} else {
|
|
||||||
roomManager.openRoomAndEvent(roomManager.roomByAliasOrId(result[1]), result[2])
|
parent: QQC2.ApplicationWindow.overlay
|
||||||
}
|
header: Kirigami.Heading {
|
||||||
} else {
|
text: i18n("Start a chat")
|
||||||
roomManager.enterRoom(roomManager.roomByAliasOrId(content))
|
}
|
||||||
}
|
contentItem: QQC2.Label {
|
||||||
} else if(content.match("^@")) {
|
text: i18n("Do you want to start a chat with %1?", user.displayName)
|
||||||
let dialog = userDialog.createObject(root.overlay, {room: currentRoom, user: currentRoom.user(content)})
|
wrapMode: Text.WordWrap
|
||||||
dialog.open()
|
}
|
||||||
console.log(dialog.user)
|
footer: QQC2.DialogButtonBox {
|
||||||
|
standardButtons: QQC2.DialogButtonBox.Ok | QQC2.DialogButtonBox.Cancel
|
||||||
|
onAccepted: {
|
||||||
|
user.requestDirectChat();
|
||||||
|
askDirectChatConfirmation.close();
|
||||||
|
}
|
||||||
|
onRejected: askDirectChatConfirmation.close();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Qt.openUrlExternally(link)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: userDialog
|
id: openLinkConfirmationComponent
|
||||||
UserDetailDialog {
|
|
||||||
|
Kirigami.OverlaySheet {
|
||||||
|
id: openLinkConfirmation
|
||||||
|
required property var url;
|
||||||
|
|
||||||
|
header: Kirigami.Heading {
|
||||||
|
text: i18n("Confirm opening a link")
|
||||||
|
}
|
||||||
|
parent: QQC2.ApplicationWindow.overlay
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Do you want to open the link to %1?", `<a href='${url}'>${url}</a>`)
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
QQC2.CheckBox {
|
||||||
|
id: dontAskAgain
|
||||||
|
text: i18n("Don't ask again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footer: QQC2.DialogButtonBox {
|
||||||
|
standardButtons: QQC2.DialogButtonBox.Ok | QQC2.DialogButtonBox.Cancel
|
||||||
|
onAccepted: {
|
||||||
|
Config.confirmLinksAction = dontAskAgain.checked;
|
||||||
|
Config.save();
|
||||||
|
Qt.openUrlExternally(url);
|
||||||
|
openLinkConfirmation.close();
|
||||||
|
}
|
||||||
|
onRejected: openLinkConfirmation.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ add_executable(neochat
|
|||||||
messageeventmodel.cpp
|
messageeventmodel.cpp
|
||||||
messagefiltermodel.cpp
|
messagefiltermodel.cpp
|
||||||
roomlistmodel.cpp
|
roomlistmodel.cpp
|
||||||
|
roommanager.cpp
|
||||||
neochatroom.cpp
|
neochatroom.cpp
|
||||||
neochatuser.cpp
|
neochatuser.cpp
|
||||||
userlistmodel.cpp
|
userlistmodel.cpp
|
||||||
|
|||||||
21
src/main.cpp
21
src/main.cpp
@@ -10,6 +10,7 @@
|
|||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
#include <QQuickWindow>
|
#include <QQuickWindow>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
#include <KAboutData>
|
#include <KAboutData>
|
||||||
#ifdef HAVE_KDBUSADDONS
|
#ifdef HAVE_KDBUSADDONS
|
||||||
@@ -41,8 +42,9 @@
|
|||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
#include "notificationsmanager.h"
|
#include "notificationsmanager.h"
|
||||||
#include "publicroomlistmodel.h"
|
#include "publicroomlistmodel.h"
|
||||||
#include "room.h"
|
#include <room.h>
|
||||||
#include "roomlistmodel.h"
|
#include "roomlistmodel.h"
|
||||||
|
#include "roommanager.h"
|
||||||
#include "sortfilterroomlistmodel.h"
|
#include "sortfilterroomlistmodel.h"
|
||||||
#include "userdirectorylistmodel.h"
|
#include "userdirectorylistmodel.h"
|
||||||
#include "userlistmodel.h"
|
#include "userlistmodel.h"
|
||||||
@@ -99,6 +101,17 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
#ifdef HAVE_KDBUSADDONS
|
#ifdef HAVE_KDBUSADDONS
|
||||||
KDBusService service(KDBusService::Unique);
|
KDBusService service(KDBusService::Unique);
|
||||||
|
service.connect(&service,
|
||||||
|
&KDBusService::activateRequested,
|
||||||
|
roomManager,
|
||||||
|
[](const QStringList &arguments, const QString &workingDirectory) {
|
||||||
|
Q_UNUSED(workingDirectory);
|
||||||
|
auto args = arguments;
|
||||||
|
args.removeFirst();
|
||||||
|
for (const auto &arg : args) {
|
||||||
|
roomManager->openResource(arg);
|
||||||
|
}
|
||||||
|
});
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef NEOCHAT_FLATPAK
|
#ifdef NEOCHAT_FLATPAK
|
||||||
@@ -117,6 +130,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
|
||||||
|
qmlRegisterSingletonInstance<RoomManager>("org.kde.neochat", 1, 0, "RoomManager", roomManager);
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "ChatBoxHelper", &chatBoxHelper);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "ChatBoxHelper", &chatBoxHelper);
|
||||||
@@ -160,6 +174,7 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
|
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
|
||||||
|
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports appstream: url scheme"));
|
||||||
|
|
||||||
about.setupCommandLine(&parser);
|
about.setupCommandLine(&parser);
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
@@ -175,6 +190,10 @@ int main(int argc, char *argv[])
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parser.positionalArguments().length() > 0) {
|
||||||
|
roomManager->setUrlArgument(parser.positionalArguments()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef HAVE_KDBUSADDONS
|
#ifdef HAVE_KDBUSADDONS
|
||||||
QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) {
|
QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) {
|
||||||
const auto rootObjects = engine.rootObjects();
|
const auto rootObjects = engine.rootObjects();
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<label>Show notifications</label>
|
<label>Show notifications</label>
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="ConfirmLinksAction" type="bool">
|
||||||
|
<label>Confirm link before opening them</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
<entry name="MergeRoomList" type="bool">
|
<entry name="MergeRoomList" type="bool">
|
||||||
<label>Merge Room Lists</label>
|
<label>Merge Room Lists</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
|
|||||||
@@ -222,20 +222,23 @@ void RoomListModel::handleNotifications()
|
|||||||
}
|
}
|
||||||
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
||||||
auto room = m_connection->room(notification["room_id"].toString());
|
auto room = m_connection->room(notification["room_id"].toString());
|
||||||
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
if (room) {
|
||||||
|
// The room might have been deleted (for example rejected invitation).
|
||||||
|
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
||||||
|
|
||||||
QImage avatar_image;
|
QImage avatar_image;
|
||||||
if (!sender->avatarUrl(room).isEmpty()) {
|
if (!sender->avatarUrl(room).isEmpty()) {
|
||||||
avatar_image = sender->avatar(128, room);
|
avatar_image = sender->avatar(128, room);
|
||||||
} else {
|
} else {
|
||||||
avatar_image = room->avatar(128);
|
avatar_image = room->avatar(128);
|
||||||
|
}
|
||||||
|
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||||
|
room->displayName(),
|
||||||
|
sender->displayname(room),
|
||||||
|
notification["event"].toObject()["content"].toObject()["body"].toString(),
|
||||||
|
avatar_image,
|
||||||
|
notification["event"].toObject()["event_id"].toString());
|
||||||
}
|
}
|
||||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
|
||||||
room->displayName(),
|
|
||||||
sender->displayname(room),
|
|
||||||
notification["event"].toObject()["content"].toObject()["body"].toString(),
|
|
||||||
avatar_image,
|
|
||||||
notification["event"].toObject()["event_id"].toString());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
197
src/roommanager.cpp
Normal file
197
src/roommanager.cpp
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2021 Alexey Rusakov <TODO>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "roommanager.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include "neochatconfig.h"
|
||||||
|
#include "controller.h"
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <csapi/joining.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
RoomManager::RoomManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_currentRoom(nullptr)
|
||||||
|
, m_lastCurrentRoom(nullptr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
RoomManager::~RoomManager()
|
||||||
|
{}
|
||||||
|
|
||||||
|
NeoChatRoom *RoomManager::currentRoom() const
|
||||||
|
{
|
||||||
|
return m_currentRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::openResource(const QString &idOrUri, const QString &action)
|
||||||
|
{
|
||||||
|
Uri uri { idOrUri };
|
||||||
|
if (!uri.isValid()) {
|
||||||
|
Q_EMIT warning(i18n("Malformed or empty Matrix id"),
|
||||||
|
i18n("%1 is not a correct Matrix identifier", idOrUri));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto account = Controller::instance().activeConnection();
|
||||||
|
|
||||||
|
if (uri.type() != Uri::NonMatrix) {
|
||||||
|
if (!account) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!action.isEmpty()) {
|
||||||
|
uri.setAction(action);
|
||||||
|
}
|
||||||
|
// TODO we should allow the user to select a connection.
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = visitResource(account, uri);
|
||||||
|
if (result == Quotient::CouldNotResolve) {
|
||||||
|
Q_EMIT warning(i18n("Room not found"),
|
||||||
|
i18n("There's no room %1 in the room list. Check the spelling and the account.", idOrUri));
|
||||||
|
} else { // Invalid cases should have been eliminated earlier
|
||||||
|
Q_ASSERT(result == Quotient::UriResolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RoomManager::hasOpenRoom() const
|
||||||
|
{
|
||||||
|
return m_currentRoom != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::setUrlArgument(const QString &arg)
|
||||||
|
{
|
||||||
|
m_arg = arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::loadInitialRoom()
|
||||||
|
{
|
||||||
|
Q_ASSERT(Controller::instance().activeConnection());
|
||||||
|
|
||||||
|
if (!m_arg.isEmpty()) {
|
||||||
|
openResource(m_arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_currentRoom) {
|
||||||
|
// we opened a room with the arg parsing already
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NeoChatConfig::self()->openRoom().isEmpty()) {
|
||||||
|
// Here we can cast because the controller has been configured to
|
||||||
|
// return NeoChatRoom instead of simple Quotient::Room
|
||||||
|
const auto room = qobject_cast<NeoChatRoom *>(
|
||||||
|
Controller::instance().activeConnection()->room(NeoChatConfig::self()->openRoom()));
|
||||||
|
m_lastCurrentRoom = std::exchange(m_currentRoom, room);
|
||||||
|
Q_EMIT currentRoomChanged();
|
||||||
|
Q_EMIT pushRoom(room, QString());
|
||||||
|
} else {
|
||||||
|
Q_EMIT pushWelcomePage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::enterRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
if (!m_currentRoom) {
|
||||||
|
m_lastCurrentRoom = std::exchange(m_currentRoom, room);
|
||||||
|
Q_EMIT currentRoomChanged();
|
||||||
|
Q_EMIT pushRoom(room, QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastCurrentRoom = std::exchange(m_currentRoom, room);
|
||||||
|
Q_EMIT currentRoomChanged();
|
||||||
|
|
||||||
|
NeoChatConfig::self()->setOpenRoom(room->id());
|
||||||
|
NeoChatConfig::self()->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::getBack()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_currentRoom);
|
||||||
|
|
||||||
|
if (!m_lastCurrentRoom) {
|
||||||
|
Q_EMIT pushWelcomePage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT replaceRoom(m_lastCurrentRoom, QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::openWindow(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
// forward the call to QML
|
||||||
|
Q_EMIT openRoomInNewWindow(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
UriResolveResult RoomManager::visitUser(User* user, const QString &action)
|
||||||
|
{
|
||||||
|
if (action == "mention" || action.isEmpty()) {
|
||||||
|
// send it has QVariantMap because the properties in the
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
user->load();
|
||||||
|
#endif
|
||||||
|
Q_EMIT showUserDetail(user);
|
||||||
|
} else if (action == "_interactive") {
|
||||||
|
user->requestDirectChat();
|
||||||
|
} else if (action == "chat") {
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
user->load();
|
||||||
|
#endif
|
||||||
|
Q_EMIT askDirectChatConfirmation(user);
|
||||||
|
} else {
|
||||||
|
return Quotient::IncorrectAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Quotient::UriResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::visitRoom(Room *room, const QString &eventId)
|
||||||
|
{
|
||||||
|
auto neoChatRoom = qobject_cast<NeoChatRoom *>(room);
|
||||||
|
Q_ASSERT(neoChatRoom != nullptr);
|
||||||
|
|
||||||
|
if (m_currentRoom) {
|
||||||
|
if (m_currentRoom->id() == room->id()) {
|
||||||
|
Q_EMIT goToEvent(eventId);
|
||||||
|
} else {
|
||||||
|
m_lastCurrentRoom = std::exchange(m_currentRoom, neoChatRoom);
|
||||||
|
Q_EMIT currentRoomChanged();
|
||||||
|
Q_EMIT replaceRoom(neoChatRoom, eventId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_lastCurrentRoom = std::exchange(m_currentRoom, neoChatRoom);
|
||||||
|
Q_EMIT currentRoomChanged();
|
||||||
|
Q_EMIT pushRoom(neoChatRoom, eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::joinRoom(Quotient::Connection *account,
|
||||||
|
const QString &roomAliasOrId,
|
||||||
|
const QStringList &viaServers)
|
||||||
|
{
|
||||||
|
// We already listen to roomJoined signal in the Controller
|
||||||
|
account->joinRoom(QUrl::toPercentEncoding(roomAliasOrId), viaServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RoomManager::visitNonMatrix(const QUrl &url)
|
||||||
|
{
|
||||||
|
// Return true if the user cancels, treating it as an alternative normal
|
||||||
|
// flow (rather than an abnormal flow when the navigation itself fails).
|
||||||
|
if (NeoChatConfig::self()->confirmLinksAction()) {
|
||||||
|
Q_EMIT openLink(url);
|
||||||
|
} else {
|
||||||
|
if (!QDesktopServices::openUrl(url)) {
|
||||||
|
Q_EMIT warning(i18n("No application for the link"),
|
||||||
|
i18n("Your operating system could not find an application for the link."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::reset()
|
||||||
|
{
|
||||||
|
m_arg = QString();
|
||||||
|
m_currentRoom = nullptr;
|
||||||
|
m_lastCurrentRoom = nullptr;
|
||||||
|
Q_EMIT currentRoomChanged();
|
||||||
|
}
|
||||||
107
src/roommanager.h
Normal file
107
src/roommanager.h
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <uriresolver.h>
|
||||||
|
|
||||||
|
class NeoChatRoom;
|
||||||
|
|
||||||
|
namespace Quotient {
|
||||||
|
class Room;
|
||||||
|
class User;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
class RoomManager : public QObject, public UriResolverBase
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
/// This property holds the current open room in the NeoChat, if any.
|
||||||
|
/// \sa hasOpenRoom
|
||||||
|
Q_PROPERTY(NeoChatRoom *currentRoom READ currentRoom NOTIFY currentRoomChanged)
|
||||||
|
|
||||||
|
/// This property holds whether a room is currently open in NeoChat.
|
||||||
|
/// \sa room
|
||||||
|
Q_PROPERTY(bool hasOpenRoom READ hasOpenRoom NOTIFY currentRoomChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RoomManager(QObject *parent = nullptr);
|
||||||
|
virtual ~RoomManager();
|
||||||
|
|
||||||
|
/// Load the last opened room or the welcome page.
|
||||||
|
Q_INVOKABLE void loadInitialRoom();
|
||||||
|
|
||||||
|
/// This method will tell the NeoChat to open the message list
|
||||||
|
/// with the given room.
|
||||||
|
Q_INVOKABLE void enterRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
/// Force refresh the view to show the last the opened room.
|
||||||
|
Q_INVOKABLE void getBack();
|
||||||
|
|
||||||
|
Q_INVOKABLE void openWindow(NeoChatRoom *room);
|
||||||
|
|
||||||
|
/// Getter for the currentRoom property.
|
||||||
|
NeoChatRoom *currentRoom() const;
|
||||||
|
|
||||||
|
/// Getter for the hasOpenRoom property.
|
||||||
|
bool hasOpenRoom() const;
|
||||||
|
|
||||||
|
// Overrided methods from UriResolverBase
|
||||||
|
UriResolveResult visitUser(User *user, const QString &action) override;
|
||||||
|
void joinRoom(Quotient::Connection *account, const QString &roomAliasOrId,
|
||||||
|
const QStringList &viaServers) override;
|
||||||
|
Q_INVOKABLE void visitRoom(Room *room, const QString &eventId) override;
|
||||||
|
Q_INVOKABLE bool visitNonMatrix(const QUrl &url) override;
|
||||||
|
|
||||||
|
Q_INVOKABLE void openResource(const QString &idOrUri, const QString &action = {});
|
||||||
|
|
||||||
|
/// Call this when the current used connection is dropped.
|
||||||
|
Q_INVOKABLE void reset();
|
||||||
|
|
||||||
|
void setUrlArgument(const QString &arg);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
/// Signal triggered when the current open room change.
|
||||||
|
void currentRoomChanged();
|
||||||
|
|
||||||
|
/// Signal triggered when the pageStack should push a new page with the
|
||||||
|
/// message list for the given room.
|
||||||
|
void pushRoom(NeoChatRoom *room, const QString &event);
|
||||||
|
|
||||||
|
/// Signal triggered when the room displayed by the message list should
|
||||||
|
/// be changed.
|
||||||
|
void replaceRoom(NeoChatRoom *room, const QString &event);
|
||||||
|
|
||||||
|
/// Go to the specified event in the current room.
|
||||||
|
void goToEvent(const QString &event);
|
||||||
|
|
||||||
|
/// Signal triggered when the pageStack should push a welcome page.
|
||||||
|
void pushWelcomePage();
|
||||||
|
|
||||||
|
/// Signal triggered when a room need to be opened in a new window.
|
||||||
|
void openRoomInNewWindow(NeoChatRoom *room);
|
||||||
|
|
||||||
|
/// Ask current room to open the user's details for the give user.
|
||||||
|
/// This can assume the user is loaded.
|
||||||
|
void showUserDetail(const User *user);
|
||||||
|
|
||||||
|
/// Ask current room to show confirmation dialog to open direct chat.
|
||||||
|
/// This can assume the user is loaded.
|
||||||
|
void askDirectChatConfirmation(const User *user);
|
||||||
|
|
||||||
|
/// Displays warning to the user.
|
||||||
|
void warning(const QString &title, const QString &message);
|
||||||
|
|
||||||
|
/// Ask user to open link and then open it.
|
||||||
|
void openLink(const QUrl &url);
|
||||||
|
|
||||||
|
private:
|
||||||
|
NeoChatRoom *m_currentRoom;
|
||||||
|
NeoChatRoom *m_lastCurrentRoom;
|
||||||
|
QString m_arg;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_GLOBAL_STATIC(RoomManager, roomManager)
|
||||||
Reference in New Issue
Block a user