diff --git a/src/apps/qml/qml/Desktop.qml b/src/apps/qml/qml/Desktop.qml index 316cc60d..413cfa36 100644 --- a/src/apps/qml/qml/Desktop.qml +++ b/src/apps/qml/qml/Desktop.qml @@ -1,509 +1,513 @@ /* * Copyright 2016 Riccardo Iaconelli * Copyright (c) 2017-2020 Laurent Montel * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ import QtQuick 2.9 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.5 as QQC2 import Ruqola 1.0 import org.kde.kirigami 2.7 as Kirigami Kirigami.ApplicationWindow { id: appid readonly property int margin: 11 property string selectedRoomID: ""; property QtObject selectedRoom property QtObject messageModel property QtObject userModel property QtObject rocketChatAccount: accountManager.account property QtObject filesModel: appid.rocketChatAccount.filesForRoomFilterProxyModel property QtObject discussionsModel: appid.rocketChatAccount.discussionsFilterProxyModel property QtObject accountManager: Ruqola.accountManager() property QtObject accountManagerModel: accountManager.rocketChatAccountModel property QtObject searchMessageModel: rocketChatAccount.searchMessageFilterProxyModel property QtObject emojiModel: rocketChatAccount.emoticonModel property QtObject threadsModel: rocketChatAccount.threadsFilterProxyModel property QtObject threadMessagesModel: rocketChatAccount.threadMessageModel property QtObject listMessagesModel: rocketChatAccount.listMessagesFilterProxyModel property QtObject autotranslateLanguagesModel: rocketChatAccount.autoTranslateLanguagesModel property string userInputMessageText: ""; width: Kirigami.Units.gridUnit * 55 height: Kirigami.Units.gridUnit * 40 title: i18n("Ruqola") function switchToRoom(roomID) { if (roomID === selectedRoomID) { return; } + if (appid.messageModel) { + appid.messageModel.deactivate() + } appid.rocketChatAccount.switchingToRoom(roomID) appid.rocketChatAccount.setUserCurrentMessage(appid.userInputMessageText, selectedRoomID) appid.selectedRoomID = roomID; appid.messageModel = appid.rocketChatAccount.messageModelForRoom(roomID) appid.messageModel.enableQmlHacks(true) + appid.messageModel.activate() appid.selectedRoom = appid.rocketChatAccount.roomWrapper(roomID) appid.userModel = appid.rocketChatAccount.usersForRoomFilterProxyModel(roomID) } pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 15 pageStack.initialPage: [roomsComponent, mainComponent] pageStack.visible: rocketChatAccount.loginStatus === DDPClient.LoggedIn globalDrawer: Kirigami.GlobalDrawer { drawerOpen: false handleVisible: true resetMenuOnTriggered: true isMenu: true topContent: [ QQC2.Label { text: rocketChatAccount.userName === "" ? "" : i18n("Hello, %1", rocketChatAccount.userName) textFormat: Text.PlainText } ] actions: [ Kirigami.Action { text: i18n("About") iconName: "ruqola" onTriggered: { pageStack.push(aboutPage) } }, Kirigami.Action { text: i18n("Report a Bug") iconName: "tools-report-bug" onTriggered: { Qt.openUrlExternally("https://bugs.kde.org/report.cgi"); } }, Kirigami.Action { text: i18n("Configure Account") iconName: "settings-configure" onTriggered: pageStack.push(Qt.resolvedUrl("ConfigureServerList.qml"), {accountModel: accountManagerModel}) }, Kirigami.Action { text: i18n("Handbook") iconName: "system-help" onTriggered: { rocketChatAccount.openDocumentation(); } }, Kirigami.Action { separator: true }, Kirigami.Action { text: i18n("Log out") iconName: "system-log-out" onTriggered: { rocketChatAccount.logOut(); appid.globalDrawer.drawerOpen = false; } }, // Kirigami.Action { // text: i18n("autotranslate") // onTriggered: { // rocketChatAccount.getSupportedLanguages(); // } // }, Kirigami.Action { separator: true }, Kirigami.Action { shortcut: StandardKey.Quit text: i18n("Quit") iconName: "application-exit" onTriggered: { Qt.quit(); } } ] } LoginPage { id: loginTab rcAccount: rocketChatAccount } Component { id: aboutPage Kirigami.AboutPage { aboutData: Ruqola.applicationData() actions.main: Kirigami.Action { text: i18n("Close") icon.name: "window-close-symbolic" onTriggered: pageStack.pop() } } } Loader { id: customUserStatusDialogLoader active: false sourceComponent: CustomUserStatusDialog { id: customUserStatusDialog parent: appid.pageStack onRejected: { customUserStatusDialogLoader.active = false } onAccepted: { customUserStatusDialogLoader.active = false } Component.onCompleted: { open() } } } Loader { id: privateChannelInfoDialogLoader active: false sourceComponent: PrivateChannelInfoDialog { id: privateChannelInfoDialog roomInfo: appid.selectedRoom parent: appid.pageStack onBlockUser: { rocketChatAccount.blockUser(rid, block) } onRejected: { privateChannelInfoDialogLoader.active = false } onAccepted: { privateChannelInfoDialogLoader.active = false } Component.onCompleted: { initializeAndOpen() } } } Loader { id: notificationsDialogLoader active: false sourceComponent: NotificationOptionsDialog { id: notificationsDialog parent: appid.pageStack onModifyNotificationsSetting: { rocketChatAccount.changeNotificationsSettings(roomId, type, newVal) } Component.onCompleted: { rid = (appid && appid.selectedRoomID) ? appid.selectedRoomID : "" roomInfo = appid ? appid.selectedRoom : "" initializeAndOpen() } onRejected: { notificationsDialogLoader.active = false } onAccepted: { notificationsDialogLoader.active = false } } } Loader { id: channelInfoDialogLoader active: false sourceComponent: ChannelInfoDialog { id: channelInfoDialog parent: appid.pageStack roomInfo: appid.selectedRoom channelName: (appid && appid.selectedRoomID) ? appid.selectedRoomID : "" onDeleteRoom: { rocketChatAccount.eraseRoom(roomId, appid.selectedRoom.channelType) } onModifyChannelSetting: { rocketChatAccount.changeChannelSettings(roomId, type, newVal, channelType) } onRejected: { channelInfoDialogLoader.active = false } onAccepted: { channelInfoDialogLoader.active = false } Component.onCompleted: { open() } } } Loader { id: leaveChannelDialogLoader active: false property string rid property string roomType sourceComponent: LeaveChannelDialog { id: leaveChannelDialog parent: appid.pageStack onLeaveChannel: { rocketChatAccount.leaveRoom(roomId, channelType) } onRejected: { leaveChannelDialogLoader.active = false } onAccepted: { leaveChannelDialogLoader.active = false } Component.onCompleted: { leaveChannelDialog.rId = leaveChannelDialogLoader.rid leaveChannelDialog.channelType = leaveChannelDialogLoader.roomType open() } } } Loader { id: addUserDialogLoader active: false sourceComponent: AddUserDialog { id: addUserDialog parent: appid.pageStack completerModel: rocketChatAccount.userCompleterFilterModelProxy roomInfo: appid.selectedRoom roomId: appid.selectedRoomID onSearchUserName: { rocketChatAccount.userAutocomplete(pattern, ""); } onAddUser: { rocketChatAccount.addUserToRoom(userId, rid, channelType) } onRejected: { addUserDialogLoader.active = false } onAccepted: { addUserDialogLoader.active = false } Component.onCompleted: { initializeAndOpen() } } } Loader { id: searchMessageDialogLoader active: false sourceComponent: ShowSearchMessageDialog { id: searchMessageDialog roomId: appid.selectedRoomID parent: appid.pageStack searchMessageModel: appid.searchMessageModel onSearchMessage: { rocketChatAccount.messageSearch(pattern, rid) } onGoToMessage: { console.log(RuqolaDebugCategorySingleton.category, "Show history to message: " + messageId) } onRejected: { rocketChatAccount.clearSearchModel() searchMessageDialogLoader.active = false } onAccepted: { rocketChatAccount.clearSearchModel() searchMessageDialogLoader.active = false } Component.onCompleted: { roomId = appid.selectedRoomID initializeAndOpen() } } } Loader { id: jobErrorMessageDialogLoader property string jobMessageError active: false sourceComponent: JobErrorMessageDialog { id: jobErrorMessageDialog parent: appid.pageStack Component.onCompleted: { jobErrorMessageDialog.jobMessageError = jobErrorMessageDialogLoader.jobMessageError open() } onRejected: { jobErrorMessageDialogLoader.active = false } onAccepted: { jobErrorMessageDialogLoader.active = false } } } Loader { id: createNewChannelDialogLoader active: false sourceComponent: CreateNewChannelDialog { parent: appid.pageStack Component.onCompleted: { encryptedRoomEnabled = appid.rocketChatAccount.encryptedEnabled initializeAndOpen() } onRejected: { createNewChannelDialogLoader.active = false } onAccepted: { createNewChannelDialogLoader.active = false } onCreateNewChannel: { rocketChatAccount.createNewChannel(name, readOnly, privateRoom, usernames, encryptedRoom, password, broadcast); } } } Loader { id: serverinfodialogLoader active: false sourceComponent: ServerInfoDialog { rcAccount: appid.rocketChatAccount parent: appid.pageStack Component.onCompleted: { open() } onRejected: { serverinfodialogLoader.active = false } } } SearchChannelDialog { id: searchChannelDialog searchChannelModel: rocketChatAccount.searchChannelFilterProxyModel onSearchChannel: { rocketChatAccount.channelAndPrivateAutocomplete(pattern); } onOpenChannel: { if (channeltype === Channel.Room) { rocketChatAccount.openChannel(channelid) } else if (channeltype === Channel.PrivateChannel) { if (rocketChatAccount.userName !== channelid) { rocketChatAccount.openDirectChannel(channelid) } } else { console.log(RuqolaDebugCategorySingleton.category, "Unknown open channel type : " + channeltype + " channelid : " + channelid + " channelname : " + channelname) } } } Loader { id: takeVideoMessageLoader active: false sourceComponent: TakeVideoMessageDialog { id: takeVideoMessage parent: appid.pageStack rcAccount: rocketChatAccount Component.onCompleted: { open() } onRejected: { takeVideoMessageLoader.active = false } onAccepted: { takeVideoMessageLoader.active = false } } } QQC2.BusyIndicator { id: busy anchors.centerIn: parent visible: rocketChatAccount.loginStatus === DDPClient.LoggingIn } RoomsComponent { id: roomsComponent } MainComponent { id: mainComponent } Loader { id: channelPasswordDialogLoader active: false property string roomId sourceComponent: ChannelPasswordDialog { id: channelPasswordDialog parent: appid.pageStack onJoinRoom: { rocketChatAccount.joinRoom(roomId, password) } Component.onCompleted: { roomId = channelPasswordDialogLoader.roomId initializeAndOpen() } onRejected: { channelPasswordDialogLoader.active = false } onAccepted: { channelPasswordDialogLoader.active = false } } } Connections { target: rocketChatAccount onMissingChannelPassword: { channelPasswordDialogLoader.roomId = roomId channelPasswordDialogLoader.active = true } onJobFailed: { jobErrorMessageDialogLoader.jobMessageError = message jobErrorMessageDialogLoader.active = true } } onClosing: { Qt.quit(); } function toggleShow() { if (visible) { hide(); } else { show(); raise(); requestActivate(); } } Component.onCompleted: { if (Qt.platform.os == "android") { return; } systrayIcon.activateRequested.connect(toggleShow); } } diff --git a/src/core/model/messagemodel.cpp b/src/core/model/messagemodel.cpp index 1d3fcedf..fbafd714 100644 --- a/src/core/model/messagemodel.cpp +++ b/src/core/model/messagemodel.cpp @@ -1,520 +1,531 @@ /* * Copyright 2016 Riccardo Iaconelli * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include #include #include #include #include #include "messagemodel.h" #include "ruqolaserverconfig.h" #include "room.h" #include "ruqola_debug.h" #include "utils.h" #include "rocketchataccount.h" #include "texthighlighter.h" #include "textconverter.h" #include "loadrecenthistorymanager.h" #include //TODO reactivate when we will able to load message between cache and official server. //#define STORE_MESSAGE 1 MessageModel::MessageModel(const QString &roomID, RocketChatAccount *account, Room *room, QObject *parent) : QAbstractListModel(parent) , mRoomId(roomID) , mRocketChatAccount(account) , mRoom(room) { mTextConverter = new TextConverter(mRocketChatAccount ? mRocketChatAccount->emojiManager() : nullptr); mLoadRecentHistoryManager = new LoadRecentHistoryManager; qCDebug(RUQOLA_LOG) << "Creating message Model"; #ifdef STORE_MESSAGE if (mRocketChatAccount) { const QString cachePath = mRocketChatAccount->settings()->cacheBasePath(); if (cachePath.isEmpty()) { qCWarning(RUQOLA_LOG) << " Cache Path is not defined"; return; } QDir cacheDir(cachePath + QStringLiteral("/rooms_cache")); // load cache if (QFile::exists(cacheDir.absoluteFilePath(roomID)) && !roomID.isEmpty()) { QFile f(cacheDir.absoluteFilePath(roomID)); if (f.open(QIODevice::ReadOnly)) { QDataStream in(&f); while (!f.atEnd()) { char *byteArray; quint32 length; in.readBytes(byteArray, length); const QByteArray arr = QByteArray::fromRawData(byteArray, length); Message m = Message::fromJSon(QJsonDocument::fromBinaryData(arr).object()); addMessage(m); } } } } #endif if (mRoom) { connect(mRoom, &Room::rolesChanged, this, &MessageModel::refresh); connect(mRoom, &Room::ignoredUsersChanged, this, &MessageModel::refresh); } - if (mRocketChatAccount) { - connect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &MessageModel::slotFileDownloaded); - } } MessageModel::~MessageModel() { #ifdef STORE_MESSAGE if (mRocketChatAccount) { const QString cachePath = mRocketChatAccount->settings()->cacheBasePath(); if (cachePath.isEmpty()) { qCWarning(RUQOLA_LOG) << " Cache Path is not defined"; return; } QDir cacheDir(cachePath + QStringLiteral("/rooms_cache")); qCDebug(RUQOLA_LOG) << "Caching to..." << cacheDir.path(); if (!cacheDir.exists(cacheDir.path())) { cacheDir.mkpath(cacheDir.path()); } QFile f(cacheDir.absoluteFilePath(mRoomID)); if (f.open(QIODevice::WriteOnly)) { QDataStream out(&f); for (const Message &m : qAsConst(mAllMessages)) { const QByteArray ms = Message::serialize(m); out.writeBytes(ms, ms.size()); } } } #endif delete mTextConverter; delete mLoadRecentHistoryManager; } void MessageModel::enableQmlHacks(bool qmlHacks) { mQmlHacks = qmlHacks; } +void MessageModel::activate() +{ + if (mRocketChatAccount) { + connect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &MessageModel::slotFileDownloaded); + } +} + +void MessageModel::deactivate() +{ + if (mRocketChatAccount) { + disconnect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &MessageModel::slotFileDownloaded); + } +} + void MessageModel::refresh() { beginResetModel(); endResetModel(); } QHash MessageModel::roleNames() const { QHash roles; roles[OriginalMessage] = QByteArrayLiteral("originalMessage"); roles[MessageConvertedText] = QByteArrayLiteral("messageConverted"); roles[Username] = QByteArrayLiteral("username"); roles[Timestamp] = QByteArrayLiteral("timestamp"); roles[UserId] = QByteArrayLiteral("userID"); roles[SystemMessageType] = QByteArrayLiteral("type"); roles[MessageId] = QByteArrayLiteral("messageID"); roles[RoomId] = QByteArrayLiteral("roomID"); roles[UpdatedAt] = QByteArrayLiteral("updatedAt"); roles[EditedAt] = QByteArrayLiteral("editedAt"); roles[EditedByUserName] = QByteArrayLiteral("editedByUsername"); roles[EditedByUserId] = QByteArrayLiteral("editedByUserID"); roles[Alias] = QByteArrayLiteral("alias"); roles[Avatar] = QByteArrayLiteral("avatar"); roles[Groupable] = QByteArrayLiteral("groupable"); roles[MessageType] = QByteArrayLiteral("messagetype"); roles[Attachments] = QByteArrayLiteral("attachments"); roles[Urls] = QByteArrayLiteral("urls"); roles[Date] = QByteArrayLiteral("date"); roles[CanEditMessage] = QByteArrayLiteral("canEditMessage"); roles[Starred] = QByteArrayLiteral("starred"); roles[UsernameUrl] = QByteArrayLiteral("usernameurl"); roles[Roles] = QByteArrayLiteral("roles"); roles[Reactions] = QByteArrayLiteral("reactions"); roles[Ignored] = QByteArrayLiteral("userIsIgnored"); roles[Pinned] = QByteArrayLiteral("pinned"); roles[DiscussionCount] = QByteArrayLiteral("discussionCount"); roles[DiscussionRoomId] = QByteArrayLiteral("discussionRoomId"); roles[DiscussionLastMessage] = QByteArrayLiteral("discussionLastMessage"); roles[ThreadCount] = QByteArrayLiteral("threadCount"); roles[ThreadLastMessage] = QByteArrayLiteral("threadLastMessage"); roles[ThreadMessageId] = QByteArrayLiteral("threadMessageId"); roles[ThreadMessagePreview] = QByteArrayLiteral("threadMessagePreview"); roles[ShowTranslatedMessage] = QByteArrayLiteral("showTranslatedMessage"); roles[DisplayAttachment] = QByteArrayLiteral("displayAttachment"); return roles; } qint64 MessageModel::lastTimestamp() const { if (!mAllMessages.isEmpty()) { //qCDebug(RUQOLA_LOG) << "returning timestamp" << mAllMessages.last().timeStamp(); return mAllMessages.first().timeStamp(); } else { return 0; } } int MessageModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return mAllMessages.size(); } static bool compareTimeStamps(const Message &lhs, const Message &rhs) { return lhs.timeStamp() < rhs.timeStamp(); } void MessageModel::addMessage(const Message &message) { auto it = std::upper_bound(mAllMessages.begin(), mAllMessages.end(), message, compareTimeStamps); auto emitChanged = [this](int rowNumber) { if (mQmlHacks) { //For the moment !!!! It's not optimal but Q_EMIT dataChanged(index, index); doesn't work beginRemoveRows(QModelIndex(), rowNumber, rowNumber); endRemoveRows(); beginInsertRows(QModelIndex(), rowNumber, rowNumber); endInsertRows(); } else { const QModelIndex index = createIndex(rowNumber, 0); Q_EMIT dataChanged(index, index); } }; //When we have 1 element. if (mAllMessages.count() == 1 && (*mAllMessages.begin()).messageId() == message.messageId()) { (*mAllMessages.begin()) = message; qCDebug(RUQOLA_LOG) << "Update first message"; emitChanged(0); } else if (((it) != mAllMessages.begin() && (*(it - 1)).messageId() == message.messageId())) { qCDebug(RUQOLA_LOG) << "Update message"; (*(it-1)) = message; emitChanged(std::distance(mAllMessages.begin(), it - 1)); } else { const int pos = it - mAllMessages.begin(); beginInsertRows(QModelIndex(), pos, pos); mAllMessages.insert(it, message); endInsertRows(); } } void MessageModel::addMessages(const QVector &messages) { if (mAllMessages.isEmpty()) { beginInsertRows(QModelIndex(), 0, messages.count() - 1); mAllMessages = messages; std::sort(mAllMessages.begin(), mAllMessages.end(), compareTimeStamps); endInsertRows(); } else { // TODO optimize this case as well? for (const Message &message : messages) { addMessage(message); } } } QVariant MessageModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { qCWarning(RUQOLA_LOG) << "ERROR: invalid index"; return {}; } const int idx = index.row(); const Message &message = mAllMessages.at(idx); switch (role) { case MessageModel::MessagePointer: return QVariant::fromValue(&message); case MessageModel::Username: return message.username(); case MessageModel::OriginalMessage: return message.text(); case MessageModel::MessageConvertedText: if (message.messageType() == Message::System) { return message.systemMessageText(); } else { if (mRoom && mRoom->userIsIgnored(message.userId())) { return QString(QStringLiteral("") + i18n("Ignored Message") + QStringLiteral("")); } const QString userName = mRocketChatAccount ? mRocketChatAccount->userName() : QString(); return convertMessageText(message, userName); } case MessageModel::Timestamp: return message.displayTime(); case MessageModel::UserId: return message.userId(); case MessageModel::SystemMessageType: return message.systemMessageType(); case MessageModel::MessageId: return message.messageId(); case MessageModel::Alias: return message.alias(); case MessageModel::MessageType: return message.messageType(); case MessageModel::Avatar: return message.avatar(); case MessageModel::EditedAt: return message.editedAt(); case MessageModel::EditedByUserName: return message.editedByUsername(); case MessageModel::Attachments: { QVariantList lst; lst.reserve(message.attachements().count()); const auto attachs = message.attachements(); for (const MessageAttachment &att : attachs) { lst.append(QVariant::fromValue(att)); } return lst; } case MessageModel::Urls: { QVariantList lst; lst.reserve(message.urls().count()); const auto urls = message.urls(); for (const MessageUrl &url : urls) { lst.append(QVariant::fromValue(url)); } return lst; } case MessageModel::Date: { const QDateTime currentDate = QDateTime::fromMSecsSinceEpoch(message.timeStamp()); return currentDate.date().toString(); } case MessageModel::DateDiffersFromPrevious: if (idx > 0) { const QDateTime currentDate = QDateTime::fromMSecsSinceEpoch(message.timeStamp()); const Message &previousMessage = mAllMessages.at(idx - 1); const QDateTime previousDate = QDateTime::fromMSecsSinceEpoch(previousMessage.timeStamp()); return currentDate.date() != previousDate.date(); } return true; // show date at the top case MessageModel::CanEditMessage: return (message.timeStamp() + (mRocketChatAccount ? mRocketChatAccount->ruqolaServerConfig()->blockEditingMessageInMinutes() * 60 * 1000 : 0)) > QDateTime::currentMSecsSinceEpoch(); case MessageModel::Starred: return message.starred(); case MessageModel::UsernameUrl: { const QString username = message.username(); if (username.isEmpty()) { return {}; } return QStringLiteral("@%1").arg(message.username()); } case MessageModel::Roles: { const QString str = roomRoles(message.userId()).join(QLatin1Char(',')); return str; } case MessageModel::Reactions: { QVariantList lst; const auto reactions = message.reactions().reactions(); lst.reserve(reactions.count()); for (const Reaction &react : reactions) { //Convert reactions lst.append(QVariant::fromValue(react)); } return lst; } case MessageModel::Ignored: return mRoom && mRoom->userIsIgnored(message.userId()); case MessageModel::Pinned: return message.messagePinned().pinned(); case MessageModel::DiscussionCount: return message.discussionCount(); case MessageModel::DiscussionRoomId: return message.discussionRoomId(); case MessageModel::DiscussionLastMessage: return message.discussionLastMessage(); case MessageModel::ThreadCount: return message.threadCount(); case MessageModel::ThreadLastMessage: return message.threadLastMessage(); case MessageModel::ThreadMessageId: return message.threadMessageId(); case MessageModel::ThreadMessagePreview: { const QString userName = mRocketChatAccount ? mRocketChatAccount->userName() : QString(); return threadMessagePreview(message.threadMessageId(), userName); } case MessageModel::Groupable: return message.groupable(); case MessageModel::ShowTranslatedMessage: return message.showTranslatedMessage(); case MessageModel::DisplayAttachment: return message.showAttachment(); } return {}; } bool MessageModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { qCWarning(RUQOLA_LOG) << "ERROR: invalid index"; return {}; } const int idx = index.row(); Message &message = mAllMessages[idx]; switch (role) { case MessageModel::DisplayAttachment: message.setShowAttachment(value.toBool()); Q_EMIT dataChanged(index, index); return true; } return false; } QStringList MessageModel::roomRoles(const QString &userId) const { if (mRoom) { return mRoom->rolesForUserId(userId); } return QStringList(); } QString MessageModel::convertMessageText(const Message &message, const QString &userName) const { QString messageStr = message.text(); if (message.showTranslatedMessage() && mRoom && mRoom->autoTranslate() && !mRoom->autoTranslateLanguage().isEmpty()) { const QString messageTranslation = message.messageTranslation().translatedStringFromLanguage(mRoom->autoTranslateLanguage()); if (!messageTranslation.isEmpty()) { messageStr = messageTranslation; } //qDebug() << " autotranslate true && mRoom->autoTranslateLanguage() :" << mRoom->autoTranslateLanguage(); } return mTextConverter->convertMessageText(messageStr, userName, mAllMessages); } void MessageModel::setRoomId(const QString &roomID) { mRoomId = roomID; } bool MessageModel::isEmpty() const { return mAllMessages.isEmpty(); } void MessageModel::clear() { if (rowCount() != 0) { beginRemoveRows(QModelIndex(), 0, mAllMessages.count() - 1); mAllMessages.clear(); endRemoveRows(); } } void MessageModel::changeShowOriginalMessage(const QString &messageId, bool showOriginal) { auto it = std::find_if(mAllMessages.begin(), mAllMessages.end(), [messageId](const Message &msg) { return msg.messageId() == messageId; }); if (it != mAllMessages.end()) { //TODO } } void MessageModel::slotFileDownloaded(const QString &filePath, const QUrl &cacheImageUrl) { Q_UNUSED(cacheImageUrl) auto matchesFilePath = [&](const QVector &msgAttachments) { return std::find_if(msgAttachments.begin(), msgAttachments.end(), [&](const MessageAttachment &attach) { return attach.link() == filePath; }) != msgAttachments.end(); }; auto it = std::find_if(mAllMessages.begin(), mAllMessages.end(), [&](const Message &msg) { if (msg.messageType() == Message::Image) { return matchesFilePath(msg.attachements()); } return false; }); if (it != mAllMessages.end()) { const QModelIndex idx = createIndex(std::distance(mAllMessages.begin(), it), 0); Q_EMIT dataChanged(idx, idx); } } void MessageModel::changeDisplayAttachment(const QString &messageId, bool displayAttachment) { auto it = std::find_if(mAllMessages.begin(), mAllMessages.end(), [messageId](const Message &msg) { return msg.messageId() == messageId; }); if (it != mAllMessages.end()) { (*it).setShowAttachment(displayAttachment); } } void MessageModel::deleteMessage(const QString &messageId) { auto it = std::find_if(mAllMessages.begin(), mAllMessages.end(), [messageId](const Message &msg) { return msg.messageId() == messageId; }); if (it != mAllMessages.end()) { const int i = std::distance(mAllMessages.begin(), it); beginRemoveRows(QModelIndex(), i, i); mAllMessages.erase(it); endRemoveRows(); } } qint64 MessageModel::generateNewStartTimeStamp(qint64 lastTimeStamp) { return mLoadRecentHistoryManager->generateNewStartTimeStamp(lastTimeStamp); } QString MessageModel::threadMessagePreview(const QString &threadMessageId, const QString &userName) const { if (!threadMessageId.isEmpty()) { auto it = std::find_if(mAllMessages.cbegin(), mAllMessages.cend(), [threadMessageId](const Message &msg) { return msg.messageId() == threadMessageId; }); if (it != mAllMessages.cend()) { QString str = convertMessageText((*it), userName); if (str.length() > 80) { str = str.left(80) + QStringLiteral("..."); } return str; } else { qCDebug(RUQOLA_LOG) << "Thread message" << threadMessageId << "not found"; // could be a very old one } } return {}; } QString MessageModel::roomId() const { return mRoomId; } diff --git a/src/core/model/messagemodel.h b/src/core/model/messagemodel.h index 28d6818c..7646d7d7 100644 --- a/src/core/model/messagemodel.h +++ b/src/core/model/messagemodel.h @@ -1,153 +1,156 @@ /* * Copyright 2016 Riccardo Iaconelli * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef MESSAGEMODEL_H #define MESSAGEMODEL_H #include "libruqola_private_export.h" #include "messages/message.h" #include #include #include class RocketChatAccount; class TextConverter; class LoadRecentHistoryManager; class Room; class LIBRUQOLACORE_TESTS_EXPORT MessageModel : public QAbstractListModel { Q_OBJECT public: enum MessageRoles { Username = Qt::UserRole + 1, MessagePointer, OriginalMessage, MessageConvertedText, Timestamp, UserId, SystemMessageType, MessageId, RoomId, UpdatedAt, EditedAt, EditedByUserName, EditedByUserId, Alias, Avatar, Groupable, ParseUrls, MessageType, Attachments, Urls, Date, DateDiffersFromPrevious, CanEditMessage, Starred, UsernameUrl, Roles, Reactions, Ignored, Pinned, DiscussionCount, DiscussionRoomId, DiscussionLastMessage, ThreadCount, ThreadLastMessage, ThreadMessageId, ThreadMessagePreview, ShowTranslatedMessage, DisplayAttachment, }; Q_ENUM(MessageRoles) explicit MessageModel(const QString &roomID = QStringLiteral("no_room"), RocketChatAccount *account = nullptr, Room *room = nullptr, QObject *parent = nullptr); ~MessageModel() override; Q_INVOKABLE void enableQmlHacks(bool qmlHacks); /** * @brief Adds a message to the model * * @param message The message to be added */ void addMessage(const Message &message); /** * @brief Adds a number of messages to the model * * @param messages The messages to be added */ void addMessages(const QVector &messages); /** * @brief returns number of messages in the model * * @param parent, it is void * @return int, The number of messages in QVector mAllMessages */ Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; /** * @brief Returns last timestamp of last message in QVector mAllMessages * * @return qint64 The last timestamp */ Q_REQUIRED_RESULT qint64 lastTimestamp() const; void deleteMessage(const QString &messageId); Q_REQUIRED_RESULT qint64 generateNewStartTimeStamp(qint64 lastTimeStamp); Q_REQUIRED_RESULT QHash roleNames() const override; void setRoomId(const QString &roomID); Q_REQUIRED_RESULT bool isEmpty() const; void clear(); void changeDisplayAttachment(const QString &messageId, bool displayAttachment); void changeShowOriginalMessage(const QString &messageId, bool showOriginal); Q_REQUIRED_RESULT QString roomId() const; + Q_INVOKABLE void activate(); + Q_INVOKABLE void deactivate(); + private Q_SLOTS: void slotFileDownloaded(const QString &filePath, const QUrl &cacheImageUrl); private: Q_DISABLE_COPY(MessageModel) void refresh(); QStringList roomRoles(const QString &userId) const; QString convertMessageText(const Message &message, const QString &userName) const; QString threadMessagePreview(const QString &threadMessageId, const QString &userName) const; QString mRoomId; QVector mAllMessages; RocketChatAccount *mRocketChatAccount = nullptr; TextConverter *mTextConverter = nullptr; Room *mRoom = nullptr; LoadRecentHistoryManager *mLoadRecentHistoryManager = nullptr; bool mQmlHacks = false; }; #endif diff --git a/src/widgets/room/messagelistview.cpp b/src/widgets/room/messagelistview.cpp index a6a556c6..acc39e22 100644 --- a/src/widgets/room/messagelistview.cpp +++ b/src/widgets/room/messagelistview.cpp @@ -1,325 +1,331 @@ /* Copyright (c) 2020 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagelistview.h" #include "ruqola.h" #include "rocketchataccount.h" #include "messagelistdelegate.h" #include "dialogs/reportmessagedialog.h" #include "dialogs/createnewdiscussiondialog.h" #include #include #include #include #include #include #include #include #include MessageListView::MessageListView(QWidget *parent) : QListView(parent) { auto *delegate = new MessageListDelegate(this); delegate->setRocketChatAccount(Ruqola::self()->rocketChatAccount()); setItemDelegate(delegate); setSelectionMode(QAbstractItemView::NoSelection); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); // nicer in case of huge messages setWordWrap(true); // so that the delegate sizeHint is called again when the width changes // only the lineedit takes focus setFocusPolicy(Qt::NoFocus); } MessageListView::~MessageListView() { } void MessageListView::setChannelSelected(const QString &roomId) { + MessageModel *oldModel = qobject_cast(model()); + if (oldModel) { + oldModel->deactivate(); + } Ruqola::self()->rocketChatAccount()->switchingToRoom(roomId); - setModel(Ruqola::self()->rocketChatAccount()->messageModelForRoom(roomId)); + MessageModel *model = Ruqola::self()->rocketChatAccount()->messageModelForRoom(roomId); + setModel(model); + model->activate(); mRoomID = roomId; } void MessageListView::setModel(QAbstractItemModel *newModel) { QAbstractItemModel *oldModel = model(); if (oldModel) { disconnect(oldModel, nullptr, this, nullptr); } QListView::setModel(newModel); connect(newModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &MessageListView::checkIfAtBottom); connect(newModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MessageListView::checkIfAtBottom); connect(newModel, &QAbstractItemModel::modelAboutToBeReset, this, &MessageListView::checkIfAtBottom); connect(newModel, &QAbstractItemModel::rowsInserted, this, &MessageListView::maybeScrollToBottom); connect(newModel, &QAbstractItemModel::rowsRemoved, this, &MessageListView::maybeScrollToBottom); connect(newModel, &QAbstractItemModel::modelReset, this, &MessageListView::maybeScrollToBottom); connect(newModel, &QAbstractItemModel::rowsInserted, this, &MessageListView::modelChanged); connect(newModel, &QAbstractItemModel::rowsRemoved, this, &MessageListView::modelChanged); connect(newModel, &QAbstractItemModel::modelReset, this, &MessageListView::modelChanged); scrollToBottom(); } void MessageListView::resizeEvent(QResizeEvent *ev) { QListView::resizeEvent(ev); // Fix not being really at bottom when the view gets reduced by the header widget becoming taller checkIfAtBottom(); maybeScrollToBottom(); // this forces a layout in QAIV, which then changes the vbar max value } void MessageListView::handleKeyPressEvent(QKeyEvent *ev) { const int key = ev->key(); if (key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_PageDown || key == Qt::Key_PageUp) { // QListView/QAIV PageUp/PageDown moves the current item, first inside visible bounds // before it triggers scrolling around. Let's just let the scrollarea handle it, // since we don't show the current item. QAbstractScrollArea::keyPressEvent(ev); ev->accept(); } else if (key == Qt::Key_Home && ev->modifiers() & Qt::ControlModifier) { scrollToTop(); ev->accept(); } else if (key == Qt::Key_End && ev->modifiers() & Qt::ControlModifier) { scrollToBottom(); ev->accept(); } } void MessageListView::checkIfAtBottom() { auto *vbar = verticalScrollBar(); mAtBottom = vbar->value() == vbar->maximum(); } void MessageListView::maybeScrollToBottom() { if (mAtBottom) { scrollToBottom(); } } void MessageListView::contextMenuEvent(QContextMenuEvent *event) { const QModelIndex index = indexAt(event->pos()); if (!index.isValid()) { return; } const bool isSystemMessage = index.data(MessageModel::MessageType).value() == Message::System; if (isSystemMessage) { return; } auto *rcAccount = Ruqola::self()->rocketChatAccount(); QMenu menu(this); QAction *copyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"), &menu); connect(copyAction, &QAction::triggered, this, [=]() { slotCopyText(index); }); QAction *setPinnedMessage = nullptr; if (rcAccount->allowMessagePinningEnabled()) { const bool isPinned = index.data(MessageModel::Pinned).toBool(); setPinnedMessage = new QAction(QIcon::fromTheme(QStringLiteral("pin")), isPinned ? i18n("Unpin Message") : i18n("Pin Message"), &menu); connect(setPinnedMessage, &QAction::triggered, this, [this, isPinned, index]() { slotSetPinnedMessage(index, isPinned); }); } QAction *setAsFavoriteAction = nullptr; if (rcAccount->allowMessageStarringEnabled()) { const bool isStarred = index.data(MessageModel::Starred).toBool(); setAsFavoriteAction = new QAction(QIcon::fromTheme(QStringLiteral("favorite")), isStarred ? i18n("Remove as Favorite") : i18n("Set as Favorite"), &menu); connect(setAsFavoriteAction, &QAction::triggered, this, [this, isStarred, index]() { slotSetAsFavorite(index, isStarred); }); } if (mMode == Mode::Editing) { QAction *startDiscussion = new QAction(i18n("Start a Discussion"), &menu); connect(startDiscussion, &QAction::triggered, this, [=]() { slotStartDiscussion(index); }); menu.addAction(startDiscussion); if (setPinnedMessage) { menu.addAction(setPinnedMessage); } if (setAsFavoriteAction) { menu.addAction(setAsFavoriteAction); } if (rcAccount->allowEditingMessages() && index.data(MessageModel::CanEditMessage).toBool() && index.data(MessageModel::UserId).toString() == rcAccount->userID()) { QAction *editAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit"), &menu); connect(editAction, &QAction::triggered, this, [=]() { slotEditMessage(index); }); menu.addAction(editAction); } menu.addAction(copyAction); if (rcAccount->allowMessageDeletingEnabled() && index.data(MessageModel::UserId).toString() == rcAccount->userID()) { createSeparator(menu); QAction *deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete"), &menu); connect(deleteAction, &QAction::triggered, this, [=]() { slotDeleteMessage(index); }); menu.addAction(deleteAction); } if (rcAccount->autoTranslateEnabled()) { createSeparator(menu); const bool isTranslated = index.data(MessageModel::ShowTranslatedMessage).toBool(); QAction *translateAction = new QAction(isTranslated ? i18n("Show Original Message") : i18n("Translate Message"), &menu); connect(translateAction, &QAction::triggered, this, [=](bool checked) { slotTranslateMessage(index, checked); }); menu.addAction(translateAction); } } else { #if 0 if (setPinnedMessage) { menu.addAction(setPinnedMessage); } if (setAsFavoriteAction) { menu.addAction(setAsFavoriteAction); } #endif menu.addAction(copyAction); createSeparator(menu); QAction *goToMessageAction = new QAction(i18n("Go to Message"), &menu); //Add icon connect(goToMessageAction, &QAction::triggered, this, [=]() { slotGoToMessage(index); }); menu.addAction(goToMessageAction); } createSeparator(menu); QAction *reportMessageAction = new QAction(QIcon::fromTheme(QStringLiteral("messagebox_warning")), i18n("Report Message"), &menu); connect(reportMessageAction, &QAction::triggered, this, [=]() { slotReportMessage(index); }); menu.addAction(reportMessageAction); if (!menu.actions().isEmpty()) { menu.exec(event->globalPos()); } } void MessageListView::createSeparator(QMenu &menu) { if (!menu.isEmpty()) { auto *separator = new QAction(&menu); separator->setSeparator(true); menu.addAction(separator); } } void MessageListView::slotTranslateMessage(const QModelIndex &index, bool checked) { qDebug() << "No implemented yet"; //TODO } void MessageListView::slotGoToMessage(const QModelIndex &index) { qDebug() << "No implemented yet"; //TODO } void MessageListView::slotEditMessage(const QModelIndex &index) { const QString text = index.data(MessageModel::OriginalMessage).toString(); const QString messageId = index.data(MessageModel::MessageId).toString(); Q_EMIT editMessageRequested(messageId, text); } void MessageListView::slotDeleteMessage(const QModelIndex &index) { if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you want to delete this message?"), i18n("Delete Message"))) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); const QString messageId = index.data(MessageModel::MessageId).toString(); rcAccount->deleteMessage(messageId, mRoomID); } } void MessageListView::slotReportMessage(const QModelIndex &index) { QPointer dlg = new ReportMessageDialog(this); if (dlg->exec()) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); const QString messageId = index.data(MessageModel::MessageId).toString(); rcAccount->reportMessage(messageId, dlg->message()); } delete dlg; } void MessageListView::slotSetAsFavorite(const QModelIndex &index, bool isStarred) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); const QString messageId = index.data(MessageModel::MessageId).toString(); rcAccount->starMessage(messageId, !isStarred); } void MessageListView::slotSetPinnedMessage(const QModelIndex &index, bool isPinned) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); const QString messageId = index.data(MessageModel::MessageId).toString(); rcAccount->pinMessage(messageId, !isPinned); //TODO fix pinMessage it seems that it doesn't work } void MessageListView::slotStartDiscussion(const QModelIndex &index) { QPointer dlg = new CreateNewDiscussionDialog(this); const QString message = index.data(MessageModel::OriginalMessage).toString(); dlg->setDiscussionName(message); //FIXME dlg->setChannelName(message); if (dlg->exec()) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); const QString messageId = index.data(MessageModel::MessageId).toString(); const CreateNewDiscussionDialog::NewDiscussionInfo info = dlg->newDiscussionInfo(); rcAccount->createDiscussion(info.channelName, info.discussionName, info.message, messageId, info.users); } delete dlg; } void MessageListView::slotCopyText(const QModelIndex &index) { const QString messageStr = index.data(MessageModel::OriginalMessage).toString(); QClipboard *clip = QApplication::clipboard(); clip->setText(messageStr, QClipboard::Clipboard); clip->setText(messageStr, QClipboard::Selection); } MessageListView::Mode MessageListView::mode() const { return mMode; } void MessageListView::setMode(const MessageListView::Mode &mode) { mMode = mode; }