diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 3731efa..63b3d5c 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -29,6 +29,7 @@ SOURCES += src/harbour-fernschreiber.cpp \ src/dbusinterface.cpp \ src/emojisearchworker.cpp \ src/fernschreiberutils.cpp \ + src/knownusersmodel.cpp \ src/mceinterface.cpp \ src/notificationmanager.cpp \ src/processlauncher.cpp \ @@ -154,6 +155,7 @@ HEADERS += \ src/debuglogjs.h \ src/emojisearchworker.h \ src/fernschreiberutils.h \ + src/knownusersmodel.h \ src/mceinterface.h \ src/notificationmanager.h \ src/processlauncher.h \ diff --git a/qml/pages/ChatPage.qml b/qml/pages/ChatPage.qml index 20a1bdc..bffe528 100644 --- a/qml/pages/ChatPage.qml +++ b/qml/pages/ChatPage.qml @@ -285,6 +285,12 @@ Page { } else { chatPage.emojiProposals = null; } + if (currentWord.length > 1 && currentWord.charAt(0) === '@') { + knownUsersRepeater.model = knownUsersProxyModel; + knownUsersProxyModel.setFilterWildcard("*" + currentWord.substring(1) + "*"); + } else { + knownUsersRepeater.model = undefined; + } } function replaceMessageText(text, cursorPosition, newText) { @@ -1271,6 +1277,83 @@ Page { } } + Column { + id: atMentionColumn + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + visible: knownUsersRepeater.count > 0 ? true : false + opacity: knownUsersRepeater.count > 0 ? 1 : 0 + Behavior on opacity { NumberAnimation {} } + spacing: Theme.paddingMedium + + Flickable { + width: parent.width + height: atMentionResultRow.height + Theme.paddingSmall + anchors.horizontalCenter: parent.horizontalCenter + contentWidth: atMentionResultRow.width + clip: true + Row { + id: atMentionResultRow + spacing: Theme.paddingMedium + Repeater { + id: knownUsersRepeater + + Item { + id: knownUserItem + height: singleAtMentionRow.height + width: singleAtMentionRow.width + + property string atMentionText: "@" + (user_name ? user_name : user_id + "(" + title + ")"); + + Row { + id: singleAtMentionRow + spacing: Theme.paddingSmall + + Item { + width: Theme.fontSizeHuge + height: Theme.fontSizeHuge + anchors.verticalCenter: parent.verticalCenter + ProfileThumbnail { + id: atMentionThumbnail + replacementStringHint: title + width: parent.width + height: parent.width + photoData: photo_small + } + } + + Column { + Text { + text: Emoji.emojify(title, Theme.fontSizeExtraSmall) + textFormat: Text.StyledText + color: Theme.primaryColor + font.pixelSize: Theme.fontSizeExtraSmall + font.bold: true + } + Text { + id: userHandleText + text: user_handle + textFormat: Text.StyledText + color: Theme.primaryColor + font.pixelSize: Theme.fontSizeExtraSmall + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + replaceMessageText(newMessageTextField.text, newMessageTextField.cursorPosition, knownUserItem.atMentionText); + knownUsersRepeater.model = undefined; + } + } + } + + } + } + } + } + Row { width: parent.width spacing: Theme.paddingSmall diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp index e35b8fd..611fd15 100644 --- a/src/harbour-fernschreiber.cpp +++ b/src/harbour-fernschreiber.cpp @@ -42,6 +42,7 @@ #include "stickermanager.h" #include "tgsplugin.h" #include "fernschreiberutils.h" +#include "knownusersmodel.h" #include "contactsmodel.h" // The default filter can be overridden by QT_LOGGING_RULES envinronment variable, e.g. @@ -97,6 +98,14 @@ int main(int argc, char *argv[]) StickerManager stickerManager(tdLibWrapper); context->setContextProperty("stickerManager", &stickerManager); + KnownUsersModel knownUsersModel(tdLibWrapper, view.data()); + context->setContextProperty("knownUsersModel", &knownUsersModel); + QSortFilterProxyModel knownUsersProxyModel(view.data()); + knownUsersProxyModel.setSourceModel(&knownUsersModel); + knownUsersProxyModel.setFilterRole(KnownUsersModel::RoleFilter); + knownUsersProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive); + context->setContextProperty("knownUsersProxyModel", &knownUsersProxyModel); + ContactsModel contactsModel(tdLibWrapper, view.data()); context->setContextProperty("contactsModel", &contactsModel); QSortFilterProxyModel contactsProxyModel(view.data()); diff --git a/src/knownusersmodel.cpp b/src/knownusersmodel.cpp new file mode 100644 index 0000000..50fdbd2 --- /dev/null +++ b/src/knownusersmodel.cpp @@ -0,0 +1,71 @@ +/* + Copyright (C) 2020 Sebastian J. Wolf and other contributors + + This file is part of Fernschreiber. + + Fernschreiber 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 3 of the License, or + (at your option) any later version. + + Fernschreiber 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 Fernschreiber. If not, see . +*/ + +#include "knownusersmodel.h" + +#define DEBUG_MODULE KnwonUsersModel +#include "debuglog.h" + +KnownUsersModel::KnownUsersModel(TDLibWrapper *tdLibWrapper, QObject *parent) + : QAbstractListModel(parent) +{ + this->tdLibWrapper = tdLibWrapper; + + connect(this->tdLibWrapper, SIGNAL(userUpdated(QString, QVariantMap)), this, SLOT(handleUserUpdated(QString, QVariantMap))); +} + +QHash KnownUsersModel::roleNames() const +{ + QHash roles; + roles.insert(KnownUserRole::RoleDisplay, "display"); + roles.insert(KnownUserRole::RoleUserId, "user_id"); + roles.insert(KnownUserRole::RoleTitle, "title"); + roles.insert(KnownUserRole::RoleUsername, "user_name"); + roles.insert(KnownUserRole::RoleUserHandle, "user_handle"); + roles.insert(KnownUserRole::RolePhotoSmall, "photo_small"); + roles.insert(KnownUserRole::RoleFilter, "filter"); + return roles; +} + +int KnownUsersModel::rowCount(const QModelIndex &) const +{ + return this->knownUsers.size(); +} + +QVariant KnownUsersModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid()) { + QVariantMap requestedUser = knownUsers.values().value(index.row()).toMap(); + switch (static_cast(role)) { + case KnownUserRole::RoleDisplay: return requestedUser; + case KnownUserRole::RoleUserId: return requestedUser.value("id"); + case KnownUserRole::RoleTitle: return QString(requestedUser.value("first_name").toString() + " " + requestedUser.value("last_name").toString()).trimmed(); + case KnownUserRole::RoleUsername: return requestedUser.value("username"); + case KnownUserRole::RoleUserHandle: return QString("@" + (requestedUser.value("username").toString().isEmpty() ? requestedUser.value("id").toString() : requestedUser.value("username").toString())); + case KnownUserRole::RolePhotoSmall: return requestedUser.value("profile_photo").toMap().value("small"); + case KnownUserRole::RoleFilter: return QString(requestedUser.value("first_name").toString() + " " + requestedUser.value("last_name").toString() + " " + requestedUser.value("username").toString()).trimmed(); + } + } + return QVariant(); +} + +void KnownUsersModel::handleUserUpdated(const QString &userId, const QVariantMap &userInformation) +{ + this->knownUsers.insert(userId, userInformation); +} diff --git a/src/knownusersmodel.h b/src/knownusersmodel.h new file mode 100644 index 0000000..fc011f0 --- /dev/null +++ b/src/knownusersmodel.h @@ -0,0 +1,57 @@ +/* + Copyright (C) 2020 Sebastian J. Wolf and other contributors + + This file is part of Fernschreiber. + + Fernschreiber 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 3 of the License, or + (at your option) any later version. + + Fernschreiber 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 Fernschreiber. If not, see . +*/ + +#ifndef KNOWNUSERSMODEL_H +#define KNOWNUSERSMODEL_H + +#include +#include +#include "tdlibwrapper.h" + +class KnownUsersModel : public QAbstractListModel +{ + Q_OBJECT +public: + + enum KnownUserRole { + RoleDisplay = Qt::DisplayRole, + RoleUserId, + RoleTitle, + RoleUsername, + RoleUserHandle, + RolePhotoSmall, + RoleFilter + }; + + KnownUsersModel(TDLibWrapper *tdLibWrapper, QObject *parent = nullptr); + + virtual QHash roleNames() const override; + virtual int rowCount(const QModelIndex &) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void handleUserUpdated(const QString &userId, const QVariantMap &userInformation); + +private: + TDLibWrapper *tdLibWrapper; + QVariantMap knownUsers; + +}; + +#endif // KNOWNUSERSMODEL_H diff --git a/src/tdlibwrapper.cpp b/src/tdlibwrapper.cpp index 2ea1c91..64f2edf 100644 --- a/src/tdlibwrapper.cpp +++ b/src/tdlibwrapper.cpp @@ -29,6 +29,9 @@ #include #include #include +#include +#include +#include #define DEBUG_MODULE TDLibWrapper #include "debuglog.h" @@ -316,6 +319,18 @@ void TDLibWrapper::unpinMessage(const QString &chatId) this->sendRequest(requestObject); } +static bool compareReplacements(const QVariant &replacement1, const QVariant &replacement2) +{ + const QVariantMap replacementMap1 = replacement1.toMap(); + const QVariantMap replacementMap2 = replacement2.toMap(); + + if (replacementMap1.value("startIndex").toInt() < replacementMap2.value("startIndex").toInt()) { + return true; + } else { + return false; + } +} + void TDLibWrapper::sendTextMessage(const QString &chatId, const QString &message, const QString &replyToMessageId) { LOG("Sending text message" << chatId << message << replyToMessageId); @@ -327,8 +342,50 @@ void TDLibWrapper::sendTextMessage(const QString &chatId, const QString &message } QVariantMap inputMessageContent; inputMessageContent.insert(_TYPE, "inputMessageText"); + + // Postprocess message (e.g. for @-mentioning) + QString processedMessage = message; + QVariantList replacements; + QRegularExpression atMentionIdRegex("\\@(\\d+)\\(([^\\)]+)\\)"); + QRegularExpressionMatchIterator atMentionIdMatchIterator = atMentionIdRegex.globalMatch(processedMessage); + while (atMentionIdMatchIterator.hasNext()) { + QRegularExpressionMatch nextAtMentionId = atMentionIdMatchIterator.next(); + LOG("@Mentioning with user ID! Start Index: " << nextAtMentionId.capturedStart(0) << ", length: " << nextAtMentionId.capturedLength(0) << ", user ID: " << nextAtMentionId.captured(1) << ", plain text: " << nextAtMentionId.captured(2)); + QVariantMap replacement; + replacement.insert("startIndex", nextAtMentionId.capturedStart(0)); + replacement.insert("length", nextAtMentionId.capturedLength(0)); + replacement.insert("userId", nextAtMentionId.captured(1)); + replacement.insert("plainText", nextAtMentionId.captured(2)); + replacements.append(replacement); + } + QVariantMap formattedText; - formattedText.insert("text", message); + + if (!replacements.isEmpty()) { + QVariantList entities; + std::sort(replacements.begin(), replacements.end(), compareReplacements); + QListIterator replacementsIterator(replacements); + int offsetCorrection = 0; + while (replacementsIterator.hasNext()) { + QVariantMap nextReplacement = replacementsIterator.next().toMap(); + int replacementStartOffset = nextReplacement.value("startIndex").toInt(); + int replacementLength = nextReplacement.value("length").toInt(); + QString replacementPlainText = nextReplacement.value("plainText").toString(); + processedMessage = processedMessage.replace(replacementStartOffset - offsetCorrection, replacementLength, replacementPlainText); + QVariantMap entity; + entity.insert("offset", replacementStartOffset - offsetCorrection); + entity.insert("length", replacementPlainText.length()); + QVariantMap entityType; + entityType.insert(_TYPE, "textEntityTypeMentionName"); + entityType.insert("user_id", nextReplacement.value("userId").toString()); + entity.insert("type", entityType); + entities.append(entity); + offsetCorrection += replacementLength - replacementPlainText.length(); + } + formattedText.insert("entities", entities); + } + + formattedText.insert("text", processedMessage); formattedText.insert(_TYPE, "formattedText"); inputMessageContent.insert("text", formattedText); requestObject.insert("input_message_content", inputMessageContent);