Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
index b987349..8eb6bca 100644
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -1,71 +1,83 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
RowLayout {
property var room
onRoomChanged: {
- textArea.text = room.localDraft
+ textArea.changeText(room.localDraft, true);
}
TextArea {
id: textArea
property var shortcutList: ["Ctrl+Return", "Ctrl+Enter"]
+ property var inhibitTyping: false
placeholderText: l10n.get('send-message-box-input-placeholder')
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
onTextChanged: {
- room.localDraft = text
+ room.localDraft = text;
+ if (!inhibitTyping) {
+ room.setTyping(true);
+ }
+ inhibitTyping = false;
+ }
+
+ function changeText(newText, inhibitTyping) {
+ textArea.inhibitTyping = inhibitTyping;
+ textArea.text = newText;
}
+
// Shortcut keys for sending messages (TODO: Shortcut keys customized by the user)
Shortcut {
sequences: textArea.shortcutList
onActivated: {
sendAction.trigger()
}
}
}
Kirigami.Action {
id: sendAction
iconName: "document-send"
text: l10n.get("send-message-box-send")
onTriggered: {
- room.sendTextMessage(textArea.text)
- textArea.text = ""
+ room.setTyping(false);
+ room.sendTextMessage(textArea.text);
+ textArea.changeText("", true);
}
}
Kirigami.Action {
id:sendMediaFileAction
iconName: "document-send-symbolic"
text: l10n.get("send-message-box-send-file")
onTriggered: {
fileDialog.open()
}
}
property var fileDialog: Platform.FileDialog {
onAccepted: {
kazvIOManager.startNewUploadJob(matrixSdk.serverUrl, file, matrixSdk.token, room.roomId, sdkVars.roomList)
}
}
ToolButton {
icon.name: "document-send-symbolic"
onClicked: sendMediaFileAction.trigger()
}
ToolButton {
icon.name: "document-send"
onClicked: sendAction.trigger()
}
}
diff --git a/src/matrix-room-list.cpp b/src/matrix-room-list.cpp
index 2f694e6..3232374 100644
--- a/src/matrix-room-list.cpp
+++ b/src/matrix-room-list.cpp
@@ -1,56 +1,57 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <lager/constant.hpp>
#include <lager/lenses/optional.hpp>
#include "matrix-room-list.hpp"
#include "matrix-room.hpp"
#include "helper.hpp"
MatrixRoomList::MatrixRoomList(Kazv::Client client, QObject *parent)
: QObject(parent)
, m_client(client)
, m_roomIds(m_client.roomIds())
, LAGER_QT(count)(m_roomIds.xform(containerSize))
, LAGER_QT(roomIds)(m_roomIds.xform(zug::map(
[](auto container) {
return zug::into(QStringList{}, strToQt, std::move(container));
})))
{
}
MatrixRoomList::~MatrixRoomList() = default;
MatrixRoom *MatrixRoomList::at(int index) const
{
qDebug() << "Room at index " << index << " requested";
return new MatrixRoom(
m_client.roomByCursor(
lager::with(m_roomIds, lager::make_constant(index))
.xform(zug::map([](auto ids, auto i) {
try {
return ids.at(i);
} catch (const std::out_of_range &) {
return std::string{};
}
- }))));
+ }))),
+ m_client.userId());
}
QString MatrixRoomList::roomIdAt(int index) const
{
using namespace Kazv::CursorOp;
return +m_roomIds[index][lager::lenses::or_default].xform(strToQt);
}
MatrixRoom *MatrixRoomList::room(QString roomId) const
{
- return new MatrixRoom(m_client.room(roomId.toStdString()));
+ return new MatrixRoom(m_client.room(roomId.toStdString()), m_client.userId());
}
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
index d8663e9..55f3dc2 100644
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -1,129 +1,159 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <lager/setter.hpp>
#include <nlohmann/json.hpp>
#include <event.hpp>
#include <QFile>
#include <QMimeDatabase>
#include "kazv-log.hpp"
#include "matrix-room.hpp"
#include "matrix-room-timeline.hpp"
#include "matrix-room-member.hpp"
#include "matrix-room-member-list-model.hpp"
#include "matrix-promise.hpp"
+#include "qfunctionutils.hpp"
+
#include "helper.hpp"
using namespace Kazv;
-MatrixRoom::MatrixRoom(Kazv::Room room, QObject *parent)
+static const int typingDebounceMs = 500;
+static const int typingTimeoutMs = 1000;
+
+MatrixRoom::MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, QObject *parent)
: QObject(parent)
, m_room(room)
+ , m_selfUserId(selfUserId)
, m_memberNames(m_room.members())
, LAGER_QT(roomId)(m_room.roomId().xform(strToQt))
, LAGER_QT(name)(m_room.nameOpt()[lager::lenses::or_default].xform(strToQt))
, LAGER_QT(heroNames)(m_room.heroDisplayNames().xform(strListToQt))
, LAGER_QT(avatarMxcUri)(m_room.avatarMxcUri().xform(strToQt))
, LAGER_QT(roomOrHeroAvatarMxcUri)(lager::with(m_room.heroMemberEvents(), m_room.avatarMxcUri())
.map([](const auto &heroes, const auto &roomAvatar) {
if (!roomAvatar.empty()) {
return roomAvatar;
}
if (heroes.size() == 1) {
return zug::reduce(roomMemberToAvatar(zug::last), std::string(), heroes);
}
return std::string();
})
.xform(strToQt))
, LAGER_QT(localDraft)(lager::with_setter(
m_room.localDraft().xform(strToQt).make(),
[this](QString localDraft) {
m_room.setLocalDraft(localDraft.toStdString());
},
lager::automatic_tag{}))
, LAGER_QT(memberNames)(m_memberNames.xform(strListToQt))
+ , m_setTypingThrottled(QFunctionUtils::Throttle([self=QPointer<MatrixRoom>(this)]() {
+ if (self) {
+ self->setTypingImpl();
+ }
+ }, typingDebounceMs))
{
}
MatrixRoom::~MatrixRoom() = default;
MatrixRoomTimeline *MatrixRoom::timeline() const
{
return new MatrixRoomTimeline(m_room);
}
void MatrixRoom::sendTextMessage(QString text) const
{
m_room.sendTextMessage(text.toStdString());
}
void MatrixRoom::sendMediaFileMessage(QString fileName, QString mimeType, qint64 fileSize, QString mxcUri) const
{
auto j = makeMediaFileMessageJson(fileName, mimeType, fileSize, mxcUri);
Kazv::Event e{j};
m_room.sendMessage(e);
}
void MatrixRoom::resendMessage(QString txnId) const
{
m_room.resendMessage(txnId.toStdString());
}
nlohmann::json MatrixRoom::makeMediaFileMessageJson(QString fileName, QString mimeType, qint64 fileSize, QString mxcUri) const
{
static auto available_msgtype = std::array<std::string, 3>{"m.audio", "m.video", "m.image"};
auto try_msgtype = std::find(available_msgtype.begin(), available_msgtype.end(),
QStringLiteral("m.").append(mimeType.split(QChar('/'))[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
return nlohmann::json {
{"type", "m.room.message"},
{"content", {
{"msgtype", msgtype},
{"body", fileName.toStdString()},
{"url", mxcUri.toStdString()},
{"info", {
{"size", fileSize},
{"mimetype", mimeType.toStdString()}
}}
}}
};
}
MatrixPromise *MatrixRoom::redactEvent(QString eventId, QString reason) const
{
qCInfo(kazvLog) << "redactEvent(" << eventId << ", " << reason << ")";
return new MatrixPromise(m_room.redactEvent(
eventId.toStdString(),
reason.isEmpty() ? std::nullopt : std::optional<std::string>(reason.toStdString())
));
}
MatrixRoomMember *MatrixRoom::memberAt(int index) const
{
return new MatrixRoomMember(m_room.memberEventByCursor(
m_memberNames[index][lager::lenses::or_default]));
}
MatrixRoomMember *MatrixRoom::member(QString userId) const
{
return new MatrixRoomMember(m_room.memberEventFor(userId.toStdString()));
}
MatrixRoomMemberListModel *MatrixRoom::typingUsers() const
{
- return new MatrixRoomMemberListModel(m_room.typingMemberEvents());
+ return new MatrixRoomMemberListModel(
+ m_room.typingMemberEvents()
+ .xform(containerMap(EventList{}, zug::filter([self=m_selfUserId.make().get()](const auto &e) {
+ return e.stateKey() != self;
+ })))
+ );
+}
+
+void MatrixRoom::setTyping(bool typing)
+{
+ if (typing) {
+ m_setTypingThrottled();
+ } else {
+ m_room.setTyping(false, std::nullopt);
+ }
+}
+
+void MatrixRoom::setTypingImpl()
+{
+ m_room.setTyping(true, typingTimeoutMs);
}
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
index da78590..7d6588d 100644
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -1,67 +1,77 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
+#include <QTimer>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
class MatrixRoomTimeline;
class MatrixRoomMember;
class MatrixPromise;
class MatrixRoomMemberListModel;
class MatrixRoom : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Kazv::Room m_room;
+ lager::reader<std::string> m_selfUserId;
lager::reader<immer::flex_vector<std::string>> m_memberNames;
public:
- explicit MatrixRoom(Kazv::Room room, QObject *parent = 0);
+ explicit MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, QObject *parent = 0);
~MatrixRoom() override;
LAGER_QT_READER(QString, roomId);
LAGER_QT_READER(QString, name);
LAGER_QT_READER(QStringList, heroNames);
LAGER_QT_READER(QString, avatarMxcUri);
LAGER_QT_READER(QString, roomOrHeroAvatarMxcUri);
LAGER_QT_CURSOR(QString, localDraft);
LAGER_QT_READER(QStringList, memberNames);
Q_INVOKABLE MatrixRoomMember *memberAt(int index) const;
Q_INVOKABLE MatrixRoomMember *member(QString userId) const;
Q_INVOKABLE MatrixRoomTimeline *timeline() const;
Q_INVOKABLE void sendTextMessage(QString text) const;
Q_INVOKABLE void sendMediaFileMessage(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri) const;
Q_INVOKABLE void resendMessage(QString txnId) const;
Q_INVOKABLE MatrixPromise *redactEvent(QString eventId, QString reason) const;
Q_INVOKABLE MatrixRoomMemberListModel *typingUsers() const;
+ Q_INVOKABLE void setTyping(bool typing);
+
protected:
nlohmann::json makeMediaFileMessageJson(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri) const;
+
+private Q_SLOTS:
+ void setTypingImpl();
+
+private:
+ std::function<void()> m_setTypingThrottled;
};
diff --git a/src/qfunctionutils.hpp b/src/qfunctionutils.hpp
new file mode 100644
index 0000000..d5771cf
--- /dev/null
+++ b/src/qfunctionutils.hpp
@@ -0,0 +1,151 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2019 juangburgos <https://github.com/juangburgos/>
+ * SPDX-License-Identifier: MIT
+ */
+
+/// https://github.com/juangburgos/QFunctionUtils/blob/master/src/qfunctionutils.h
+
+#pragma once
+
+#include <libkazv-config.hpp>
+
+#include <functional>
+#include <QSharedPointer>
+#include <QTimer>
+
+namespace QFunctionUtils
+{
+
+// function used to cache user arguments to be able to use them once timer times out
+template<typename R, class ...Args>
+void debounce_args_cache(const std::function<R(Args(...args))> &funcFinish, Args(&...args))
+{
+ funcFinish(args...);
+}
+
+typedef QSharedPointer<QMetaObject::Connection> QMetaConnPtr;
+// debounce implementation
+// gets a user defined function as an argument and a 'delay' in miliseconds
+// returns a new function that the user can call many times, but will only execute the last
+// call after 'delay' miliseconds have passed
+template<typename R, class ...Args>
+std::function<R(Args(...args))> debounce_internal(const std::function<R(Args(...args))> &callback, const int &delay)
+{
+ QTimer * timer = new QTimer;
+ QMetaConnPtr conn = QMetaConnPtr(new QMetaObject::Connection,
+ [timer](QMetaObject::Connection * pConn) mutable {
+ // connection will be deleted when returned function goes out of scope, but not the timer
+ // so we need to delete it manually
+ delete pConn;
+ delete timer;
+ });
+ timer->setInterval(delay);
+ // return function that the user can call
+ return [callback, timer, conn](Args(...args)) {
+ // disconnect old timer callback, stop timer
+ QObject::disconnect(*conn);
+ timer->stop();
+ // create function that caches user arguments
+ std::function<void(std::function<R(Args(...args))>)> funcCache = std::bind(debounce_args_cache<R, Args...>, std::placeholders::_1, args...);
+ // connect new timer callback, restart timer
+ *conn = QObject::connect(timer, &QTimer::timeout,
+ [callback, timer, funcCache]() {
+ // stop timer
+ timer->stop();
+ // call after delay
+ funcCache(callback);
+ });
+ timer->start();
+ // return default constructed return type (just to support any return type)
+ return R();
+ };
+}
+
+// throttle implementation
+// same as debounce, but also calls the given function the first time is called
+template<typename R, class ...Args>
+std::function<R(Args(...args))> throttle_internal(const std::function<R(Args(...args))> &callback, const int &delay)
+{
+ QTimer * timer = new QTimer;
+ QMetaConnPtr conn = QMetaConnPtr(new QMetaObject::Connection,
+ [timer](QMetaObject::Connection * pConn) mutable {
+ // same logic as in debounce_internal
+ delete pConn;
+ delete timer;
+ });
+ timer->setInterval(delay);
+ // return function that the user can call
+ return [callback, timer, conn](Args(...args)) {
+ // disconnect old timer callback
+ QObject::disconnect(*conn);
+ // check timer (throttle) state
+ if (!timer->isActive())
+ {
+ // connect new timer callback
+ *conn = QObject::connect(timer, &QTimer::timeout,
+ [timer]() {
+ // stop timer only (this happens if user did not call the function during the delay)
+ timer->stop();
+ });
+ // start timer to start throttle
+ timer->start();
+ // call inmediatly and return
+ return callback(args...);
+ }
+ // create function that caches user arguments
+ std::function<void(std::function<R(Args(...args))>)> funcCache = std::bind(debounce_args_cache<R, Args...>, std::placeholders::_1, args...);
+ // connect new timer callback
+ *conn = QObject::connect(timer, &QTimer::timeout,
+ [callback, timer, funcCache]() {
+ // stop timer
+ timer->stop();
+ // call after delay
+ funcCache(callback);
+ });
+ // return default constructed return type (just to support any return type)
+ return R();
+ };
+}
+
+// for any class implementing operator() (e.g. lambdas)
+template<typename T> struct functor_traits : functor_traits<decltype(&T::operator())>
+{ };
+// specialization - lambda
+template<typename C, typename R, typename... Args>
+struct functor_traits<R(C::*)(Args...) const>
+{
+ using lambda_type = std::function<R(Args...)>;
+};
+// specialization - lambda mutable
+template<typename C, typename R, typename... Args>
+struct functor_traits<R(C::*)(Args...)>
+{
+ using lambda_type = std::function<R(Args...)>;
+};
+// specialization - function pointer
+template<class R, class... Args>
+struct functor_traits<R(*)(Args...)>
+{
+ using lambda_type = std::function<R(Args...)>;
+};
+// wrap any of the specializations above into an std::function
+template<typename T>
+auto make_std_function(const T& fn) -> typename functor_traits<T>::lambda_type
+{
+ return typename functor_traits<T>::lambda_type(fn);
+}
+// convert callback into std::function and call internal debounce
+template<typename T>
+auto Debounce(T callback, const int &delay) -> typename functor_traits<T>::lambda_type
+{
+ return debounce_internal(make_std_function(callback), delay);
+}
+// convert callback into std::function and call internal throttle
+template<typename T>
+auto Throttle(T callback, const int &delay) -> typename functor_traits<T>::lambda_type
+{
+ return throttle_internal(make_std_function(callback), delay);
+}
+
+}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 10:14 AM (2 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804433
Default Alt Text
(17 KB)

Event Timeline