Migrate to mtxclient for the http calls

This commit is contained in:
Konstantinos Sideris 2018-06-09 16:03:14 +03:00
parent 1366b01790
commit b89257a34b
44 changed files with 1624 additions and 2430 deletions

View File

@ -53,7 +53,6 @@ include(LMDB)
# Discover Qt dependencies.
#
find_package(Qt5Widgets REQUIRED)
find_package(Qt5Network REQUIRED)
find_package(Qt5LinguistTools REQUIRED)
find_package(Qt5Concurrent REQUIRED)
find_package(Qt5Svg REQUIRED)
@ -181,6 +180,7 @@ set(SRC_FILES
src/Community.cc
src/InviteeItem.cc
src/LoginPage.cc
src/Logging.cpp
src/MainWindow.cc
src/MatrixClient.cc
src/QuickSwitcher.cc
@ -287,7 +287,6 @@ qt5_wrap_cpp(MOC_HEADERS
include/LoginPage.h
include/MainWindow.h
include/InviteeItem.h
include/MatrixClient.h
include/QuickSwitcher.h
include/RegisterPage.h
include/RoomInfoListItem.h
@ -314,7 +313,6 @@ set(COMMON_LIBS
MatrixStructs::MatrixStructs
MatrixClient::MatrixClient
Qt5::Widgets
Qt5::Network
Qt5::Svg
Qt5::Concurrent)

2
deps/CMakeLists.txt vendored
View File

@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs)
set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de)
set(MTXCLIENT_URL https://github.com/mujx/mtxclient)
set(MTXCLIENT_TAG 219d2a8887376122e76ba0f64c0cc9935f62f308)
set(MTXCLIENT_TAG 57f56d1fe73989dbe041a7ac0a28bf2e3286bf98)
set(OLM_URL https://git.matrix.org/git/olm.git)
set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae)

View File

@ -15,6 +15,7 @@ ExternalProject_Add(
CONFIGURE_COMMAND ""
BUILD_COMMAND ${MAKE_CMD} static
INSTALL_COMMAND
mkdir -p ${DEPS_INSTALL_DIR}/lib &&
cp -R ${DEPS_BUILD_DIR}/olm/include ${DEPS_INSTALL_DIR} &&
cp ${DEPS_BUILD_DIR}/olm/build/libolm.a ${DEPS_INSTALL_DIR}/lib
)

View File

@ -8,6 +8,8 @@ ExternalProject_Add(
SOURCE_DIR ${DEPS_BUILD_DIR}/spdlog
CONFIGURE_COMMAND ${CMAKE_COMMAND}
-DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR}
-DSPDLOG_BUILD_EXAMPLES=0
-DSPDLOG_BUILD_TESTING=0
-DCMAKE_BUILD_TYPE=Release
${DEPS_BUILD_DIR}/spdlog
)

View File

@ -20,15 +20,17 @@
#include <QImage>
#include <functional>
class AvatarProvider : public QObject
class AvatarProxy : public QObject
{
Q_OBJECT
public:
//! The callback is called with the downloaded avatar for the given user
//! or the avatar is downloaded first and then saved for re-use.
static void resolve(const QString &room_id,
const QString &userId,
QObject *receiver,
std::function<void(QImage)> callback);
signals:
void avatarDownloaded(const QByteArray &data);
};
using AvatarCallback = std::function<void(QImage)>;
namespace AvatarProvider {
void
resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb);
}

View File

@ -192,7 +192,7 @@ public:
void saveState(const mtx::responses::Sync &res);
bool isInitialized() const;
QString nextBatchToken() const;
std::string nextBatchToken() const;
void deleteData();
@ -237,6 +237,7 @@ public:
{
return image(QString::fromStdString(url));
}
void saveImage(const std::string &url, const std::string &data);
void saveImage(const QString &url, const QByteArray &data);
RoomInfo singleRoomInfo(const std::string &room_id);

View File

@ -17,6 +17,8 @@
#pragma once
#include <atomic>
#include <QFrame>
#include <QHBoxLayout>
#include <QMap>
@ -50,9 +52,6 @@ constexpr int CONSENSUS_TIMEOUT = 1000;
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
constexpr int TYPING_REFRESH_TIMEOUT = 10000;
Q_DECLARE_METATYPE(mtx::responses::Rooms)
Q_DECLARE_METATYPE(std::vector<std::string>)
class ChatPage : public QWidget
{
Q_OBJECT
@ -71,7 +70,37 @@ public:
QSharedPointer<UserSettings> userSettings() { return userSettings_; }
void deleteConfigs();
public slots:
void leaveRoom(const QString &room_id);
signals:
void connectionLost();
void connectionRestored();
void notificationsRetrieved(const mtx::responses::Notifications &);
void uploadFailed(const QString &msg);
void imageUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
qint64 dsize);
void fileUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
qint64 dsize);
void audioUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
qint64 dsize);
void videoUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
qint64 dsize);
void contentLoaded();
void closing();
void changeWindowTitle(const QString &msg);
@ -82,30 +111,44 @@ signals:
void showOverlayProgressBar();
void startConsesusTimer();
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void ownProfileOk();
void setUserDisplayName(const QString &name);
void setUserAvatar(const QImage &avatar);
void loggedOut();
void trySyncCb();
void tryInitialSyncCb();
void leftRoom(const QString &room_id);
void initializeRoomList(QMap<QString, RoomInfo>);
void initializeViews(const mtx::responses::Rooms &rooms);
void initializeEmptyViews(const std::vector<std::string> &rooms);
void syncUI(const mtx::responses::Rooms &rooms);
void continueSync(const QString &next_batch);
void syncRoomlist(const std::map<QString, RoomInfo> &updates);
void syncTopBar(const std::map<QString, RoomInfo> &updates);
void dropToLoginPageCb(const QString &msg);
private slots:
void showUnreadMessageNotification(int count);
void updateTopBarAvatar(const QString &roomid, const QPixmap &img);
void updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name);
void updateOwnCommunitiesInfo(const QList<QString> &own_communities);
void initialSyncCompleted(const mtx::responses::Sync &response);
void syncCompleted(const mtx::responses::Sync &response);
void changeTopRoomInfo(const QString &room_id);
void logout();
void removeRoom(const QString &room_id);
//! Handles initial sync failures.
void retryInitialSync(int status_code = -1);
void dropToLoginPage(const QString &msg);
void joinRoom(const QString &room);
void createRoom(const mtx::requests::CreateRoom &req);
void sendTypingNotifications();
private:
static ChatPage *instance_;
void tryInitialSync();
void trySync();
//! Check if the given room is currently open.
bool isRoomActive(const QString &room_id)
{
@ -161,8 +204,8 @@ private:
// Safety net if consensus is not possible or too slow.
QTimer *showContentTimer_;
QTimer *consensusTimer_;
QTimer *syncTimeoutTimer_;
QTimer *initialSyncTimer_;
QTimer connectivityTimer_;
std::atomic_bool isConnected_;
QString current_room_;
QString current_community_;

View File

@ -23,12 +23,14 @@ public:
signals:
void communityChanged(const QString &id);
void avatarRetrieved(const QString &id, const QPixmap &img);
public slots:
void updateCommunityAvatar(const QString &id, const QPixmap &img);
void highlightSelectedCommunity(const QString &id);
private:
void fetchCommunityAvatar(const QString &id, const QString &avatarUrl);
void addGlobalItem() { addCommunity(QSharedPointer<Community>(new Community), "world"); }
//! Check whether or not a community id is currently managed.

18
include/Logging.hpp Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <memory>
#include <spdlog/spdlog.h>
namespace log {
void
init(const std::string &file);
std::shared_ptr<spdlog::logger>
main();
std::shared_ptr<spdlog::logger>
net();
std::shared_ptr<spdlog::logger>
db();
}

View File

@ -28,6 +28,12 @@ class OverlayModal;
class RaisedButton;
class TextField;
namespace mtx {
namespace responses {
struct Login;
}
}
class LoginPage : public QWidget
{
Q_OBJECT
@ -42,12 +48,19 @@ signals:
void loggingIn();
void errorOccurred();
//! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err);
void loginErrorCb(const QString &err);
void versionOkCb();
void loginOk(const mtx::responses::Login &res);
protected:
void paintEvent(QPaintEvent *event) override;
public slots:
// Displays errors produced during the login.
void loginError(QString msg) { error_label_->setText(msg); }
void loginError(const QString &msg) { error_label_->setText(msg); }
private slots:
// Callback for the back button.
@ -63,13 +76,25 @@ private slots:
void onServerAddressEntered();
// Callback for errors produced during server probing
void versionError(QString error_message);
void versionError(const QString &error_message);
// Callback for successful server probing
void versionSuccess();
void versionOk();
private:
bool isMatrixIdValid();
void checkHomeserverVersion();
std::string initialDeviceName()
{
#if defined(Q_OS_MAC)
return "nheko on macOS";
#elif defined(Q_OS_LINUX)
return "nheko on Linux";
#elif defined(Q_OS_WIN)
return "nheko on Windows";
#else
return "nheko";
#endif
}
QVBoxLayout *top_layout_;

View File

@ -59,6 +59,7 @@ class MainWindow : public QMainWindow
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
static MainWindow *instance() { return instance_; };
void saveCurrentWindowSize();
@ -96,7 +97,7 @@ private slots:
void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); }
//! Show the chat page and start communicating with the given access token.
void showChatPage(QString user_id, QString home_server, QString token);
void showChatPage();
void showOverlayProgressBar();
void removeOverlayProgressBar();

View File

@ -1,287 +1,25 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFileInfo>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <memory>
#include <mtx.hpp>
#include <mtx/errors.hpp>
#include <QMetaType>
class DownloadMediaProxy : public QObject
{
Q_OBJECT
signals:
void imageDownloaded(const QPixmap &data);
void fileDownloaded(const QByteArray &data);
void avatarDownloaded(const QImage &img);
};
class StateEventProxy : public QObject
{
Q_OBJECT
signals:
void stateEventSent();
void stateEventError(const QString &msg);
};
#include <mtx/responses.hpp>
#include <mtxclient/http/client.hpp>
Q_DECLARE_METATYPE(mtx::responses::Login)
Q_DECLARE_METATYPE(mtx::responses::Messages)
Q_DECLARE_METATYPE(mtx::responses::Notifications)
Q_DECLARE_METATYPE(mtx::responses::Rooms)
Q_DECLARE_METATYPE(mtx::responses::Sync)
/*
* MatrixClient provides the high level API to communicate with
* a Matrix homeserver. All the responses are returned through signals.
*/
class MatrixClient : public QNetworkAccessManager
{
Q_OBJECT
public:
MatrixClient(QObject *parent = 0);
// Client API.
void initialSync() noexcept;
void sync() noexcept;
template<class EventBody, mtx::events::EventType EventT>
std::shared_ptr<StateEventProxy> sendStateEvent(const EventBody &body,
const QString &roomId,
const QString &stateKey = "");
void sendRoomMessage(mtx::events::MessageType ty,
int txnId,
const QString &roomid,
const QString &msg,
const QString &mime,
uint64_t media_size,
const QString &url = "") noexcept;
void login(const QString &username, const QString &password) noexcept;
void registerUser(const QString &username,
const QString &password,
const QString &server,
const QString &session = "") noexcept;
void versions() noexcept;
void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url);
//! Download user's avatar.
QSharedPointer<DownloadMediaProxy> fetchUserAvatar(const QUrl &avatarUrl);
void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl);
void fetchCommunityProfile(const QString &communityId);
void fetchCommunityRooms(const QString &communityId);
QSharedPointer<DownloadMediaProxy> downloadImage(const QUrl &url);
QSharedPointer<DownloadMediaProxy> downloadFile(const QUrl &url);
void messages(const QString &room_id, const QString &from_token, int limit = 30) noexcept;
void uploadImage(const QString &roomid,
const QString &filename,
const QSharedPointer<QIODevice> data);
void uploadFile(const QString &roomid,
const QString &filename,
const QSharedPointer<QIODevice> data);
void uploadAudio(const QString &roomid,
const QString &filename,
const QSharedPointer<QIODevice> data);
void uploadVideo(const QString &roomid,
const QString &filename,
const QSharedPointer<QIODevice> data);
void uploadFilter(const QString &filter) noexcept;
void joinRoom(const QString &roomIdOrAlias);
void leaveRoom(const QString &roomId);
void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000);
void removeTypingNotification(const QString &roomid);
void readEvent(const QString &room_id, const QString &event_id);
void redactEvent(const QString &room_id, const QString &event_id);
void inviteUser(const QString &room_id, const QString &user);
void createRoom(const mtx::requests::CreateRoom &request);
void getNotifications() noexcept;
QUrl getHomeServer() { return server_; };
int transactionId() { return txn_id_; };
int incrementTransactionId() { return ++txn_id_; };
void reset() noexcept;
public slots:
void getOwnProfile() noexcept;
void getOwnCommunities() noexcept;
void logout() noexcept;
void setServer(const QString &server)
{
server_ = QUrl(QString("%1://%2").arg(serverProtocol_).arg(server));
};
void setAccessToken(const QString &token) { token_ = token; };
void setNextBatchToken(const QString &next_batch) { next_batch_ = next_batch; };
signals:
void loginError(const QString &error);
void registerError(const QString &error);
void registrationFlow(const QString &user,
const QString &pass,
const QString &server,
const QString &session);
void versionError(const QString &error);
void loggedOut();
void invitedUser(const QString &room_id, const QString &user);
void roomCreated(const QString &room_id);
void loginSuccess(const QString &userid, const QString &homeserver, const QString &token);
void registerSuccess(const QString &userid,
const QString &homeserver,
const QString &token);
void versionSuccess();
void uploadFailed(int statusCode, const QString &msg);
void imageUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size);
void fileUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size);
void audioUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size);
void videoUploaded(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size);
void roomAvatarRetrieved(const QString &roomid,
const QPixmap &img,
const QString &url,
const QByteArray &data);
void userAvatarRetrieved(const QString &userId, const QImage &img);
void communityAvatarRetrieved(const QString &communityId, const QPixmap &img);
void communityProfileRetrieved(const QString &communityId, const QJsonObject &profile);
void communityRoomsRetrieved(const QString &communityId, const QJsonObject &rooms);
// Returned profile data for the user's account.
void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name);
void getOwnCommunitiesResponse(const QList<QString> &own_communities);
void initialSyncCompleted(const mtx::responses::Sync &response);
void initialSyncFailed(int status_code = -1);
void syncCompleted(const mtx::responses::Sync &response);
void syncFailed(const QString &msg);
void joinFailed(const QString &msg);
void messageSent(const QString &event_id, const QString &roomid, int txn_id);
void messageSendFailed(const QString &roomid, int txn_id);
void emoteSent(const QString &event_id, const QString &roomid, int txn_id);
void messagesRetrieved(const QString &room_id, const mtx::responses::Messages &msgs);
void joinedRoom(const QString &room_id);
void leftRoom(const QString &room_id);
void roomCreationFailed(const QString &msg);
void redactionFailed(const QString &error);
void redactionCompleted(const QString &room_id, const QString &event_id);
void invalidToken();
void syncError(const QString &error);
void notificationsRetrieved(const mtx::responses::Notifications &notifications);
private:
QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
QJsonObject getUploadReply(QNetworkReply *reply);
void setupAuth(QNetworkRequest &req)
{
req.setRawHeader("Authorization", QString("Bearer %1").arg(token_).toLocal8Bit());
}
// Client API prefix.
QString clientApiUrl_;
// Media API prefix.
QString mediaApiUrl_;
// The Matrix server used for communication.
QUrl server_;
// The access token used for authentication.
QString token_;
// Increasing transaction ID.
int txn_id_;
//! Token to be used for the next sync.
QString next_batch_;
//! http or https (default).
QString serverProtocol_;
//! Filter to be send as filter-param for (initial) /sync requests.
QString filter_;
};
Q_DECLARE_METATYPE(std::string)
Q_DECLARE_METATYPE(std::vector<std::string>);
namespace http {
//! Initialize the http module
void
init();
//! Retrieve the client instance.
MatrixClient *
namespace v2 {
mtx::http::Client *
client();
}
template<class EventBody, mtx::events::EventType EventT>
std::shared_ptr<StateEventProxy>
MatrixClient::sendStateEvent(const EventBody &body, const QString &roomId, const QString &stateKey)
{
QUrl endpoint(server_);
endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/state/%2/%3")
.arg(roomId)
.arg(QString::fromStdString(to_string(EventT)))
.arg(stateKey));
QNetworkRequest request(QString(endpoint.toEncoded()));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
setupAuth(request);
auto proxy = std::shared_ptr<StateEventProxy>(new StateEventProxy,
[](StateEventProxy *p) { p->deleteLater(); });
auto serializedBody = nlohmann::json(body).dump();
auto reply = put(request, QByteArray(serializedBody.data(), serializedBody.size()));
connect(reply, &QNetworkReply::finished, this, [reply, proxy]() {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
auto data = reply->readAll();
if (status == 0 || status >= 400) {
try {
mtx::errors::Error res = nlohmann::json::parse(data);
emit proxy->stateEventError(QString::fromStdString(res.error));
} catch (const std::exception &e) {
emit proxy->stateEventError(QString::fromStdString(e.what()));
}
return;
}
try {
mtx::responses::EventId res = nlohmann::json::parse(data);
emit proxy->stateEventSent();
} catch (const std::exception &e) {
emit proxy->stateEventError(QString::fromStdString(e.what()));
}
});
return proxy;
//! Initialize the http module
void
init();
}

View File

@ -44,6 +44,11 @@ signals:
void backButtonClicked();
void errorOccurred();
void registering();
void registerOk();
void registerErrorCb(const QString &msg);
void registrationFlow(const std::string &user,
const std::string &pass,
const std::string &session);
private slots:
void onBackButtonClicked();

View File

@ -60,6 +60,8 @@ signals:
void acceptInvite(const QString &room_id);
void declineInvite(const QString &room_id);
void roomAvatarChanged(const QString &room_id, const QPixmap &img);
void joinRoom(const QString &room_id);
void updateRoomAvatarCb(const QString &room_id, const QPixmap &img);
public slots:
void updateRoomAvatar(const QString &roomid, const QPixmap &img);

View File

@ -129,6 +129,16 @@ public:
QColor borderColor() const { return borderColor_; }
void setBorderColor(QColor &color) { borderColor_ = color; }
void disableInput()
{
input_->setEnabled(false);
input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect..."));
}
void enableInput()
{
input_->setEnabled(true);
input_->setPlaceholderText(tr("Write a message..."));
}
public slots:
void openFileSelection();

View File

@ -12,7 +12,7 @@ class ReCaptcha : public QWidget
Q_OBJECT
public:
ReCaptcha(const QString &server, const QString &session, QWidget *parent = nullptr);
ReCaptcha(const QString &session, QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;

View File

@ -30,6 +30,9 @@ public:
signals:
void nameChanged(const QString &roomName);
void nameEventSentCb(const QString &newName);
void topicEventSentCb();
void stateEventErrorCb(const QString &msg);
private:
QString roomId_;

View File

@ -197,12 +197,24 @@ public:
void sendReadReceipt() const
{
if (!event_id_.isEmpty())
http::client()->readEvent(room_id_, event_id_);
http::v2::client()->read_event(
room_id_.toStdString(),
event_id_.toStdString(),
[this](mtx::http::RequestErr err) {
if (err) {
qWarning() << QString("failed to read_event (%1, %2)")
.arg(room_id_, event_id_);
}
});
}
//! Add a user avatar for this event.
void addAvatar();
signals:
void eventRedacted(const QString &event_id);
void redactionFailed(const QString &msg);
protected:
void paintEvent(QPaintEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;

View File

@ -18,7 +18,6 @@
#pragma once
#include <QApplication>
#include <QDebug>
#include <QLayout>
#include <QList>
#include <QQueue>
@ -42,31 +41,13 @@ struct DescInfo;
struct PendingMessage
{
mtx::events::MessageType ty;
int txn_id;
std::string txn_id;
QString body;
QString filename;
QString mime;
uint64_t media_size;
QString event_id;
TimelineItem *widget;
PendingMessage(mtx::events::MessageType ty,
int txn_id,
QString body,
QString filename,
QString mime,
uint64_t media_size,
QString event_id,
TimelineItem *widget)
: ty(ty)
, txn_id(txn_id)
, body(body)
, filename(filename)
, mime(mime)
, media_size(media_size)
, event_id(event_id)
, widget(widget)
{}
};
// In which place new TimelineItems should be inserted.
@ -129,7 +110,7 @@ public:
const QString &filename,
const QString &mime,
uint64_t size);
void updatePendingMessage(int txn_id, QString event_id);
void updatePendingMessage(const std::string &txn_id, const QString &event_id);
void scrollDown();
QLabel *createDateSeparator(QDateTime datetime);
@ -142,18 +123,21 @@ public slots:
void fetchHistory();
// Add old events at the top of the timeline.
void addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs);
void addBackwardsEvents(const mtx::responses::Messages &msgs);
// Whether or not the initial batch has been loaded.
bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; }
void handleFailedMessage(int txnid);
void handleFailedMessage(const std::string &txn_id);
private slots:
void sendNextPendingMessage();
signals:
void updateLastTimelineMessage(const QString &user, const DescInfo &info);
void messagesRetrieved(const mtx::responses::Messages &res);
void messageFailed(const std::string &txn_id);
void messageSent(const std::string &txn_id, const QString &event_id);
protected:
void paintEvent(QPaintEvent *event) override;
@ -165,6 +149,13 @@ private:
QWidget *relativeWidget(TimelineItem *item, int dt) const;
//! Callback for all message sending.
void sendRoomMessageHandler(const std::string &txn_id,
const mtx::responses::EventId &res,
mtx::http::RequestErr err);
//! Call the /messages endpoint to fill the timeline.
void getMessages();
//! HACK: Fixing layout flickering when adding to the bottom
//! of the timeline.
void pushTimelineItem(TimelineItem *item)
@ -230,8 +221,10 @@ private:
uint64_t origin_server_ts,
TimelineDirection direction);
bool isPendingMessage(const QString &txnid, const QString &sender, const QString &userid);
void removePendingMessage(const QString &txnid);
bool isPendingMessage(const std::string &txn_id,
const QString &sender,
const QString &userid);
void removePendingMessage(const std::string &txn_id);
bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
@ -320,9 +313,15 @@ TimelineView::addUserMessage(const QString &url,
// Keep track of the sender and the timestamp of the current message.
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
int txn_id = http::client()->incrementTransactionId();
PendingMessage message;
message.ty = MsgType;
message.txn_id = mtx::client::utils::random_token();
message.body = url;
message.filename = trimmed;
message.mime = mime;
message.media_size = size;
message.widget = view_item;
PendingMessage message(MsgType, txn_id, url, trimmed, mime, size, "", view_item);
handleNewUserMessage(message);
}
@ -351,10 +350,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
const auto event_id = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
const auto txn_id = event.unsigned_data.transaction_id;
if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
isDuplicate(event_id)) {
removePendingMessage(txnid);
removePendingMessage(txn_id);
return nullptr;
}
@ -376,10 +375,10 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
const auto event_id = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
const auto txn_id = event.unsigned_data.transaction_id;
if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
isDuplicate(event_id)) {
removePendingMessage(txnid);
removePendingMessage(txn_id);
return nullptr;
}

View File

@ -56,6 +56,8 @@ signals:
void updateRoomsLastMessage(const QString &user, const DescInfo &info);
public slots:
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void setHistoryView(const QString &room_id);
void queueTextMessage(const QString &msg);
void queueEmoteMessage(const QString &msg);
@ -80,10 +82,6 @@ public slots:
const QString &mime,
uint64_t dsize);
private slots:
void messageSent(const QString &eventid, const QString &roomid, int txnid);
void messageSendFailed(const QString &roomid, int txnid);
private:
//! Check if the given room id is managed by a TimelineView.
bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }

View File

@ -69,9 +69,14 @@ protected:
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
signals:
void fileDownloadedCb(const QByteArray &data);
private slots:
void fileDownloaded(const QByteArray &data);
private:
void init();
void fileDownloaded(const QByteArray &data);
enum class AudioState
{

View File

@ -52,15 +52,20 @@ public:
QColor iconColor() const { return iconColor_; }
QColor backgroundColor() const { return backgroundColor_; }
signals:
void fileDownloadedCb(const QByteArray &data);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private slots:
void fileDownloaded(const QByteArray &data);
private:
void openUrl();
void init();
void fileDownloaded(const QByteArray &data);
QUrl url_;
QString text_;

View File

@ -40,13 +40,17 @@ public:
uint64_t size,
QWidget *parent = nullptr);
void setImage(const QPixmap &image);
QSize sizeHint() const override;
public slots:
//! Show a save as dialog for the image.
void saveAs();
void setImage(const QPixmap &image);
void saveImage(const QString &filename, const QByteArray &data);
signals:
void imageDownloaded(const QPixmap &img);
void imageSaved(const QString &filename, const QByteArray &data);
protected:
void paintEvent(QPaintEvent *event) override;
@ -57,7 +61,9 @@ protected:
bool isInteractive_ = true;
private:
void init();
void openUrl();
void downloadMedia(const QUrl &url);
int max_width_ = 500;
int max_height_ = 300;

View File

@ -16,17 +16,17 @@
*/
#include <QBuffer>
#include <QtConcurrent>
#include <memory>
#include "AvatarProvider.h"
#include "Cache.h"
#include "Logging.hpp"
#include "MatrixClient.h"
namespace AvatarProvider {
void
AvatarProvider::resolve(const QString &room_id,
const QString &user_id,
QObject *receiver,
std::function<void(QImage)> callback)
resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback)
{
const auto key = QString("%1 %2").arg(room_id).arg(user_id);
const auto avatarUrl = Cache::avatarUrl(room_id, user_id);
@ -43,24 +43,30 @@ AvatarProvider::resolve(const QString &room_id,
return;
}
auto proxy = http::client()->fetchUserAvatar(avatarUrl);
auto proxy = std::make_shared<AvatarProxy>();
QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded,
receiver,
[callback](const QByteArray &data) { callback(QImage::fromData(data)); });
if (proxy.isNull())
return;
mtx::http::ThumbOpts opts;
opts.mxc_url = avatarUrl.toStdString();
connect(proxy.data(),
&DownloadMediaProxy::avatarDownloaded,
receiver,
[user_id, proxy, callback, avatarUrl](const QImage &img) {
proxy->deleteLater();
QtConcurrent::run([img, avatarUrl]() {
QByteArray data;
QBuffer buffer(&data);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "PNG");
http::v2::client()->get_thumbnail(
opts,
[opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) {
if (err) {
log::net()->warn("failed to download avatar: {} - ({} {})",
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
cache::client()->saveImage(avatarUrl, data);
});
callback(img);
});
cache::client()->saveImage(opts.mxc_url, res);
auto data = QByteArray(res.data(), res.size());
emit proxy->avatarDownloaded(data);
});
}
}

View File

@ -19,7 +19,6 @@
#include <stdexcept>
#include <QByteArray>
#include <QDebug>
#include <QFile>
#include <QHash>
#include <QStandardPaths>
@ -27,6 +26,7 @@
#include <variant.hpp>
#include "Cache.h"
#include "Logging.hpp"
#include "Utils.h"
//! Should be changed when a breaking change occurs in the cache format.
@ -62,6 +62,14 @@ namespace cache {
void
init(const QString &user_id)
{
qRegisterMetaType<SearchResult>();
qRegisterMetaType<QVector<SearchResult>>();
qRegisterMetaType<RoomMember>();
qRegisterMetaType<RoomSearchResult>();
qRegisterMetaType<RoomInfo>();
qRegisterMetaType<QMap<QString, RoomInfo>>();
qRegisterMetaType<std::map<QString, RoomInfo>>();
if (!instance_)
instance_ = std::make_unique<Cache>(user_id);
}
@ -88,7 +96,7 @@ Cache::Cache(const QString &userId, QObject *parent)
void
Cache::setup()
{
qDebug() << "Setting up cache";
log::db()->debug("setting up cache");
auto statePath = QString("%1/%2/state")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
@ -105,7 +113,7 @@ Cache::setup()
env_.set_max_dbs(1024UL);
if (isInitial) {
qDebug() << "First time initializing LMDB";
log::db()->info("initializing LMDB");
if (!QDir().mkpath(statePath)) {
throw std::runtime_error(
@ -121,7 +129,7 @@ Cache::setup()
std::string(e.what()));
}
qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what();
log::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
QDir stateDir(statePath);
@ -142,29 +150,34 @@ Cache::setup()
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
txn.commit();
qRegisterMetaType<RoomInfo>();
}
void
Cache::saveImage(const QString &url, const QByteArray &image)
Cache::saveImage(const std::string &url, const std::string &img_data)
{
auto key = url.toUtf8();
if (url.empty() || img_data.empty())
return;
try {
auto txn = lmdb::txn::begin(env_);
lmdb::dbi_put(txn,
mediaDb_,
lmdb::val(key.data(), key.size()),
lmdb::val(image.data(), image.size()));
lmdb::val(url.data(), url.size()),
lmdb::val(img_data.data(), img_data.size()));
txn.commit();
} catch (const lmdb::error &e) {
qCritical() << "saveImage:" << e.what();
log::db()->critical("saveImage: {}", e.what());
}
}
void
Cache::saveImage(const QString &url, const QByteArray &image)
{
saveImage(url.toStdString(), std::string(image.constData(), image.length()));
}
QByteArray
Cache::image(lmdb::txn &txn, const std::string &url) const
{
@ -180,7 +193,7 @@ Cache::image(lmdb::txn &txn, const std::string &url) const
return QByteArray(image.data(), image.size());
} catch (const lmdb::error &e) {
qCritical() << "image:" << e.what() << QString::fromStdString(url);
log::db()->critical("image: {}, {}", e.what(), url);
}
return QByteArray();
@ -208,7 +221,7 @@ Cache::image(const QString &url) const
return QByteArray(image.data(), image.size());
} catch (const lmdb::error &e) {
qCritical() << "image:" << e.what() << url;
log::db()->critical("image: {} {}", e.what(), url.toStdString());
}
return QByteArray();
@ -271,7 +284,7 @@ Cache::isInitialized() const
return res;
}
QString
std::string
Cache::nextBatchToken() const
{
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
@ -281,13 +294,13 @@ Cache::nextBatchToken() const
txn.commit();
return QString::fromUtf8(token.data(), token.size());
return std::string(token.data(), token.size());
}
void
Cache::deleteData()
{
qInfo() << "Deleting cache data";
log::db()->info("deleting data");
if (!cacheDirectory_.isEmpty())
QDir(cacheDirectory_).removeRecursively();
@ -309,8 +322,9 @@ Cache::isFormatValid()
std::string stored_version(current_version.data(), current_version.size());
if (stored_version != CURRENT_CACHE_FORMAT_VERSION) {
qWarning() << "Stored format version" << QString::fromStdString(stored_version);
qWarning() << "There are breaking changes in the cache format.";
log::db()->warn("breaking changes in the cache format. stored: {}, current: {}",
stored_version,
CURRENT_CACHE_FORMAT_VERSION);
return false;
}
@ -360,7 +374,7 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
}
} catch (const lmdb::error &e) {
qCritical() << "readReceipts:" << e.what();
log::db()->critical("readReceipts: {}", e.what());
}
return receipts;
@ -410,7 +424,7 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
lmdb::val(merged_receipts.data(), merged_receipts.size()));
} catch (const lmdb::error &e) {
qCritical() << "updateReadReceipts:" << e.what();
log::db()->critical("updateReadReceipts: {}", e.what());
}
}
}
@ -568,9 +582,9 @@ Cache::singleRoomInfo(const std::string &room_id)
return tmp;
} catch (const json::exception &e) {
qWarning()
<< "failed to parse room info:" << QString::fromStdString(room_id)
<< QString::fromStdString(std::string(data.data(), data.size()));
log::db()->warn("failed to parse room info: room_id ({}), {}",
room_id,
std::string(data.data(), data.size()));
}
}
@ -600,9 +614,9 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
room_info.emplace(QString::fromStdString(room), std::move(tmp));
} catch (const json::exception &e) {
qWarning()
<< "failed to parse room info:" << QString::fromStdString(room)
<< QString::fromStdString(std::string(data.data(), data.size()));
log::db()->warn("failed to parse room info: room_id ({}), {}",
room,
std::string(data.data(), data.size()));
}
} else {
// Check if the room is an invite.
@ -615,10 +629,10 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
room_info.emplace(QString::fromStdString(room),
std::move(tmp));
} catch (const json::exception &e) {
qWarning() << "failed to parse room info for invite:"
<< QString::fromStdString(room)
<< QString::fromStdString(
std::string(data.data(), data.size()));
log::db()->warn(
"failed to parse room info for invite: room_id ({}), {}",
room,
std::string(data.data(), data.size()));
}
}
}
@ -703,7 +717,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
return QString::fromStdString(msg.content.url);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.avatar event: {}", e.what());
}
}
@ -726,7 +740,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
cursor.close();
return QString::fromStdString(m.avatar_url);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse member info: {}", e.what());
}
}
@ -753,7 +767,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
if (!msg.content.name.empty())
return QString::fromStdString(msg.content.name);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.name event: {}", e.what());
}
}
@ -768,7 +782,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
if (!msg.content.alias.empty())
return QString::fromStdString(msg.content.alias);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.canonical_alias event: {}",
e.what());
}
}
@ -784,7 +799,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
try {
members.emplace(user_id, json::parse(member_data));
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse member info: {}", e.what());
}
ii++;
@ -828,7 +843,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
json::parse(std::string(event.data(), event.size()));
return msg.content.join_rule;
} catch (const json::exception &e) {
qWarning() << e.what();
log::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
}
}
return JoinRule::Knock;
@ -850,7 +865,7 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
json::parse(std::string(event.data(), event.size()));
return msg.content.guest_access == AccessState::CanJoin;
} catch (const json::exception &e) {
qWarning() << e.what();
log::db()->warn("failed to parse m.room.guest_access event: {}", e.what());
}
}
return false;
@ -874,7 +889,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
if (!msg.content.topic.empty())
return QString::fromStdString(msg.content.topic);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.topic event: {}", e.what());
}
}
@ -897,7 +912,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
json::parse(std::string(event.data(), event.size()));
return QString::fromStdString(msg.content.name);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.name event: {}", e.what());
}
}
@ -914,7 +929,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
return QString::fromStdString(tmp.name);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse member info: {}", e.what());
}
}
@ -939,7 +954,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
json::parse(std::string(event.data(), event.size()));
return QString::fromStdString(msg.content.url);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.avatar event: {}", e.what());
}
}
@ -956,7 +971,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
return QString::fromStdString(tmp.avatar_url);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse member info: {}", e.what());
}
}
@ -981,7 +996,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
json::parse(std::string(event.data(), event.size()));
return QString::fromStdString(msg.content.topic);
} catch (const json::exception &e) {
qWarning() << QString::fromStdString(e.what());
log::db()->warn("failed to parse m.room.topic event: {}", e.what());
}
}
@ -1017,8 +1032,9 @@ Cache::getRoomAvatar(const std::string &room_id)
return QImage();
}
} catch (const json::exception &e) {
qWarning() << "failed to parse room info" << e.what()
<< QString::fromStdString(std::string(response.data(), response.size()));
log::db()->warn("failed to parse room info: {}, {}",
e.what(),
std::string(response.data(), response.size()));
}
if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) {
@ -1054,7 +1070,7 @@ void
Cache::populateMembers()
{
auto rooms = joinedRooms();
qDebug() << "loading" << rooms.size() << "rooms";
log::db()->info("loading {} rooms", rooms.size());
auto txn = lmdb::txn::begin(env_);
@ -1182,7 +1198,7 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_
QString::fromStdString(tmp.name),
QImage::fromData(image(txn, tmp.avatar_url))});
} catch (const json::exception &e) {
qWarning() << e.what();
log::db()->warn("{}", e.what());
}
currentIndex += 1;
@ -1253,7 +1269,7 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
std::min(min_event_level,
(uint16_t)msg.content.state_level(to_string(ty)));
} catch (const json::exception &e) {
qWarning() << "hasEnoughPowerLevel: " << e.what();
log::db()->warn("failed to parse m.room.power_levels event: {}", e.what());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
#include "Cache.h"
#include "CommunitiesList.h"
#include "Logging.hpp"
#include "MatrixClient.h"
#include <QLabel>
@ -38,17 +40,14 @@ CommunitiesList::CommunitiesList(QWidget *parent)
scrollArea_->setWidget(scrollAreaContents_);
topLayout_->addWidget(scrollArea_);
connect(http::client(),
&MatrixClient::communityProfileRetrieved,
this,
[](QString communityId, QJsonObject profile) {
http::client()->fetchCommunityAvatar(
communityId, QUrl(profile["avatar_url"].toString()));
});
connect(http::client(),
SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)),
this,
SLOT(updateCommunityAvatar(const QString &, const QPixmap &)));
// connect(http::client(),
// &MatrixClient::communityProfileRetrieved,
// this,
// [this](QString communityId, QJsonObject profile) {
// fetchCommunityAvatar(communityId, profile["avatar_url"].toString());
// });
connect(
this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar);
}
void
@ -61,8 +60,8 @@ CommunitiesList::setCommunities(const std::map<QString, QSharedPointer<Community
for (const auto &community : communities) {
addCommunity(community.second, community.first);
http::client()->fetchCommunityProfile(community.first);
http::client()->fetchCommunityRooms(community.first);
// http::client()->fetchCommunityProfile(community.first);
// http::client()->fetchCommunityRooms(community.first);
}
communities_["world"]->setPressedState(true);
@ -77,7 +76,7 @@ CommunitiesList::addCommunity(QSharedPointer<Community> community, const QString
communities_.emplace(community_id, QSharedPointer<CommunitiesListItem>(list_item));
http::client()->fetchCommunityAvatar(community_id, community->getAvatar());
fetchCommunityAvatar(community_id, community->getAvatar().toString());
contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item);
@ -117,3 +116,37 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id)
}
}
}
void
CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
{
auto savedImgData = cache::client()->image(avatarUrl);
if (!savedImgData.isNull()) {
QPixmap pix;
pix.loadFromData(savedImgData);
emit avatarRetrieved(id, pix);
return;
}
mtx::http::ThumbOpts opts;
opts.mxc_url = avatarUrl.toStdString();
http::v2::client()->get_thumbnail(
opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) {
if (err) {
log::net()->warn("failed to download avatar: {} - ({} {})",
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
cache::client()->saveImage(opts.mxc_url, res);
auto data = QByteArray(res.data(), res.size());
QPixmap pix;
pix.loadFromData(data);
emit avatarRetrieved(id, pix);
});
}

50
src/Logging.cpp Normal file
View File

@ -0,0 +1,50 @@
#include "Logging.hpp"
#include <iostream>
#include <spdlog/sinks/file_sinks.h>
namespace {
std::shared_ptr<spdlog::logger> db_logger = nullptr;
std::shared_ptr<spdlog::logger> net_logger = nullptr;
std::shared_ptr<spdlog::logger> main_logger = nullptr;
constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
constexpr auto MAX_LOG_FILES = 3;
}
namespace log {
void
init(const std::string &file_path)
{
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
file_path, MAX_FILE_SIZE, MAX_LOG_FILES);
auto console_sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
std::vector<spdlog::sink_ptr> sinks;
sinks.push_back(file_sink);
sinks.push_back(console_sink);
net_logger = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
main_logger = std::make_shared<spdlog::logger>("main", std::begin(sinks), std::end(sinks));
db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
}
std::shared_ptr<spdlog::logger>
main()
{
return main_logger;
}
std::shared_ptr<spdlog::logger>
net()
{
return net_logger;
}
std::shared_ptr<spdlog::logger>
db()
{
return db_logger;
}
}

View File

@ -137,16 +137,16 @@ LoginPage::LoginPage(QWidget *parent)
setLayout(top_layout_);
connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk);
connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError);
connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError);
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(http::client(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString)));
connect(http::client(), SIGNAL(loginError(QString)), this, SIGNAL(errorOccurred()));
connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
connect(http::client(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString)));
connect(http::client(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess()));
connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
}
@ -180,17 +180,47 @@ LoginPage::onMatrixIdEntered()
inferredServerAddress_ = homeServer;
serverInput_->setText(homeServer);
http::client()->setServer(homeServer);
http::client()->versions();
http::v2::client()->set_server(user.hostname());
checkHomeserverVersion();
}
}
void
LoginPage::checkHomeserverVersion()
{
http::v2::client()->versions(
[this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
using namespace boost::beast::http;
if (err->status_code == status::not_found) {
emit versionErrorCb(tr("The required endpoints were not found. "
"Possibly not a Matrix server."));
return;
}
if (!err->parse_error.empty()) {
emit versionErrorCb(tr("Received malformed response. Make sure "
"the homeserver domain is valid."));
return;
}
emit versionErrorCb(tr(
"An unknown error occured. Make sure the homeserver domain is valid."));
return;
}
emit versionOkCb();
});
}
void
LoginPage::onServerAddressEntered()
{
error_label_->setText("");
http::client()->setServer(serverInput_->text());
http::client()->versions();
http::v2::client()->set_server(serverInput_->text().toStdString());
checkHomeserverVersion();
serverLayout_->removeWidget(errorIcon_);
errorIcon_->hide();
@ -199,11 +229,8 @@ LoginPage::onServerAddressEntered()
}
void
LoginPage::versionError(QString error)
LoginPage::versionError(const QString &error)
{
QUrl currentServer = http::client()->getHomeServer();
QString mxidAddress = matrixid_input_->text().split(":").at(1);
error_label_->setText(error);
serverInput_->show();
@ -215,7 +242,7 @@ LoginPage::versionError(QString error)
}
void
LoginPage::versionSuccess()
LoginPage::versionOk()
{
serverLayout_->removeWidget(spinner_);
matrixidLayout_->removeWidget(spinner_);
@ -241,8 +268,20 @@ LoginPage::onLoginButtonClicked()
if (password_input_->text().isEmpty())
return loginError(tr("Empty password"));
http::client()->setServer(serverInput_->text());
http::client()->login(QString::fromStdString(user.localpart()), password_input_->text());
http::v2::client()->set_server(serverInput_->text().toStdString());
http::v2::client()->login(
user.localpart(),
password_input_->text().toStdString(),
initialDeviceName(),
[this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
if (err) {
emit loginError(QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
return;
}
emit loginOk(res);
});
emit loggingIn();
}

View File

@ -17,7 +17,6 @@
#include <QApplication>
#include <QLayout>
#include <QNetworkReply>
#include <QSettings>
#include <QShortcut>
@ -26,6 +25,7 @@
#include "ChatPage.h"
#include "Config.h"
#include "LoadingIndicator.h"
#include "Logging.hpp"
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@ -46,6 +46,15 @@
MainWindow *MainWindow::instance_ = nullptr;
MainWindow::~MainWindow()
{
if (http::v2::client() != nullptr) {
http::v2::client()->shutdown();
// TODO: find out why waiting for the threads to join is slow.
http::v2::client()->close();
}
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, progressModal_{nullptr}
@ -54,9 +63,6 @@ MainWindow::MainWindow(QWidget *parent)
setWindowTitle("nheko");
setObjectName("MainWindow");
// Initialize the http client.
http::init();
restoreWindowSize();
QFont font("Open Sans");
@ -124,21 +130,13 @@ MainWindow::MainWindow(QWidget *parent)
connect(
chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
connect(http::client(),
SIGNAL(loginSuccess(QString, QString, QString)),
this,
SLOT(showChatPage(QString, QString, QString)));
connect(http::client(),
SIGNAL(registerSuccess(QString, QString, QString)),
this,
SLOT(showChatPage(QString, QString, QString)));
connect(http::client(), &MatrixClient::invalidToken, this, [this]() {
chat_page_->deleteConfigs();
showLoginPage();
login_page_->loginError("Invalid token detected. Please try to login again.");
connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
http::v2::client()->set_user(res.user_id);
showChatPage();
});
connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
@ -157,7 +155,18 @@ MainWindow::MainWindow(QWidget *parent)
QString home_server = settings.value("auth/home_server").toString();
QString user_id = settings.value("auth/user_id").toString();
showChatPage(user_id, home_server, token);
http::v2::client()->set_access_token(token.toStdString());
http::v2::client()->set_server(home_server.toStdString());
try {
using namespace mtx::identifiers;
http::v2::client()->set_user(parse<User>(user_id.toStdString()));
} catch (const std::invalid_argument &e) {
log::main()->critical("bootstrapped with invalid user_id: {}",
user_id.toStdString());
}
showChatPage();
}
}
@ -216,8 +225,13 @@ MainWindow::removeOverlayProgressBar()
}
void
MainWindow::showChatPage(QString userid, QString homeserver, QString token)
MainWindow::showChatPage()
{
auto userid = QString::fromStdString(http::v2::client()->user_id().to_string());
auto homeserver = QString::fromStdString(http::v2::client()->server() + ":" +
std::to_string(http::v2::client()->port()));
auto token = QString::fromStdString(http::v2::client()->access_token());
QSettings settings;
settings.setValue("auth/access_token", token);
settings.setValue("auth/home_server", homeserver);
@ -317,7 +331,7 @@ MainWindow::openLeaveRoomDialog(const QString &room_id)
leaveRoomModal_->hide();
if (leaving)
http::client()->leaveRoom(roomToLeave);
chat_page_->leaveRoom(roomToLeave);
});
leaveRoomModal_ =

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
#include "Config.h"
#include "FlatButton.h"
#include "Logging.hpp"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "RaisedButton.h"
@ -125,35 +126,53 @@ RegisterPage::RegisterPage(QWidget *parent)
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(http::client(),
SIGNAL(registerError(const QString &)),
this,
SLOT(registerError(const QString &)));
connect(http::client(),
&MatrixClient::registrationFlow,
this,
[this](const QString &user,
const QString &pass,
const QString &server,
const QString &session) {
emit errorOccurred();
connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError);
connect(
this,
&RegisterPage::registrationFlow,
this,
[this](const std::string &user, const std::string &pass, const std::string &session) {
emit errorOccurred();
if (!captchaDialog_) {
captchaDialog_ =
std::make_shared<dialogs::ReCaptcha>(server, session, this);
connect(captchaDialog_.get(),
&dialogs::ReCaptcha::closing,
this,
[this, user, pass, server, session]() {
captchaDialog_->close();
emit registering();
http::client()->registerUser(
user, pass, server, session);
});
}
if (!captchaDialog_) {
captchaDialog_ = std::make_shared<dialogs::ReCaptcha>(
QString::fromStdString(session), this);
connect(
captchaDialog_.get(),
&dialogs::ReCaptcha::closing,
this,
[this, user, pass, session]() {
captchaDialog_->close();
emit registering();
QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
});
http::v2::client()->flow_response(
user,
pass,
session,
"m.login.recaptcha",
[this](const mtx::responses::Register &res,
mtx::http::RequestErr err) {
if (err) {
log::net()->warn(
"failed to retrieve registration flows: {}",
err->matrix_error.error);
emit errorOccurred();
emit registerErrorCb(QString::fromStdString(
err->matrix_error.error));
return;
}
http::v2::client()->set_user(res.user_id);
http::v2::client()->set_access_token(
res.access_token);
emit registerOk();
});
});
}
QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); });
});
setLayout(top_layout_);
}
@ -185,11 +204,56 @@ RegisterPage::onRegisterButtonClicked()
} else if (!server_input_->hasAcceptableInput()) {
registerError(tr("Invalid server name"));
} else {
QString username = username_input_->text();
QString password = password_input_->text();
QString server = server_input_->text();
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
auto server = server_input_->text().toStdString();
http::v2::client()->set_server(server);
http::v2::client()->registration(
username,
password,
[this, username, password](const mtx::responses::Register &res,
mtx::http::RequestErr err) {
if (!err) {
http::v2::client()->set_user(res.user_id);
http::v2::client()->set_access_token(res.access_token);
emit registerOk();
return;
}
// The server requires registration flows.
if (err->status_code == boost::beast::http::status::unauthorized) {
http::v2::client()->flow_register(
username,
password,
[this, username, password](
const mtx::responses::RegistrationFlows &res,
mtx::http::RequestErr err) {
if (res.session.empty() && err) {
log::net()->warn(
"failed to retrieve registration flows: ({}) "
"{}",
static_cast<int>(err->status_code),
err->matrix_error.error);
emit errorOccurred();
emit registerErrorCb(QString::fromStdString(
err->matrix_error.error));
return;
}
emit registrationFlow(username, password, res.session);
});
return;
}
log::net()->warn("failed to register: status_code ({})",
static_cast<int>(err->status_code));
emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
});
http::client()->registerUser(username, password, server);
emit registering();
}
}

View File

@ -16,11 +16,11 @@
*/
#include <QBuffer>
#include <QDebug>
#include <QObject>
#include <QTimer>
#include "Cache.h"
#include "Logging.hpp"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "OverlayModal.h"
@ -55,18 +55,7 @@ RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent)
scrollArea_->setWidget(scrollAreaContents_);
topLayout_->addWidget(scrollArea_);
connect(http::client(),
&MatrixClient::roomAvatarRetrieved,
this,
[this](const QString &room_id,
const QPixmap &img,
const QString &url,
const QByteArray &data) {
if (cache::client())
cache::client()->saveImage(url, data);
updateRoomAvatar(room_id, img);
});
connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar);
}
void
@ -101,7 +90,28 @@ RoomList::updateAvatar(const QString &room_id, const QString &url)
savedImgData = cache::client()->image(url);
if (savedImgData.isEmpty()) {
http::client()->fetchRoomAvatar(room_id, url);
mtx::http::ThumbOpts opts;
opts.mxc_url = url.toStdString();
http::v2::client()->get_thumbnail(
opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) {
if (err) {
log::net()->warn(
"failed to download thumbnail: {}, {} - {}",
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
if (cache::client())
cache::client()->saveImage(opts.mxc_url, res);
auto data = QByteArray(res.data(), res.size());
QPixmap pixmap;
pixmap.loadFromData(data);
emit updateRoomAvatarCb(room_id, pixmap);
});
} else {
QPixmap img;
img.loadFromData(savedImgData);
@ -131,7 +141,8 @@ void
RoomList::updateUnreadMessageCount(const QString &roomid, int count)
{
if (!roomExists(roomid)) {
qWarning() << "UpdateUnreadMessageCount: Unknown roomid";
log::main()->warn("updateUnreadMessageCount: unknown room_id {}",
roomid.toStdString());
return;
}
@ -156,7 +167,7 @@ RoomList::calculateUnreadMessageCount()
void
RoomList::initialize(const QMap<QString, RoomInfo> &info)
{
qDebug() << "initialize room list";
log::main()->info("initialize room list");
rooms_.clear();
@ -209,7 +220,7 @@ RoomList::highlightSelectedRoom(const QString &room_id)
emit roomChanged(room_id);
if (!roomExists(room_id)) {
qDebug() << "RoomList: clicked unknown roomid";
log::main()->warn("roomlist: clicked unknown room_id");
return;
}
@ -232,7 +243,8 @@ void
RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img)
{
if (!roomExists(roomid)) {
qWarning() << "Avatar update on non existent room" << roomid;
log::main()->warn("avatar update on non-existent room_id: {}",
roomid.toStdString());
return;
}
@ -246,7 +258,9 @@ void
RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
{
if (!roomExists(roomid)) {
qWarning() << "Description update on non existent room" << roomid << info.body;
log::main()->warn("description update on non-existent room_id: {}, {}",
roomid.toStdString(),
info.body.toStdString());
return;
}
@ -314,7 +328,7 @@ RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
joinRoomModal_->hide();
if (isJoining)
http::client()->joinRoom(roomAlias);
emit joinRoom(roomAlias);
}
void

View File

@ -71,8 +71,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
this,
&FilteredTextEdit::uploadData);
qRegisterMetaType<SearchResult>();
qRegisterMetaType<QVector<SearchResult>>();
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
popup_.hide();

View File

@ -6,6 +6,7 @@
#include "Config.h"
#include "FlatButton.h"
#include "MatrixClient.h"
#include "RaisedButton.h"
#include "Theme.h"
@ -13,7 +14,7 @@
using namespace dialogs;
ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *parent)
ReCaptcha::ReCaptcha(const QString &session, QWidget *parent)
: QWidget(parent)
{
setAutoFillBackground(true);
@ -51,12 +52,12 @@ ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *par
layout->addWidget(label);
layout->addLayout(buttonLayout);
connect(openCaptchaBtn_, &QPushButton::clicked, [server, session]() {
const auto url =
QString(
"https://%1/_matrix/client/r0/auth/m.login.recaptcha/fallback/web?session=%2")
.arg(server)
.arg(session);
connect(openCaptchaBtn_, &QPushButton::clicked, [session]() {
const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/"
"fallback/web?session=%3")
.arg(QString::fromStdString(http::v2::client()->server()))
.arg(http::v2::client()->port())
.arg(session);
QDesktopServices::openUrl(url);
});

View File

@ -67,6 +67,20 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
labelLayout->addWidget(errorField_);
layout->addLayout(labelLayout);
connect(this, &EditModal::stateEventErrorCb, this, [this](const QString &msg) {
errorField_->setText(msg);
errorField_->show();
});
connect(this, &EditModal::nameEventSentCb, this, [this](const QString &newName) {
errorField_->hide();
emit nameChanged(newName);
close();
});
connect(this, &EditModal::topicEventSentCb, this, [this]() {
errorField_->hide();
close();
});
connect(applyBtn_, &QPushButton::clicked, [this]() {
// Check if the values are changed from the originals.
auto newName = nameInput_->text().trimmed();
@ -85,53 +99,37 @@ EditModal::EditModal(const QString &roomId, QWidget *parent)
state::Name body;
body.name = newName.toStdString();
auto proxy =
http::client()->sendStateEvent<state::Name, EventType::RoomName>(body,
roomId_);
connect(proxy.get(),
&StateEventProxy::stateEventSent,
this,
[this, proxy, newName]() {
Q_UNUSED(proxy);
errorField_->hide();
emit nameChanged(newName);
close();
});
http::v2::client()->send_state_event<state::Name, EventType::RoomName>(
roomId_.toStdString(),
body,
[this, newName](const mtx::responses::EventId &,
mtx::http::RequestErr err) {
if (err) {
emit stateEventErrorCb(
QString::fromStdString(err->matrix_error.error));
return;
}
connect(proxy.get(),
&StateEventProxy::stateEventError,
this,
[this, proxy, newName](const QString &msg) {
Q_UNUSED(proxy);
errorField_->setText(msg);
errorField_->show();
});
emit nameEventSentCb(newName);
});
}
if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
state::Topic body;
body.topic = newTopic.toStdString();
auto proxy =
http::client()->sendStateEvent<state::Topic, EventType::RoomTopic>(
body, roomId_);
connect(proxy.get(),
&StateEventProxy::stateEventSent,
this,
[this, proxy, newTopic]() {
Q_UNUSED(proxy);
errorField_->hide();
close();
});
http::v2::client()->send_state_event<state::Topic, EventType::RoomTopic>(
roomId_.toStdString(),
body,
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit stateEventErrorCb(
QString::fromStdString(err->matrix_error.error));
return;
}
connect(proxy.get(),
&StateEventProxy::stateEventError,
this,
[this, proxy, newTopic](const QString &msg) {
Q_UNUSED(proxy);
errorField_->setText(msg);
errorField_->show();
});
emit topicEventSentCb();
});
}
});
connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);

View File

@ -22,15 +22,17 @@
#include <QLabel>
#include <QLayout>
#include <QLibraryInfo>
#include <QNetworkProxy>
#include <QPalette>
#include <QPoint>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QTranslator>
#include "Config.h"
#include "Logging.hpp"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "RaisedButton.h"
#include "RunGuard.h"
#include "version.hpp"
@ -46,32 +48,6 @@ screenCenter(int width, int height)
return QPoint(x, y);
}
void
setupProxy()
{
QSettings settings;
/**
To set up a SOCKS proxy:
[user]
proxy\socks\host=<>
proxy\socks\port=<>
proxy\socks\user=<>
proxy\socks\password=<>
**/
if (settings.contains("user/proxy/socks/host")) {
QNetworkProxy proxy;
proxy.setType(QNetworkProxy::Socks5Proxy);
proxy.setHostName(settings.value("user/proxy/socks/host").toString());
proxy.setPort(settings.value("user/proxy/socks/port").toInt());
if (settings.contains("user/proxy/socks/user"))
proxy.setUser(settings.value("user/proxy/socks/user").toString());
if (settings.contains("user/proxy/socks/password"))
proxy.setPassword(settings.value("user/proxy/socks/password").toString());
QNetworkProxy::setApplicationProxy(proxy);
}
}
int
main(int argc, char *argv[])
{
@ -133,7 +109,17 @@ main(int argc, char *argv[])
QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
app.setWindowIcon(QIcon(":/logos/nheko.png"));
qSetMessagePattern("%{time process}: [%{type}] - %{message}");
http::init();
try {
log::init(QString("%1/nheko.log")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.toStdString());
} catch (const spdlog::spdlog_ex &ex) {
std::cout << "Log initialization failed: " << ex.what() << std::endl;
std::exit(1);
}
QSettings settings;
@ -154,8 +140,6 @@ main(int argc, char *argv[])
appTranslator.load("nheko_" + lang, ":/translations");
app.installTranslator(&appTranslator);
setupProxy();
MainWindow w;
// Move the MainWindow to the center
@ -167,5 +151,7 @@ main(int argc, char *argv[])
QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize);
log::main()->info("starting nheko {}", nheko::version);
return app.exec();
}

View File

@ -62,9 +62,27 @@ TimelineItem::init()
ChatPage::instance()->showReadReceipts(event_id_);
});
connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {
emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);
});
connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {
emit ChatPage::instance()->showNotification(msg);
});
connect(redactMsg_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
http::client()->redactEvent(room_id_, event_id_);
http::v2::client()->redact_event(
room_id_.toStdString(),
event_id_.toStdString(),
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit redactionFailed(tr("Message redaction failed: %1")
.arg(QString::fromStdString(
err->matrix_error.error)));
return;
}
emit eventRedacted(event_id_);
});
});
connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); });

View File

@ -23,6 +23,7 @@
#include "ChatPage.h"
#include "Config.h"
#include "FloatingButton.h"
#include "Logging.hpp"
#include "UserSettingsPage.h"
#include "Utils.h"
@ -100,7 +101,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent)
, room_id_{room_id}
{
init();
http::client()->messages(room_id_, "");
getMessages();
}
void
@ -140,7 +141,7 @@ TimelineView::fetchHistory()
return;
isPaginationInProgress_ = true;
http::client()->messages(room_id_, prev_batch_token_);
getMessages();
paginationTimer_->start(5000);
return;
@ -189,18 +190,13 @@ TimelineView::sliderMoved(int position)
isPaginationInProgress_ = true;
// FIXME: Maybe move this to TimelineViewManager to remove the
// extra calls?
http::client()->messages(room_id_, prev_batch_token_);
getMessages();
}
}
void
TimelineView::addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs)
TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs)
{
if (room_id_ != room_id)
return;
// We've reached the start of the timline and there're no more messages.
if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) {
isTimelineFinished = true;
@ -427,10 +423,10 @@ TimelineView::init()
paginationTimer_ = new QTimer(this);
connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
connect(http::client(),
&MatrixClient::messagesRetrieved,
this,
&TimelineView::addBackwardsEvents);
connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
connect(scroll_area_->verticalScrollBar(),
SIGNAL(valueChanged(int)),
@ -442,6 +438,27 @@ TimelineView::init()
SLOT(sliderRangeChanged(int, int)));
}
void
TimelineView::getMessages()
{
mtx::http::MessagesOpts opts;
opts.room_id = room_id_.toStdString();
opts.from = prev_batch_token_.toStdString();
http::v2::client()->messages(
opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
if (err) {
log::net()->error("failed to call /messages ({}): {} - {}",
opts.room_id,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
emit messagesRetrieved(std::move(res));
});
}
void
TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
{
@ -513,7 +530,7 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
}
void
TimelineView::updatePendingMessage(int txn_id, QString event_id)
TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id)
{
if (!pending_msgs_.isEmpty() &&
pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
@ -548,8 +565,11 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
int txn_id = http::client()->incrementTransactionId();
PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item);
PendingMessage message;
message.ty = ty;
message.txn_id = mtx::client::utils::random_token();
message.body = body;
message.widget = view_item;
handleNewUserMessage(message);
}
@ -567,19 +587,119 @@ TimelineView::sendNextPendingMessage()
if (pending_msgs_.size() == 0)
return;
using namespace mtx::events;
PendingMessage &m = pending_msgs_.head();
switch (m.ty) {
case mtx::events::MessageType::Audio:
case mtx::events::MessageType::Image:
case mtx::events::MessageType::Video:
case mtx::events::MessageType::File:
// FIXME: Improve the API
http::client()->sendRoomMessage(
m.ty, m.txn_id, room_id_, m.filename, m.mime, m.media_size, m.body);
case mtx::events::MessageType::Audio: {
msg::Audio audio;
audio.info.mimetype = m.mime.toStdString();
audio.info.size = m.media_size;
audio.body = m.filename.toStdString();
audio.url = m.body.toStdString();
http::v2::client()->send_room_message<msg::Audio, EventType::RoomMessage>(
room_id_.toStdString(),
m.txn_id,
audio,
std::bind(&TimelineView::sendRoomMessageHandler,
this,
m.txn_id,
std::placeholders::_1,
std::placeholders::_2));
break;
}
case mtx::events::MessageType::Image: {
msg::Image image;
image.info.mimetype = m.mime.toStdString();
image.info.size = m.media_size;
image.body = m.filename.toStdString();
image.url = m.body.toStdString();
http::v2::client()->send_room_message<msg::Image, EventType::RoomMessage>(
room_id_.toStdString(),
m.txn_id,
image,
std::bind(&TimelineView::sendRoomMessageHandler,
this,
m.txn_id,
std::placeholders::_1,
std::placeholders::_2));
break;
}
case mtx::events::MessageType::Video: {
msg::Video video;
video.info.mimetype = m.mime.toStdString();
video.info.size = m.media_size;
video.body = m.filename.toStdString();
video.url = m.body.toStdString();
http::v2::client()->send_room_message<msg::Video, EventType::RoomMessage>(
room_id_.toStdString(),
m.txn_id,
video,
std::bind(&TimelineView::sendRoomMessageHandler,
this,
m.txn_id,
std::placeholders::_1,
std::placeholders::_2));
break;
}
case mtx::events::MessageType::File: {
msg::File file;
file.info.mimetype = m.mime.toStdString();
file.info.size = m.media_size;
file.body = m.filename.toStdString();
file.url = m.body.toStdString();
http::v2::client()->send_room_message<msg::File, EventType::RoomMessage>(
room_id_.toStdString(),
m.txn_id,
file,
std::bind(&TimelineView::sendRoomMessageHandler,
this,
m.txn_id,
std::placeholders::_1,
std::placeholders::_2));
break;
}
case mtx::events::MessageType::Text: {
msg::Text text;
text.body = m.body.toStdString();
http::v2::client()->send_room_message<msg::Text, EventType::RoomMessage>(
room_id_.toStdString(),
m.txn_id,
text,
std::bind(&TimelineView::sendRoomMessageHandler,
this,
m.txn_id,
std::placeholders::_1,
std::placeholders::_2));
break;
}
case mtx::events::MessageType::Emote: {
msg::Emote emote;
emote.body = m.body.toStdString();
http::v2::client()->send_room_message<msg::Emote, EventType::RoomMessage>(
room_id_.toStdString(),
m.txn_id,
emote,
std::bind(&TimelineView::sendRoomMessageHandler,
this,
m.txn_id,
std::placeholders::_1,
std::placeholders::_2));
break;
}
default:
http::client()->sendRoomMessage(
m.ty, m.txn_id, room_id_, m.body, m.mime, m.media_size);
log::main()->warn("cannot send unknown message type: {}", m.body.toStdString());
break;
}
}
@ -593,7 +713,7 @@ TimelineView::notifyForLastEvent()
if (lastTimelineItem)
emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
else
qWarning() << "Cast to TimelineView failed" << room_id_;
log::main()->warn("cast to TimelineView failed: {}", room_id_.toStdString());
}
void
@ -606,29 +726,27 @@ TimelineView::notifyForLastEvent(const TimelineEvent &event)
}
bool
TimelineView::isPendingMessage(const QString &txnid,
TimelineView::isPendingMessage(const std::string &txn_id,
const QString &sender,
const QString &local_userid)
{
if (sender != local_userid)
return false;
auto match_txnid = [txnid](const auto &msg) -> bool {
return QString::number(msg.txn_id) == txnid;
};
auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; };
return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) ||
std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid);
}
void
TimelineView::removePendingMessage(const QString &txnid)
TimelineView::removePendingMessage(const std::string &txn_id)
{
if (txnid.isEmpty())
if (txn_id.empty())
return;
for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
if (QString::number(it->txn_id) == txnid) {
if (it->txn_id == txn_id) {
int index = std::distance(pending_sent_msgs_.begin(), it);
pending_sent_msgs_.removeAt(index);
@ -639,7 +757,7 @@ TimelineView::removePendingMessage(const QString &txnid)
}
}
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
if (QString::number(it->txn_id) == txnid) {
if (it->txn_id == txn_id) {
int index = std::distance(pending_msgs_.begin(), it);
pending_msgs_.removeAt(index);
return;
@ -648,9 +766,9 @@ TimelineView::removePendingMessage(const QString &txnid)
}
void
TimelineView::handleFailedMessage(int txnid)
TimelineView::handleFailedMessage(const std::string &txn_id)
{
Q_UNUSED(txnid);
Q_UNUSED(txn_id);
// Note: We do this even if the message has already been echoed.
QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage()));
}
@ -673,7 +791,16 @@ TimelineView::readLastEvent() const
const auto eventId = getLastEventId();
if (!eventId.isEmpty())
http::client()->readEvent(room_id_, eventId);
http::v2::client()->read_event(room_id_.toStdString(),
eventId.toStdString(),
[this, eventId](mtx::http::RequestErr err) {
if (err) {
log::net()->warn(
"failed to read event ({}, {})",
room_id_.toStdString(),
eventId.toStdString());
}
});
}
QString
@ -743,7 +870,8 @@ void
TimelineView::removeEvent(const QString &event_id)
{
if (!eventIds_.contains(event_id)) {
qWarning() << "unknown event_id couldn't be removed:" << event_id;
log::main()->warn("cannot remove widget with unknown event_id: {}",
event_id.toStdString());
return;
}
@ -860,3 +988,16 @@ TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second)
return diffInSeconds > fifteenMins;
}
void
TimelineView::sendRoomMessageHandler(const std::string &txn_id,
const mtx::responses::EventId &res,
mtx::http::RequestErr err)
{
if (err) {
emit messageFailed(txn_id);
return;
}
emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string()));
}

View File

@ -35,42 +35,15 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
: QStackedWidget(parent)
{
setStyleSheet("border: none;");
connect(
http::client(), &MatrixClient::messageSent, this, &TimelineViewManager::messageSent);
connect(http::client(),
&MatrixClient::messageSendFailed,
this,
&TimelineViewManager::messageSendFailed);
connect(http::client(),
&MatrixClient::redactionCompleted,
this,
[this](const QString &room_id, const QString &event_id) {
auto view = views_[room_id];
if (view)
view->removeEvent(event_id);
});
}
void
TimelineViewManager::messageSent(const QString &event_id, const QString &roomid, int txn_id)
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
{
// We save the latest valid transaction ID for later use.
QSettings settings;
settings.setValue("client/transaction_id", txn_id + 1);
auto view = views_[room_id];
auto view = views_[roomid];
view->updatePendingMessage(txn_id, event_id);
}
void
TimelineViewManager::messageSendFailed(const QString &roomid, int txn_id)
{
auto view = views_[roomid];
view->handleFailedMessage(txn_id);
if (view)
view->removeEvent(event_id);
}
void

View File

@ -50,21 +50,12 @@ AudioItem::init()
playIcon_.addFile(":/icons/icons/ui/play-sign.png");
pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
QList<QString> url_parts = url_.toString().split("mxc://");
if (url_parts.size() != 2) {
qDebug() << "Invalid format for image" << url_.toString();
return;
}
QString media_params = url_parts[1];
url_ = QString("%1/_matrix/media/r0/download/%2")
.arg(http::client()->getHomeServer().toString(), media_params);
player_ = new QMediaPlayer;
player_->setMedia(QUrl(url_));
player_->setVolume(100);
player_->setNotifyInterval(1000);
connect(this, &AudioItem::fileDownloadedCb, this, &AudioItem::fileDownloaded);
connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
if (state == QMediaPlayer::StoppedState) {
state_ = AudioState::Play;
@ -129,14 +120,19 @@ AudioItem::mousePressEvent(QMouseEvent *event)
if (filenameToSave_.isEmpty())
return;
auto proxy = http::client()->downloadFile(url_);
connect(proxy.data(),
&DownloadMediaProxy::fileDownloaded,
this,
[proxy, this](const QByteArray &data) {
proxy->deleteLater();
fileDownloaded(data);
});
http::v2::client()->download(
url_.toString().toStdString(),
[this](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
qWarning() << "failed to retrieve m.audio content:" << url_;
return;
}
emit fileDownloadedCb(QByteArray(data.data(), data.size()));
});
}
}

View File

@ -49,17 +49,9 @@ FileItem::init()
icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
QList<QString> url_parts = url_.toString().split("mxc://");
if (url_parts.size() != 2) {
qDebug() << "Invalid format for image" << url_.toString();
return;
}
QString media_params = url_parts[1];
url_ = QString("%1/_matrix/media/r0/download/%2")
.arg(http::client()->getHomeServer().toString(), media_params);
setFixedHeight(Height);
connect(this, &FileItem::fileDownloadedCb, this, &FileItem::fileDownloaded);
}
FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
@ -89,8 +81,15 @@ FileItem::openUrl()
if (url_.toString().isEmpty())
return;
if (!QDesktopServices::openUrl(url_))
qWarning() << "Could not open url" << url_.toString();
auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
.arg(QString::fromStdString(http::v2::client()->server()))
.arg(http::v2::client()->port())
.arg(QString::fromStdString(mxc_parts.server))
.arg(QString::fromStdString(mxc_parts.media_id));
if (!QDesktopServices::openUrl(urlToOpen))
qWarning() << "Could not open url" << urlToOpen;
}
QSize
@ -115,14 +114,19 @@ FileItem::mousePressEvent(QMouseEvent *event)
if (filenameToSave_.isEmpty())
return;
auto proxy = http::client()->downloadFile(url_);
connect(proxy.data(),
&DownloadMediaProxy::fileDownloaded,
this,
[proxy, this](const QByteArray &data) {
proxy->deleteLater();
fileDownloaded(data);
});
http::v2::client()->download(
url_.toString().toStdString(),
[this](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
qWarning() << "failed to retrieve m.file content:" << url_;
return;
}
emit fileDownloadedCb(QByteArray(data.data(), data.size()));
});
} else {
openUrl();
}

View File

@ -30,37 +30,62 @@
#include "dialogs/ImageOverlay.h"
#include "timeline/widgets/ImageItem.h"
ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
: QWidget(parent)
, event_{event}
void
ImageItem::downloadMedia(const QUrl &url)
{
http::v2::client()->download(url.toString().toStdString(),
[this, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
qWarning()
<< "failed to retrieve image:" << url;
return;
}
QPixmap img;
img.loadFromData(QByteArray(data.data(), data.size()));
emit imageDownloaded(img);
});
}
void
ImageItem::saveImage(const QString &filename, const QByteArray &data)
{
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(data);
file.close();
} catch (const std::exception &ex) {
qDebug() << "Error while saving file to:" << ex.what();
}
}
void
ImageItem::init()
{
setMouseTracking(true);
setCursor(Qt::PointingHandCursor);
setAttribute(Qt::WA_Hover, true);
connect(this, &ImageItem::imageDownloaded, this, &ImageItem::setImage);
connect(this, &ImageItem::imageSaved, this, &ImageItem::saveImage);
downloadMedia(url_);
}
ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
: QWidget(parent)
, event_{event}
{
url_ = QString::fromStdString(event.content.url);
text_ = QString::fromStdString(event.content.body);
QList<QString> url_parts = url_.toString().split("mxc://");
if (url_parts.size() != 2) {
qDebug() << "Invalid format for image" << url_.toString();
return;
}
QString media_params = url_parts[1];
url_ = QString("%1/_matrix/media/r0/download/%2")
.arg(http::client()->getHomeServer().toString(), media_params);
auto proxy = http::client()->downloadImage(url_);
connect(proxy.data(),
&DownloadMediaProxy::imageDownloaded,
this,
[this, proxy](const QPixmap &img) {
proxy->deleteLater();
setImage(img);
});
init();
}
ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
@ -69,31 +94,7 @@ ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size,
, text_{filename}
{
Q_UNUSED(size);
setMouseTracking(true);
setCursor(Qt::PointingHandCursor);
setAttribute(Qt::WA_Hover, true);
QList<QString> url_parts = url_.toString().split("mxc://");
if (url_parts.size() != 2) {
qDebug() << "Invalid format for image" << url_.toString();
return;
}
QString media_params = url_parts[1];
url_ = QString("%1/_matrix/media/r0/download/%2")
.arg(http::client()->getHomeServer().toString(), media_params);
auto proxy = http::client()->downloadImage(url_);
connect(proxy.data(),
&DownloadMediaProxy::imageDownloaded,
this,
[proxy, this](const QPixmap &img) {
proxy->deleteLater();
setImage(img);
});
init();
}
void
@ -102,8 +103,15 @@ ImageItem::openUrl()
if (url_.toString().isEmpty())
return;
if (!QDesktopServices::openUrl(url_))
qWarning() << "Could not open url" << url_.toString();
auto mxc_parts = mtx::client::utils::parse_mxc_url(url_.toString().toStdString());
auto urlToOpen = QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
.arg(QString::fromStdString(http::v2::client()->server()))
.arg(http::v2::client()->port())
.arg(QString::fromStdString(mxc_parts.server))
.arg(QString::fromStdString(mxc_parts.media_id));
if (!QDesktopServices::openUrl(urlToOpen))
qWarning() << "Could not open url" << urlToOpen;
}
QSize
@ -231,23 +239,17 @@ ImageItem::saveAs()
if (filename.isEmpty())
return;
auto proxy = http::client()->downloadFile(url_);
connect(proxy.data(),
&DownloadMediaProxy::fileDownloaded,
this,
[proxy, filename](const QByteArray &data) {
proxy->deleteLater();
http::v2::client()->download(
url_.toString().toStdString(),
[this, filename](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
qWarning() << "failed to retrieve image:" << url_;
return;
}
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(data);
file.close();
} catch (const std::exception &ex) {
qDebug() << "Error while saving file to:" << ex.what();
}
});
emit imageSaved(filename, QByteArray(data.data(), data.size()));
});
}

View File

@ -27,15 +27,15 @@
void
VideoItem::init()
{
QList<QString> url_parts = url_.toString().split("mxc://");
if (url_parts.size() != 2) {
qDebug() << "Invalid format for image" << url_.toString();
return;
}
// QList<QString> url_parts = url_.toString().split("mxc://");
// if (url_parts.size() != 2) {
// qDebug() << "Invalid format for image" << url_.toString();
// return;
// }
QString media_params = url_parts[1];
url_ = QString("%1/_matrix/media/r0/download/%2")
.arg(http::client()->getHomeServer().toString(), media_params);
// QString media_params = url_parts[1];
// url_ = QString("%1/_matrix/media/r0/download/%2")
// .arg(http::client()->getHomeServer().toString(), media_params);
}
VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)