Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85833201
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 2, 10:14 AM (3 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804433
Default Alt Text
(17 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline