qtgrpc/examples/grpc/chat/client/chatengine.cpp

263 lines
8.0 KiB
C++

// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "chatengine.h"
#include "chatmessages.qpb.h"
#include "clientworker.h"
#include <QtGui/QClipboard>
#include <QtGui/QDesktopServices>
#include <QtGui/QGuiApplication>
#include <QtGui/QImage>
#include <QtNetwork/QHostAddress>
#include <QtNetwork/QNetworkInterface>
#include <QtCore/QDateTime>
#include <QtCore/QFile>
#include <QtCore/QList>
#include <QtCore/QUrl>
ChatEngine::ChatEngine(QObject *parent)
: QObject(parent), m_clientWorker(new ClientWorker()), m_clipboard(QGuiApplication::clipboard())
{
initialize();
#if QT_CONFIG(ssl)
setHostUri(QUrl("https://localhost:65002"));
#else
setHostUri(QUrl("http://localhost:65003"));
#endif
//! [client-3]
m_clientWorker->moveToThread(&m_clientThread);
m_clientThread.start();
connect(&m_clientThread, &QThread::finished, m_clientWorker, &QObject::deleteLater);
connect(m_clientWorker, &ClientWorker::registerFinished, this, &ChatEngine::registerFinished);
connect(m_clientWorker, &ClientWorker::chatError, this, &ChatEngine::chatError);
//! [client-3]
connect(m_clientWorker, &ClientWorker::chatStreamFinished, this,
[this](const QGrpcStatus &status) {
initialize();
emit chatStreamFinished(status);
});
connect(m_clientWorker, &ClientWorker::chatStateChanged, this,
[this](Backend::ChatState state) {
if (state == Backend::ChatState::Connected) {
m_inactivityTimer.start();
m_userStatus = chat::UserStatus::Type::ACTIVE;
}
m_chatState = state;
emit chatStateChanged();
});
connect(m_clientWorker, &ClientWorker::chatStreamMessageReceived, this,
[this](const chat::ChatMessage &message) {
// The client worker already does some message pre-processing so that
// we just have to care about updating the models correctly.
bool newUser = m_userStatusModel->updateUserStatus(message);
if (message.hasUserStatus()
&& message.userStatus().type() == chat::UserStatus::Type::LOGIN) {
if (!newUser)
return; // don't append reconnects
if (message.username() == username())
return; // don't append our own login
}
m_chatMessageModel->appendMessage(message);
});
// Simple mechanism to detect user absence from the application
m_inactivityTimer.setInterval(120'000); // Inactive after 2min
connect(&m_inactivityTimer, &QTimer::timeout, this, [this]() {
setUserStatus(chat::UserStatus::Type::INACTIVE);
});
}
ChatEngine::~ChatEngine()
{
m_clientThread.quit();
m_clientThread.wait();
}
//! [client-4a]
void ChatEngine::registerUser(const chat::Credentials &credentials)
{
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::registerUser, credentials);
}
//! [client-4a]
void ChatEngine::login(const chat::Credentials &credentials)
{
m_userCredentials = credentials;
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::login, credentials);
}
void ChatEngine::logout()
{
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::logout);
}
//! [client-7a]
void ChatEngine::sendText(const QString &message)
{
if (message.trimmed().isEmpty())
return;
if (auto request = m_clientWorker->createMessage()) {
chat::TextMessage tmsg;
tmsg.setContent(message.toUtf8());
request->setText(std::move(tmsg));
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendMessage, *request);
m_chatMessageModel->appendMessage(*request);
}
}
//! [client-7a]
void ChatEngine::sendFile(const QUrl &url)
{
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendFile, url);
}
void ChatEngine::sendFiles(const QList<QUrl> &urls)
{
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendFiles, urls);
}
bool ChatEngine::sendFilesFromClipboard()
{
if (!m_clipboard)
return false;
const QMimeData *mime = m_clipboard->mimeData();
if (!mime)
return false;
if (mime->hasImage()) {
auto img = mime->imageData().value<QImage>();
if (img.isNull()) {
qDebug("Received empty clipbaord image");
return false;
}
const auto downloadDir = m_clientWorker->getValidDownloadDir();
if (!downloadDir)
return false;
const auto path = downloadDir->path() + QDir::separator() + "clipboard_"
+ QUuid::createUuid().toString() + ".png";
if (!img.save(path)) {
qWarning() << "Couldn't write clipboard image: " << path;
return false;
}
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendFile,
QUrl::fromLocalFile(path));
return true;
} else if (mime->hasUrls()) {
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendFiles, mime->urls());
return true;
}
return false;
}
void ChatEngine::openUrl(const QByteArray &url)
{
QDesktopServices::openUrl(QUrl(url));
}
QList<QString> ChatEngine::findLocalIps()
{
QList<QString> out;
const QList<QHostAddress> addresses = QNetworkInterface::allAddresses();
for (const QHostAddress &address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol && !address.isLoopback())
out.append(address.toString());
}
return out;
}
Q_INVOKABLE QString ChatEngine::getSchemeSymbol()
{
#ifdef USE_EMOJI_FONT
const auto s = m_hostUri.scheme();
if (s == "https")
return "🛡️";
return "🔓";
#else
return "";
#endif
}
void ChatEngine::setHostUri(const QUrl &hostUri)
{
if (m_hostUri == hostUri)
return;
m_hostUri = hostUri;
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::setHostUri, hostUri);
emit hostUriChanged();
}
bool ChatEngine::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::MouseMove
|| event->type() == QEvent::TouchBegin || event->type() == QEvent::Enter) {
if (m_userStatus == chat::UserStatus::Type::INACTIVE)
setUserStatus(chat::UserStatus::Type::ACTIVE);
// Restart timer only if user is active
if (m_userStatus == chat::UserStatus::Type::ACTIVE)
m_inactivityTimer.start();
}
if (event->type() == QEvent::Close) {
// Log out active session and delay quitting to ensure graceful shutdown.
logout();
QTimer::singleShot(125, QCoreApplication::quit);
return true;
}
return QObject::eventFilter(obj, event);
}
void ChatEngine::initialize()
{
m_userCredentials = {};
m_userStatus = {};
m_userStatusModel = std::make_unique<UserStatusModel>();
m_chatMessageModel = std::make_unique<ChatMessageModel>();
connect(m_clientWorker, &ClientWorker::fileMessageSent, m_chatMessageModel.get(),
&ChatMessageModel::appendMessage);
}
void ChatEngine::setUserStatus(chat::UserStatus::Type status)
{
if (m_userStatus == status || m_userStatus == chat::UserStatus::Type::NONE)
return;
switch (status) {
case chat::UserStatus::Type::ACTIVE:
m_inactivityTimer.start();
break;
case chat::UserStatus::Type::INACTIVE:
m_inactivityTimer.stop();
break;
default:
return;
}
m_userStatus = status;
auto request = m_clientWorker->createMessage();
if (!request)
return;
chat::UserStatus userStatus;
userStatus.setType(m_userStatus);
request->setUserStatus(std::move(userStatus));
// Send status update to the server and update all models.
m_userStatusModel->updateUserStatus(*request);
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendMessage, *request);
m_chatMessageModel->appendMessage(*request);
}