├── NOTICE ├── default.nix ├── src ├── sort.hpp ├── spinner_test.cpp ├── QStringHash.hpp ├── matrix │ ├── pixmaps.hpp │ ├── pixmaps.cpp │ ├── hash_combine.hpp │ ├── hash.hpp │ ├── utils.hpp │ ├── CMakeLists.txt │ ├── Matrix.hpp │ ├── Content.cpp │ ├── Matrix.cpp │ ├── utils.cpp │ ├── MemberListModel.hpp │ ├── proto.hpp │ ├── Content.hpp │ ├── ID.hpp │ ├── proto.cpp │ ├── TimelineWindow.hpp │ ├── Session.hpp │ ├── MemberListModel.cpp │ ├── TimelineWindow.cpp │ ├── Room.hpp │ ├── Event.cpp │ ├── Event.hpp │ └── Session.cpp ├── utils.hpp ├── MessageBox.hpp ├── sort.cpp ├── MessageBox.cpp ├── RedactDialog.cpp ├── version.hpp ├── version_string.cpp ├── RoomMenu.hpp ├── JoinDialog.cpp ├── RedactDialog.hpp ├── JoinDialog.hpp ├── EventSourceView.hpp ├── EventSourceView.cpp ├── LoginDialog.hpp ├── Spinner.hpp ├── EntryBox.hpp ├── ContentCache.cpp ├── EventSourceView.ui ├── LoginDialog.cpp ├── RoomViewList.hpp ├── ChatWindow.hpp ├── ChatWindow.ui ├── RoomView.hpp ├── MainWindow.hpp ├── JoinedRoomListModel.hpp ├── CMakeLists.txt ├── RedactDialog.ui ├── RoomMenu.cpp ├── JoinDialog.ui ├── ContentCache.hpp ├── Spinner.cpp ├── MainWindow.ui ├── LoginDialog.ui ├── main.cpp ├── RoomView.ui ├── FixedVector.hpp ├── RoomViewList.cpp ├── ChatWindow.cpp ├── timeline_view_test.cpp ├── JoinedRoomListModel.cpp ├── EntryBox.cpp ├── RoomView.cpp ├── MainWindow.cpp └── TimelineView.hpp ├── .gitmodules ├── cmake ├── FindLMDB.cmake ├── AddVersion.cmake └── version.sh ├── trusty.sh ├── package.nix ├── CMakeLists.txt ├── .travis.yml ├── .appveyor.yml ├── README.org ├── TODO.org └── LICENSE /NOTICE: -------------------------------------------------------------------------------- 1 | NaChat 2 | Copyright 2016 Benjamin Saunders 3 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | libsForQt5.callPackage ./package.nix { } 4 | -------------------------------------------------------------------------------- /src/sort.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_SORT_ 2 | #define NATIVE_CHAT_SORT_ 3 | 4 | #include 5 | 6 | QString room_sort_key(const QString &name); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/GSL"] 2 | path = deps/GSL 3 | url = https://github.com/Microsoft/GSL.git 4 | [submodule "deps/lmdbxx"] 5 | path = deps/lmdbxx 6 | url = https://github.com/bendiken/lmdbxx.git 7 | -------------------------------------------------------------------------------- /src/spinner_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Spinner.hpp" 4 | 5 | int main(int argc, char *argv[]) { 6 | QApplication a(argc, argv); 7 | 8 | Spinner spinner; 9 | spinner.show(); 10 | 11 | return a.exec(); 12 | } 13 | -------------------------------------------------------------------------------- /src/QStringHash.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_QSTRING_HASH_HPP_ 2 | #define NATIVE_CHAT_QSTRING_HASH_HPP_ 3 | 4 | #include 5 | 6 | struct QStringHash { 7 | size_t operator()(const QString &s) const { return qHash(s); } 8 | }; 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/matrix/pixmaps.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_MTARIX_PIXMAPS_HPP_ 2 | #define NACHAT_MTARIX_PIXMAPS_HPP_ 3 | 4 | #include 5 | 6 | namespace matrix { 7 | 8 | QPixmap decode(const QString &type, const QByteArray &data); 9 | 10 | } 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /src/utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_UTILS_HPP_ 2 | #define NACHAT_UTILS_HPP_ 3 | 4 | inline static QSize initial_icon_size(QWidget &widget) { 5 | int i = widget.style()->pixelMetric(QStyle::PM_ListViewIconSize, nullptr, &widget); 6 | return QSize(i, i); 7 | } 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /cmake/FindLMDB.cmake: -------------------------------------------------------------------------------- 1 | find_path(LMDB_INCLUDE_DIR NAMES lmdb.h PATHS "$ENV{LMDB_DIR}/include") 2 | find_library(LMDB_LIBRARY NAMES lmdb PATHS "$ENV{LMDB_DIR}/lib" ) 3 | 4 | include(FindPackageHandleStandardArgs) 5 | find_package_handle_standard_args(LMDB DEFAULT_MSG LMDB_INCLUDE_DIR LMDB_LIBRARY) 6 | -------------------------------------------------------------------------------- /src/MessageBox.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MESSAGEBOX_HPP_ 2 | #define NATIVE_CHAT_MESSAGEBOX_HPP_ 3 | 4 | #include 5 | 6 | class MessageBox { 7 | public: 8 | static void critical(const QString &title, const QString &message, QWidget *parent = nullptr); 9 | }; 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /src/sort.cpp: -------------------------------------------------------------------------------- 1 | #include "sort.hpp" 2 | 3 | QString room_sort_key(const QString &n) { 4 | int i = 0; 5 | while((n[i] == '#' || n[i] == '@') && (i < n.size())) { 6 | ++i; 7 | } 8 | if(i == n.size()) return n.toCaseFolded(); 9 | return QString(n.data() + i, n.size() - i).toCaseFolded(); 10 | } 11 | -------------------------------------------------------------------------------- /src/MessageBox.cpp: -------------------------------------------------------------------------------- 1 | #include "MessageBox.hpp" 2 | 3 | void MessageBox::critical(const QString &title, const QString &message, QWidget *parent) { 4 | auto box = new QMessageBox(QMessageBox::Critical, title, message, QMessageBox::Close, parent); 5 | box->setAttribute(Qt::WA_DeleteOnClose); 6 | box->open(); 7 | } 8 | -------------------------------------------------------------------------------- /trusty.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 5 | sudo add-apt-repository -y ppa:beineri/opt-qt562-trusty 6 | sudo add-apt-repository -y ppa:george-edison55/cmake-3.x 7 | sudo apt-get update -qq 8 | sudo apt-get install -qq -y gcc-${GCC_VERSION} g++-${GCC_VERSION} liblmdb-dev qt56base cmake 9 | -------------------------------------------------------------------------------- /cmake/AddVersion.cmake: -------------------------------------------------------------------------------- 1 | set(VERSION_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/version.sh") 2 | 3 | function(add_version SOURCE) 4 | add_custom_command( 5 | COMMAND "${VERSION_SCRIPT}" "${CMAKE_CURRENT_SOURCE_DIR}" > "${SOURCE}" 6 | COMMENT "Generating version file ${SOURCE}" 7 | OUTPUT "${SOURCE}" .PHONY 8 | VERBATIM 9 | ) 10 | endfunction() 11 | -------------------------------------------------------------------------------- /src/RedactDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "RedactDialog.hpp" 2 | #include "ui_RedactDialog.h" 3 | 4 | RedactDialog::RedactDialog(QWidget *parent) : QDialog(parent), ui_(new Ui::RedactDialog) { 5 | ui_->setupUi(this); 6 | } 7 | RedactDialog::~RedactDialog() { delete ui_; } 8 | 9 | QString RedactDialog::reason() const { 10 | return ui_->reason->toPlainText(); 11 | } 12 | -------------------------------------------------------------------------------- /src/version.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CLIENT_VERSION_H_ 2 | #define NATIVE_CLIENT_VERSION_H_ 3 | 4 | #include 5 | 6 | class QString; 7 | 8 | namespace version { 9 | extern const uint8_t commit[20]; 10 | extern const char tag[]; 11 | extern const bool dirty; 12 | extern const uint32_t commits_since_tag; 13 | 14 | QString string(); 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | {stdenv, cmake, ninja, qtbase, qtsvg, lmdb, git}: 2 | 3 | stdenv.mkDerivation rec { 4 | name = "nachat-${version}"; 5 | version = "0.0"; 6 | 7 | buildInputs = [ cmake ninja qtbase qtsvg lmdb git ]; 8 | 9 | src = builtins.filterSource 10 | (path: type: type != "directory" || baseNameOf path != "build") 11 | ./.; 12 | enableParallelBuilding = true; 13 | } 14 | -------------------------------------------------------------------------------- /src/version_string.cpp: -------------------------------------------------------------------------------- 1 | #include "version.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace version { 7 | QString string() { 8 | QString result = tag; 9 | if(commits_since_tag != 0) { 10 | result += "-" + QString::number(commits_since_tag); 11 | } 12 | if(dirty) { 13 | result += "-" + QObject::tr("dirty"); 14 | } 15 | return result; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RoomMenu.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_ROOM_MENU_HPP_ 2 | #define NATIVE_CHAT_ROOM_MENU_HPP_ 3 | 4 | #include 5 | 6 | #include "matrix/Room.hpp" 7 | 8 | class RoomMenu : public QMenu { 9 | Q_OBJECT 10 | 11 | public: 12 | RoomMenu(matrix::Room &room, QWidget *parent = nullptr); 13 | 14 | private: 15 | matrix::Room &room_; 16 | 17 | void upload_file(const QString &path); 18 | }; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/matrix/pixmaps.cpp: -------------------------------------------------------------------------------- 1 | #include "pixmaps.hpp" 2 | 3 | #include 4 | 5 | namespace matrix { 6 | 7 | QPixmap decode(const QString &type, const QByteArray &data) { 8 | QPixmap pixmap; 9 | pixmap.loadFromData(data, QMimeDatabase().mimeTypeForName(type.toUtf8()).preferredSuffix().toUtf8().constData()); 10 | if(pixmap.isNull()) pixmap.loadFromData(data); 11 | return pixmap; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/JoinDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "JoinDialog.hpp" 2 | #include "ui_JoinDialog.h" 3 | 4 | JoinDialog::JoinDialog(QWidget *parent) : QDialog(parent), ui(new Ui::JoinDialog) { 5 | ui->setupUi(this); 6 | } 7 | 8 | JoinDialog::~JoinDialog() { delete ui; } 9 | 10 | QString JoinDialog::room() { return ui->lineEdit->text(); } 11 | 12 | void JoinDialog::accept() { 13 | setEnabled(false); 14 | setResult(QDialog::Accepted); 15 | accepted(); 16 | } 17 | -------------------------------------------------------------------------------- /src/RedactDialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_REDACT_DIALOG_HPP_ 2 | #define NATIVE_CHAT_REDACT_DIALOG_HPP_ 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class RedactDialog; 8 | } 9 | 10 | class RedactDialog : public QDialog { 11 | Q_OBJECT 12 | 13 | public: 14 | explicit RedactDialog(QWidget *parent = 0); 15 | ~RedactDialog(); 16 | 17 | QString reason() const; 18 | 19 | private: 20 | Ui::RedactDialog *ui_; 21 | }; 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /src/JoinDialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_JOIN_DIALOG_HPP_ 2 | #define NATIVE_CHAT_JOIN_DIALOG_HPP_ 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class JoinDialog; 8 | } 9 | 10 | class JoinDialog : public QDialog { 11 | Q_OBJECT 12 | 13 | public: 14 | JoinDialog(QWidget *parent = nullptr); 15 | ~JoinDialog(); 16 | 17 | QString room(); 18 | 19 | void accept() override; 20 | 21 | private: 22 | Ui::JoinDialog *ui; 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/matrix/hash_combine.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_MATRIX_HASH_COMBINE_HPP_ 2 | #define NACHAT_MATRIX_HASH_COMBINE_HPP_ 3 | 4 | #include 5 | 6 | inline std::uint64_t hash_combine(std::uint64_t x, std::uint64_t y) { 7 | static constexpr std::uint64_t factor = 0x9ddfea08eb382d69ULL; 8 | std::uint64_t a = (y ^ x) * factor; 9 | a ^= (a >> 47); 10 | std::uint64_t b = (x ^ a) * factor; 11 | b ^= (b >> 47); 12 | return b * factor; 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/EventSourceView.hpp: -------------------------------------------------------------------------------- 1 | #ifndef EVENTSOURCEVIEW_HPP 2 | #define EVENTSOURCEVIEW_HPP 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class EventSourceView; 8 | } 9 | 10 | class QJsonObject; 11 | 12 | class EventSourceView : public QWidget { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit EventSourceView(const QJsonObject &source, QWidget *parent = nullptr); 17 | ~EventSourceView(); 18 | 19 | private: 20 | Ui::EventSourceView *ui; 21 | }; 22 | 23 | #endif // EVENTSOURCEVIEW_HPP 24 | -------------------------------------------------------------------------------- /src/matrix/hash.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_HASH_HPP_ 2 | #define NATIVE_CHAT_MATRIX_HASH_HPP_ 3 | 4 | #include 5 | 6 | template 7 | inline std::size_t hash_combine(std::size_t seed, const T& v) { 8 | std::hash hasher; 9 | constexpr std::size_t factor = 0x9ddfea08eb382d69ULL; 10 | std::size_t a = (hasher(v) ^ seed) * factor; 11 | a ^= (a >> 47); 12 | std::size_t b = (seed ^ a) * factor; 13 | b ^= (b >> 47); 14 | return b * factor; 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /src/matrix/utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_UTILS_HPP_ 2 | #define NATIVE_CHAT_MATRIX_UTILS_HPP_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | class QNetworkReply; 10 | 11 | namespace matrix { 12 | 13 | QByteArray encode(QJsonObject o); 14 | 15 | struct Response { 16 | int code; 17 | QJsonObject object; 18 | std::experimental::optional error; 19 | }; 20 | 21 | Response decode(QNetworkReply *reply); 22 | 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/matrix/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FindLMDB) 2 | find_package(LMDB REQUIRED) 3 | 4 | add_library(matrix 5 | utils.cpp 6 | Matrix.cpp 7 | Session.cpp 8 | Room.cpp 9 | proto.cpp 10 | Content.cpp 11 | Event.cpp 12 | TimelineWindow.cpp 13 | MemberListModel.cpp 14 | pixmaps.cpp 15 | ) 16 | 17 | target_include_directories(matrix 18 | PUBLIC "${PROJECT_SOURCE_DIR}/deps/lmdbxx" "${LMDB_INCLUDE_DIR}" 19 | ) 20 | 21 | 22 | target_link_libraries(matrix 23 | Qt5::Network 24 | Qt5::Gui 25 | ${LMDB_LIBRARY} 26 | ) 27 | -------------------------------------------------------------------------------- /src/EventSourceView.cpp: -------------------------------------------------------------------------------- 1 | #include "EventSourceView.hpp" 2 | #include "ui_EventSourceView.h" 3 | 4 | #include 5 | 6 | EventSourceView::EventSourceView(const QJsonObject &obj, QWidget *parent) : 7 | QWidget(parent), 8 | ui(new Ui::EventSourceView) 9 | { 10 | ui->setupUi(this); 11 | setAttribute(Qt::WA_DeleteOnClose); 12 | setWindowFlags(Qt::Window); 13 | 14 | ui->text->setPlainText(QString::fromUtf8(QJsonDocument(obj).toJson())); 15 | } 16 | 17 | EventSourceView::~EventSourceView() 18 | { 19 | delete ui; 20 | } 21 | -------------------------------------------------------------------------------- /src/LoginDialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LOGINDIALOG_H 2 | #define LOGINDIALOG_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "matrix/Matrix.hpp" 9 | 10 | namespace Ui { 11 | class LoginDialog; 12 | } 13 | 14 | class LoginDialog : public QDialog { 15 | Q_OBJECT 16 | 17 | public: 18 | LoginDialog(QWidget *parent = nullptr); 19 | ~LoginDialog(); 20 | 21 | void accept() override; 22 | 23 | QString username() const; 24 | QString password() const; 25 | QString homeserver() const; 26 | 27 | private: 28 | Ui::LoginDialog *ui; 29 | }; 30 | 31 | #endif // LOGINDIALOG_H 32 | -------------------------------------------------------------------------------- /src/Spinner.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_SPINNER_HPP_ 2 | #define NATIVE_CHAT_SPINNER_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Spinner : public QWidget { 9 | Q_OBJECT 10 | public: 11 | Spinner(QWidget *parent = nullptr); 12 | 13 | QSize sizeHint() const override; 14 | 15 | static void paint(const QColor &head, const QColor &tail, QPainter &, int extent); 16 | 17 | protected: 18 | void paintEvent(QPaintEvent *) override; 19 | void showEvent(QShowEvent *) override; 20 | void hideEvent(QHideEvent *) override; 21 | void resizeEvent(QResizeEvent *) override; 22 | 23 | private: 24 | QTimer timer_; 25 | QPixmap pixmap_; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.1) 2 | 3 | project(native-chat CXX) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Wextra -Wno-missing-braces -pthread") 9 | 10 | # Qt 11 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 12 | set(CMAKE_AUTOMOC ON) 13 | 14 | set(GSL_PATH "${PROJECT_SOURCE_DIR}/deps/GSL") 15 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/") 16 | 17 | # Options 18 | set(BUILD_DEMOS ON CACHE BOOL "Whether to build demo executables for certain individual custom widgets") 19 | 20 | include_directories("${GSL_PATH}/include") 21 | 22 | add_subdirectory(src) 23 | -------------------------------------------------------------------------------- /src/matrix/Matrix.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_MATRIX_H_ 2 | #define NATIVE_CHAT_MATRIX_MATRIX_H_ 3 | 4 | #include 5 | 6 | class QNetworkAccessManager; 7 | 8 | namespace matrix { 9 | 10 | class UserID; 11 | 12 | class Matrix : public QObject { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit Matrix(QNetworkAccessManager &net, QObject *parent = 0); 17 | 18 | Matrix(const Matrix &) = delete; 19 | Matrix &operator=(const Matrix &) = delete; 20 | 21 | void login(QUrl homeserver, QString username, QString password); 22 | 23 | signals: 24 | void logged_in(const UserID &user_id, const QString &access_token); 25 | void login_error(QString message); 26 | 27 | private: 28 | friend class Session; 29 | QNetworkAccessManager &net; 30 | }; 31 | 32 | } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /cmake/version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -eu 3 | 4 | usage() { 5 | echo "$0 " 6 | exit 1 7 | } 8 | 9 | [ "$#" -eq 1 ] || usage 10 | 11 | SOURCE_DIR=$1 12 | 13 | _git() { 14 | git -C "$SOURCE_DIR" "$@" 15 | } 16 | 17 | COMMIT=$(_git rev-parse HEAD) 18 | COMMIT_STR="0x$(echo $COMMIT |cut -b 1-2)" 19 | for i in `seq 3 2 39`; do 20 | COMMIT_STR="$COMMIT_STR, 0x$(echo $COMMIT |cut -b $i-$((i+1)))" 21 | done 22 | TAG=$(_git describe --abbrev=0 --tags --always) 23 | cat < 5 | 6 | #include 7 | 8 | class QCompleter; 9 | class QAbstractListModel; 10 | 11 | class EntryBox : public QTextEdit { 12 | Q_OBJECT 13 | 14 | public: 15 | EntryBox(QAbstractListModel *members, QWidget *parent = nullptr); 16 | 17 | QSize sizeHint() const override; 18 | QSize minimumSizeHint() const override; 19 | 20 | void send(); 21 | 22 | signals: 23 | void message(const QString &); 24 | void command(const QString &name, const QString &args); 25 | void pageUp(); 26 | void pageDown(); 27 | void activity(); 28 | 29 | protected: 30 | void keyPressEvent(QKeyEvent *event) override; 31 | 32 | private: 33 | std::deque true_history_, working_history_; 34 | size_t history_index_; 35 | QCompleter *completer_; 36 | 37 | void text_changed(); 38 | void after_completion(int); 39 | }; 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: cpp 4 | 5 | matrix: 6 | include: 7 | - os: osx 8 | osx_image: xcode8 9 | compiler: clang 10 | env: BUILD_TYPE=Debug 11 | - os: osx 12 | osx_image: xcode8 13 | compiler: clang 14 | env: BUILD_TYPE=Release 15 | - os: linux 16 | env: BUILD_TYPE=Debug GCC_VERSION=6 17 | - os: linux 18 | env: BUILD_TYPE=Release GCC_VERSION=6 19 | 20 | install: 21 | - if [ $TRAVIS_OS_NAME == osx ]; then brew update && brew install qt5 lmdb; fi 22 | - if [ $TRAVIS_OS_NAME == osx ]; then export CMAKE_PREFIX_PATH=/usr/local/opt/qt5; fi 23 | - if [ $TRAVIS_OS_NAME == linux ]; then ./trusty.sh; fi 24 | - if [ $TRAVIS_OS_NAME == linux ]; then export CC=gcc-${GCC_VERSION} CXX=g++-${GCC_VERSION}; fi 25 | 26 | before_script: 27 | - if [ $TRAVIS_OS_NAME == linux ]; then source /opt/qt56/bin/qt56-env.sh; fi 28 | - cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -H. -Bbuild 29 | 30 | script: 31 | - make -C build -j2 32 | -------------------------------------------------------------------------------- /src/ContentCache.cpp: -------------------------------------------------------------------------------- 1 | #include "ContentCache.hpp" 2 | 3 | using matrix::Thumbnail; 4 | 5 | void ThumbnailCache::ref(const Thumbnail &x) { 6 | auto i = items_.emplace(std::piecewise_construct, 7 | std::forward_as_tuple(x), 8 | std::forward_as_tuple()); 9 | if(i.second) { 10 | needs(x); 11 | } else { 12 | ++i.first->second.refs; 13 | } 14 | } 15 | 16 | void ThumbnailCache::unref(const Thumbnail &x) { 17 | auto it = items_.find(x); 18 | if(it->second.refs == 0) { 19 | items_.erase(it); 20 | } else { 21 | --it->second.refs; 22 | } 23 | } 24 | 25 | const std::experimental::optional &ThumbnailCache::get(const Thumbnail &x) const { 26 | return items_.at(x).pixmap; 27 | } 28 | 29 | void ThumbnailCache::set(const Thumbnail &x, QPixmap p) { 30 | auto it = items_.find(x); 31 | if(it != items_.end()) { 32 | p.setDevicePixelRatio(device_pixel_ratio_); 33 | it->second.pixmap = p; 34 | updated(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | global: 3 | MSYSTEM: "MINGW64" 4 | LMDB_VERSION: "0.9.18" 5 | 6 | version: 0.0.0.{build} 7 | 8 | platform: 9 | - x64 10 | 11 | configuration: 12 | - Debug 13 | - Release 14 | 15 | install: 16 | - git submodule update --init 17 | - C:\msys64\usr\bin\bash.exe -lc "pacman --noconfirm -Syuu mingw-w64-x86_64-qt5 mingw-w64-x86_64-lmdb" 18 | 19 | build_script: 20 | - C:\msys64\usr\bin\bash.exe -lc "cd $APPVEYOR_BUILD_FOLDER; cmake -DCMAKE_BUILD_TYPE=$CONFIGURATION -H. -Bbuild -G 'MSYS Makefiles'; make -C build -j2" 21 | 22 | after_build: 23 | - ps: md nachat 24 | - ps: Copy-Item "build/src/nachat.exe" -Destination "nachat/" 25 | - C:\msys64\usr\bin\bash.exe -lc "cp `ldd $APPVEYOR_BUILD_FOLDER/nachat/nachat.exe |cut -d' ' -f3 |grep /mingw64/bin/` $APPVEYOR_BUILD_FOLDER/nachat/" 26 | - C:\msys64\usr\bin\bash.exe -lc "windeployqt.exe --release --no-libraries $APPVEYOR_BUILD_FOLDER/nachat/nachat.exe" 27 | - 7z a nachat.zip nachat/ 28 | - 7z a nachat.exe.zip ./nachat/nachat.exe 29 | 30 | artifacts: 31 | - path: nachat.zip 32 | - path: nachat.exe.zip 33 | -------------------------------------------------------------------------------- /src/EventSourceView.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EventSourceView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 256 10 | 192 11 | 12 | 13 | 14 | Event Source 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/LoginDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "LoginDialog.hpp" 2 | #include "ui_LoginDialog.h" 3 | 4 | #include 5 | 6 | #include "matrix/Session.hpp" 7 | 8 | LoginDialog::LoginDialog(QWidget *parent) 9 | : QDialog(parent), ui(new Ui::LoginDialog) { 10 | ui->setupUi(this); 11 | 12 | ui->buttonBox->addButton(tr("Quit"), QDialogButtonBox::RejectRole); 13 | ui->buttonBox->addButton(tr("Sign In"), QDialogButtonBox::AcceptRole); 14 | 15 | QSettings settings; 16 | 17 | auto username = settings.value("login/username"); 18 | if(!username.isNull()) { 19 | ui->username->setText(settings.value("login/username").toString()); 20 | ui->password->setFocus(Qt::OtherFocusReason); 21 | } 22 | 23 | auto homeserver = settings.value("login/homeserver"); 24 | if(!homeserver.isNull()) { 25 | ui->homeserver->setText(homeserver.toString()); 26 | } 27 | } 28 | 29 | LoginDialog::~LoginDialog() { delete ui; } 30 | 31 | void LoginDialog::accept() { 32 | setDisabled(true); 33 | setResult(QDialog::Accepted); 34 | accepted(); 35 | } 36 | 37 | QString LoginDialog::username() const { return ui->username->text(); } 38 | QString LoginDialog::password() const { return ui->password->text(); } 39 | QString LoginDialog::homeserver() const { return ui->homeserver->text(); } 40 | -------------------------------------------------------------------------------- /src/RoomViewList.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_ROOM_VIEW_LIST_HPP_ 2 | #define NATIVE_CHAT_ROOM_VIEW_LIST_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "matrix/ID.hpp" 9 | 10 | class QMenu; 11 | 12 | namespace matrix { 13 | class Room; 14 | } 15 | 16 | class RoomViewList : public QListWidget { 17 | Q_OBJECT 18 | public: 19 | RoomViewList(QWidget *parent = nullptr); 20 | 21 | void add(matrix::Room &room); 22 | void release(const matrix::RoomID &room); 23 | void activate(const matrix::RoomID &room); 24 | void update_display(matrix::Room &room); 25 | 26 | QSize sizeHint() const override; 27 | 28 | signals: 29 | void released(const matrix::RoomID &); 30 | void claimed(const matrix::RoomID &); 31 | void activated(const matrix::RoomID &); 32 | void pop_out(const matrix::RoomID &); 33 | 34 | protected: 35 | void contextMenuEvent(QContextMenuEvent *) override; 36 | 37 | private: 38 | struct RoomInfo { 39 | RoomInfo(QListWidgetItem *i, const matrix::Room &r); 40 | 41 | QListWidgetItem *item; 42 | bool has_unread; 43 | QString name; 44 | size_t highlight_count; 45 | }; 46 | 47 | std::unordered_map items_; 48 | QMenu *menu_; 49 | QListWidgetItem *context_; 50 | 51 | void update_item(const RoomInfo &i); 52 | }; 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /src/matrix/Content.cpp: -------------------------------------------------------------------------------- 1 | #include "Content.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace matrix { 10 | 11 | Content::Content(const QUrl &url) { 12 | if(url.scheme() != "mxc") 13 | throw illegal_content_scheme(); 14 | host_ = url.host(QUrl::FullyDecoded); 15 | id_ = url.path(QUrl::FullyDecoded).remove(0, 1); 16 | } 17 | 18 | QUrl Content::url() const noexcept { 19 | QUrl url; 20 | url.setScheme("mxc"); 21 | url.setHost(host_); 22 | url.setPath("/" + QUrl::toPercentEncoding(id_), QUrl::StrictMode); 23 | return url; 24 | } 25 | 26 | QUrl Content::url_on(const QUrl &homeserver) const noexcept { 27 | QUrl url = homeserver; 28 | url.setPath(QString("/_matrix/media/r0/download/" % QUrl::toPercentEncoding(host_) % "/" % QUrl::toPercentEncoding(id_)), QUrl::StrictMode); 29 | return url; 30 | } 31 | 32 | QUrl Thumbnail::url_on(const QUrl &homeserver) const { 33 | QUrl url = homeserver; 34 | QUrlQuery query; 35 | query.addQueryItem("width", QString::number(size().width())); 36 | query.addQueryItem("height", QString::number(size().height())); 37 | query.addQueryItem("method", method() == ThumbnailMethod::SCALE ? "scale" : "crop"); 38 | url.setQuery(std::move(query)); 39 | url.setPath(QString("/_matrix/media/r0/thumbnail/" % content().host() % "/" % content().id())); 40 | return url; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/ChatWindow.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_CHAT_WINDOW_HPP_ 2 | #define NATIVE_CHAT_CHAT_WINDOW_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "matrix/Room.hpp" 10 | 11 | class RoomView; 12 | class RoomViewList; 13 | class ThumbnailCache; 14 | 15 | namespace matrix { 16 | class Room; 17 | } 18 | 19 | namespace Ui { 20 | class ChatWindow; 21 | } 22 | 23 | class ChatWindow : public QWidget 24 | { 25 | Q_OBJECT 26 | 27 | public: 28 | explicit ChatWindow(ThumbnailCache &cache, QWidget *parent = 0); 29 | ~ChatWindow(); 30 | 31 | void add(matrix::Room &r, RoomView *); // Takes ownership 32 | void add_or_focus(matrix::Room &); 33 | void room_display_changed(matrix::Room &); 34 | 35 | RoomView *take(const matrix::RoomID &); // Releases ownership 36 | 37 | const matrix::RoomID &focused_room() const; 38 | 39 | signals: 40 | void focused(const matrix::RoomID &); 41 | void released(const matrix::RoomID &); 42 | void claimed(const matrix::RoomID &); 43 | void pop_out(const matrix::RoomID &, RoomView *); 44 | 45 | protected: 46 | void changeEvent(QEvent *event) override; 47 | void closeEvent(QCloseEvent *event) override; 48 | 49 | private: 50 | Ui::ChatWindow *ui; 51 | RoomViewList *room_list_; 52 | std::unordered_map rooms_; 53 | ThumbnailCache &cache_; 54 | 55 | void update_title(); 56 | void current_changed(int i); 57 | }; 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /src/ChatWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChatWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 311 10 | 250 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | Qt::Horizontal 36 | 37 | 38 | 39 | 40 | 1 41 | 0 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/matrix/Matrix.cpp: -------------------------------------------------------------------------------- 1 | #include "Matrix.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "utils.hpp" 8 | #include "Event.hpp" 9 | 10 | namespace matrix { 11 | 12 | Matrix::Matrix(QNetworkAccessManager &net, QObject *parent) : QObject(parent), net(net) {} 13 | 14 | void Matrix::login(QUrl homeserver, QString username, QString password) { 15 | QUrl login_url(homeserver); 16 | login_url.setPath("/_matrix/client/r0/login"); 17 | QNetworkRequest request(login_url); 18 | request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); 19 | QJsonObject body{ 20 | {"type", "m.login.password"}, 21 | {"user", username}, 22 | {"password", password} 23 | }; 24 | 25 | auto reply = net.post(request, encode(body)); 26 | 27 | connect(reply, &QNetworkReply::finished, [this, reply, homeserver](){ 28 | auto r = decode(reply); 29 | if(r.code == 403) { 30 | login_error(tr("Login failed. Check username/password.")); 31 | return; 32 | } 33 | if(r.error) { 34 | login_error(*r.error); 35 | return; 36 | } 37 | auto token = r.object["access_token"]; 38 | auto user_id = r.object["user_id"]; 39 | if(!token.isString() || !user_id.isString()) { 40 | login_error(tr("Malformed response from server")); 41 | return; 42 | } 43 | logged_in(UserID(user_id.toString()), token.toString()); 44 | }); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/RoomView.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ROOMVIEW_H 2 | #define ROOMVIEW_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "QStringHash.hpp" 9 | 10 | namespace Ui { 11 | class RoomView; 12 | } 13 | 14 | namespace matrix { 15 | class Room; 16 | class RoomState; 17 | enum class Membership; 18 | class TimelineManager; 19 | class UserID; 20 | class EventType; 21 | class MemberListModel; 22 | 23 | namespace event { 24 | class Room; 25 | class Content; 26 | 27 | namespace room { 28 | class MemberContent; 29 | } 30 | } 31 | } 32 | 33 | class TimelineView; 34 | class EntryBox; 35 | class MemberList; 36 | class ThumbnailCache; 37 | 38 | class RoomView : public QWidget 39 | { 40 | Q_OBJECT 41 | 42 | public: 43 | explicit RoomView(ThumbnailCache &cache, matrix::Room &room, QWidget *parent = nullptr); 44 | ~RoomView(); 45 | 46 | const matrix::Room &room() const { return room_; } 47 | matrix::Room &room() { return room_; } 48 | 49 | void selected(); 50 | // Notify that user action has brought the room into view. Triggers read receipts. 51 | 52 | private: 53 | Ui::RoomView *ui; 54 | TimelineView *timeline_view_; 55 | matrix::Room &room_; 56 | matrix::TimelineManager *timeline_manager_; 57 | matrix::MemberListModel *member_list_; 58 | EntryBox *entry_; 59 | 60 | void topic_changed(); 61 | void command(const QString &name, const QString &args); 62 | void send(const matrix::EventType &ty, const matrix::event::Content &content); 63 | void update_last_read(); 64 | }; 65 | 66 | #endif // ROOMVIEW_H 67 | -------------------------------------------------------------------------------- /src/MainWindow.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "matrix/Matrix.hpp" 10 | #include "matrix/ID.hpp" 11 | 12 | #include "ContentCache.hpp" 13 | #include "JoinedRoomListModel.hpp" 14 | 15 | class QProgressBar; 16 | class QLabel; 17 | class ChatWindow; 18 | class QListWidgetItem; 19 | 20 | namespace Ui { 21 | class MainWindow; 22 | } 23 | 24 | namespace matrix { 25 | class Room; 26 | class Session; 27 | } 28 | 29 | class MainWindow : public QMainWindow { 30 | Q_OBJECT 31 | 32 | public: 33 | explicit MainWindow(matrix::Session &session); 34 | ~MainWindow(); 35 | 36 | signals: 37 | void quit(); 38 | void log_out(); 39 | 40 | private: 41 | Ui::MainWindow *ui; 42 | matrix::Session &session_; 43 | QProgressBar *progress_; 44 | QLabel *sync_label_; 45 | QPointer last_focused_; 46 | ThumbnailCache thumbnail_cache_; 47 | JoinedRoomListModel rooms_; 48 | std::unordered_map windows_; 49 | 50 | void sync_progress(qint64 received, qint64 total); 51 | ChatWindow *spawn_chat_window(); 52 | void highlight(const matrix::RoomID &room); 53 | }; 54 | 55 | class RoomWindowBridge : public QObject { 56 | Q_OBJECT 57 | public: 58 | RoomWindowBridge(matrix::Room &room, ChatWindow &parent); 59 | 60 | void display_changed(); 61 | void check_release(const matrix::RoomID &room); 62 | 63 | private: 64 | matrix::Room &room_; 65 | ChatWindow &window_; 66 | }; 67 | 68 | #endif // MAINWINDOW_H 69 | -------------------------------------------------------------------------------- /src/JoinedRoomListModel.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_JOINED_ROOM_LIST_MODEL_HPP_ 2 | #define NACHAT_JOINED_ROOM_LIST_MODEL_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "matrix/ID.hpp" 13 | 14 | namespace matrix { 15 | class Room; 16 | class Session; 17 | } 18 | 19 | struct RoomInfo { 20 | matrix::RoomID id; 21 | QString display_name; 22 | bool unread; 23 | std::size_t highlight_count; 24 | QUrl avatar_url; 25 | std::experimental::optional avatar; 26 | std::size_t avatar_generation; 27 | 28 | explicit RoomInfo(const matrix::Room &); 29 | void update(const matrix::Room &); 30 | }; 31 | 32 | class JoinedRoomListModel : public QAbstractListModel { 33 | Q_OBJECT 34 | 35 | public: 36 | enum Role { 37 | IDRole = Qt::UserRole, UnreadRole 38 | }; 39 | 40 | explicit JoinedRoomListModel(matrix::Session &session, QSize icon_size, qreal device_pixel_ratio); 41 | 42 | int rowCount(const QModelIndex &parent) const override; 43 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 44 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 45 | 46 | void icon_size_changed(const QSize &size); 47 | 48 | private: 49 | matrix::Session &session_; 50 | std::vector rooms_; 51 | std::unordered_map index_; 52 | QSize icon_size_; 53 | qreal device_pixel_ratio_; 54 | 55 | void joined(matrix::Room &room); 56 | void update_room(matrix::Room &room); 57 | 58 | void update_avatar(RoomInfo &); 59 | }; 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /src/matrix/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace matrix { 8 | 9 | QByteArray encode(QJsonObject o) { 10 | return QJsonDocument(o).toJson(QJsonDocument::Compact); 11 | } 12 | 13 | Response decode(QNetworkReply *reply) { 14 | Response r; 15 | auto data = reply->readAll(); 16 | r.code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 17 | if(r.code == 0) { 18 | r.error = reply->errorString(); 19 | return r; 20 | } 21 | QJsonParseError err{0, QJsonParseError::NoError}; 22 | auto json = QJsonDocument::fromJson(data, &err); 23 | if(err.error) { 24 | if(r.code >= 300) { 25 | // If we couldn't parse the json returned with an error, we probably aren't talking to a matrix server, so just return the HTTP code. 26 | r.error = QObject::tr("HTTP %1 %2").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()); 27 | return r; 28 | } 29 | 30 | QString msg; 31 | msg = QObject::tr("Malformed response from server: %1").arg(err.errorString()); 32 | if(data.size()) { 33 | msg += QObject::tr("\nResponse was:\n%1").arg(QString::fromUtf8(data)); 34 | } 35 | r.error = msg; 36 | return r; 37 | } 38 | 39 | if(!json.isObject()) { 40 | r.error = QObject::tr("Malformed response from server: not a json object\nResponse was:\n%1").arg(QString::fromUtf8(data)); 41 | return r; 42 | } 43 | 44 | r.object = json.object(); 45 | 46 | if(r.code >= 300) { 47 | r.error = r.object["error"].toString(); 48 | if(!r.error->size()) { 49 | r.error = QObject::tr("HTTP %1 %2").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()); 50 | } 51 | } 52 | return r; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(AddVersion) 2 | 3 | find_package(Qt5Network 5.6.0 REQUIRED) 4 | find_package(Qt5Widgets 5.6.0 REQUIRED) 5 | 6 | qt5_wrap_ui(UI_HEADERS 7 | LoginDialog.ui 8 | MainWindow.ui 9 | RoomView.ui 10 | ChatWindow.ui 11 | RedactDialog.ui 12 | JoinDialog.ui 13 | EventSourceView.ui 14 | ) 15 | 16 | add_subdirectory(matrix) 17 | 18 | add_version(version.cpp) 19 | 20 | add_executable(nachat WIN32 21 | main.cpp 22 | LoginDialog.cpp 23 | MainWindow.cpp 24 | ChatWindow.cpp 25 | RoomView.cpp 26 | TimelineView.cpp 27 | EntryBox.cpp 28 | RoomMenu.cpp 29 | sort.cpp 30 | RoomViewList.cpp 31 | Spinner.cpp 32 | RedactDialog.cpp 33 | JoinDialog.cpp 34 | version.cpp 35 | version_string.cpp 36 | MessageBox.cpp 37 | EventSourceView.cpp 38 | ContentCache.cpp 39 | JoinedRoomListModel.cpp 40 | ${UI_HEADERS} 41 | ) 42 | 43 | target_link_libraries(nachat 44 | matrix 45 | Qt5::Widgets 46 | Qt5::Network 47 | ) 48 | 49 | if(BUILD_DEMOS) 50 | add_executable(spinner-test WIN32 51 | spinner_test.cpp 52 | Spinner.cpp 53 | ) 54 | 55 | target_link_libraries(spinner-test 56 | Qt5::Widgets 57 | ) 58 | 59 | add_executable(timeline-view-test WIN32 60 | timeline_view_test.cpp 61 | TimelineView.cpp 62 | ContentCache.cpp 63 | Spinner.cpp 64 | RedactDialog.cpp 65 | EventSourceView.cpp 66 | ${UI_HEADERS} 67 | ) 68 | 69 | target_link_libraries(timeline-view-test 70 | matrix 71 | Qt5::Widgets 72 | ) 73 | endif(BUILD_DEMOS) 74 | 75 | if(WIN32) 76 | target_link_libraries(nachat Qt5::WinMain) 77 | target_link_libraries(spinner-test Qt5::WinMain) 78 | if(BUILD_DEMOS) 79 | target_link_libraries(timeline-view-test Qt5::WinMain) 80 | endif(BUILD_DEMOS) 81 | endif(WIN32) 82 | 83 | install( 84 | TARGETS nachat 85 | DESTINATION bin 86 | ) 87 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | This project is in early development and is not yet suitable for regular use. 2 | 3 | Development builds: [[https://ci.appveyor.com/api/projects/Ralith/nachat/artifacts/nachat.zip?branch=master&job=Configuration%3A+Release][Windows 64-bit]] 4 | 5 | * Dependencies 6 | - LMDB 7 | - Qt >= 5.6 8 | 9 | Qt 5.6 provides extended text layout support nachat relies on to implement clickable URLs in the custom timeline 10 | widget, and provides superior support for high-DPI displays. Supporting earlier versions is unfortunately not trivial. 11 | 12 | * Build Requirements 13 | - CMake >= 3.1 14 | - A C++14 compiler and stdlib providing ~std::experimental::optional~ (OSX users need Xcode >= 8) 15 | 16 | * Building 17 | The binary will be output at ~build/src/nachat~ and can be executed in-place. Building on Windows and OSX should be 18 | possible but is not yet documented. 19 | ** Linux 20 | 1. Clone this repository and its submodules: 21 | #+BEGIN_SRC sh 22 | git clone --recursive https://github.com/Ralith/nachat.git 23 | cd nachat 24 | #+END_SRC 25 | or 26 | #+BEGIN_SRC sh 27 | git clone https://github.com/Ralith/nachat.git 28 | cd nachat 29 | git submodule update --init 30 | #+END_SRC 31 | 2. ~cmake -H. -Bbuild~ 32 | 3. ~make -C build~ 33 | ** Nix 34 | A nix environment is provided. Execute ~nix-shell .~ before ~cmake~. 35 | 36 | * Installation 37 | ** Using Nix 38 | ~nix-env -f . -i~ will add ~nachat~ to your path. Development takes place on the ~nixos-unstable~ channel, so 39 | building on ~nixos-16.03~ may require some tweaking of ~default.nix~. 40 | 41 | * Run-time data 42 | ** Linux 43 | Most recent username, homeserver, and access token are stored in Qt INI format at 44 | ~$XDG_CONFIG_HOME/nachat/nachat.conf~. Per-account data, including room state, is cached at 45 | ~$XDG_CACHE_HOME/nachat/nachat/~. 46 | -------------------------------------------------------------------------------- /src/matrix/MemberListModel.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_MATRIX_MEMBER_LIST_MODEL_HPP_ 2 | #define NACHAT_MATRIX_MEMBER_LIST_MODEL_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "ID.hpp" 13 | #include "Event.hpp" 14 | 15 | namespace matrix { 16 | class Room; 17 | class ContentFetch; 18 | 19 | class MemberListModel : public QAbstractListModel { 20 | public: 21 | static constexpr int IDRole = Qt::UserRole; 22 | 23 | explicit MemberListModel(Room &room, QSize icon_size, qreal device_pixel_ratio, QObject *parent = nullptr); 24 | 25 | int rowCount(const QModelIndex &parent) const override; 26 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 27 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 28 | 29 | private: 30 | struct Info { 31 | UserID id; 32 | event::room::MemberContent content; 33 | std::experimental::optional disambiguation; 34 | std::experimental::optional avatar; 35 | 36 | Info(UserID, event::room::MemberContent, std::experimental::optional); 37 | }; 38 | 39 | Room &room_; 40 | std::vector members_; 41 | std::unordered_map index_; 42 | QSize icon_size_; 43 | qreal device_pixel_ratio_; 44 | std::unordered_map avatar_fetch_queue_; 45 | 46 | void member_changed(const UserID &id, const event::room::MemberContent &old, const event::room::MemberContent ¤t); 47 | void member_disambiguation_changed(const UserID &id, const std::experimental::optional &old, const std::experimental::optional ¤t); 48 | 49 | void queue_fetch(const Info &info); 50 | void do_fetch(); 51 | void finish_fetch(UserID id, QUrl url); 52 | }; 53 | 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /src/matrix/proto.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_PROTO_HPP_ 2 | #define NATIVE_CHAT_MATRIX_PROTO_HPP_ 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "Event.hpp" 9 | #include "ID.hpp" 10 | 11 | class QJsonValue; 12 | 13 | namespace matrix { 14 | 15 | namespace proto { 16 | 17 | struct Presence { 18 | std::vector events; 19 | }; 20 | 21 | struct State { 22 | std::vector events; 23 | }; 24 | 25 | struct Timeline { 26 | bool limited; 27 | TimelineCursor prev_batch; 28 | std::vector events; 29 | 30 | explicit Timeline(TimelineCursor &&prev) : prev_batch{std::move(prev)} {} 31 | }; 32 | 33 | struct UnreadNotifications { 34 | uint64_t highlight_count; 35 | uint64_t notification_count; 36 | }; 37 | 38 | struct AccountData { 39 | std::vector events; 40 | }; 41 | 42 | struct Ephemeral { 43 | std::vector events; 44 | }; 45 | 46 | struct JoinedRoom { 47 | RoomID id; 48 | UnreadNotifications unread_notifications; 49 | Timeline timeline; 50 | State state; 51 | AccountData account_data; 52 | Ephemeral ephemeral; 53 | 54 | JoinedRoom(RoomID &&id, Timeline &&t) : id(std::move(id)), timeline(std::move(t)) {} 55 | }; 56 | 57 | struct LeftRoom { 58 | RoomID id; 59 | Timeline timeline; 60 | State state; 61 | 62 | LeftRoom(RoomID &&id, Timeline &&timeline) : id(std::move(id)), timeline(std::move(timeline)) {} 63 | }; 64 | 65 | struct InviteState { 66 | std::vector events; 67 | }; 68 | 69 | struct InvitedRoom { 70 | InviteState invite_state; 71 | }; 72 | 73 | struct Rooms { 74 | std::vector join; 75 | std::vector leave; 76 | std::vector invite; 77 | }; 78 | 79 | struct Sync { 80 | SyncCursor next_batch; 81 | Presence presence; 82 | Rooms rooms; 83 | 84 | explicit Sync(SyncCursor &&next) : next_batch{std::move(next)} {} 85 | }; 86 | 87 | } 88 | 89 | proto::Sync parse_sync(QJsonValue v); 90 | 91 | } 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /src/RedactDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RedactDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 268 10 | 224 11 | 12 | 13 | 14 | Redact Event 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | 23 | Redacted because... 24 | 25 | 26 | 27 | 28 | 29 | 30 | Qt::Horizontal 31 | 32 | 33 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | buttonBox 43 | accepted() 44 | RedactDialog 45 | accept() 46 | 47 | 48 | 248 49 | 254 50 | 51 | 52 | 157 53 | 274 54 | 55 | 56 | 57 | 58 | buttonBox 59 | rejected() 60 | RedactDialog 61 | reject() 62 | 63 | 64 | 316 65 | 260 66 | 67 | 68 | 286 69 | 274 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/RoomMenu.cpp: -------------------------------------------------------------------------------- 1 | #include "RoomMenu.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "matrix/Session.hpp" 10 | #include "MessageBox.hpp" 11 | 12 | RoomMenu::RoomMenu(matrix::Room &room, QWidget *parent) : QMenu(parent), room_(room) { 13 | { 14 | auto info = addAction(QIcon::fromTheme("emblem-information"), tr("&Info...")); 15 | } 16 | 17 | { 18 | auto upload = addAction(QIcon::fromTheme("document-open"), tr("Upload &file...")); 19 | auto file_dialog = new QFileDialog(parent); 20 | connect(upload, &QAction::triggered, file_dialog, &QDialog::open); 21 | connect(file_dialog, &QFileDialog::fileSelected, this, &RoomMenu::upload_file); 22 | } 23 | 24 | addSeparator(); 25 | 26 | { 27 | auto leave = addAction(QIcon::fromTheme("system-log-out"), tr("Leave")); 28 | connect(leave, &QAction::triggered, &room_, &matrix::Room::leave); 29 | } 30 | } 31 | 32 | void RoomMenu::upload_file(const QString &path) { 33 | auto file = std::make_shared(path); 34 | QFileInfo info(*file); 35 | if(!file->open(QIODevice::ReadOnly)) { 36 | MessageBox::critical(tr("Error opening file"), tr("Couldn't open %1: %2").arg(info.fileName()).arg(file->errorString()), parentWidget()); 37 | return; 38 | } 39 | 40 | const QString &type = QMimeDatabase().mimeTypeForFile(info).name(); 41 | auto reply = room_.session().upload(*file, type, info.fileName()); 42 | QPointer room(&room_); 43 | // This closure captures 'file' to ensure its outlives the network request 44 | connect(reply, &matrix::ContentPost::success, [file, room, info, type](const QString &uri) { 45 | if(!room) return; 46 | room->send_file(uri, info.fileName(), type, info.size()); 47 | }); 48 | QPointer parent(parentWidget()); 49 | connect(reply, &matrix::ContentPost::error, [parent, info](const QString &msg) { 50 | MessageBox::critical(tr("Error uploading file"), tr("Couldn't upload %1: %2").arg(info.fileName()).arg(msg), parent); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/JoinDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | JoinDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 176 10 | 54 11 | 12 | 13 | 14 | Join room 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | 23 | Room alias or ID 24 | 25 | 26 | #room:example.com 27 | 28 | 29 | 30 | 31 | 32 | 33 | Qt::Horizontal 34 | 35 | 36 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | buttonBox 46 | accepted() 47 | JoinDialog 48 | accept() 49 | 50 | 51 | 248 52 | 254 53 | 54 | 55 | 157 56 | 274 57 | 58 | 59 | 60 | 61 | buttonBox 62 | rejected() 63 | JoinDialog 64 | reject() 65 | 66 | 67 | 316 68 | 260 69 | 70 | 71 | 286 72 | 274 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/ContentCache.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_CONTENT_CACHE_HPP_ 2 | #define NACHAT_CONTENT_CACHE_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "matrix/Content.hpp" 11 | 12 | class ThumbnailCache : public QObject { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit ThumbnailCache(qreal device_pixel_ratio = 1.0) : device_pixel_ratio_{device_pixel_ratio} {} 17 | 18 | void ref(const matrix::Thumbnail &); 19 | void unref(const matrix::Thumbnail &); 20 | 21 | const std::experimental::optional &get(const matrix::Thumbnail &) const; 22 | void set(const matrix::Thumbnail &, QPixmap); 23 | 24 | signals: 25 | void needs(const matrix::Thumbnail &); 26 | void updated(); 27 | 28 | private: 29 | struct Item { 30 | size_t refs = 0; 31 | std::experimental::optional pixmap; 32 | }; 33 | 34 | qreal device_pixel_ratio_; 35 | std::unordered_map items_; 36 | }; 37 | 38 | class ThumbnailRef { 39 | public: 40 | ThumbnailRef(const matrix::Thumbnail &content, ThumbnailCache &cache) : content_{content}, cache_{&cache} { 41 | cache_->ref(content); 42 | } 43 | ~ThumbnailRef() { 44 | if(cache_) cache_->unref(content_); 45 | } 46 | 47 | ThumbnailRef(const ThumbnailRef &other) : content_{other.content_}, cache_{other.cache_} { 48 | cache_->ref(content_); 49 | } 50 | ThumbnailRef(ThumbnailRef &&other) : content_{other.content_}, cache_{other.cache_} { 51 | other.cache_ = nullptr; 52 | } 53 | 54 | ThumbnailRef &operator=(ThumbnailRef &&other) { 55 | if(cache_) cache_->unref(content_); 56 | content_ = other.content_; 57 | cache_ = other.cache_; 58 | other.cache_ = nullptr; 59 | return *this; 60 | } 61 | ThumbnailRef &operator=(const ThumbnailRef &other) { 62 | content_ = other.content_; 63 | cache_ = other.cache_; 64 | cache_->ref(content_); 65 | return *this; 66 | } 67 | 68 | const matrix::Thumbnail &content() { return content_; } 69 | 70 | const std::experimental::optional &operator*() const { 71 | return cache_->get(content_); 72 | } 73 | 74 | private: 75 | matrix::Thumbnail content_; 76 | 77 | ThumbnailCache *cache_; 78 | }; 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | * High 2 | - Buffer and retry failed sends inside lib 3 | - User feedback for connection/input/permissions errors 4 | - Sort user list by activity, then power, then name 5 | https://github.com/matrix-org/matrix-react-sdk/blob/507f5e2ca19156a2afd3470fc9b17fb5e65cdf9b/src/components/views/rooms/MemberList.js#L383 6 | - Fix history pages being downloaded from past pruned batches 7 | - Fix kicks being displayed as bans 8 | 9 | * Medium 10 | - Desktop notifications 11 | - Track member/room power levels 12 | - Clickable URLs in topics 13 | - Test on windows 14 | - Outlines or tinted bg instead of bolding for highlight/disambig? 15 | - Display read receipts? 16 | - Package icon theme on non-linux 17 | - Clickable MXIDs/room IDs/aliases 18 | - Creating rooms 19 | - Per-mxid storage of last transaction ID and open windows+rooms in a non-cache lmdb 20 | - Large avatars in user profile views 21 | - Room info dialog containing aliases, ID in selectable labels 22 | - Only open URLs on mouse release if the mouse was on the same item when originally clicked 23 | - Spec-compliant logins with compatibility checking and browser fallback 24 | - Check HS's advertised API version support before attempting login 25 | - Include date in timestamps >24h ago 26 | - Factor out reusable custom widgets into a lib 27 | - Rewrite timeline view to improved model/view 28 | 29 | * Low 30 | - Fix duplication of first message loaded from cache(?) by first message pulled from backlog 31 | This is a serverside issue, see SYN-645 32 | - Tearable tabs 33 | - Display disambiguated name changes? 34 | - Handle error 502 in content fetch as federation failure 35 | - Factor application logic out of widget subclasses 36 | - Room categories with drag&drop (QTreeView?) 37 | - Special-case block header/avatar for blocks whose first message is header/avatar change, using prev_content 38 | - Factor event rendering and input response out of TimelineView 39 | - WebRTC 40 | - Drag-and-drop file sharing 41 | - Persist arbitrary room state? 42 | - Fix flickering sync status 43 | - Save/restore open rooms 44 | - Spellcheck in entry box? 45 | - Configuration dialog 46 | - Event font 47 | - Maximum scrollback size 48 | - Timeline cache size 49 | - Timeline rendering parameters? 50 | - Clickable email addresses 51 | - matrix::Room::send_now(event) 52 | -------------------------------------------------------------------------------- /src/Spinner.cpp: -------------------------------------------------------------------------------- 1 | #include "Spinner.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | Spinner::Spinner(QWidget *parent) : QWidget(parent) { 11 | connect(&timer_, SIGNAL(timeout()), this, SLOT(update())); 12 | } 13 | 14 | void Spinner::paintEvent(QPaintEvent *) { 15 | QPainter painter(this); 16 | painter.setRenderHint(QPainter::SmoothPixmapTransform); 17 | 18 | const qreal rotation_seconds = 2; 19 | 20 | auto t = std::chrono::time_point_cast(std::chrono::steady_clock::now()); 21 | const qreal angle = 360. * static_cast(t.time_since_epoch().count() % static_cast(1000 * rotation_seconds)) / (1000 * rotation_seconds); 22 | const auto extent = std::min(width(), height()); 23 | 24 | painter.translate(extent/2, extent/2); 25 | painter.rotate(angle); 26 | painter.drawPixmap(QPoint(-extent/2, -extent/2), pixmap_); 27 | } 28 | 29 | void Spinner::showEvent(QShowEvent *) { 30 | timer_.start(30); 31 | } 32 | 33 | void Spinner::hideEvent(QHideEvent *) { 34 | timer_.stop(); 35 | } 36 | 37 | QSize Spinner::sizeHint() const { 38 | auto extent = fontMetrics().height() * 4; 39 | return QSize(extent, extent); 40 | } 41 | 42 | void Spinner::resizeEvent(QResizeEvent *) { 43 | const auto extent = std::min(width(), height()); 44 | pixmap_ = QPixmap(extent, extent); 45 | pixmap_.fill(Qt::transparent); 46 | QPainter painter(&pixmap_); 47 | painter.setRenderHint(QPainter::Antialiasing); 48 | paint(palette().color(QPalette::Shadow), palette().color(QPalette::Base), painter, extent); 49 | } 50 | 51 | void Spinner::paint(const QColor &head, const QColor &tail, QPainter &painter, int extent) { 52 | constexpr qreal margin = 1; 53 | const qreal thickness = extent / 7.0; 54 | const qreal inset = margin + thickness / 2.0; 55 | const qreal diameter = extent - thickness - 2*margin; 56 | 57 | const qreal angle = 0; 58 | const qreal angular_gap = 45; 59 | 60 | QConicalGradient gradient; 61 | gradient.setCenter(extent/2.0, extent/2.0); 62 | gradient.setAngle(angle - angular_gap / 2.0); 63 | gradient.setColorAt(0, head); 64 | gradient.setColorAt(1, tail); 65 | 66 | QPen pen(QBrush(gradient), thickness); 67 | pen.setCapStyle(Qt::RoundCap); 68 | 69 | QPainterPath path; 70 | path.arcMoveTo(inset, inset, diameter, diameter, angle); 71 | path.arcTo(inset, inset, diameter, diameter, angle, 360 - angular_gap); 72 | 73 | painter.setPen(pen); 74 | painter.drawPath(path); 75 | } 76 | -------------------------------------------------------------------------------- /src/matrix/Content.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_CONTENT_HPP_ 2 | #define NATIVE_CHAT_MATRIX_CONTENT_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "hash_combine.hpp" 12 | 13 | class QUrl; 14 | 15 | namespace matrix { 16 | 17 | class illegal_content_scheme : public std::invalid_argument { 18 | public: 19 | illegal_content_scheme() : std::invalid_argument{"content URLs had scheme other than \"mxc\""} {} 20 | }; 21 | 22 | class Content { 23 | public: 24 | Content(const QString &host, const QString &id) noexcept : host_(host), id_(id) {} 25 | explicit Content(const QUrl &url); 26 | 27 | const QString &host() const noexcept { return host_; } 28 | const QString &id() const noexcept { return id_; } 29 | 30 | QUrl url() const noexcept; 31 | QUrl url_on(const QUrl &homeserver) const noexcept; 32 | 33 | private: 34 | QString host_, id_; 35 | }; 36 | 37 | inline bool operator==(const Content &a, const Content &b) { 38 | return a.host() == b.host() && a.id() == b.id(); 39 | } 40 | 41 | enum class ThumbnailMethod { CROP, SCALE }; 42 | 43 | class Thumbnail { 44 | public: 45 | Thumbnail(Content content, QSize size, ThumbnailMethod method) noexcept : content_(content), size_(size), method_(method) {} 46 | 47 | const Content &content() const noexcept { return content_; } 48 | const QSize &size() const noexcept { return size_; } 49 | ThumbnailMethod method() const noexcept { return method_; } 50 | 51 | QUrl url_on(const QUrl &homeserver) const; 52 | 53 | private: 54 | Content content_; 55 | QSize size_; 56 | ThumbnailMethod method_; 57 | }; 58 | 59 | inline bool operator==(const Thumbnail &a, const Thumbnail &b) { 60 | return a.content() == b.content() && a.size() == b.size() && a.method() == b.method(); 61 | } 62 | 63 | } 64 | 65 | 66 | namespace std { 67 | 68 | template<> 69 | struct hash { 70 | size_t operator()(const matrix::Content &c) const { 71 | return hash_combine(qHash(c.host()), qHash(c.id())); 72 | } 73 | }; 74 | 75 | template<> 76 | struct hash { 77 | size_t operator()(const matrix::Thumbnail &t) const { 78 | return 79 | hash_combine( 80 | hash_combine( 81 | hash_combine( 82 | hash()(t.content()), 83 | hash()(t.size().width())), 84 | hash()(t.size().height())), 85 | hash()(static_cast(t.method()))); // Cast to int is unnecessary in C++14, but gcc 5 is missing support 86 | } 87 | }; 88 | 89 | } 90 | 91 | #endif 92 | -------------------------------------------------------------------------------- /src/matrix/ID.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_ID_HPP_ 2 | #define NATIVE_CHAT_MATRIX_ID_HPP_ 3 | 4 | #include 5 | 6 | #include "hash.hpp" 7 | 8 | namespace matrix { 9 | 10 | enum class Direction { FORWARD, BACKWARD }; 11 | 12 | template 13 | class ID { 14 | public: 15 | explicit ID(T value) : s(std::move(value)) {} 16 | explicit operator const T &() const noexcept { return s; } 17 | explicit operator T &() noexcept { return s; } 18 | T &value() noexcept { return s; } 19 | const T &value() const noexcept { return s; } 20 | private: 21 | T s; 22 | }; 23 | 24 | template inline bool operator==(const ID &x, const ID &y) noexcept { return x.value() == y.value(); } 25 | template inline bool operator!=(const ID &x, const ID &y) noexcept { return x.value() != y.value(); } 26 | template inline bool operator<(const ID &x, const ID &y) noexcept { return x.value() < y.value(); } 27 | 28 | class TransactionID : public ID { using ID::ID; }; 29 | 30 | class TimelineCursor : public ID { using ID::ID; }; 31 | class SyncCursor : public ID { using ID::ID; }; 32 | 33 | class EventID : public ID { using ID::ID; }; 34 | class RoomID : public ID { using ID::ID; }; 35 | 36 | class EventType : public ID { using ID::ID; }; 37 | class MessageType : public ID { using ID::ID; }; 38 | 39 | class StateKey : public ID { using ID::ID; }; 40 | 41 | class UserID : public ID { 42 | using ID::ID; 43 | explicit UserID(const StateKey &key) : ID(key.value()) {} 44 | }; 45 | 46 | struct StateID { 47 | EventType type; 48 | StateKey key; 49 | 50 | StateID(EventType type, StateKey key) : type(type), key(key) {} 51 | }; 52 | 53 | } 54 | 55 | namespace std { 56 | 57 | template<> 58 | struct hash { 59 | size_t operator()(const matrix::EventID &id) const { 60 | return qHash(id.value()); 61 | } 62 | }; 63 | 64 | template<> 65 | struct hash { 66 | size_t operator()(const matrix::RoomID &id) const { 67 | return qHash(id.value()); 68 | } 69 | }; 70 | 71 | template<> 72 | struct hash { 73 | size_t operator()(const matrix::UserID &id) const { 74 | return qHash(id.value()); 75 | } 76 | }; 77 | 78 | template<> 79 | struct hash { 80 | size_t operator()(const matrix::EventType &id) const { 81 | return qHash(id.value()); 82 | } 83 | }; 84 | 85 | template<> 86 | struct hash { 87 | size_t operator()(const matrix::StateKey &id) const { 88 | return qHash(id.value()); 89 | } 90 | }; 91 | 92 | template<> 93 | struct hash { 94 | size_t operator()(const matrix::StateID &e) const { 95 | return hash_combine(std::hash()(e.type), e.key); 96 | } 97 | }; 98 | 99 | } 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /src/MainWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | NaChat 15 | 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 32 | 33 | QAbstractItemView::ExtendedSelection 34 | 35 | 36 | QAbstractItemView::ScrollPerPixel 37 | 38 | 39 | true 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 0 49 | 0 50 | 400 51 | 22 52 | 53 | 54 | 55 | 56 | &Matrix 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | .. 71 | 72 | 73 | &Log out 74 | 75 | 76 | 77 | 78 | 79 | .. 80 | 81 | 82 | &Quit 83 | 84 | 85 | 86 | 87 | 88 | .. 89 | 90 | 91 | &Join room... 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/LoginDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LoginDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 166 10 | 94 11 | 12 | 13 | 14 | Sign In 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | User ID 23 | 24 | 25 | 26 | 27 | 28 | 29 | QLineEdit::Password 30 | 31 | 32 | Password 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Homeserver 42 | 43 | 44 | 45 | 46 | 47 | 48 | https://matrix.org 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Qt::Horizontal 60 | 61 | 62 | QDialogButtonBox::NoButton 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | buttonBox 72 | accepted() 73 | LoginDialog 74 | accept() 75 | 76 | 77 | 248 78 | 254 79 | 80 | 81 | 157 82 | 274 83 | 84 | 85 | 86 | 87 | buttonBox 88 | rejected() 89 | LoginDialog 90 | reject() 91 | 92 | 93 | 316 94 | 260 95 | 96 | 97 | 286 98 | 274 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/matrix/proto.cpp: -------------------------------------------------------------------------------- 1 | #include "proto.hpp" 2 | 3 | #include 4 | 5 | namespace matrix { 6 | 7 | using namespace proto; 8 | 9 | template 10 | std::vector> parse_array(QJsonValue v, F &&f) { 11 | auto a = v.toArray(); 12 | std::vector> out; 13 | out.reserve(a.size()); 14 | std::transform(a.begin(), a.end(), std::back_inserter(out), std::forward(f)); 15 | return out; 16 | } 17 | 18 | Timeline parse_timeline(QJsonValue v) { 19 | auto o = v.toObject(); 20 | Timeline t{TimelineCursor{o["prev_batch"].toString()}}; 21 | 22 | t.limited = o["limited"].toBool(); 23 | t.events = parse_array(o["events"], [](QJsonValue v) { 24 | return event::Room(event::Identifiable(Event(v.toObject()))); 25 | }); 26 | 27 | return t; 28 | } 29 | 30 | JoinedRoom parse_joined_room(QString id, QJsonValue v) { 31 | auto o = v.toObject(); 32 | JoinedRoom room{RoomID{id}, parse_timeline(o["timeline"])}; 33 | 34 | auto un = o["unread_notifications"].toObject(); 35 | room.unread_notifications.highlight_count = un["highlight_count"].toDouble(); 36 | room.unread_notifications.notification_count = un["notification_count"].toDouble(); 37 | room.state.events = parse_array(o["state"].toObject()["events"], [](QJsonValue v) { 38 | return event::room::State(event::Room(event::Identifiable(Event(v.toObject())))); 39 | }); 40 | room.account_data.events = parse_array(o["account_data"].toObject()["events"], [](QJsonValue v) { 41 | return Event(v.toObject()); 42 | }); 43 | room.ephemeral.events = parse_array(o["ephemeral"].toObject()["events"], [](QJsonValue v) { 44 | return Event(v.toObject()); 45 | }); 46 | 47 | // Work around SYN-766 48 | std::unordered_set event_ids; 49 | for(const auto &s : room.state.events) { 50 | event_ids.insert(s.id()); 51 | } 52 | if(room.timeline.events.size() && event_ids.count(room.timeline.events.front().id())) { 53 | room.timeline.events = std::vector(room.timeline.events.begin() + 1, room.timeline.events.end()); 54 | } 55 | 56 | return room; 57 | } 58 | 59 | Sync parse_sync(QJsonValue v) { 60 | auto o = v.toObject(); 61 | Sync sync{SyncCursor{o["next_batch"].toString()}}; 62 | QJsonObject::iterator i; 63 | 64 | { 65 | auto rooms = o["rooms"].toObject(); 66 | 67 | auto join = rooms["join"].toObject(); 68 | sync.rooms.join.reserve(join.size()); 69 | for(auto i = join.begin(); i != join.end(); ++i) { 70 | sync.rooms.join.push_back(parse_joined_room(i.key(), i.value())); 71 | } 72 | 73 | auto leave = rooms["leave"].toObject(); 74 | sync.rooms.leave.reserve(leave.size()); 75 | for(auto i = leave.begin(); i != leave.end(); ++i) { 76 | sync.rooms.leave.emplace_back(RoomID{i.key()}, parse_timeline(i.value().toObject()["timeline"])); 77 | } 78 | } 79 | 80 | sync.presence.events = parse_array(o["presence"].toObject()["events"], [](QJsonValue v) { 81 | return Event(v.toObject()); 82 | }); 83 | 84 | return sync; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "matrix/Matrix.hpp" 8 | #include "matrix/Session.hpp" 9 | 10 | #include "LoginDialog.hpp" 11 | #include "MainWindow.hpp" 12 | #include "MessageBox.hpp" 13 | 14 | #include "version.hpp" 15 | 16 | int main(int argc, char *argv[]) { 17 | printf("NaChat %s\n", version::string().toStdString().c_str()); 18 | 19 | QCoreApplication::setOrganizationName("nachat"); 20 | QCoreApplication::setApplicationName("nachat"); 21 | QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); 22 | 23 | QApplication app(argc, argv); 24 | app.setQuitOnLastWindowClosed(false); 25 | 26 | QSettings settings; 27 | 28 | QNetworkAccessManager net; // Create this on another thread for faster startup? 29 | matrix::Matrix matrix{net}; 30 | 31 | LoginDialog login; 32 | std::unique_ptr main_window; 33 | std::unique_ptr session; 34 | 35 | auto &&session_established = [&]() { 36 | QObject::connect(session.get(), &matrix::Session::logged_out, [&]() { 37 | main_window.reset(); 38 | 39 | // Pass ownership to Qt for disposal 40 | session->deleteLater(); 41 | session.release(); 42 | 43 | login.show(); 44 | }); 45 | main_window = std::make_unique(*session); 46 | QObject::connect(main_window.get(), &MainWindow::quit, &app, &QApplication::quit); 47 | QObject::connect(main_window.get(), &MainWindow::log_out, session.get(), &matrix::Session::log_out); 48 | QObject::connect(main_window.get(), &MainWindow::log_out, [&settings]() { 49 | settings.remove("session/access_token"); 50 | settings.remove("session/user_id"); 51 | }); 52 | main_window->show(); 53 | }; 54 | 55 | QObject::connect(&matrix, &matrix::Matrix::logged_in, [&](const matrix::UserID &user_id, const QString &access_token) { 56 | session = std::make_unique(matrix, login.homeserver(), user_id, access_token); 57 | settings.setValue("login/username", login.username()); 58 | settings.setValue("login/homeserver", login.homeserver()); 59 | settings.setValue("session/access_token", access_token); 60 | settings.setValue("session/user_id", user_id.value()); 61 | login.hide(); 62 | login.setDisabled(false); 63 | session_established(); 64 | }); 65 | 66 | QObject::connect(&matrix, &matrix::Matrix::login_error, [&](QString err){ 67 | login.setDisabled(false); 68 | MessageBox::critical(QObject::tr("Login Error"), err, &login); 69 | }); 70 | 71 | QObject::connect(&login, &LoginDialog::accepted, [&](){ 72 | matrix.login(login.homeserver(), login.username(), login.password()); 73 | }); 74 | 75 | auto homeserver = settings.value("login/homeserver"); 76 | auto access_token = settings.value("session/access_token"); 77 | auto user_id = settings.value("session/user_id"); 78 | if(access_token.isNull() || homeserver.isNull() || user_id.isNull()) { 79 | login.show(); 80 | } else { 81 | session = std::make_unique(matrix, homeserver.toString(), matrix::UserID(user_id.toString()), access_token.toString()); 82 | session_established(); 83 | } 84 | 85 | return app.exec(); 86 | } 87 | -------------------------------------------------------------------------------- /src/matrix/TimelineWindow.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_TIMELINE_WINDOW_HPP_ 2 | #define NATIVE_CHAT_MATRIX_TIMELINE_WINDOW_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include "Event.hpp" 14 | #include "Room.hpp" 15 | 16 | namespace matrix { 17 | 18 | namespace proto { 19 | struct Timeline; 20 | } 21 | 22 | class TimelineManager; 23 | 24 | class TimelineWindow { 25 | public: 26 | TimelineWindow(std::deque batches, const RoomState &final_state); 27 | 28 | void discard(const TimelineCursor &, Direction dir); 29 | 30 | bool at_start() const; 31 | bool at_end() const; 32 | 33 | TimelineCursor begin() const { return batches_.front().begin; } 34 | 35 | // Empty => window includes present 36 | std::experimental::optional end() const { return batches_end_; } 37 | 38 | TimelineCursor sync_begin() const { 39 | return sync_batch_.begin; 40 | } 41 | 42 | void prepend_batch(const TimelineCursor &start, const TimelineCursor &end, gsl::span reversed_events, 43 | TimelineManager *mgr); 44 | void append_batch(const TimelineCursor &start, const TimelineCursor &end, gsl::span events, 45 | TimelineManager *mgr); 46 | void append_sync(const proto::Timeline &t, TimelineManager *mgr); 47 | 48 | void reset(const RoomState ¤t_state); 49 | // Discard all but latest 50 | 51 | const RoomState &initial_state() { return initial_state_; } 52 | const std::deque &batches() const { return batches_; } 53 | const RoomState &final_state() { return final_state_; } 54 | 55 | private: 56 | RoomState initial_state_, final_state_; 57 | std::deque batches_; // have nonempty events 58 | std::experimental::optional batches_end_; 59 | Batch sync_batch_; // may equal batches_.back() 60 | }; 61 | 62 | class TimelineManager : public QObject { 63 | Q_OBJECT 64 | 65 | public: 66 | explicit TimelineManager(Room &room, QObject *parent = nullptr); 67 | 68 | TimelineWindow &window() { return window_; } 69 | const TimelineWindow &window() const { return window_; } 70 | 71 | void grow(Direction dir); 72 | 73 | void replay(); 74 | 75 | signals: 76 | void grew(Direction dir, const TimelineCursor &begin, const RoomState &state, const event::Room &evt); 77 | 78 | void discontinuity(); 79 | // Gap between successive syncs; if latest batch is being displayed, user should discard it and proceed as if paging 80 | // forwards in time 81 | 82 | private: 83 | Room &room_; 84 | TimelineWindow window_; 85 | 86 | MessageFetch *forward_req_; 87 | MessageFetch *backward_req_; 88 | 89 | QTimer retry_timer_; 90 | Direction retry_dir_; 91 | 92 | // Helpers 93 | void retry(); 94 | void error(Direction dir, const QString &msg); 95 | 96 | // Signal handlers 97 | void forward_fetch_error(const QString &msg); 98 | void backward_fetch_error(const QString &msg); 99 | void got_backward(const TimelineCursor &start, const TimelineCursor &end, gsl::span reversed_events); 100 | void got_forward(const TimelineCursor &start, const TimelineCursor &end, gsl::span events); 101 | void batch(const proto::Timeline &t); 102 | }; 103 | 104 | } 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /src/RoomView.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RoomView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 268 10 | 248 11 | 12 | 13 | 14 | 15 | 0 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | Qt::Vertical 33 | 34 | 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 46 | 47 | 48 | Qt::PlainText 49 | 50 | 51 | true 52 | 53 | 54 | true 55 | 56 | 57 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 0 67 | 68 | 69 | 70 | Qt::TabFocus 71 | 72 | 73 | 74 | .. 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 0 84 | 1 85 | 86 | 87 | 88 | Qt::Horizontal 89 | 90 | 91 | 92 | QAbstractItemView::ScrollPerPixel 93 | 94 | 95 | QAbstractItemView::ScrollPerPixel 96 | 97 | 98 | true 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/FixedVector.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NACHAT_FIXED_VECTOR_HPP_ 2 | #define NACHAT_FIXED_VECTOR_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | template 10 | class FixedVector { 11 | private: 12 | using storage_type = std::aligned_storage_t; 13 | 14 | public: 15 | using value_type = T; 16 | using size_type = std::size_t; 17 | using difference_type = std::ptrdiff_t; 18 | using reference = value_type &; 19 | using const_reference = const value_type &; 20 | using pointer = T *; 21 | using const_pointer = const T *; 22 | using iterator = T *; 23 | using const_iterator = const T *; 24 | using reverse_iterator = std::reverse_iterator; 25 | using const_reverse_iterator = std::reverse_iterator; 26 | 27 | explicit FixedVector(size_type capacity) : size_{0}, capacity_{capacity}, data_{new storage_type[capacity]} {} 28 | FixedVector() noexcept : FixedVector{0} {} 29 | ~FixedVector() noexcept(std::is_nothrow_destructible::value) { 30 | for(auto &x : *this) { 31 | x.~T(); 32 | } 33 | } 34 | 35 | FixedVector(FixedVector &&other) noexcept : size_{other.size}, capacity_{other.capacity}, data_{std::move(other.data_)} { 36 | other.size_ = 0; 37 | other.capacity_ = 0; 38 | } 39 | 40 | FixedVector &operator=(FixedVector &&other) { 41 | size_ = other.size_; 42 | capacity_ = other.capacity_; 43 | data_ = std::move(other.data_); 44 | other.size_ = 0; 45 | other.capacity_ = 0; 46 | return *this; 47 | } 48 | 49 | template 50 | void emplace_back(Ts &&...ts) noexcept(std::is_nothrow_constructible::value) { 51 | new (data_.get() + size_) T(std::forward(ts)...); 52 | ++size_; 53 | } 54 | 55 | void pop_back() noexcept(std::is_nothrow_destructible::value) { 56 | (*this)[size_ - 1].~T(); 57 | --size_; 58 | } 59 | 60 | iterator begin() noexcept { return reinterpret_cast(data_.get()); } 61 | iterator end() noexcept { return reinterpret_cast(data_.get()) + size_; } 62 | const_iterator begin() const noexcept { return reinterpret_cast(data_.get()); } 63 | const_iterator end() const noexcept { return reinterpret_cast(data_.get()) + size_; } 64 | const_iterator cbegin() const noexcept { return begin(); } 65 | const_iterator cend() const noexcept { return end(); } 66 | 67 | reverse_iterator rbegin() noexcept { return std::reverse_iterator(end()); } 68 | reverse_iterator rend() noexcept { return std::reverse_iterator(begin()); } 69 | const_reverse_iterator rbegin() const noexcept { return std::reverse_iterator(end()); } 70 | const_reverse_iterator rend() const noexcept { return std::reverse_iterator(begin()); } 71 | const_reverse_iterator crbegin() const noexcept { return rbegin(); } 72 | const_reverse_iterator crend() const noexcept { return rend(); } 73 | 74 | reference operator[](size_type i) noexcept { return reinterpret_cast(data_[i]); } 75 | const_reference operator[](size_type i) const noexcept { return reinterpret_cast(data_[i]); } 76 | 77 | reference front() noexcept { return reinterpret_cast(data_[0]); } 78 | const_reference front() const noexcept { return reinterpret_cast(data_[0]); } 79 | 80 | reference back() noexcept { return reinterpret_cast(data_[size_-1]); } 81 | const_reference back() const noexcept { return reinterpret_cast(data_[size_-1]); } 82 | 83 | size_type size() const noexcept { return size_; } 84 | size_type capacity() const noexcept { return capacity_; } 85 | 86 | bool empty() const noexcept { return size() == 0; } 87 | 88 | private: 89 | size_type size_, capacity_; 90 | std::unique_ptr data_; 91 | }; 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /src/RoomViewList.cpp: -------------------------------------------------------------------------------- 1 | #include "RoomViewList.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "matrix/Room.hpp" 10 | 11 | RoomViewList::RoomViewList(QWidget *parent) : QListWidget{parent}, menu_{new QMenu(this)}, context_{nullptr} { 12 | connect(this, &QListWidget::currentItemChanged, [this](QListWidgetItem *item, QListWidgetItem *previous) { 13 | (void)previous; 14 | if(item != nullptr) { 15 | auto room = matrix::RoomID(item->data(Qt::UserRole).toString()); 16 | activated(room); 17 | } 18 | }); 19 | 20 | auto move_up = menu_->addAction(tr("Move up")); 21 | connect(move_up, &QAction::triggered, [this]() { 22 | auto r = row(context_); 23 | auto item = takeItem(r); 24 | insertItem(r > 0 ? r - 1 : 0, item); 25 | setCurrentItem(item); 26 | }); 27 | auto move_down = menu_->addAction(tr("Move down")); 28 | connect(move_down, &QAction::triggered, [this]() { 29 | auto r = row(context_); 30 | auto item = takeItem(r); 31 | insertItem(r+1, item); 32 | setCurrentItem(item); 33 | }); 34 | menu_->addSeparator(); 35 | auto pop_out_action = menu_->addAction(QIcon::fromTheme("window-new"), tr("&Pop out")); 36 | connect(pop_out_action, &QAction::triggered, [this]() { 37 | pop_out(matrix::RoomID{context_->data(Qt::UserRole).toString()}); 38 | }); 39 | auto close = menu_->addAction(QIcon::fromTheme("window-close"), tr("&Close")); 40 | connect(close, &QAction::triggered, [this]() { 41 | release(matrix::RoomID{context_->data(Qt::UserRole).toString()}); 42 | }); 43 | 44 | QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Preferred); 45 | setSizePolicy(policy); 46 | } 47 | 48 | RoomViewList::RoomInfo::RoomInfo(QListWidgetItem *i, const matrix::Room &r) 49 | : item{i}, has_unread{false}, name{r.pretty_name_highlights()}, highlight_count{r.highlight_count() + r.notification_count()} 50 | {} 51 | 52 | void RoomViewList::add(matrix::Room &room) { 53 | auto item = new QListWidgetItem; 54 | item->setToolTip(room.id().value()); 55 | item->setData(Qt::UserRole, room.id().value()); 56 | addItem(item); 57 | auto r = items_.emplace(room.id(), RoomInfo{item, room}); 58 | assert(r.second); 59 | claimed(room.id()); 60 | update_display(room); 61 | updateGeometry(); 62 | viewport()->update(); 63 | } 64 | 65 | void RoomViewList::release(const matrix::RoomID &room) { 66 | auto it = items_.find(room); 67 | assert(it != items_.end()); 68 | delete it->second.item; 69 | items_.erase(it); 70 | released(room); 71 | updateGeometry(); 72 | viewport()->update(); 73 | } 74 | 75 | void RoomViewList::activate(const matrix::RoomID &room) { 76 | auto &item = *items_.at(room).item; 77 | scrollToItem(&item); 78 | setCurrentItem(&item); 79 | activated(room); 80 | } 81 | 82 | void RoomViewList::update_display(matrix::Room &room) { 83 | auto &i = items_.at(room.id()); 84 | i.name = room.pretty_name_highlights(); 85 | i.highlight_count = room.highlight_count() + room.notification_count(); 86 | i.has_unread = room.has_unread(); 87 | update_item(i); 88 | } 89 | 90 | void RoomViewList::contextMenuEvent(QContextMenuEvent *e) { 91 | if(auto item = itemAt(e->pos())) { 92 | context_ = item; 93 | menu_->popup(e->globalPos()); 94 | } 95 | } 96 | 97 | QSize RoomViewList::sizeHint() const { 98 | auto margins = viewportMargins(); 99 | return QSize(sizeHintForColumn(0) + verticalScrollBar()->sizeHint().width() + margins.left() + margins.right(), 100 | fontMetrics().lineSpacing() + horizontalScrollBar()->sizeHint().height()); 101 | } 102 | 103 | void RoomViewList::update_item(const RoomInfo &i) { 104 | i.item->setText(i.name); 105 | auto font = i.item->font(); 106 | font.setBold(i.has_unread || i.highlight_count != 0); 107 | i.item->setFont(font); 108 | } 109 | -------------------------------------------------------------------------------- /src/ChatWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "ChatWindow.hpp" 2 | #include "ui_ChatWindow.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "matrix/Room.hpp" 8 | 9 | #include "RoomView.hpp" 10 | #include "RoomViewList.hpp" 11 | 12 | ChatWindow::ChatWindow(ThumbnailCache &cache, QWidget *parent) 13 | : QWidget(parent), ui(new Ui::ChatWindow), room_list_(new RoomViewList(this)), cache_{cache} { 14 | ui->setupUi(this); 15 | 16 | setAttribute(Qt::WA_DeleteOnClose); 17 | setWindowFlags(Qt::Window); 18 | 19 | connect(ui->room_stack, &QStackedWidget::currentChanged, this, &ChatWindow::current_changed); 20 | ui->splitter->insertWidget(0, room_list_); 21 | ui->splitter->setCollapsible(1, false); 22 | room_list_->hide(); 23 | 24 | connect(room_list_, &RoomViewList::activated, [this](const matrix::RoomID &room) { 25 | auto &view = *rooms_.at(room); 26 | ui->room_stack->setCurrentWidget(&view); 27 | view.setFocus(); 28 | if(isActiveWindow()) 29 | view.selected(); 30 | focused(room); 31 | }); 32 | connect(room_list_, &RoomViewList::claimed, this, &ChatWindow::claimed); 33 | connect(room_list_, &RoomViewList::released, [this](const matrix::RoomID &room) { 34 | auto it = rooms_.find(room); 35 | if(it != rooms_.end()) { 36 | auto view = it->second; 37 | ui->room_stack->removeWidget(view); 38 | // TODO: Pass ownership elsewhere if necessary 39 | view->setParent(nullptr); 40 | rooms_.erase(it); 41 | delete view; 42 | } 43 | switch(ui->room_stack->count()) { 44 | case 0: close(); break; 45 | case 1: room_list_->hide(); break; 46 | default: break; 47 | } 48 | released(room); 49 | }); 50 | connect(room_list_, &RoomViewList::pop_out, [this](const matrix::RoomID &room) { 51 | auto it = rooms_.find(room); 52 | auto view = it->second; 53 | rooms_.erase(it); 54 | room_list_->release(room); 55 | pop_out(room, view); 56 | }); 57 | } 58 | 59 | ChatWindow::~ChatWindow() { delete ui; } 60 | 61 | void ChatWindow::add(matrix::Room &r, RoomView *v) { 62 | v->setParent(this); 63 | rooms_.emplace( 64 | std::piecewise_construct, 65 | std::forward_as_tuple(r.id()), 66 | std::forward_as_tuple(v)); 67 | ui->room_stack->addWidget(v); 68 | room_list_->add(r); 69 | if(room_list_->count() == 2) { 70 | room_list_->show(); 71 | } 72 | room_list_->activate(r.id()); 73 | v->setFocus(); 74 | } 75 | 76 | void ChatWindow::add_or_focus(matrix::Room &room) { 77 | RoomView *view; 78 | if(rooms_.find(room.id()) == rooms_.end()) { 79 | view = new RoomView(cache_, room, this); 80 | add(room, view); 81 | } else { 82 | room_list_->activate(room.id()); 83 | view = static_cast(ui->room_stack->currentWidget()); 84 | } 85 | view->setFocus(); 86 | } 87 | 88 | void ChatWindow::room_display_changed(matrix::Room &room) { 89 | room_list_->update_display(room); 90 | update_title(); 91 | } 92 | 93 | RoomView *ChatWindow::take(const matrix::RoomID &room) { 94 | auto it = rooms_.find(room); 95 | auto view = it->second; 96 | rooms_.erase(it); 97 | released(room); 98 | return view; 99 | } 100 | 101 | void ChatWindow::update_title() { 102 | if(auto w = ui->room_stack->currentWidget()) { 103 | setWindowTitle(static_cast(w)->room().pretty_name_highlights()); 104 | } else { 105 | setWindowTitle(""); 106 | } 107 | } 108 | 109 | void ChatWindow::current_changed(int i) { 110 | (void)i; 111 | update_title(); 112 | } 113 | 114 | void ChatWindow::changeEvent(QEvent *e) { 115 | QWidget::changeEvent(e); 116 | if(e->type() == QEvent::ActivationChange && isActiveWindow()) { 117 | focused(focused_room()); 118 | } 119 | } 120 | 121 | void ChatWindow::closeEvent(QCloseEvent *evt) { 122 | for(auto &r : rooms_) { 123 | released(r.first); 124 | } 125 | evt->accept(); 126 | } 127 | 128 | const matrix::RoomID &ChatWindow::focused_room() const { 129 | return static_cast(ui->room_stack->currentWidget())->room().id(); 130 | } 131 | -------------------------------------------------------------------------------- /src/timeline_view_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "matrix/Room.hpp" 6 | 7 | #include "ContentCache.hpp" 8 | #include "TimelineView.hpp" 9 | 10 | matrix::event::Room room_evt(const QJsonObject &o) { 11 | return matrix::event::Room{matrix::event::Identifiable{matrix::Event{o}}}; 12 | } 13 | 14 | matrix::event::room::Message message_evt(const QJsonObject &o) { 15 | return matrix::event::room::Message{room_evt(o)}; 16 | } 17 | 18 | matrix::event::room::Member member_evt(const QJsonObject &o) { 19 | return matrix::event::room::Member{matrix::event::room::State{room_evt(o)}}; 20 | } 21 | 22 | int main(int argc, char *argv[]) { 23 | QApplication a(argc, argv); 24 | 25 | ThumbnailCache c; 26 | 27 | QObject::connect(&c, &ThumbnailCache::needs, [&c](const matrix::Thumbnail &thumb) { 28 | QPixmap pixmap(thumb.size()); 29 | pixmap.fill(Qt::black); 30 | c.set(thumb, pixmap); 31 | }); 32 | 33 | TimelineView tv(QUrl("https://example.com/"), c); 34 | tv.show(); 35 | 36 | matrix::RoomState rs; 37 | matrix::TimelineCursor cursor1{"1"}; 38 | const char *somebody = "@somebody:example.com"; 39 | const char *somebody_else = "@somebody_else:example.com"; 40 | 41 | auto join_evt = member_evt(QJsonObject{ 42 | {"type", "m.room.member"}, 43 | {"event_id", "2"}, 44 | {"sender", somebody}, 45 | {"origin_server_ts", 42000000LL}, 46 | {"state_key", somebody}, 47 | {"content", QJsonObject{ 48 | {"membership", "join"}, 49 | {"displayname", "SOMEBODY"}, 50 | {"avatar_url", "mxc://example.com/foo.png"} 51 | }} 52 | }); 53 | 54 | tv.append(cursor1, rs, join_evt); 55 | 56 | rs.apply(join_evt); 57 | 58 | tv.append(cursor1, rs, message_evt(QJsonObject{ 59 | {"type", "m.room.message"}, 60 | {"event_id", "3"}, 61 | {"sender", somebody}, 62 | {"origin_server_ts", 42000001LL}, 63 | {"content", QJsonObject{ 64 | {"body", "hello world https://example.com/ whee\nnew line! https://example.com/\nhttp://example.com/"}, 65 | {"msgtype", "m.text"} 66 | }} 67 | })); 68 | 69 | tv.append(cursor1, rs, message_evt(QJsonObject{ 70 | {"type", "m.room.message"}, 71 | {"event_id", "3.1"}, 72 | {"sender", somebody}, 73 | {"origin_server_ts", 42000002LL}, 74 | {"content", QJsonObject{ 75 | {"body", "this will be redacted!"}, 76 | {"msgtype", "m.text"} 77 | }} 78 | })); 79 | 80 | matrix::event::room::Redaction redact_evt{room_evt(QJsonObject{ 81 | {"type", "m.room.redaction"}, 82 | {"event_id", "5"}, 83 | {"origin_server_ts", 42000003LL}, 84 | {"redacts", "3.1"}, 85 | {"sender", somebody}, 86 | {"content", QJsonObject{ 87 | {"reason", "idk lol"} 88 | }} 89 | })}; 90 | 91 | tv.append(cursor1, rs, redact_evt); 92 | 93 | auto leave_evt = member_evt(QJsonObject{ 94 | {"type", "m.room.member"}, 95 | {"event_id", "4"}, 96 | {"sender", somebody}, 97 | {"origin_server_ts", 52000002LL}, 98 | {"state_key", somebody}, 99 | {"content", QJsonObject{ 100 | {"membership", "leave"}, 101 | }} 102 | }); 103 | 104 | tv.append(cursor1, rs, leave_evt); 105 | 106 | rs.apply(leave_evt); 107 | 108 | // tv.prepend(cursor1, matrix::RoomState(), matrix::event::room::Create{matrix::event::room::State{room_evt(QJsonObject{ 109 | // {"type", "m.room.create"}, 110 | // {"event_id", "1"}, 111 | // {"sender", somebody}, 112 | // {"origin_server_ts", 42}, 113 | // {"state_key", ""}, 114 | // {"content", QJsonObject{ 115 | // {"creator", "you"} 116 | // }} 117 | // })}}); 118 | 119 | tv.add_pending(matrix::TransactionID{"txn"}, rs, matrix::UserID(somebody_else), Time{}, matrix::event::room::Message::tag(), 120 | matrix::event::Content{QJsonObject{{"msgtype", "m.text"}, {"body", "hello"}}}); 121 | 122 | //tv.set_at_bottom(true); 123 | 124 | return a.exec(); 125 | } 126 | -------------------------------------------------------------------------------- /src/JoinedRoomListModel.cpp: -------------------------------------------------------------------------------- 1 | #include "JoinedRoomListModel.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "matrix/Session.hpp" 9 | #include "matrix/Room.hpp" 10 | #include "matrix/pixmaps.hpp" 11 | 12 | RoomInfo::RoomInfo(const matrix::Room &room) : 13 | id{room.id()}, avatar_generation{0} 14 | { 15 | update(room); 16 | } 17 | 18 | void RoomInfo::update(const matrix::Room &room) { 19 | assert(id == room.id()); 20 | display_name = room.pretty_name(); 21 | unread = room.has_unread(); 22 | highlight_count = room.highlight_count() + room.notification_count(); 23 | avatar_url = room.state().avatar(); 24 | } 25 | 26 | JoinedRoomListModel::JoinedRoomListModel(matrix::Session &session, QSize icon_size, qreal dpr) : session_{session}, icon_size_{icon_size}, device_pixel_ratio_{dpr} { 27 | connect(&session, &matrix::Session::joined, this, &JoinedRoomListModel::joined); 28 | 29 | for(auto room : session.rooms()) { 30 | joined(*room); 31 | } 32 | } 33 | 34 | int JoinedRoomListModel::rowCount(const QModelIndex &parent) const { 35 | if(parent != QModelIndex()) return 0; 36 | return rooms_.size(); 37 | } 38 | 39 | QVariant JoinedRoomListModel::data(const QModelIndex &index, int role) const { 40 | if(index.column() != 0 || static_cast(index.row()) >= rooms_.size()) { 41 | return QVariant(); 42 | } 43 | const auto &info = rooms_[index.row()]; 44 | switch(role) { 45 | case Qt::DisplayRole: 46 | return info.display_name; 47 | case Qt::ToolTipRole: 48 | case IDRole: 49 | return info.id.value(); 50 | case UnreadRole: 51 | return info.unread; 52 | case Qt::FontRole: { 53 | QFont font; 54 | font.setBold(info.unread); 55 | return font; 56 | } 57 | case Qt::DecorationRole: { 58 | if(auto avatar = info.avatar) { 59 | return *avatar; 60 | } 61 | return QVariant(); 62 | } 63 | default: 64 | return QVariant(); 65 | } 66 | } 67 | 68 | QVariant JoinedRoomListModel::headerData(int section, Qt::Orientation orientation, int role) const { 69 | if(role != Qt::DisplayRole || section != 0) { 70 | return QVariant(); 71 | } 72 | 73 | if(orientation == Qt::Horizontal) { 74 | return tr("Room"); 75 | } 76 | 77 | return QVariant(); 78 | } 79 | 80 | void JoinedRoomListModel::joined(matrix::Room &room) { 81 | beginInsertRows(QModelIndex(), rooms_.size(), rooms_.size()); 82 | index_.emplace(room.id(), rooms_.size()); 83 | rooms_.emplace_back(room); 84 | connect(&room, &matrix::Room::sync_complete, [this, &room]() { update_room(room); }); 85 | endInsertRows(); 86 | 87 | update_avatar(rooms_.back()); 88 | 89 | } 90 | 91 | void JoinedRoomListModel::update_room(matrix::Room &room) { 92 | auto i = index_.at(room.id()); 93 | rooms_[i].update(room); 94 | dataChanged(index(i), index(i)); 95 | } 96 | 97 | void JoinedRoomListModel::update_avatar(RoomInfo &info) { 98 | if(info.avatar_url.isEmpty()) { 99 | info.avatar = {}; 100 | return; 101 | } 102 | 103 | try { 104 | auto thumbnail = matrix::Thumbnail{matrix::Content{info.avatar_url}, icon_size_ * device_pixel_ratio_, matrix::ThumbnailMethod::SCALE}; 105 | auto fetch = session_.get_thumbnail(thumbnail); 106 | QPointer self(this); 107 | matrix::RoomID id = info.id; 108 | info.avatar_generation += 1; 109 | std::size_t generation = info.avatar_generation; 110 | connect(fetch, &matrix::ContentFetch::finished, [=](const QString &type, const QString &disposition, const QByteArray &data) { 111 | (void)disposition; 112 | if(!self) return; 113 | 114 | auto it = self->index_.find(id); 115 | if(it == self->index_.end() || self->rooms_[it->second].avatar_generation != generation) return; 116 | 117 | QPixmap pixmap = matrix::decode(type, data); 118 | 119 | if(pixmap.width() > thumbnail.size().width() || pixmap.height() > thumbnail.size().height()) 120 | pixmap = pixmap.scaled(thumbnail.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); 121 | pixmap.setDevicePixelRatio(self->device_pixel_ratio_); 122 | 123 | self->rooms_[it->second].avatar = std::move(pixmap); 124 | self->dataChanged(index(it->second), index(it->second)); 125 | }); 126 | } catch(matrix::illegal_content_scheme &e) { 127 | qDebug() << "ignoring avatar with illegal scheme for room" << info.display_name; 128 | } 129 | } 130 | 131 | void JoinedRoomListModel::icon_size_changed(const QSize &size) { 132 | icon_size_ = size; 133 | for(auto &room: rooms_) { 134 | update_avatar(room); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/matrix/Session.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_SESSION_H_ 2 | #define NATIVE_CHAT_MATRIX_SESSION_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include "../QStringHash.hpp" 17 | 18 | #include "Room.hpp" 19 | #include "Content.hpp" 20 | 21 | class QNetworkRequest; 22 | class QNetworkReply; 23 | class QIODevice; 24 | 25 | namespace matrix { 26 | 27 | namespace proto { 28 | struct Sync; 29 | } 30 | 31 | class Matrix; 32 | 33 | class JoinRequest : public QObject { 34 | Q_OBJECT 35 | 36 | public: 37 | explicit JoinRequest(QObject *parent) : QObject(parent) {} 38 | 39 | signals: 40 | void success(const RoomID &id); 41 | void error(const QString &msg); 42 | }; 43 | 44 | class ContentFetch : public QObject { 45 | Q_OBJECT 46 | 47 | public: 48 | explicit ContentFetch(QObject *parent = nullptr) : QObject(parent) {} 49 | 50 | signals: 51 | void finished(const QString &type, const QString &disposition, const QByteArray &data); 52 | void error(const QString &msg); 53 | }; 54 | 55 | class ContentPost : public QObject { 56 | Q_OBJECT 57 | 58 | public: 59 | explicit ContentPost(QObject *parent = nullptr) : QObject(parent) {} 60 | 61 | signals: 62 | void success(const QString &content_uri); 63 | void progress(qint64 completed, qint64 total); 64 | void error(const QString &msg); 65 | }; 66 | 67 | struct SessionInit; 68 | 69 | class Session : public QObject { 70 | Q_OBJECT 71 | 72 | public: 73 | Session(Matrix& universe, QUrl homeserver, UserID user_id, QString access_token); 74 | 75 | Session(const Session &) = delete; 76 | Session &operator=(const Session &) = delete; 77 | 78 | const QString &access_token() const { return access_token_; } 79 | const UserID &user_id() const { return user_id_; } 80 | const QUrl &homeserver() const { return homeserver_; } 81 | 82 | void log_out(); 83 | 84 | bool synced() const { return synced_; } 85 | std::vector rooms(); 86 | Room *room_from_id(const RoomID &r) { 87 | auto it = rooms_.find(r); 88 | if(it == rooms_.end()) return nullptr; 89 | return &it->second.room; 90 | } 91 | const Room *room_from_id(const RoomID &r) const { 92 | auto it = rooms_.find(r); 93 | if(it == rooms_.end()) return nullptr; 94 | return &it->second.room; 95 | } 96 | 97 | size_t buffer_size() const { return buffer_size_; } 98 | void set_buffer_size(size_t size) { buffer_size_ = size; } 99 | 100 | QNetworkReply *get(const QString &path, QUrlQuery query = QUrlQuery()); 101 | 102 | QNetworkReply *post(const QString &path, QJsonObject body = QJsonObject(), QUrlQuery query = QUrlQuery()); 103 | 104 | QNetworkReply *put(const QString &path, QJsonObject body); 105 | 106 | ContentFetch *get(const Content &); 107 | 108 | ContentFetch *get_thumbnail(const Thumbnail &); 109 | 110 | ContentPost *upload(QIODevice &data, const QString &content_type, const QString &filename); 111 | 112 | TransactionID get_transaction_id(); 113 | 114 | JoinRequest *join(const QString &id_or_alias); 115 | 116 | QUrl ensure_http(const QUrl &) const; 117 | // Converts mxc URLs to http URLs on this homeserver, otherwise passes through 118 | 119 | signals: 120 | void logged_out(); 121 | void error(QString message); 122 | void synced_changed(); 123 | void joined(matrix::Room &room); 124 | void sync_progress(qint64 received, qint64 total); 125 | void sync_complete(); 126 | 127 | private: 128 | struct RoomInfo { 129 | Room room; 130 | std::experimental::optional members; 131 | std::vector member_changes; 132 | 133 | RoomInfo(Matrix &universe, Session &session, const proto::JoinedRoom &joined_room) : room{universe, session, joined_room} {} 134 | RoomInfo(Matrix &universe, Session &session, RoomID id, const QJsonObject &initial, 135 | gsl::span members) : room{universe, session, id, initial, members} {} 136 | }; 137 | 138 | Matrix &universe_; 139 | const QUrl homeserver_; 140 | const UserID user_id_; 141 | QString access_token_; 142 | lmdb::env env_; 143 | lmdb::dbi state_db_, room_db_; 144 | size_t buffer_size_; 145 | std::unordered_map rooms_; 146 | bool synced_; 147 | std::experimental::optional next_batch_; 148 | QNetworkReply *sync_reply_; 149 | QTimer sync_retry_timer_; 150 | 151 | std::chrono::steady_clock::time_point last_sync_error_; 152 | // Last time a sync failed. Used to ensure we don't spin if errors happen quickly. 153 | 154 | Session(Matrix& universe, QUrl homeserver, UserID user_id, QString access_token, SessionInit &&init); 155 | 156 | QNetworkRequest request(const QString &path, QUrlQuery query = QUrlQuery(), const QString &content_type = "application/json"); 157 | 158 | void sync(); 159 | void sync(QUrlQuery query); 160 | void handle_sync_reply(); 161 | void dispatch(const proto::Sync &sync); 162 | void update_cache(const proto::Sync &sync); 163 | 164 | template 165 | RoomInfo &add_room(const RoomID &id, Ts &&...ts); 166 | }; 167 | 168 | } 169 | 170 | #endif 171 | -------------------------------------------------------------------------------- /src/EntryBox.cpp: -------------------------------------------------------------------------------- 1 | #include "EntryBox.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static constexpr size_t INPUT_HISTORY_SIZE = 127; 11 | 12 | EntryBox::EntryBox(QAbstractListModel *members, QWidget *parent) 13 | : QTextEdit(parent), true_history_(INPUT_HISTORY_SIZE), working_history_(1), history_index_(0), completer_{new QCompleter{members, this}} { 14 | connect(document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, this, &EntryBox::updateGeometry); 15 | QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Maximum); 16 | policy.setHorizontalStretch(1); 17 | policy.setVerticalStretch(1); 18 | setSizePolicy(policy); 19 | setAcceptRichText(false); 20 | document()->setDocumentMargin(2); 21 | working_history_.push_back(""); 22 | connect(this, &QTextEdit::textChanged, this, &EntryBox::text_changed); 23 | 24 | completer_->setWidget(this); 25 | completer_->setCaseSensitivity(Qt::CaseInsensitive); 26 | completer_->setCompletionMode(QCompleter::PopupCompletion); 27 | connect(completer_, static_cast(&QCompleter::activated), [this](const QString& completion) { 28 | QTextCursor tc = textCursor(); 29 | tc.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); 30 | tc.insertText(completion); 31 | setTextCursor(tc); 32 | after_completion(completion.size()); 33 | }); 34 | } 35 | 36 | QSize EntryBox::sizeHint() const { 37 | ensurePolished(); 38 | auto margins = viewportMargins(); 39 | margins += document()->documentMargin(); 40 | QSize size = document()->size().toSize(); 41 | size.rwidth() += margins.left() + margins.right(); 42 | size.rheight() += margins.top() + margins.bottom(); 43 | return size; 44 | } 45 | 46 | QSize EntryBox::minimumSizeHint() const { 47 | ensurePolished(); 48 | auto margins = viewportMargins(); 49 | margins += document()->documentMargin(); 50 | margins += contentsMargins(); 51 | QSize size(fontMetrics().averageCharWidth() * 10, fontMetrics().lineSpacing() + margins.top() + margins.bottom()); 52 | return size; 53 | } 54 | 55 | void EntryBox::keyPressEvent(QKeyEvent *event) { 56 | activity(); 57 | 58 | if(completer_->popup()->isVisible()) { 59 | switch (event->key()) { 60 | case Qt::Key_Enter: 61 | case Qt::Key_Return: 62 | case Qt::Key_Escape: 63 | case Qt::Key_Tab: 64 | case Qt::Key_Backtab: 65 | event->ignore(); 66 | return; // let the completer do default behavior 67 | default: 68 | completer_->popup()->hide(); 69 | break; 70 | } 71 | } 72 | 73 | auto modifiers = QGuiApplication::keyboardModifiers(); 74 | switch(event->key()) { 75 | case Qt::Key_Return: 76 | case Qt::Key_Enter: 77 | if(!(modifiers & Qt::ShiftModifier)) { 78 | send(); 79 | } else { 80 | QTextEdit::keyPressEvent(event); 81 | } 82 | break; 83 | case Qt::Key_PageUp: 84 | pageUp(); 85 | break; 86 | case Qt::Key_PageDown: 87 | pageDown(); 88 | break; 89 | case Qt::Key_Up: { 90 | auto initial_cursor = textCursor(); 91 | QTextEdit::keyPressEvent(event); 92 | if(textCursor() == initial_cursor && history_index_ + 1 < working_history_.size()) { 93 | ++history_index_; 94 | setPlainText(working_history_[history_index_]); 95 | moveCursor(QTextCursor::End); 96 | } 97 | break; 98 | } 99 | case Qt::Key_Down: { 100 | auto initial_cursor = textCursor(); 101 | QTextEdit::keyPressEvent(event); 102 | if(textCursor() == initial_cursor && history_index_ > 0) { 103 | --history_index_; 104 | setPlainText(working_history_[history_index_]); 105 | moveCursor(QTextCursor::End); 106 | } 107 | break; 108 | } 109 | case Qt::Key_Tab: { 110 | QTextCursor tc = textCursor(); 111 | tc.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); 112 | const QString word = tc.selectedText(); 113 | if(word != completer_->completionPrefix()) { 114 | completer_->setCompletionPrefix(word); 115 | } 116 | if(completer_->completionCount() == 1) { 117 | QString completion = completer_->currentCompletion(); 118 | tc.insertText(completion); 119 | after_completion(completion.size()); 120 | } else { 121 | QRect cr = cursorRect(); 122 | cr.setWidth(completer_->popup()->sizeHintForColumn(0) 123 | + completer_->popup()->verticalScrollBar()->sizeHint().width()); 124 | completer_->complete(cr); 125 | } 126 | break; 127 | } 128 | default: 129 | QTextEdit::keyPressEvent(event); 130 | break; 131 | } 132 | } 133 | 134 | void EntryBox::text_changed() { 135 | working_history_[history_index_] = toPlainText(); 136 | } 137 | 138 | void EntryBox::send() { 139 | if(true_history_.size() == INPUT_HISTORY_SIZE) true_history_.pop_back(); 140 | true_history_.push_front(toPlainText()); 141 | working_history_ = true_history_; 142 | working_history_.push_front(""); 143 | history_index_ = 0; 144 | 145 | const QString text = toPlainText(); 146 | if(text.startsWith('/')) { 147 | int command_end = text.indexOf(' '); 148 | if(command_end == -1) command_end = text.size(); 149 | const auto &name = text.mid(1, command_end - 1); 150 | const auto &args = text.mid(command_end + 1); 151 | if(name.isEmpty()) { 152 | message(args); 153 | } else { 154 | command(name, args); 155 | } 156 | } else { 157 | message(text); 158 | } 159 | 160 | clear(); 161 | } 162 | 163 | void EntryBox::after_completion(int completion_size) { 164 | QTextCursor tc = textCursor(); 165 | if(tc.position() == completion_size) { 166 | // Completion is the first thing in the message 167 | tc.insertText(": "); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/RoomView.cpp: -------------------------------------------------------------------------------- 1 | #include "RoomView.hpp" 2 | #include "ui_RoomView.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "matrix/Room.hpp" 13 | #include "matrix/Session.hpp" 14 | #include "matrix/TimelineWindow.hpp" 15 | #include "matrix/MemberListModel.hpp" 16 | 17 | #include "TimelineView.hpp" 18 | #include "EntryBox.hpp" 19 | #include "RoomMenu.hpp" 20 | #include "utils.hpp" 21 | 22 | using std::experimental::optional; 23 | 24 | RoomView::RoomView(ThumbnailCache &cache, matrix::Room &room, QWidget *parent) 25 | : QWidget(parent), ui(new Ui::RoomView), 26 | timeline_view_(new TimelineView(room.session().homeserver(), cache, this)), 27 | room_(room), 28 | timeline_manager_{new matrix::TimelineManager(room, this)}, 29 | member_list_(new matrix::MemberListModel(room, initial_icon_size(*this), devicePixelRatioF(), this)), 30 | entry_(new EntryBox{member_list_, this}) { 31 | ui->setupUi(this); 32 | 33 | connect(timeline_manager_, &matrix::TimelineManager::grew, 34 | [this](matrix::Direction dir, const matrix::TimelineCursor &begin, const matrix::RoomState &state, const matrix::event::Room &evt) { 35 | if(dir == matrix::Direction::BACKWARD) { 36 | timeline_view_->prepend(begin, state, evt); 37 | } else { 38 | timeline_view_->append(begin, state, evt); 39 | timeline_view_->set_at_bottom(timeline_manager_->window().at_end()); 40 | } 41 | }); 42 | connect(timeline_manager_, &matrix::TimelineManager::discontinuity, [this]() { 43 | timeline_view_->set_at_bottom(false); // FIXME: Reset view history 44 | }); 45 | connect(timeline_view_, &TimelineView::discarded_before, [this](const matrix::TimelineCursor &c) { 46 | timeline_manager_->window().discard(c, matrix::Direction::BACKWARD); 47 | }); 48 | connect(timeline_view_, &TimelineView::discarded_after, [this](const matrix::TimelineCursor &c) { 49 | timeline_manager_->window().discard(c, matrix::Direction::FORWARD); 50 | }); 51 | 52 | connect(timeline_view_, &TimelineView::need_backwards, [this]() { timeline_manager_->grow(matrix::Direction::BACKWARD); }); 53 | connect(timeline_view_, &TimelineView::need_forwards, [this]() { timeline_manager_->grow(matrix::Direction::FORWARD); }); 54 | connect(timeline_view_, &TimelineView::redact_requested, &room, &matrix::Room::redact); // TODO: Add to timeline_view_'s pending events 55 | connect(timeline_view_, &TimelineView::event_read, &room, &matrix::Room::send_read_receipt); 56 | connect(&room, &matrix::Room::receipts_changed, this, &RoomView::update_last_read); 57 | 58 | // Ensure redactions apply instantly even when the view is scrolled back and therefore not receiving sync events. 59 | connect(&room, &matrix::Room::redaction, timeline_view_, &TimelineView::redact); 60 | 61 | timeline_manager_->replay(); 62 | timeline_view_->set_at_bottom(timeline_manager_->window().at_end()); 63 | 64 | auto menu = new RoomMenu(room, this); 65 | connect(ui->menu_button, &QAbstractButton::clicked, [this, menu](bool) { 66 | menu->popup(QCursor::pos()); 67 | }); 68 | 69 | ui->central_splitter->insertWidget(0, timeline_view_); 70 | ui->central_splitter->setCollapsible(0, false); 71 | ui->central_splitter->setSizes({-1, fontMetrics().width('x')*24}); 72 | 73 | ui->member_list->setModel(member_list_); 74 | 75 | layout()->addWidget(entry_); 76 | setFocusProxy(entry_); 77 | 78 | ui->header_splitter->setStretchFactor(0, 0); 79 | ui->header_splitter->setStretchFactor(1, 1); 80 | ui->header_splitter->setSizes({ui->header->minimumSize().height(), ui->central_splitter->maximumSize().height()}); 81 | 82 | connect(entry_, &EntryBox::message, [this](const QString &msg) { 83 | send(matrix::event::room::Message::tag(), 84 | matrix::event::Content{{ 85 | {"msgtype", "m.text"}, 86 | {"body", msg}}}); 87 | }); 88 | connect(entry_, &EntryBox::command, this, &RoomView::command); 89 | connect(entry_, &EntryBox::pageUp, [this]() { 90 | timeline_view_->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); 91 | }); 92 | connect(entry_, &EntryBox::pageDown, [this]() { 93 | timeline_view_->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); 94 | }); 95 | connect(entry_, &EntryBox::activity, timeline_view_, &TimelineView::mark_read); 96 | 97 | connect(&room_, &matrix::Room::topic_changed, this, &RoomView::topic_changed); 98 | topic_changed(); 99 | } 100 | 101 | RoomView::~RoomView() { delete ui; } 102 | 103 | void RoomView::topic_changed() { 104 | if(!room_.state().topic()) { 105 | ui->topic->setTextFormat(Qt::RichText); 106 | ui->topic->setText("

" + room_.pretty_name() + "

"); 107 | } else { 108 | ui->topic->setTextFormat(Qt::PlainText); 109 | ui->topic->setText(*room_.state().topic()); 110 | } 111 | } 112 | 113 | void RoomView::command(const QString &name, const QString &args) { 114 | if(name == "me") { 115 | send(matrix::event::room::Message::tag(), 116 | matrix::event::Content{{ 117 | {{"msgtype", "m.emote"}, 118 | {"body", args}}}}); 119 | } else if(name == "join") { 120 | auto req = room_.session().join(args); 121 | connect(req, &matrix::JoinRequest::error, [=](const QString &msg) { qCritical() << tr("failed to join \"%1\": %2").arg(args).arg(msg); }); 122 | } else { 123 | qCritical() << tr("Unrecognized command: %1").arg(name); 124 | } 125 | } 126 | 127 | void RoomView::selected() { 128 | timeline_view_->mark_read(); 129 | } 130 | 131 | void RoomView::send(const matrix::EventType &ty, const matrix::event::Content &content) { 132 | timeline_view_->add_pending(room_.send(ty, content), room_.state(), room_.session().user_id(), 133 | std::chrono::time_point_cast(std::chrono::system_clock::now()), ty, content); 134 | } 135 | 136 | void RoomView::update_last_read() { 137 | auto r = room_.receipt_from(room_.session().user_id()); 138 | if(!r) return; 139 | timeline_view_->set_last_read(r->event); 140 | } 141 | -------------------------------------------------------------------------------- /src/matrix/MemberListModel.cpp: -------------------------------------------------------------------------------- 1 | #include "MemberListModel.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "Room.hpp" 9 | #include "Session.hpp" 10 | #include "pixmaps.hpp" 11 | 12 | using std::experimental::optional; 13 | 14 | namespace matrix { 15 | 16 | MemberListModel::Info::Info(UserID id, event::room::MemberContent content, optional disambiguation) : 17 | id{id}, content{content}, disambiguation{disambiguation} 18 | {} 19 | 20 | MemberListModel::MemberListModel(Room &room, QSize icon_size, qreal device_pixel_ratio, QObject *parent) : QAbstractListModel{parent}, room_{room}, icon_size_{icon_size}, device_pixel_ratio_{device_pixel_ratio} { 21 | connect(&room, &Room::member_changed, this, &MemberListModel::member_changed); 22 | connect(&room, &Room::member_disambiguation_changed, this, &MemberListModel::member_disambiguation_changed); 23 | 24 | auto members = room.state().members(); 25 | beginInsertRows(QModelIndex(), 0, members.size()-1); 26 | members_.reserve(members.size()); 27 | for(auto member: members) { 28 | index_.emplace(member->first, members_.size()); 29 | members_.emplace_back(member->first, member->second, room_.state().member_disambiguation(member->first)); 30 | queue_fetch(members_.back()); 31 | } 32 | endInsertRows(); 33 | } 34 | 35 | int MemberListModel::rowCount(const QModelIndex &parent) const { 36 | if(parent != QModelIndex()) return 0; 37 | return members_.size(); 38 | } 39 | 40 | QVariant MemberListModel::data(const QModelIndex &index, int role) const { 41 | if(index.column() != 0 || static_cast(index.row()) >= members_.size()) { 42 | return QVariant(); 43 | } 44 | const auto &info = members_[index.row()]; 45 | switch(role) { 46 | case Qt::DisplayRole: 47 | case Qt::EditRole: 48 | return pretty_name(info.id, info.content); 49 | case Qt::ToolTipRole: 50 | case IDRole: 51 | return info.id.value(); 52 | case Qt::DecorationRole: { 53 | if(auto avatar = info.avatar) { 54 | return *avatar; 55 | } 56 | return QVariant(); 57 | } 58 | default: 59 | return QVariant(); 60 | } 61 | } 62 | 63 | QVariant MemberListModel::headerData(int section, Qt::Orientation orientation, int role) const { 64 | if(role != Qt::DisplayRole || section != 0) { 65 | return QVariant(); 66 | } 67 | 68 | if(orientation == Qt::Horizontal) { 69 | return tr("Member"); 70 | } 71 | 72 | return QVariant(); 73 | } 74 | 75 | void MemberListModel::member_changed(const UserID &id, const event::room::MemberContent ¤t, const event::room::MemberContent &next) { 76 | switch(current.membership()) { 77 | case Membership::LEAVE: 78 | case Membership::BAN: 79 | switch(next.membership()) { 80 | case Membership::JOIN: 81 | case Membership::INVITE: 82 | beginInsertRows(QModelIndex(), members_.size(), members_.size()); 83 | index_.emplace(id, members_.size()); 84 | members_.emplace_back(id, next, room_.state().nonmember_disambiguation(id, next.displayname())); 85 | endInsertRows(); 86 | break; 87 | 88 | case Membership::LEAVE: 89 | case Membership::BAN: 90 | break; 91 | } 92 | break; 93 | 94 | case Membership::JOIN: 95 | case Membership::INVITE: 96 | switch(next.membership()) { 97 | case Membership::LEAVE: 98 | case Membership::BAN: { 99 | auto it = index_.find(id); 100 | beginRemoveRows(QModelIndex(), it->second, it->second); 101 | if(it->second != members_.size() - 1) { 102 | layoutAboutToBeChanged(); 103 | auto &swap_target = members_[it->second]; 104 | std::swap(swap_target, members_.back()); 105 | std::swap(index_.at(swap_target.id), it->second); 106 | changePersistentIndex(index(members_.size()-1), index(it->second)); 107 | layoutChanged(); 108 | } 109 | members_.pop_back(); 110 | index_.erase(it); 111 | endRemoveRows(); 112 | break; 113 | } 114 | 115 | case Membership::JOIN: 116 | case Membership::INVITE: { 117 | std::size_t i = index_.at(id); 118 | bool update_avatar = members_[i].content.avatar_url() != next.avatar_url(); 119 | members_[i].content = next; 120 | members_[i].disambiguation = room_.state().member_disambiguation(id); 121 | dataChanged(index(i), index(i)); 122 | if(update_avatar) { 123 | queue_fetch(members_[i]); 124 | } 125 | break; 126 | } 127 | } 128 | break; 129 | } 130 | } 131 | 132 | void MemberListModel::member_disambiguation_changed(const UserID &id, const optional &old, const optional ¤t) { 133 | (void)old; 134 | std::size_t i = index_.at(id); 135 | members_[i].disambiguation = current; 136 | dataChanged(index(i), index(i)); 137 | } 138 | 139 | // TODO: Replace queue with view-dependent fetching 140 | void MemberListModel::queue_fetch(const Info &info) { 141 | QUrl url(info.content.avatar_url().value_or(QString()), QUrl::StrictMode); 142 | if(!url.isValid()) return; 143 | 144 | bool first_fetch = avatar_fetch_queue_.empty(); 145 | avatar_fetch_queue_[info.id] = url; 146 | if(first_fetch) do_fetch(); 147 | } 148 | 149 | void MemberListModel::do_fetch() { 150 | auto it = avatar_fetch_queue_.begin(); 151 | UserID id = it->first; 152 | QUrl url = it->second; 153 | 154 | try { 155 | auto thumbnail = matrix::Thumbnail{matrix::Content{url}, icon_size_ * device_pixel_ratio_, matrix::ThumbnailMethod::SCALE}; 156 | auto fetch = room_.session().get_thumbnail(thumbnail); 157 | QPointer self(this); 158 | connect(fetch, &matrix::ContentFetch::finished, [=](const QString &type, const QString &disposition, const QByteArray &data) { 159 | (void)disposition; 160 | if(!self) return; 161 | 162 | auto it = self->index_.find(id); 163 | if(it == self->index_.end()) return; 164 | 165 | QPixmap pixmap = decode(type, data); 166 | 167 | if(pixmap.width() > thumbnail.size().width() || pixmap.height() > thumbnail.size().height()) 168 | pixmap = pixmap.scaled(thumbnail.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); 169 | pixmap.setDevicePixelRatio(self->device_pixel_ratio_); 170 | 171 | self->members_[it->second].avatar = std::move(pixmap); 172 | self->dataChanged(index(it->second), index(it->second)); 173 | self->finish_fetch(id, url); 174 | }); 175 | connect(fetch, &matrix::ContentFetch::error, [=](const QString &msg) { 176 | if(!self) return; 177 | self->finish_fetch(id, url); 178 | }); 179 | } catch(matrix::illegal_content_scheme &e) { 180 | qDebug() << "ignoring avatar with illegal scheme" << url.scheme() << "for user" << id.value(); 181 | } 182 | } 183 | 184 | void MemberListModel::finish_fetch(UserID id, QUrl url) { 185 | auto queue_it = avatar_fetch_queue_.find(id); 186 | if(queue_it->second == url) { 187 | avatar_fetch_queue_.erase(queue_it); 188 | } 189 | if(!avatar_fetch_queue_.empty()) { 190 | do_fetch(); 191 | } 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /src/MainWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "MainWindow.hpp" 2 | #include "ui_MainWindow.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "matrix/Room.hpp" 13 | #include "matrix/Session.hpp" 14 | #include "matrix/pixmaps.hpp" 15 | 16 | #include "sort.hpp" 17 | #include "RoomView.hpp" 18 | #include "ChatWindow.hpp" 19 | #include "JoinDialog.hpp" 20 | #include "MessageBox.hpp" 21 | #include "utils.hpp" 22 | 23 | MainWindow::MainWindow(matrix::Session &session) 24 | : ui(new Ui::MainWindow), session_(session), 25 | progress_(new QProgressBar(this)), sync_label_(new QLabel(this)), 26 | thumbnail_cache_{devicePixelRatioF()}, rooms_{session, initial_icon_size(*this), devicePixelRatioF()} { 27 | ui->setupUi(this); 28 | 29 | ui->status_bar->addPermanentWidget(sync_label_); 30 | ui->status_bar->addPermanentWidget(progress_); 31 | 32 | auto tray = new QSystemTrayIcon(QIcon::fromTheme("user-available"), this); 33 | tray->setContextMenu(ui->menu_matrix); 34 | connect(tray, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason reason) { 35 | if(reason == QSystemTrayIcon::Trigger) { 36 | setVisible(!isVisible()); 37 | } 38 | }); 39 | tray->show(); 40 | 41 | connect(ui->action_log_out, &QAction::triggered, this, &MainWindow::log_out); 42 | 43 | connect(ui->action_join, &QAction::triggered, [this]() { 44 | QPointer dialog(new JoinDialog); 45 | dialog->setAttribute(Qt::WA_DeleteOnClose); 46 | connect(dialog, &QDialog::accepted, [this, dialog]() { 47 | const QString room = dialog->room(); 48 | auto reply = session_.join(room); 49 | connect(reply, &matrix::JoinRequest::error, [room, dialog](const QString &msg) { 50 | if(!dialog) return; 51 | dialog->setEnabled(true); 52 | MessageBox::critical(tr("Failed to join room"), tr("Couldn't join %1: %2").arg(room).arg(msg), dialog); 53 | }); 54 | connect(reply, &matrix::JoinRequest::success, dialog, &QWidget::close); 55 | }); 56 | dialog->open(); 57 | }); 58 | 59 | connect(&session_, &matrix::Session::error, [this](QString msg) { 60 | qDebug() << "Session error: " << msg; 61 | }); 62 | 63 | connect(&session_, &matrix::Session::synced_changed, [this]() { 64 | if(session_.synced()) { 65 | sync_label_->hide(); 66 | } else { 67 | sync_label_->setText(tr("Disconnected")); 68 | sync_label_->show(); 69 | } 70 | }); 71 | 72 | connect(&session_, &matrix::Session::sync_progress, this, &MainWindow::sync_progress); 73 | connect(&session_, &matrix::Session::sync_complete, [this]() { 74 | progress_->hide(); 75 | sync_label_->hide(); 76 | }); 77 | 78 | ui->action_quit->setShortcuts(QKeySequence::Quit); 79 | connect(ui->action_quit, &QAction::triggered, this, &MainWindow::quit); 80 | 81 | connect(ui->room_list, &QListView::activated, [this](const QModelIndex &){ 82 | std::unordered_set windows; 83 | for(auto index : ui->room_list->selectionModel()->selectedIndexes()) { 84 | auto &room = *session_.room_from_id(matrix::RoomID{rooms_.data(index, JoinedRoomListModel::IDRole).toString()}); 85 | auto it = windows_.find(room.id()); 86 | ChatWindow *window = it != windows_.end() ? it->second : nullptr; 87 | if(!window) { 88 | if(last_focused_) { 89 | window = last_focused_; // Add to most recently used window 90 | } else if(!windows_.empty()) { 91 | // Select arbitrary window 92 | window = windows_.begin()->second; 93 | } else { 94 | // Create first window 95 | window = spawn_chat_window(); 96 | } 97 | } 98 | window->add_or_focus(room); 99 | windows.insert(window); 100 | } 101 | for(auto window : windows) { 102 | window->show(); 103 | window->activateWindow(); 104 | } 105 | }); 106 | connect(ui->room_list, &QAbstractItemView::iconSizeChanged, &rooms_, &JoinedRoomListModel::icon_size_changed); 107 | ui->room_list->setModel(&rooms_); 108 | 109 | connect(&thumbnail_cache_, &ThumbnailCache::needs, [this](const matrix::Thumbnail &t) { 110 | auto fetch = session_.get_thumbnail(t); 111 | QPointer self(this); 112 | connect(fetch, &matrix::ContentFetch::finished, [=](const QString &type, const QString &disposition, const QByteArray &data) { 113 | (void)disposition; 114 | if(!self) return; 115 | QPixmap pixmap = matrix::decode(type, data); 116 | 117 | if(pixmap.width() > t.size().width() || pixmap.height() > t.size().height()) 118 | pixmap = pixmap.scaled(t.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); 119 | pixmap.setDevicePixelRatio(self->devicePixelRatioF()); 120 | 121 | self->thumbnail_cache_.set(t, pixmap); 122 | }); 123 | }); 124 | 125 | sync_progress(0, -1); 126 | } 127 | 128 | MainWindow::~MainWindow() { 129 | std::unordered_set windows; 130 | for(auto &window : windows_) { 131 | windows.insert(window.second); 132 | } 133 | for(auto window : windows) { 134 | delete window; 135 | } 136 | delete ui; 137 | } 138 | 139 | void MainWindow::highlight(const matrix::RoomID &room) { 140 | QWidget *window = windows_.at(room); 141 | if(!window) { 142 | window = this; 143 | } 144 | window->show(); 145 | QApplication::alert(window); 146 | } 147 | 148 | void MainWindow::sync_progress(qint64 received, qint64 total) { 149 | sync_label_->setText(tr("Synchronizing...")); 150 | sync_label_->show(); 151 | progress_->show(); 152 | if(total == -1 || total == 0) { 153 | progress_->setMaximum(0); 154 | } else { 155 | progress_->setMaximum(1000); 156 | progress_->setValue(1000 * static_cast(received)/static_cast(total)); 157 | } 158 | } 159 | 160 | RoomWindowBridge::RoomWindowBridge(matrix::Room &room, ChatWindow &parent) : QObject(&parent), room_(room), window_(parent) { 161 | connect(&room, &matrix::Room::sync_complete, this, &RoomWindowBridge::display_changed); 162 | connect(&parent, &ChatWindow::released, this, &RoomWindowBridge::check_release); 163 | } 164 | 165 | void RoomWindowBridge::display_changed() { 166 | window_.room_display_changed(room_); 167 | } 168 | 169 | void RoomWindowBridge::check_release(const matrix::RoomID &room) { 170 | if(room_.id() == room) deleteLater(); 171 | } 172 | 173 | ChatWindow *MainWindow::spawn_chat_window() { 174 | // We don't create these as children to prevent Qt from hinting to WMs that they should be floating 175 | auto window = new ChatWindow(thumbnail_cache_); 176 | connect(window, &ChatWindow::focused, [this, window]() { 177 | last_focused_ = window; 178 | }); 179 | connect(window, &ChatWindow::claimed, [this, window](const matrix::RoomID &r) { 180 | windows_[r] = window; 181 | new RoomWindowBridge(*session_.room_from_id(r), *window); 182 | }); 183 | connect(window, &ChatWindow::released, [this](const matrix::RoomID &rid) { 184 | windows_.erase(rid); 185 | }); 186 | connect(window, &ChatWindow::pop_out, [this](const matrix::RoomID &r, RoomView *v) { 187 | auto w = spawn_chat_window(); 188 | w->add(*session_.room_from_id(r), v); 189 | w->show(); 190 | w->raise(); 191 | w->activateWindow(); 192 | }); 193 | return window; 194 | } 195 | -------------------------------------------------------------------------------- /src/matrix/TimelineWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "TimelineWindow.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "Room.hpp" 7 | #include "proto.hpp" 8 | 9 | using std::experimental::optional; 10 | 11 | namespace matrix { 12 | 13 | namespace { 14 | 15 | constexpr size_t BATCH_SIZE = 50; 16 | 17 | void revert_batch(RoomState &state, const Batch &batch) { 18 | for(auto it = batch.events.crbegin(); it != batch.events.crend(); ++it) { 19 | if(auto s = it->to_state()) state.revert(*s); 20 | } 21 | } 22 | 23 | } 24 | 25 | TimelineWindow::TimelineWindow(std::deque batches, const RoomState &final_state) 26 | : initial_state_{final_state}, final_state_{final_state}, 27 | batches_{std::move(batches)}, 28 | sync_batch_{batches_.empty() ? throw std::invalid_argument("timeline window must be construct from at least one batch") : batches_.back()} 29 | { 30 | for(auto it = batches_.crbegin(); it != batches_.crend(); ++it) { 31 | revert_batch(initial_state_, *it); 32 | } 33 | } 34 | 35 | void TimelineWindow::discard(const TimelineCursor &batch, Direction dir) { 36 | if(dir == Direction::FORWARD) { 37 | for(auto it = batches_.crbegin(); it != batches_.crend(); ++it) { 38 | if(it->begin == batch) { 39 | if(it.base() != batches_.cend()) { 40 | batches_end_ = it.base()->begin; 41 | } 42 | batches_.erase(it.base(), batches_.end()); 43 | return; 44 | } 45 | revert_batch(final_state_, *it); 46 | } 47 | } else { 48 | for(auto it = batches_.cbegin(); it != batches_.cend(); ++it) { 49 | if(it->begin == batch) { 50 | batches_.erase(batches_.cbegin(), it); 51 | return; 52 | } 53 | for(const auto &evt : it->events) { 54 | if(auto s = evt.to_state()) initial_state_.apply(*s); 55 | } 56 | } 57 | } 58 | qCritical() << "timeline window tried to discard unknown batch" << batch.value(); 59 | } 60 | 61 | bool TimelineWindow::at_start() const { 62 | return batches_.front().events.front().type() == event::room::Create::tag(); 63 | } 64 | 65 | bool TimelineWindow::at_end() const { 66 | return batches_.empty() || sync_batch_.begin == batches_.back().begin; 67 | } 68 | 69 | void TimelineWindow::append_batch(const TimelineCursor &batch_start, const TimelineCursor &batch_end, gsl::span events, 70 | TimelineManager *mgr) { 71 | if(!end() || batch_start != *this->end()) { 72 | if(end()) mgr->grow(Direction::FORWARD); 73 | return; 74 | } 75 | 76 | size_t new_batches = 0; 77 | if(!events.empty()) { 78 | batches_.emplace_back(batch_start, std::vector(events.begin(), events.end())); 79 | batches_end_ = batch_end; 80 | ++new_batches; 81 | } 82 | 83 | if(static_cast(events.size()) < BATCH_SIZE) { 84 | batches_.emplace_back(sync_batch_); 85 | batches_end_ = {}; 86 | ++new_batches; 87 | } 88 | 89 | for(size_t i = 0; i < new_batches; ++i) { 90 | for(const auto &evt : batches_[batches_.size()-new_batches+i].events) { 91 | mgr->grew(Direction::FORWARD, batches_.back().begin, final_state_, evt); 92 | if(auto s = evt.to_state()) { 93 | final_state_.apply(*s); 94 | } 95 | } 96 | } 97 | } 98 | 99 | void TimelineWindow::prepend_batch(const TimelineCursor &batch_start, const TimelineCursor &batch_end, gsl::span reversed_events, 100 | TimelineManager *mgr) { 101 | if(batch_start != begin()) { // we compare begin to begin here because start/end are reversed for backwards fetches 102 | mgr->grow(Direction::BACKWARD); 103 | return; 104 | } 105 | 106 | if(reversed_events.empty()) return; 107 | 108 | batches_.emplace_front(batch_end, std::vector(reversed_events.rbegin(), reversed_events.rend())); 109 | 110 | for(auto it = batches_.front().events.crbegin(); it != batches_.front().events.crend(); ++it) { 111 | if(auto s = it->to_state()) initial_state_.revert(*s); 112 | mgr->grew(Direction::BACKWARD, batch_start, initial_state_, *it); 113 | } 114 | } 115 | 116 | void TimelineWindow::append_sync(const proto::Timeline &t, TimelineManager *mgr) { 117 | if(t.events.empty()) return; 118 | 119 | if(at_end()) { 120 | if(t.limited) { 121 | batches_.clear(); // FIXME: Don't nuke history 122 | } 123 | batches_.emplace_back(t.prev_batch, t.events); 124 | } 125 | 126 | sync_batch_ = Batch{t.prev_batch, t.events}; 127 | 128 | if(at_end()) { 129 | if(t.limited) { 130 | mgr->discontinuity(); 131 | } 132 | 133 | for(const auto &evt : sync_batch_.events) { 134 | mgr->grew(Direction::FORWARD, sync_batch_.begin, final_state_, evt); 135 | if(auto s = evt.to_state()) { 136 | final_state_.apply(*s); 137 | } 138 | } 139 | } 140 | } 141 | 142 | void TimelineWindow::reset(const RoomState ¤t_state) { 143 | batches_.clear(); 144 | batches_.emplace_back(sync_batch_); 145 | batches_end_ = {}; 146 | final_state_ = current_state; 147 | initial_state_ = current_state; 148 | revert_batch(initial_state_, sync_batch_); 149 | } 150 | 151 | 152 | TimelineManager::TimelineManager(Room &room, QObject *parent) 153 | : QObject(parent), room_(room), window_{room.buffer(), room.state()}, forward_req_{nullptr}, backward_req_{nullptr} 154 | { 155 | retry_timer_.setSingleShot(true); 156 | retry_timer_.setInterval(1000); 157 | connect(&retry_timer_, &QTimer::timeout, this, &TimelineManager::retry); 158 | connect(&room, &Room::sync_complete, this, &TimelineManager::batch); 159 | } 160 | 161 | void TimelineManager::grow(Direction dir) { 162 | optional start, end; 163 | if(dir == Direction::FORWARD) { 164 | if(forward_req_ || window_.at_end()) return; 165 | start = window_.end(); 166 | end = window_.sync_begin(); 167 | } else { 168 | if(backward_req_ || window_.at_start()) return; 169 | start = window_.begin(); 170 | } 171 | 172 | if(!start) { 173 | throw std::logic_error("tried to grow from an undefined cursor"); 174 | } 175 | auto reply = room_.get_messages(dir, *start, BATCH_SIZE, end); 176 | 177 | if(dir == Direction::FORWARD) { 178 | connect(reply, &MessageFetch::finished, this, &TimelineManager::got_forward); 179 | connect(reply, &MessageFetch::error, this, &TimelineManager::forward_fetch_error); 180 | forward_req_ = reply; 181 | } else { 182 | connect(reply, &MessageFetch::finished, this, &TimelineManager::got_backward); 183 | connect(reply, &MessageFetch::error, this, &TimelineManager::backward_fetch_error); 184 | backward_req_ = reply; 185 | } 186 | } 187 | 188 | void TimelineManager::replay() { 189 | auto replay = window().initial_state(); 190 | for(const auto &batch : window().batches()) { 191 | for(const auto &evt : batch.events) { 192 | grew(Direction::FORWARD, batch.begin, replay, evt); 193 | if(auto s = evt.to_state()) replay.apply(*s); 194 | } 195 | } 196 | } 197 | 198 | void TimelineManager::retry() { 199 | grow(retry_dir_); 200 | } 201 | 202 | void TimelineManager::forward_fetch_error(const QString &msg) { 203 | forward_req_ = nullptr; 204 | error(Direction::BACKWARD, msg); 205 | } 206 | 207 | void TimelineManager::backward_fetch_error(const QString &msg) { 208 | backward_req_ = nullptr; 209 | error(Direction::FORWARD, msg); 210 | } 211 | 212 | void TimelineManager::error(Direction dir, const QString &msg) { 213 | // TODO: Check whether error is retry-worthy? 214 | qWarning() << room_.pretty_name() << "retrying timeline fetch due to error:" << msg; 215 | retry_dir_ = dir; 216 | if(!retry_timer_.isActive()) retry_timer_.start(); 217 | } 218 | 219 | void TimelineManager::got_backward(const TimelineCursor &start, const TimelineCursor &end, gsl::span reversed_events) { 220 | backward_req_ = nullptr; 221 | window_.prepend_batch(start, end, reversed_events, this); 222 | } 223 | 224 | void TimelineManager::got_forward(const TimelineCursor &start, const TimelineCursor &end, gsl::span events) { 225 | forward_req_ = nullptr; 226 | window_.append_batch(start, end, events, this); 227 | } 228 | 229 | void TimelineManager::batch(const proto::Timeline &t) { 230 | window_.append_sync(t, this); 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/matrix/Room.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_MATRIX_ROOM_H_ 2 | #define NATIVE_CHAT_MATRIX_ROOM_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include "../QStringHash.hpp" 17 | 18 | #include "Event.hpp" 19 | 20 | class QNetworkReply; 21 | 22 | namespace matrix { 23 | 24 | class Matrix; 25 | class Session; 26 | class Room; 27 | 28 | namespace proto { 29 | struct JoinedRoom; 30 | struct Timeline; 31 | } 32 | 33 | inline QString pretty_name(const UserID &user, const event::room::MemberContent &profile) { 34 | return profile.displayname() ? *profile.displayname() : user.value(); 35 | } 36 | 37 | using Member = std::pair; 38 | 39 | class RoomState { 40 | public: 41 | RoomState() = default; // New, empty room 42 | RoomState(const QJsonObject &state, gsl::span members); // Loaded from db 43 | 44 | void apply(const event::room::State &e) { 45 | dispatch(e, nullptr); 46 | } 47 | void revert(const event::room::State &e); // Reverts an event that, if a state event, has prev_content 48 | 49 | bool dispatch(const event::room::State &e, Room *room); 50 | // Returns true if changes were made. Emits state change events on room if supplied. 51 | 52 | const std::experimental::optional &name() const { return name_; } 53 | const std::experimental::optional &canonical_alias() const { return canonical_alias_; } 54 | gsl::span aliases() const { return aliases_; } 55 | const std::experimental::optional &topic() const { return topic_; } 56 | const QUrl &avatar() const { return avatar_; } 57 | 58 | std::vector members() const; 59 | const event::room::MemberContent *member_from_id(const UserID &id) const; 60 | 61 | QString pretty_name(const UserID &own_id) const; 62 | // Matrix r0.1.0 11.2.2.5 ish (like vector-web) 63 | 64 | std::experimental::optional member_disambiguation(const UserID &member) const; 65 | std::experimental::optional nonmember_disambiguation(const UserID &id, const std::experimental::optional &displayname) const; 66 | QString member_name(const UserID &member) const; 67 | // Matrix r0.1.0 11.2.2.3 68 | 69 | QJsonObject to_json() const; 70 | // For serialization 71 | 72 | private: 73 | std::experimental::optional name_, canonical_alias_, topic_; 74 | std::vector aliases_; 75 | QUrl avatar_; 76 | std::unordered_map members_by_id_; 77 | std::unordered_map, QStringHash> members_by_displayname_; 78 | 79 | void forget_displayname(const UserID &member, const QString &old_name, Room *room); 80 | void record_displayname(const UserID &member, const QString &name, Room *room); 81 | std::vector &members_named(QString displayname); 82 | const std::vector &members_named(QString displayname) const; 83 | 84 | bool update_membership(const UserID &user_id, const event::room::MemberContent &content, Room *room); 85 | }; 86 | 87 | class MessageFetch : public QObject { 88 | Q_OBJECT 89 | 90 | public: 91 | MessageFetch(QObject *parent = nullptr) : QObject(parent) {} 92 | 93 | signals: 94 | void finished(const TimelineCursor &start, const TimelineCursor &end, gsl::span events); 95 | void error(const QString &message); 96 | }; 97 | 98 | class EventSend : public QObject { 99 | Q_OBJECT 100 | 101 | public: 102 | EventSend(QObject *parent = nullptr) : QObject(parent) {} 103 | 104 | signals: 105 | void finished(); 106 | void error(const QString &message); 107 | }; 108 | 109 | struct Batch { 110 | TimelineCursor begin; 111 | std::vector events; 112 | 113 | Batch(TimelineCursor begin, std::vector events) : begin{begin}, events{std::move(events)} {} 114 | Batch(const QJsonObject &o); 115 | 116 | QJsonObject to_json() const; 117 | }; 118 | 119 | class Room : public QObject { 120 | Q_OBJECT 121 | 122 | public: 123 | struct Receipt { 124 | EventID event; 125 | uint64_t ts; 126 | }; 127 | 128 | struct PendingEvent { 129 | TransactionID transaction_id; 130 | EventType type; 131 | event::Content content; 132 | }; 133 | 134 | Room(Matrix &universe, Session &session, RoomID id, const QJsonObject &initial, 135 | gsl::span members); 136 | Room(Matrix &universe, Session &session, const proto::JoinedRoom &joined_room); 137 | 138 | Room(const Room &) = delete; 139 | Room &operator=(const Room &) = delete; 140 | 141 | const Session &session() const { return session_; } 142 | Session &session() { return session_; } 143 | const RoomID &id() const { return id_; } 144 | uint64_t highlight_count() const { return highlight_count_; } 145 | uint64_t notification_count() const { return notification_count_; } 146 | 147 | const RoomState &state() const { return state_; } 148 | 149 | QString pretty_name() const; 150 | QString pretty_name_highlights() const { 151 | return pretty_name() + (highlight_count() != 0 ? " (" + QString::number(highlight_count()) + ")" : ""); 152 | } 153 | 154 | bool dispatch(const proto::JoinedRoom &); 155 | 156 | QJsonObject to_json() const; 157 | 158 | MessageFetch *get_messages(Direction dir, const TimelineCursor &from, uint64_t limit = 0, std::experimental::optional to = {}); 159 | 160 | EventSend *leave(); 161 | 162 | TransactionID send(const EventType &type, event::Content content); 163 | 164 | TransactionID redact(const EventID &event, const QString &reason = ""); 165 | 166 | TransactionID send_file(const QString &uri, const QString &name, const QString &media_type, size_t size); 167 | TransactionID send_message(const QString &body); 168 | TransactionID send_emote(const QString &body); 169 | 170 | void send_read_receipt(const EventID &event); 171 | 172 | gsl::span typing() const { return typing_; } 173 | gsl::span receipts_for(const EventID &id) const; 174 | const Receipt *receipt_from(const UserID &id) const; 175 | 176 | const std::deque &pending_events() const { return pending_events_; } 177 | // Events that have not yet been successfully transmitted 178 | 179 | const std::deque &buffer() const { return buffer_; } 180 | 181 | bool has_unread() const; 182 | 183 | signals: 184 | void member_changed(const UserID &, const event::room::MemberContent ¤t, const event::room::MemberContent &next); 185 | void member_disambiguation_changed(const UserID &, const std::experimental::optional ¤t, const std::experimental::optional &next); 186 | void state_changed(); 187 | void highlight_count_changed(uint64_t old); 188 | void notification_count_changed(uint64_t old); 189 | void name_changed(); 190 | void canonical_alias_changed(); 191 | void aliases_changed(); 192 | void topic_changed(const std::experimental::optional &old); 193 | void avatar_changed(); 194 | void typing_changed(); 195 | void receipts_changed(); 196 | 197 | void sync_start(const proto::Timeline &); 198 | void sync_complete(const proto::Timeline &); 199 | 200 | void prev_batch(const TimelineCursor &); 201 | void message(const event::Room &); 202 | void redaction(const event::room::Redaction &); 203 | 204 | void error(const QString &msg); 205 | void left(Membership reason); 206 | 207 | private: 208 | Matrix &universe_; 209 | Session &session_; 210 | const RoomID id_; 211 | 212 | RoomState state_; 213 | std::deque buffer_; 214 | 215 | uint64_t highlight_count_ = 0, notification_count_ = 0; 216 | 217 | std::unordered_map> receipts_by_event_; 218 | std::unordered_map receipts_by_user_; 219 | 220 | std::vector typing_; 221 | 222 | // State used for reliable in-order message delivery in send, transmit_event, and transmit_finished 223 | std::deque pending_events_; 224 | QNetworkReply *transmitting_; 225 | QTimer transmit_retry_timer_; 226 | std::chrono::steady_clock::duration retry_backoff_; 227 | 228 | void update_receipt(const UserID &user, const EventID &event, uint64_t ts); 229 | 230 | void transmit_event(); 231 | void transmit_finished(); 232 | }; 233 | 234 | } 235 | 236 | #endif 237 | -------------------------------------------------------------------------------- /src/matrix/Event.cpp: -------------------------------------------------------------------------------- 1 | #include "Event.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace matrix { 7 | 8 | static Membership parse_membership(const QString &m) { 9 | static const std::pair table[] = { 10 | {"invite", Membership::INVITE}, 11 | {"join", Membership::JOIN}, 12 | {"leave", Membership::LEAVE}, 13 | {"ban", Membership::BAN} 14 | }; 15 | for(const auto &x : table) { 16 | if(x.first == m) return x.second; 17 | } 18 | throw malformed_event("unrecognized membership value"); 19 | } 20 | 21 | QString to_qstring(Membership m) { 22 | switch(m) { 23 | case Membership::INVITE: return "invite"; 24 | case Membership::JOIN: return "join"; 25 | case Membership::LEAVE: return "leave"; 26 | case Membership::BAN: return "ban"; 27 | } 28 | } 29 | 30 | struct EventInfo { 31 | const char *real_name; 32 | const char *name; 33 | QJsonValue::Type type; 34 | bool required; 35 | 36 | EventInfo(const char *name, QJsonValue::Type type, bool required = true) : real_name(name), name(name), type(type), required(required) {} 37 | EventInfo(const char *real_name, const char *name, QJsonValue::Type type, bool required = true) : real_name(real_name), name(name), type(type), required(required) {} 38 | }; 39 | 40 | static void check(const QJsonObject &o, std::initializer_list fields) { 41 | for(auto field : fields) { 42 | auto it = o.find(field.real_name); 43 | if(it != o.end()) { 44 | if((field.required || !it->isNull()) && it->type() != field.type) { 45 | throw ill_typed_field(field.name, field.type, it->type()); 46 | } 47 | } else if(field.required) { 48 | throw missing_field(field.name); 49 | } 50 | } 51 | } 52 | 53 | namespace event { 54 | 55 | UnsignedData::UnsignedData(QJsonObject o) : json_{std::move(o)} { 56 | check(json(), { 57 | {"age", "unsigned.age", QJsonValue::Double, false}, 58 | {"redacted_because", "unsigned.redacted_because", QJsonValue::Object, false} 59 | }); 60 | 61 | auto it = json().find("redacted_because"); 62 | if(it != json().end()) { 63 | redacted_because_ = std::make_shared(event::Room{event::Identifiable{Event{it->toObject()}}}); 64 | } 65 | } 66 | 67 | } 68 | 69 | Event::Event(QJsonObject o) : json_(std::move(o)) { 70 | check(json(), { 71 | {"content", QJsonValue::Object}, 72 | {"type", QJsonValue::String}, 73 | {"unsigned", QJsonValue::Object, false} 74 | }); 75 | 76 | auto it = json().find("unsigned"); 77 | if(it != json().end()) { 78 | unsigned_data_ = event::UnsignedData{it->toObject()}; 79 | } 80 | } 81 | 82 | void Event::redact(const event::room::Redaction &because) { 83 | struct ContentRule { 84 | EventType type; 85 | std::initializer_list preserved_keys; 86 | }; 87 | 88 | using namespace event::room; 89 | 90 | // section 6.5 91 | const char *const preserved_keys[] = {"event_id", "type", "room_id", "sender", "state_key", "prev_content", "content"}; 92 | const ContentRule content_rules[] = { 93 | {Member::tag(), {"membership"}}, 94 | {Create::tag(), {"creator"}}, 95 | {JoinRules::tag(), {"join_rule"}}, 96 | {PowerLevels::tag(), {"ban", "events", "events_default", "kick", "redact", "state_default", "users", "users_default"}}, 97 | {Aliases::tag(), {"aliases"}}, 98 | }; 99 | 100 | for(auto it = json_.begin(); it != json_.end();) { 101 | auto p = std::find(std::begin(preserved_keys), std::end(preserved_keys), it.key()); 102 | if(p == std::end(preserved_keys)) { 103 | it = json_.erase(it); 104 | } else { 105 | ++it; 106 | } 107 | } 108 | 109 | auto content_rule = std::find_if(std::begin(content_rules), std::end(content_rules), [this](const ContentRule &c) { 110 | return c.type == type(); 111 | }); 112 | if(content_rule != std::end(content_rules)) { 113 | const auto &keys = content_rule->preserved_keys; 114 | auto content_obj = json_.take("content").toObject(); 115 | for(auto it = content_obj.begin(); it != content_obj.end();) { 116 | auto p = std::find(std::begin(keys), std::end(keys), it.key()); 117 | if(p == std::end(keys)) { 118 | it = content_obj.erase(it); 119 | } else { 120 | ++it; 121 | } 122 | } 123 | json_.insert("content", content_obj); 124 | } 125 | 126 | auto unsigned_it = json_.insert("unsigned", QJsonObject{{"redacted_because", because.json()}}); 127 | unsigned_data_ = event::UnsignedData{unsigned_it->toObject()}; 128 | } 129 | 130 | namespace event { 131 | 132 | Receipt::Receipt(Event e) : Event(std::move(e)) { 133 | if(type() != tag()) throw type_mismatch(); 134 | } 135 | 136 | Typing::Typing(Event e) : Event(std::move(e)) { 137 | if(type() != tag()) throw type_mismatch(); 138 | check(content().json(), { 139 | {"user_ids", "content.user_ids", QJsonValue::Array} 140 | }); 141 | } 142 | 143 | std::vector Typing::user_ids() const { 144 | std::vector result; 145 | const auto ids = content().json()["user_ids"].toArray(); 146 | result.reserve(ids.size()); 147 | for(const auto &x : ids) { 148 | result.push_back(UserID(x.toString())); 149 | } 150 | return result; 151 | } 152 | 153 | Identifiable::Identifiable(Event e) : Event(e) { 154 | check(json(), { 155 | {"event_id", QJsonValue::String} 156 | }); 157 | } 158 | 159 | Room::Room(Identifiable e) : Identifiable(std::move(e)) { 160 | check(json(), {{"sender", QJsonValue::String}}); 161 | 162 | if(redacted()) return; 163 | 164 | check(json(), { 165 | {"origin_server_ts", QJsonValue::Double}, 166 | {"unsigned", QJsonValue::Object, false} 167 | }); 168 | } 169 | 170 | namespace room { 171 | 172 | MessageContent::MessageContent(Content c) : Content(std::move(c)) { 173 | check(json(), { 174 | {"msgtype", "content.msgtype", QJsonValue::String}, 175 | {"body", "content.body", QJsonValue::String} 176 | }); 177 | } 178 | 179 | Message::Message(Room e) : Room(std::move(e)) { 180 | if(type() != tag()) throw type_mismatch(); 181 | if(redacted()) return; 182 | content_ = MessageContent(Event::content()); 183 | } 184 | 185 | namespace message { 186 | 187 | FileLike::FileLike(MessageContent m) : MessageContent(std::move(m)) { 188 | check(json(), { 189 | {"url", QJsonValue::String}, 190 | }); 191 | QJsonObject i; 192 | { 193 | auto it = json().find("info"); 194 | if(it == json().end() || it->isNull()) return; 195 | i = it->toObject(); 196 | } 197 | { 198 | auto it = i.find("mimetype"); 199 | if(it != i.end() && !it->isString() && !it->isNull()) 200 | throw ill_typed_field("info.mimetype", QJsonValue::String, it->type()); 201 | } 202 | { 203 | auto it = i.find("size"); 204 | if(it != i.end() && !it->isDouble() && !it->isNull()) 205 | throw ill_typed_field("info.size", QJsonValue::Double, it->type()); 206 | } 207 | } 208 | 209 | File::File(FileLike m) : FileLike(std::move(m)) { 210 | if(type() != tag()) throw type_mismatch(); 211 | check(json(), { 212 | {"filename", QJsonValue::String} 213 | }); 214 | } 215 | 216 | } 217 | 218 | State::State(Room e) : Room(std::move(e)) { 219 | check(json(), { 220 | {"state_key", QJsonValue::String} 221 | }); 222 | } 223 | 224 | MemberContent::MemberContent(Content c) : Content(std::move(c)) { 225 | check(json(), { 226 | {"membership", "content.membership", QJsonValue::String}, 227 | {"avatar_url", "content.avatar_url", QJsonValue::String, false}, 228 | {"displayname", "content.displayname", QJsonValue::String, false} 229 | }); 230 | membership_ = parse_membership(json()["membership"].toString()); 231 | { 232 | auto it = json().find("avatar_url"); 233 | if(it != json().end() && !it->isNull() && it->toString() != "") 234 | avatar_url_ = json()["avatar_url"].toString(); 235 | } 236 | { 237 | auto it = json().find("displayname"); 238 | if(it != json().end() && !it->isNull() && it->toString() != "") 239 | displayname_ = json()["displayname"].toString(); 240 | } 241 | } 242 | 243 | MemberContent::MemberContent(Membership membership, 244 | std::experimental::optional displayname, 245 | std::experimental::optional avatar_url) 246 | : Content({ 247 | {"membership", to_qstring(membership)}, 248 | {"displayname", displayname ? QJsonValue(*displayname) : QJsonValue()}, 249 | {"avatar_url", avatar_url ? QJsonValue(*avatar_url) : QJsonValue()} 250 | }), 251 | membership_{membership}, avatar_url_{avatar_url}, displayname_{displayname} {} 252 | 253 | const MemberContent MemberContent::leave(Content({{"membership", "leave"}})); 254 | 255 | Member::Member(State e) : State(std::move(e)), content_{State::content()} { 256 | if(type() != tag()) throw type_mismatch(); 257 | auto prev = State::prev_content(); 258 | if(prev) { 259 | prev_content_ = MemberContent(*prev); 260 | } 261 | } 262 | 263 | void Member::redact(const event::room::Redaction &because) { 264 | Event::redact(because); 265 | content_ = MemberContent{State::content()}; 266 | } 267 | 268 | Aliases::Aliases(State e) : State(std::move(e)) { 269 | check(content().json(), {{"aliases", "content.aliases", QJsonValue::Array}}); 270 | } 271 | 272 | CanonicalAlias::CanonicalAlias(State e) : State(std::move(e)) {} 273 | 274 | Topic::Topic(State e) : State(std::move(e)) { 275 | if(redacted()) return; 276 | check(content().json(), {{"topic", "content.topic", QJsonValue::String}}); 277 | } 278 | 279 | Avatar::Avatar(State e) : State(std::move(e)) { 280 | if(redacted()) return; 281 | check(content().json(), {{"url", "content.url", QJsonValue::String}}); 282 | } 283 | 284 | Create::Create(State e) : State(std::move(e)) { 285 | check(content().json(), {{"creator", "content.create", QJsonValue::String}}); 286 | } 287 | 288 | JoinRules::JoinRules(State e) : State(std::move(e)) {} 289 | 290 | PowerLevels::PowerLevels(State e) : State(std::move(e)) {} 291 | 292 | Redaction::Redaction(Room r) : Room(std::move(r)) { 293 | if(redacted()) return; 294 | check(json(), {{"redacts", QJsonValue::String}}); 295 | check(content().json(), { 296 | {"reason", "content.reason", QJsonValue::String, false} 297 | }); 298 | } 299 | 300 | } 301 | 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/TimelineView.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_CHAT_TIMELINE_VIEW_HPP_ 2 | #define NATIVE_CHAT_TIMELINE_VIEW_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "matrix/Event.hpp" 15 | 16 | #include "ContentCache.hpp" 17 | #include "FixedVector.hpp" 18 | 19 | class QEvent; 20 | class QShortcut; 21 | 22 | namespace matrix { 23 | class RoomState; 24 | } 25 | 26 | using Time = std::chrono::time_point; 27 | 28 | class TimelineEventID : public matrix::ID { using ID::ID; }; 29 | 30 | struct EventLike { 31 | struct MemberInfo { 32 | matrix::UserID user; 33 | matrix::event::room::MemberContent prev_content; 34 | }; 35 | 36 | TimelineEventID id; 37 | std::experimental::optional event; 38 | matrix::EventType type; 39 | std::experimental::optional