Compare commits

..

2 Commits

Author SHA1 Message Date
Tobias Fella
91c6bd2ca6 Test 2024-08-31 16:24:05 +02:00
Tobias Fella
944e7ffa32 Adapt to vodozemac 2024-08-25 15:10:58 +02:00
111 changed files with 17371 additions and 22420 deletions

View File

@@ -8,6 +8,15 @@
"tags": [
"nightly"
],
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin",
"build-args" : [
"--share=network"
]
},
"desktop-file-name-suffix": " (Nightly)",
"finish-args": [
"--share=network",
@@ -40,23 +49,6 @@
}
]
},
{
"name": "olm",
"buildsystem": "cmake-ninja",
"config-opts": [ "-DOLM_TESTS=OFF" ],
"sources": [
{
"type": "git",
"url": "https://gitlab.matrix.org/matrix-org/olm.git",
"tag": "3.2.10",
"x-checker-data": {
"type": "git",
"tag-pattern": "^([\\d.]+)$"
},
"commit": "9908862979147a71dc6abaecd521be526ae77be1"
}
]
},
{
"name": "libsecret",
"buildsystem": "meson",
@@ -103,6 +95,31 @@
"-DBUILD_TRANSLATIONS=NO"
]
},
{
"name": "corrosion-rs",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/corrosion-rs/corrosion.git",
"branch": "master"
}
]
},
{
"name": "vodozemac-cpp",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/tobiasfella/vodozemac-cpp.git",
"branch": "main"
}
],
"config-opts": [
"-DBUILD_SHARED_LIBS=false"
]
},
{
"name": "libQuotient",
"buildsystem": "cmake-ninja",
@@ -110,7 +127,7 @@
{
"type": "git",
"url": "https://github.com/quotient-im/libQuotient.git",
"branch": "0.8.x",
"branch": "tobias/vodozemac",
"disable-submodules": true
}
],

View File

@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.6")
set(KF_MIN_VERSION "6.4")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -64,6 +64,8 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
link_directories("/home/user/CraftRoot/lib")
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
@@ -105,7 +107,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.8.2)
find_package(QuotientQt6 0.9)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
@@ -113,11 +115,6 @@ set_package_properties(QuotientQt6 PROPERTIES
PURPOSE "Talk with matrix server"
)
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
find_package(cmark)
set_package_properties(cmark PROPERTIES
TYPE REQUIRED

View File

@@ -107,13 +107,8 @@ void ChatBarCacheTest::reply()
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, QString(QLatin1String("$153456789:example.org")));
});
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));

View File

@@ -46,7 +46,6 @@ private Q_SLOTS:
void sendCustomEmojiCode_data();
void sendCustomEmojiCode();
void receiveSpacelessSelfClosingTag();
void receiveStripReply();
void receivePlainTextIn();
@@ -253,19 +252,6 @@ void TextHandlerTest::sendCustomEmojiCode()
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveSpacelessSelfClosingTag()
{
const QString testInputString = QStringLiteral("Test...<br/>...ing");
const QString testRichOutputString = QStringLiteral("Test...<br/>...ing");
const QString testPlainOutputString = QStringLiteral("Test...\n...ing");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString = QStringLiteral(
@@ -463,9 +449,6 @@ void TextHandlerTest::receiveRichPlainUrl()
QString testOutputStringMxId = QStringLiteral(
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
QString testInputStringMxIdWithPrefix = QStringLiteral("a @user:kde.org b");
QString testOutputStringMxIdWithPrefix = QStringLiteral("a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringLink1);
@@ -479,9 +462,6 @@ void TextHandlerTest::receiveRichPlainUrl()
testTextHandler.setData(testInputStringMxId);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
testTextHandler.setData(testInputStringMxIdWithPrefix);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix);
}
void TextHandlerTest::receiveRichEdited_data()
@@ -556,7 +536,7 @@ void TextHandlerTest::componentOutput_data()
"someField }\nCustomQml {\n someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you "
"defined the id")
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like<br/>"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like"), {}},
MessageComponent{
MessageComponentType::Code,
QStringLiteral(

View File

@@ -17,6 +17,7 @@ class WindowControllerTest : public QObject
private Q_SLOTS:
void nullWindow();
void geometry();
void showAndRaise();
void toggle();
@@ -29,10 +30,32 @@ void WindowControllerTest::nullWindow()
auto &instance = WindowController::instance();
QCOMPARE(instance.window(), nullptr);
instance.restoreGeometry();
instance.saveGeometry();
instance.showAndRaiseWindow({});
instance.toggleWindow();
}
void WindowControllerTest::geometry()
{
auto &instance = WindowController::instance();
QWindow window;
window.setGeometry(0, 0, 200, 200);
instance.setWindow(&window);
QCOMPARE(instance.window(), &window);
instance.saveGeometry();
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
QCOMPARE(KWindowConfig::hasSavedWindowSize(windowGroup), true);
window.setGeometry(0, 0, 400, 400);
QCOMPARE(window.geometry(), QRect(0, 0, 400, 400));
instance.restoreGeometry();
QCOMPARE(window.geometry(), QRect(0, 0, 200, 200));
}
void WindowControllerTest::showAndRaise()
{
auto &instance = WindowController::instance();

View File

@@ -429,7 +429,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.08.1" date="2024-09-12"/>
<release version="24.08.0" date="2024-08-22"/>
<release version="24.05.2" date="2024-07-04"/>
<release version="24.05.1" date="2024-06-13"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -243,7 +243,10 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/RoomSearchPage.qml
qml/LocationChooser.qml
qml/TimelineView.qml
@@ -288,7 +291,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml
qml/ReasonDialog.qml
DEPENDENCIES
QtCore
QtQuick

View File

@@ -139,8 +139,8 @@ void ChatBarCache::setThreadId(const QString &threadId)
if (m_threadId == threadId) {
return;
}
const auto oldThreadId = std::exchange(m_threadId, threadId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
m_threadId = threadId;
Q_EMIT threadIdChanged();
}
QString ChatBarCache::attachmentPath() const
@@ -163,10 +163,10 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
void ChatBarCache::clearRelations()
{
const auto oldEventId = std::exchange(m_relationId, QString());
const auto oldThreadId = std::exchange(m_threadId, QString());
m_threadId = QString();
m_attachmentPath = QString();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
Q_EMIT threadIdChanged();
Q_EMIT attachmentPathChanged();
}

View File

@@ -195,7 +195,7 @@ public:
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
void threadIdChanged(const QString &oldThreadId, const QString &newThreadId);
void threadIdChanged();
void attachmentPathChanged();
private:

View File

@@ -26,9 +26,23 @@ void ColorSchemer::apply(int idx)
c->activateScheme(c->model()->index(idx, 0));
}
int ColorSchemer::indexForCurrentScheme()
void ColorSchemer::apply(const QString &name)
{
return c->indexForSchemeId(c->activeSchemeId()).row();
c->activateScheme(c->indexForScheme(name));
}
int ColorSchemer::indexForScheme(const QString &name) const
{
auto index = c->indexForScheme(name).row();
if (index == -1) {
index = 0;
}
return index;
}
QString ColorSchemer::nameForIndex(int index) const
{
return c->model()->data(c->model()->index(index, 0), Qt::DisplayRole).toString();
}
#include "moc_colorschemer.cpp"

View File

@@ -44,11 +44,21 @@ public:
Q_INVOKABLE void apply(int idx);
/**
* @brief Get the row for the current color scheme.
* @brief Activates the KColorScheme with the given name.
*
* @sa KColorScheme
*/
Q_INVOKABLE int indexForCurrentScheme();
Q_INVOKABLE void apply(const QString &name);
/**
* @brief Returns the index for the scheme with the given name.
*/
Q_INVOKABLE int indexForScheme(const QString &name) const;
/**
* @brief Returns the name for the scheme with the given index.
*/
Q_INVOKABLE QString nameForIndex(int index) const;
private:
KColorSchemeManager *c;

View File

@@ -439,14 +439,4 @@ void Controller::revertToDefaultConfig()
config->save();
}
bool Controller::isImageShown(const QString &eventId)
{
return m_shownImages.contains(eventId);
}
void Controller::markImageShown(const QString &eventId)
{
m_shownImages.append(eventId);
}
#include "moc_controller.cpp"

View File

@@ -116,9 +116,6 @@ public:
*/
Q_INVOKABLE void revertToDefaultConfig();
Q_INVOKABLE bool isImageShown(const QString &eventId);
Q_INVOKABLE void markImageShown(const QString &eventId);
private:
explicit Controller(QObject *parent = nullptr);
@@ -131,7 +128,6 @@ private:
QStringList m_accountsLoading;
QMap<QString, QPointer<NeoChatConnection>> m_connectionsLoading;
QString m_endpoint;
QStringList m_shownImages;
private Q_SLOTS:
void invokeLogin();

View File

@@ -50,7 +50,7 @@ public:
Reply, /**< A component to show a replied-to message. */
LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */
ChatBar, /**< A text edit for editing a message. */
Edit, /**< A text edit for editing a message. */
Verification, /**< A user verification session start message. */
Loading, /**< The component is loading. */
Other, /**< Anything that cannot be classified as another type. */

View File

@@ -154,8 +154,7 @@ bool EventHandler::isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return true;
} else if (roomMemberEvent->isRename() && roomMemberEvent->prevContent() && roomMemberEvent->prevContent()->membership == roomMemberEvent->membership()
&& !NeoChatConfig::self()->showRename()) {
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
@@ -367,13 +366,9 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
if (e.senderId() == e.userId()) {
return i18n("left the room");
}
if (const auto &reason = e.contentJson()["reason"_ls].toString().toHtmlEscaped(); !reason.isEmpty()) {
return i18n("has put %1 out of the room: %2", subjectName, reason);
}
return i18n("has put %1 out of the room", subjectName);
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {

View File

@@ -40,7 +40,7 @@ float LocationHelper::zoomToFit(const QRectF &r, float mapWidth, float mapHeight
const auto zy = std::log2((mapHeight / (p2.y() - p1.y())));
const auto z = std::min(zx, zy);
return z;
return std::clamp(z, 5.0, 18.0);
}
#include "moc_locationhelper.cpp"

View File

@@ -11,7 +11,7 @@ import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
import org.kde.neochat.settings
Kirigami.Page {
FormCard.FormCardPage {
id: root
property bool showExisting: false
@@ -23,237 +23,202 @@ Kirigami.Page {
signal connectionChosen
title: i18n("Welcome")
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
header: QQC2.Control {
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
contentItem: ColumnLayout {
spacing: 0
Kirigami.Icon {
source: "org.kde.neochat"
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
implicitHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
}
Kirigami.Separator {
Layout.fillWidth: true
Kirigami.Heading {
id: welcomeMessage
text: i18n("Welcome to NeoChat")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormHeader {
id: existingAccountsHeader
title: i18nc("@title", "Continue with an existing account")
visible: (loadedAccounts.count > 0 || loadingAccounts.count > 0) && root._showExisting
}
FormCard.FormCard {
visible: existingAccountsHeader.visible
Repeater {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
text: model.userId
onClicked: {
Controller.activeConnection = model.connection;
root.connectionChosen();
}
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
background: null
contentItem: RowLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
color: Kirigami.Theme.disabledTextColor
Accessible.ignored: true // base class sets this text on root already
}
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "edit-delete-remove"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
enabled: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
}
FormCard.FormArrow {
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
direction: Qt.RightArrow
visible: root.background.visible
}
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
}
}
}
contentItem: Item {
ColumnLayout {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
}
spacing: 0
FormCard.FormCard {
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
Kirigami.Icon {
source: "org.kde.neochat"
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
implicitHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
}
Connections {
id: stepConnections
target: currentStep
Kirigami.Heading {
id: welcomeMessage
text: i18n("NeoChat")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormHeader {
id: existingAccountsHeader
title: i18nc("@title", "Continue with an existing account")
visible: (loadedAccounts.count > 0 || loadingAccounts.count > 0) && root._showExisting
maximumWidth: Kirigami.Units.gridUnit * 20
}
FormCard.FormCard {
visible: existingAccountsHeader.visible
maximumWidth: Kirigami.Units.gridUnit * 20
Repeater {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
text: model.userId
onClicked: {
Controller.activeConnection = model.connection;
root.connectionChosen();
}
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
background: null
contentItem: RowLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
color: Kirigami.Theme.disabledTextColor
Accessible.ignored: true // base class sets this text on root already
}
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "im-kick-user"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
enabled: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
}
FormCard.FormArrow {
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
direction: Qt.RightArrow
visible: root.background.visible
}
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
maximumWidth: Kirigami.Units.gridUnit * 20
}
FormCard.FormCard {
maximumWidth: Kirigami.Units.gridUnit * 20
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
Connections {
id: stepConnections
target: currentStep
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
} else {
continueButton.forceActiveFocus();
}
}
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
function onCloseDialog(): void {
root.closeDialog();
}
}
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading");
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
}
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
} else {
continueButton.forceActiveFocus();
}
}
FormCard.FormDelegateSeparator {
below: continueButton
visible: root.currentStep.nextAction
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
function onCloseDialog(): void {
root.closeDialog();
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing * 2
maximumWidth: Kirigami.Units.gridUnit * 20
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Settings")
icon.name: "settings-configure"
onClicked: NeoChatSettingsView.open()
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading");
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
}
}
}
FormCard.FormDelegateSeparator {
below: continueButton
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Open proxy settings")
icon.name: "settings-configure"
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "NetworkProxyPage"), {}, {
title: i18nc("@title:window", "Proxy Settings")
});
}
}

View File

@@ -185,6 +185,9 @@ int main(int argc, char *argv[])
#endif
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
@@ -304,6 +307,7 @@ int main(int argc, char *argv[])
QWindow *window = windowFromEngine(&engine);
WindowController::instance().setWindow(window);
WindowController::instance().restoreGeometry();
return app.exec();
}

View File

@@ -211,7 +211,7 @@ QList<ActionsModel::Action> actions{
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("You are already in this room."));
return QString();
}
if (room->joinedMemberIds().contains(text)) {
if (room->members().contains(room->member(text))) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
return QString();
}

View File

@@ -84,7 +84,7 @@ void MessageContentModel::initializeModel()
});
if (m_event == nullptr) {
intiializeEvent(m_room->getEvent(m_eventId));
m_room->getEvent(m_eventId);
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
}
@@ -140,13 +140,6 @@ void MessageContentModel::initializeModel()
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (m_event != nullptr && (oldThreadId == m_eventId || newThreadId == m_eventId)) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
@@ -189,13 +182,14 @@ void MessageContentModel::intiializeEvent(const QString &eventId)
void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
return;
}
m_event = loadEvent<RoomEvent>(event->fullJson());
// a pending event may not previously have had an event ID so update.
m_eventId = EventHandler::id(m_event.get());
if (m_eventId.isEmpty()) {
m_eventId = m_event->id();
if (m_eventId.isEmpty()) {
m_eventId = m_event->transactionId();
}
}
auto senderId = m_event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
@@ -221,7 +215,7 @@ void MessageContentModel::setShowAuthor(bool showAuthor)
}
m_showAuthor = showAuthor;
if (m_event != nullptr && m_room->connection()->isIgnored(m_event->senderId())) {
if (m_event != nullptr && m_room->connection()->ignoredUsers().contains(m_event->senderId())) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
@@ -251,7 +245,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
const auto component = m_components[index.row()];
if (role == DisplayRole) {
if (m_notFound || (m_event && m_room->connection()->isIgnored(m_event->senderId()))) {
if (m_notFound || (m_event && m_room->connection()->ignoredUsers().contains(m_event->senderId()))) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
@@ -347,12 +341,6 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == ChatBarCacheRole) {
if (m_room->threadCache()->threadId() == m_eventId) {
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
}
return {};
}
@@ -384,7 +372,6 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roles;
}
@@ -393,7 +380,7 @@ void MessageContentModel::resetModel()
beginResetModel();
m_components.clear();
if ((m_event && m_room->connection()->isIgnored(m_event->senderId())) || m_notFound) {
if ((m_event && m_room->connection()->ignoredUsers().contains(m_event->senderId())) || m_notFound) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
@@ -413,7 +400,7 @@ void MessageContentModel::resetModel()
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
void MessageContentModel::resetContent(bool isEditing)
{
Q_ASSERT(m_event != nullptr);
@@ -422,7 +409,7 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing, isThreading);
const auto newComponents = messageContentComponents(isEditing);
if (newComponents.size() == 0) {
return;
}
@@ -431,7 +418,7 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
{
QList<MessageComponent> newComponents;
@@ -451,7 +438,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
} else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*m_event.get())));
}
@@ -460,11 +447,6 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
newComponents = addLinkPreviews(newComponents);
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
if (isThreading && !EventHandler::isThreaded(m_event.get())) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
return newComponents;
}

View File

@@ -70,7 +70,6 @@ public:
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
LinkPreviewerRole, /**< The link preview details. */
ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
@@ -134,8 +133,8 @@ private:
QList<MessageComponent> m_components;
void resetModel();
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();

View File

@@ -2,40 +2,20 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "permissionsmodel.h"
#include "powerlevel.h"
#include <Quotient/events/roompowerlevelsevent.h>
#include <KLazyLocalizedString>
#include "powerlevel.h"
using namespace Qt::Literals::StringLiterals;
namespace
{
constexpr auto UsersDefaultKey = "users_default"_L1;
constexpr auto StateDefaultKey = "state_default"_L1;
constexpr auto EventsDefaultKey = "events_default"_L1;
constexpr auto InviteKey = "invite"_L1;
constexpr auto KickKey = "kick"_L1;
constexpr auto BanKey = "ban"_L1;
constexpr auto RedactKey = "redact"_L1;
static const QStringList defaultPermissions = {
UsersDefaultKey,
StateDefaultKey,
EventsDefaultKey,
};
static const QStringList basicPermissions = {
InviteKey,
KickKey,
BanKey,
RedactKey,
};
static const QStringList knownPermissions = {
QStringLiteral("users_default"),
QStringLiteral("state_default"),
QStringLiteral("events_default"),
QStringLiteral("invite"),
QStringLiteral("kick"),
QStringLiteral("ban"),
QStringLiteral("redact"),
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
QStringLiteral("m.room.power_levels"),
@@ -53,14 +33,14 @@ static const QStringList knownPermissions = {
};
// Alternate name text for default permissions.
static const QHash<QString, KLazyLocalizedString> permissionNames = {
{UsersDefaultKey, kli18nc("Room permission type", "Default user power level")},
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
{EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")},
{InviteKey, kli18nc("Room permission type", "Invite users")},
{KickKey, kli18nc("Room permission type", "Kick users")},
{BanKey, kli18nc("Room permission type", "Ban users")},
{RedactKey, kli18nc("Room permission type", "Remove messages sent by other users")},
static const QHash<QString, KLazyLocalizedString> defaultPermissionNames = {
{QStringLiteral("users_default"), kli18nc("Room permission type", "Default user power level")},
{QStringLiteral("state_default"), kli18nc("Room permission type", "Default power level to set the room state")},
{QStringLiteral("events_default"), kli18nc("Room permission type", "Default power level to send messages")},
{QStringLiteral("invite"), kli18nc("Room permission type", "Invite users")},
{QStringLiteral("kick"), kli18nc("Room permission type", "Kick users")},
{QStringLiteral("ban"), kli18nc("Room permission type", "Ban users")},
{QStringLiteral("redact"), kli18nc("Room permission type", "Remove messages sent by other users")},
{QStringLiteral("m.reaction"), kli18nc("Room permission type", "Send reactions")},
{QStringLiteral("m.room.redaction"), kli18nc("Room permission type", "Remove their own messages")},
{QStringLiteral("m.room.power_levels"), kli18nc("Room permission type", "Change user permissions")},
@@ -78,10 +58,10 @@ static const QHash<QString, KLazyLocalizedString> permissionNames = {
};
// Subtitles for the default values.
static const QHash<QString, KLazyLocalizedString> permissionSubtitles = {
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
static const QHash<QString, KLazyLocalizedString> defaultSubtitles = {
{QStringLiteral("users_default"), kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{QStringLiteral("state_default"), kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{QStringLiteral("events_default"), kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
};
// Permissions that should use the event default.
@@ -90,7 +70,6 @@ static const QStringList eventPermissions = {
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
};
};
PermissionsModel::PermissionsModel(QObject *parent)
: QAbstractListModel(parent)
@@ -130,8 +109,6 @@ void PermissionsModel::initializeModel()
}
m_permissions.append(defaultPermissions);
m_permissions.append(basicPermissions);
m_permissions.append(knownPermissions);
for (const auto &event : currentPowerLevelEvent->events().keys()) {
if (!m_permissions.contains(event)) {
@@ -154,17 +131,17 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
const auto permission = m_permissions.value(index.row());
if (role == NameRole) {
if (permissionNames.keys().contains(permission)) {
return permissionNames.value(permission).toString();
if (defaultPermissionNames.keys().contains(permission)) {
return defaultPermissionNames.value(permission).toString();
}
return permission;
}
if (role == SubtitleRole) {
if (knownPermissions.contains(permission) && permissionNames.keys().contains(permission)) {
if (permission.startsWith(QLatin1String("m.")) && defaultPermissionNames.keys().contains(permission)) {
return permission;
}
if (permissionSubtitles.contains(permission)) {
return permissionSubtitles.value(permission).toString();
if (defaultSubtitles.contains(permission)) {
return defaultSubtitles.value(permission).toString();
}
return QString();
}
@@ -189,10 +166,11 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
return QString();
}
if (role == IsDefaultValueRole) {
return defaultPermissions.contains(permission);
return permission.contains(QLatin1String("default"));
}
if (role == IsBasicPermissionRole) {
return basicPermissions.contains(permission);
return permission == QStringLiteral("invite") || permission == QStringLiteral("kick") || permission == QStringLiteral("ban")
|| permission == QStringLiteral("redact");
}
return {};
}
@@ -223,19 +201,19 @@ std::optional<int> PermissionsModel::powerLevel(const QString &permission) const
}
if (const auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>()) {
if (permission == BanKey) {
if (permission == QStringLiteral("ban")) {
return currentPowerLevelEvent->ban();
} else if (permission == KickKey) {
} else if (permission == QStringLiteral("kick")) {
return currentPowerLevelEvent->kick();
} else if (permission == InviteKey) {
} else if (permission == QStringLiteral("invite")) {
return currentPowerLevelEvent->invite();
} else if (permission == RedactKey) {
} else if (permission == QStringLiteral("redact")) {
return currentPowerLevelEvent->redact();
} else if (permission == UsersDefaultKey) {
} else if (permission == QStringLiteral("users_default")) {
return currentPowerLevelEvent->usersDefault();
} else if (permission == StateDefaultKey) {
} else if (permission == QStringLiteral("state_default")) {
return currentPowerLevelEvent->stateDefault();
} else if (permission == EventsDefaultKey) {
} else if (permission == QStringLiteral("events_default")) {
return currentPowerLevelEvent->eventsDefault();
} else if (eventPermissions.contains(permission)) {
return currentPowerLevelEvent->powerLevelForEvent(permission);
@@ -263,9 +241,6 @@ void PermissionsModel::setPowerLevel(const QString &permission, const int &newPo
auto powerLevelContent = currentPowerLevelEvent->contentJson();
if (powerLevelContent.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
// Deal with the case where a default or basic permission is missing from the event content erroneously.
} else if (defaultPermissions.contains(permission) || basicPermissions.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
} else {
auto eventPowerLevels = powerLevelContent[QLatin1String("events")].toObject();
eventPowerLevels[permission] = clampPowerLevel;

View File

@@ -2,10 +2,15 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "reactionmodel.h"
#include "utils.h"
#include <QDebug>
#include <QFont>
#ifdef HAVE_ICU
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#endif
#include <KLocalizedString>
@@ -149,6 +154,30 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
};
}
bool isEmoji(const QString &text)
{
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
}
QString ReactionModel::reactionText(QString text) const
{
text = text.toHtmlEscaped();
@@ -162,7 +191,7 @@ QString ReactionModel::reactionText(QString text) const
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
}
return Utils::isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}
#include "moc_reactionmodel.cpp"

View File

@@ -9,15 +9,12 @@
#include <Quotient/omittable.h>
#include <memory>
#include "chatbarcache.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "neochatroom.h"
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
: QConcatenateTablesProxyModel(room)
, m_threadRootId(threadRootId)
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
{
Q_ASSERT(!m_threadRootId.isEmpty());
Q_ASSERT(room);
@@ -28,6 +25,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(event);
clearModels();
addModels();
}
}
@@ -40,6 +38,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
}
}
}
clearModels();
addModels();
});
@@ -47,11 +46,6 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
addModels();
}
QString ThreadModel::threadRootId() const
{
return m_threadRootId;
}
MessageContentModel *ThreadModel::threadRootContentModel() const
{
return m_threadRootContentModel.get();
@@ -83,6 +77,7 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
m_contentModels.push_back(new MessageContentModel(room, event.get()));
}
clearModels();
addModels();
const auto newNextBatch = m_currentJob->nextBatch();
@@ -108,15 +103,10 @@ void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
void ThreadModel::addModels()
{
if (!sourceModels().isEmpty()) {
clearModels();
}
addSourceModel(m_threadRootContentModel.get());
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
addSourceModel(*it);
}
addSourceModel(m_threadChatBarModel);
beginResetModel();
endResetModel();
@@ -130,61 +120,6 @@ void ThreadModel::clearModels()
removeSourceModel(model);
}
}
removeSourceModel(m_threadChatBarModel);
}
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
: QAbstractListModel(parent)
, m_room(room)
{
if (m_room != nullptr) {
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel != nullptr && (oldThreadId == threadModel->threadRootId() || newThreadId == threadModel->threadRootId())) {
beginResetModel();
endResetModel();
}
});
}
}
QVariant ThreadChatBarModel::data(const QModelIndex &idx, int role) const
{
if (idx.row() > 1) {
return {};
}
if (role == ComponentTypeRole) {
return MessageComponentType::ChatBar;
}
if (role == ChatBarCacheRole) {
if (m_room == nullptr) {
return {};
}
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return {};
}
int ThreadChatBarModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (m_room == nullptr) {
return 0;
}
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel != nullptr) {
return m_room->threadCache()->threadId() == threadModel->threadRootId() ? 1 : 0;
}
return 0;
}
QHash<int, QByteArray> ThreadChatBarModel::roleNames() const
{
return {
{ComponentTypeRole, "componentType"},
{ChatBarCacheRole, "chatBarCache"},
};
}
#include "moc_threadmodel.cpp"

View File

@@ -21,56 +21,6 @@
class NeoChatRoom;
class ReactionModel;
/**
* @class ThreadChatBarModel
*
* A model to provide a chat bar component to send new messages in a thread.
*/
class ThreadChatBarModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*
* The role values need to match MessageContentModel not to blow up.
*
* @sa MessageContentModel
*/
enum Roles {
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
ChatBarCacheRole = MessageContentModel::ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit ThreadChatBarModel(QObject *parent, NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1 or 0, depending on whether a chat bar should be shown.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with ComponentTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
QPointer<NeoChatRoom> m_room;
};
/**
* @class ThreadModel
*
@@ -88,8 +38,6 @@ class ThreadModel : public QConcatenateTablesProxyModel
public:
explicit ThreadModel(const QString &threadRootId, NeoChatRoom *room);
QString threadRootId() const;
/**
* @brief The content model for the thread root event.
*/
@@ -122,7 +70,6 @@ private:
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
std::deque<MessageContentModel *> m_contentModels;
ThreadChatBarModel *m_threadChatBarModel;
QList<QString> m_events;
QList<QString> m_pendingEvents;

View File

@@ -62,7 +62,7 @@ void TimelineBeginningModel::setRoom(NeoChatRoom *room)
if (m_room != nullptr) {
Quotient::connectUntil(m_room.get(), &Quotient::Room::eventsHistoryJobChanged, this, [this]() {
if (m_room && m_room->allHistoryLoaded()) {
if (m_room->allHistoryLoaded()) {
// HACK: We have to do it this way because DelegateChooser doesn't update dynamically.
beginRemoveRows({}, 0, 0);
endRemoveRows();

View File

@@ -125,7 +125,7 @@ void WebShortcutModel::trigger(const QString &data)
void WebShortcutModel::configureWebShortcuts()
{
#ifdef HAVE_KIO
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), QStringList() << QStringLiteral("webshortcuts"), this);
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"), this);
job->exec();
#endif
}

View File

@@ -15,6 +15,9 @@
<entry name="OpenRoom" type="String">
<label>Latest opened room</label>
</entry>
<entry name="ColorScheme" type="String">
<label>Color scheme</label>
</entry>
<entry name="Blur" type="bool">
<label>Make NeoChat blurry</label>
<default>false</default>
@@ -86,10 +89,6 @@
<label>Show deleted messages in the timeline</label>
<default>false</default>
</entry>
<entry name="HideImages" type="bool">
<label>Hide images in the timeline</label>
<default>false</default>
</entry>
<entry name="ShowLinkPreview" type="bool">
<label>Show preview of the links in the chat messages</label>
</entry>

View File

@@ -9,6 +9,7 @@
#include "controller.h"
#include "jobs/neochatchangepasswordjob.h"
#include "jobs/neochatdeactivateaccountjob.h"
#include "linkpreviewer.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
@@ -44,7 +45,6 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
, m_threePIdModel(new ThreePIdModel(this))
{
m_linkPreviewers.setMaxCost(20);
connectSignals();
}
@@ -52,7 +52,6 @@ NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
, m_threePIdModel(new ThreePIdModel(this))
{
m_linkPreviewers.setMaxCost(20);
connectSignals();
}
@@ -564,13 +563,13 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
return nullptr;
}
auto previewer = m_linkPreviewers.object(link);
auto previewer = m_linkPreviewers.value(link, nullptr);
if (previewer != nullptr) {
return previewer;
}
previewer = new LinkPreviewer(link, this);
m_linkPreviewers.insert(link, previewer);
m_linkPreviewers[link] = previewer;
return previewer;
}

View File

@@ -3,7 +3,6 @@
#pragma once
#include <QCache>
#include <QObject>
#include <QQmlEngine>
@@ -14,9 +13,10 @@
#include <Quotient/keyimport.h>
#endif
#include "linkpreviewer.h"
#include "models/threepidmodel.h"
class LinkPreviewer;
class NeoChatConnection : public Quotient::Connection
{
Q_OBJECT
@@ -222,7 +222,7 @@ private:
int m_badgeNotificationCount = 0;
QCache<QUrl, LinkPreviewer> m_linkPreviewers;
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
bool m_canCheckMutualRooms = false;
};

View File

@@ -149,7 +149,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
if (NeoChatConfig::rejectUnknownInvites()) {
auto job = this->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
connect(job, &BaseJob::result, this, [this, job, showNotification] {
connect(job, &BaseJob::result, this, [this, job, roomMemberEvent, showNotification] {
QJsonObject replyData = job->jsonData();
if (replyData.contains(QStringLiteral("joined"))) {
const bool inAnyOfOurRooms = !replyData[QStringLiteral("joined")].toArray().isEmpty();

View File

@@ -4,34 +4,9 @@
{
"Name": "Tobias Fella",
"Name[ar]": "توبياس فلة",
"Name[ca@valencia]": "Tobias Fella",
"Name[ca]": "Tobias Fella",
"Name[cs]": "Tobias Fella",
"Name[de]": "Tobias Fella",
"Name[en_GB]": "Tobias Fella",
"Name[eo]": "Tobias Fella",
"Name[es]": "Tobias Fella",
"Name[eu]": "Tobias Fella",
"Name[fi]": "Tobias Fella",
"Name[fr]": "Tobias Fella",
"Name[gl]": "Tobias Fella",
"Name[he]": "טוביאס פלה",
"Name[hu]": "Tobias Fella",
"Name[ia]": "Tobias Fella",
"Name[it]": "Tobias Fella",
"Name[ka]": "Tobias Fella",
"Name[lv]": "Tobias Fella",
"Name[nl]": "Tobias Fella",
"Name[nn]": "Tobias Fella",
"Name[pl]": "Tobias Fella",
"Name[ru]": "Tobias Fella",
"Name[sl]": "Tobias Fella",
"Name[sv]": "Tobias Fella",
"Name[ta]": "டோபியாஸ் ஃபெல்லா",
"Name[tr]": "Tobias Fella",
"Name[uk]": "Tobias Fella",
"Name[x-test]": "xxTobias Fellaxx",
"Name[zh_TW]": "Tobias Fella"
"Name[x-test]": "xxTobias Fellaxx"
}
],
"Category": "Utilities",
@@ -40,7 +15,6 @@
"Description[ca@valencia]": "Compartix a través de NeoChat",
"Description[ca]": "Comparteix a través del NeoChat",
"Description[de]": "Über NeoChat teilen",
"Description[en_GB]": "Share via NeoChat",
"Description[eo]": "Kundividi per NeoChat",
"Description[es]": "Compartir mediante NeoChat",
"Description[eu]": "Partekatu NeoChat bidez",
@@ -68,35 +42,9 @@
"License": "GPL",
"Name": "NeoChat",
"Name[ar]": "نيوتشات",
"Name[ast]": "NeoChat",
"Name[ca@valencia]": "NeoChat",
"Name[ca]": "NeoChat",
"Name[cs]": "NeoChat",
"Name[de]": "NeoChat",
"Name[en_GB]": "NeoChat",
"Name[eo]": "NeoChat",
"Name[es]": "NeoChat",
"Name[eu]": "NeoChat",
"Name[fi]": "NeoChat",
"Name[fr]": "NeoChat",
"Name[gl]": "NeoChat",
"Name[he]": "NeoChat",
"Name[hu]": "NeoChat",
"Name[ia]": "Neochat",
"Name[it]": "NeoChat",
"Name[ka]": "NeoChat",
"Name[lv]": "NeoChat",
"Name[nl]": "NeoChat",
"Name[nn]": "NeoChat",
"Name[pl]": "NeoChat",
"Name[ru]": "NeoChat",
"Name[sl]": "NeoChat",
"Name[sv]": "NeoChat",
"Name[ta]": "நியோச்சாட்",
"Name[tr]": "NeoChat",
"Name[uk]": "NeoChat",
"Name[x-test]": "xxNeoChatxx",
"Name[zh_TW]": "NeoChat",
"X-Purpose-ActionDisplay": "NeoChat"
},
"X-Purpose-PluginTypes": [

View File

@@ -86,7 +86,7 @@ QQC2.Menu {
}
QQC2.MenuItem {
text: i18n("Logout")
icon.name: "im-kick-user"
icon.name: "list-remove-user"
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.window.overlay).open()
}

View File

@@ -66,7 +66,7 @@ Kirigami.Dialog {
if (switchUserButton.checked) {
switchUserButton.checked = false;
}
root.close();
accountView.currentIndex = Controller.activeConnectionIndex;
}
Keys.onUpPressed: {
accountView.currentIndex = accountView.count - 1;

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
@@ -12,11 +12,10 @@ import org.kde.neochat
Kirigami.Page {
id: root
required property string placeholder
required property string actionText
required property string icon
property NeoChatRoom room
property string userId
signal accepted(reason: string)
title: i18n("Ban User")
leftPadding: 0
rightPadding: 0
@@ -25,7 +24,7 @@ Kirigami.Page {
QQC2.TextArea {
id: reason
placeholderText: root.placeholder
placeholderText: i18n("Reason for banning this user")
anchors.fill: parent
wrapMode: TextEdit.Wrap
@@ -41,11 +40,11 @@ Kirigami.Page {
Layout.fillWidth: true
}
QQC2.Button {
text: root.actionText
icon.name: root.icon
text: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban")
icon.name: "im-ban-user"
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.accepted(reason.text);
root.room.ban(root.userId, reason.text);
root.closeDialog();
}
}

View File

@@ -70,11 +70,6 @@ Loader {
*/
property list<Kirigami.Action> actions
/**
* @brief Whether the web search menu should be shown or not.
*/
property bool enableWebSearch: true
/**
* Some common actions shared between menus
*/
@@ -87,23 +82,16 @@ Loader {
component RemoveMessageAction: Kirigami.Action {
visible: author.isLocalMember || currentRoom.canSendState("redact")
text: i18nc("@action:button", "Remove")
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Message"),
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title:dialog", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
currentRoom.redactEvent(root.eventId, reason);
});
}
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: currentRoom,
eventId: eventId
}, {
title: i18nc("@title", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
})
}
component ReplyMessageAction: Kirigami.Action {
@@ -120,28 +108,13 @@ Loader {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: !author.isLocalMember
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report Message"),
placeholder: i18nc("@info:placeholder", "Reason for reporting this message"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
currentRoom.reportEvent(root.eventId, reason);
});
}
}
component ShowUserAction: Kirigami.Action {
text: i18nc("@action:inmenu", "Show User")
icon.name: "username-copy"
onTriggered: {
RoomManager.resolveResource(author.id)
}
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReportSheet'), {
room: currentRoom,
eventId: eventId
}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
}
Component {
@@ -155,7 +128,6 @@ Loader {
id: menuItem
visible: modelData.visible
title: modelData.text
icon: modelData.icon
Instantiator {
model: modelData.children
@@ -186,8 +158,7 @@ Loader {
QQC2.Menu {
id: webshortcutmenu
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
icon.name: "search-symbolic"
property bool isVisible: webshortcutmodel.enabled && root.enableWebSearch
property bool isVisible: webshortcutmodel.enabled
Component.onCompleted: {
webshortcutmenu.parent.visible = isVisible;
}

View File

@@ -32,9 +32,6 @@ DelegateContextMenu {
*/
required property var progressInfo
// Web search isn't useful for images
enableWebSearch: false
/**
* @brief The main list of menu item actions.
*
@@ -70,23 +67,15 @@ DelegateContextMenu {
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Message"),
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title:dialog", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
currentRoom.redactEvent(root.eventId, reason);
});
}
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: currentRoom,
eventId: eventId
}, {
title: i18nc("@title", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
})
},
DelegateContextMenu.ReportMessageAction {},
DelegateContextMenu.ShowUserAction {},
DelegateContextMenu.ViewSourceAction {}
]

View File

@@ -123,10 +123,9 @@ QQC2.Control {
text: i18n("Reply in Thread")
icon.name: "dialog-messages"
onTriggered: {
root.currentRoom.threadCache.replyId = "";
root.currentRoom.threadCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId;
root.currentRoom.mainCache.clearRelations();
root.currentRoom.editCache.clearRelations();
root.currentRoom.mainCache.replyId = "";
root.currentRoom.mainCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId;
root.currentRoom.editCache.editId = "";
root.focusChatBar();
}
}

View File

@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -13,77 +11,111 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
SearchPage {
Kirigami.ScrollablePage {
id: root
property NeoChatRoom room
title: i18nc("@title:dialog", "Invite a User")
title: i18n("Invite a User")
searchFieldPlaceholder: i18nc("@info:placeholder", "Find a user…")
noResultPlaceholderMessage: i18nc("@info:placeholder", "No users found")
actions: [
Kirigami.Action {
icon.name: "dialog-close"
text: i18nc("@action", "Cancel")
onTriggered: root.closeDialog()
}
]
header: RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
headerTrailing: QQC2.Button {
icon.name: "list-add"
display: QQC2.Button.IconOnly
enabled: root.model.searchText.match(/@(.+):(.+)/g) && !root.room.containsUser(root.model.searchText)
Kirigami.SearchField {
id: identifierField
property bool isUserId: text.match(/@(.+):(.+)/g)
Layout.fillWidth: true
text: i18nc("@action:button", "Invite this User")
placeholderText: i18n("Find a user...")
onAccepted: userDictListModel.search()
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: root.room.containsUser(root.model.searchText) ? i18nc("@info:tooltip", "User is either already a member or has been invited") : text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.Button {
visible: identifierField.isUserId
onClicked: root.room.inviteToRoom(root.model.searchText);
text: i18n("Add")
highlighted: true
onClicked: {
room.inviteToRoom(identifierField.text);
}
}
}
model: UserDirectoryListModel {
id: userDictListModel
ListView {
id: userDictListView
connection: root.room.connection
}
clip: true
modelDelegate: Delegates.RoundedItemDelegate {
id: delegate
model: UserDirectoryListModel {
id: userDictListModel
required property string userId
required property string displayName
required property url avatarUrl
connection: root.room.connection
searchText: identifierField.text
}
text: displayName
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
contentItem: RowLayout {
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: delegate.avatarUrl
name: delegate.displayName
}
visible: userDictListView.count < 1
Delegates.SubtitleContentItem {
itemDelegate: delegate
subtitle: delegate.userId
labelItem.textFormat: Text.PlainText
}
text: i18n("No users available")
}
QQC2.ToolButton {
id: inviteButton
delegate: Delegates.RoundedItemDelegate {
id: delegate
readonly property bool inRoom: root.room && root.room.containsUser(delegate.userId)
required property string userId
required property string displayName
required property url avatarUrl
icon.name: "document-send"
text: i18nc("@action:button", "Send invitation")
opacity: inRoom ? 0.5 : 1
enabled: !inRoom
property bool inRoom: room && room.containsUser(userId)
onClicked: {
inviteButton.enabled = false;
root.room.inviteToRoom(delegate.userId);
text: displayName
contentItem: RowLayout {
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: delegate.avatarUrl
name: delegate.displayName
}
QQC2.ToolTip.text: !inRoom ? text : i18nc("@info:tooltip", "User is either already a member or has been invited")
QQC2.ToolTip.visible: inviteButton.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Delegates.SubtitleContentItem {
itemDelegate: delegate
subtitle: delegate.userId
labelItem.textFormat: Text.PlainText
}
QQC2.ToolButton {
id: inviteButton
icon.name: "document-send"
text: i18n("Send invitation")
checkable: true
checked: inRoom
opacity: inRoom ? 0.5 : 1
onToggled: {
if (inRoom) {
checked = true;
} else {
room.inviteToRoom(delegate.userId);
applicationWindow().pageStack.layers.pop();
}
}
QQC2.ToolTip.text: !inRoom ? text : i18n("User is either already a member or has been invited")
QQC2.ToolTip.visible: inviteButton.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
}

View File

@@ -30,36 +30,9 @@ Components.AbstractMaximizeComponent {
root.close();
}
enabled: !!root.location
},
Kirigami.Action {
text: i18nc("@action:intoolbar Re-center the map onto the set location", "Re-Center")
icon.name: "snap-bounding-box-center-symbolic"
onTriggered: mapView.map.fitViewportToMapItems([mapView.locationMapItem])
enabled: root.location !== undefined
},
Kirigami.Action {
text: i18nc("@action:intoolbar Determine the device's location", "Locate")
icon.name: "mark-location-symbolic"
enabled: positionSource.valid
onTriggered: positionSource.update()
}
]
PositionSource {
id: positionSource
active: false
onPositionChanged: {
const coord = position.coordinate;
mapView.gpsMapItem.latitude = coord.latitude;
mapView.gpsMapItem.longitude = coord.longitude;
mapView.map.addMapItem(mapView.gpsMapItem);
mapView.map.fitViewportToMapItems([mapView.gpsMapItem])
}
}
content: MapView {
id: mapView
map.plugin: OsmLocationPlugin.plugin
@@ -81,15 +54,6 @@ Components.AbstractMaximizeComponent {
author: null
}
readonly property LocationMapItem gpsMapItem: LocationMapItem {
latitude: 0.0
longitude: 0.0
isLive: true
heading: NaN
asset: ""
author: null
}
Connections {
target: mapView.map
function onCopyrightLinkActivated(link: string) {

View File

@@ -27,10 +27,7 @@ Kirigami.Page {
let c = LocationHelper.center(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox));
return QtPositioning.coordinate(c.y, c.x);
}
map.zoomLevel: {
const zoom = LocationHelper.zoomToFit(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox), mapView.width, mapView.height)
return Math.min(Math.max(zoom, map.minimumZoomLevel), map.maximumZoomLevel);
}
map.zoomLevel: LocationHelper.zoomToFit(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox), mapView.width, mapView.height)
MapItemView {
Component.onCompleted: mapView.map.addMapItemView(this)

View File

@@ -6,7 +6,6 @@ import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.config as KConfig
import org.kde.neochat
import org.kde.neochat.login
@@ -69,8 +68,36 @@ Kirigami.ApplicationWindow {
}
}
KConfig.WindowStateSaver {
configGroupName: "Window"
// This timer allows to batch update the window size change to reduce
// the io load and also work around the fact that x/y/width/height are
// changed when loading the page and overwrite the saved geometry from
// the previous session.
Timer {
id: saveWindowGeometryTimer
interval: 1000
onTriggered: WindowController.saveGeometry()
}
Connections {
id: saveWindowGeometryConnections
enabled: false // Disable on startup to avoid writing wrong values if the window is hidden
target: root
function onClosing() {
WindowController.saveGeometry();
}
function onWidthChanged() {
saveWindowGeometryTimer.restart();
}
function onHeightChanged() {
saveWindowGeometryTimer.restart();
}
function onXChanged() {
saveWindowGeometryTimer.restart();
}
function onYChanged() {
saveWindowGeometryTimer.restart();
}
}
QuickSwitcher {
@@ -171,9 +198,11 @@ Kirigami.ApplicationWindow {
if (ShareHandler.text && root.connection) {
root.handleShare()
}
const hasSystemTray = Controller.supportSystemTray && NeoChatConfig.systemTray;
if (Kirigami.Settings.isMobile || !(hasSystemTray && NeoChatConfig.minimizeToSystemTrayOnStartup)) {
if (NeoChatConfig.minimizeToSystemTrayOnStartup && !Kirigami.Settings.isMobile && Controller.supportSystemTray && NeoChatConfig.systemTray) {
restoreWindowGeometryConnections.enabled = true; // To restore window size and position
} else {
visible = true;
saveWindowGeometryConnections.enabled = true;
}
}
Connections {
@@ -247,7 +276,22 @@ Kirigami.ApplicationWindow {
target: Controller
function onErrorOccured(error, detail) {
showPassiveNotification(detail.length > 0 ? i18n("%1: %2", error, detail) : error, "short");
showPassiveNotification(detail.length > 0 ? i18n("%1: %2", error, detail) : error);
}
}
Connections {
id: restoreWindowGeometryConnections
enabled: false
target: root
function onVisibleChanged() {
if (!visible) {
return;
}
Controller.restoreWindowGeometry(root);
restoreWindowGeometryConnections.enabled = false; // Only restore window geometry for the first time
saveWindowGeometryConnections.enabled = true;
}
}

View File

@@ -66,7 +66,13 @@ DelegateContextMenu {
onTriggered: Clipboard.saveText(root.selectedText.length > 0 ? root.selectedText : root.plainText)
},
DelegateContextMenu.ReportMessageAction {},
DelegateContextMenu.ShowUserAction {},
Kirigami.Action {
text: i18nc("@action:inmenu", "Show User")
icon.name: "username-copy"
onTriggered: {
RoomManager.resolveResource(author.id)
}
},
DelegateContextMenu.ViewSourceAction {},
Kirigami.Action {
text: i18n("Copy Link")

View File

@@ -33,7 +33,6 @@ Kirigami.Dialog {
spacing: Kirigami.Units.largeSpacing * 4
Avatar {
source: root.connection.makeMediaUrl(SpaceHierarchyCache.recommendedSpaceAvatar)
name: SpaceHierarchyCache.recommendedSpaceDisplayName
}
ColumnLayout {
Layout.fillWidth: true

63
src/qml/RemoveSheet.qml Normal file
View File

@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root
property NeoChatRoom room
property string eventId
property string userId: ""
title: userId.length > 0 ? i18n("Remove Messages") : i18n("Remove Message")
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
QQC2.TextArea {
id: reason
placeholderText: userId.length > 0 ? i18n("Reason for removing this user's recent messages") : i18n("Reason for removing this message")
anchors.fill: parent
wrapMode: TextEdit.Wrap
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
}
footer: QQC2.ToolBar {
QQC2.DialogButtonBox {
anchors.fill: parent
Item {
Layout.fillWidth: true
}
QQC2.Button {
text: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove")
icon.name: "delete"
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
if (root.userId.length > 0) {
root.room.deleteMessagesByUser(root.userId, reason.text);
} else {
root.room.redactEvent(root.eventId, reason.text);
}
root.closeDialog();
}
}
QQC2.Button {
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: root.closeDialog()
}
}
}
}

58
src/qml/ReportSheet.qml Normal file
View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root
property NeoChatRoom room
property string eventId
title: i18n("Report Message")
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
QQC2.TextArea {
id: reason
placeholderText: i18n("Reason for reporting this message")
anchors.fill: parent
wrapMode: TextEdit.Wrap
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
}
footer: QQC2.ToolBar {
QQC2.DialogButtonBox {
anchors.fill: parent
Item {
Layout.fillWidth: true
}
QQC2.Button {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.room.reportEvent(eventId, reason.text);
root.closeDialog();
}
}
QQC2.Button {
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: root.closeDialog()
}
}
}
}

View File

@@ -145,7 +145,7 @@ Delegates.RoundedItemDelegate {
text: i18n("Configure room")
display: QQC2.Button.IconOnly
icon.name: "overflow-menu-symbolic"
icon.name: "configure"
onClicked: createRoomListContextMenu()
}
}

View File

@@ -119,7 +119,10 @@ QQC2.Control {
activeFocusOnTab: true
checked: RoomManager.currentSpace.length === 0
onSelected: RoomManager.currentSpace = ""
onSelected: {
RoomManager.currentSpace = "";
root.selectionChanged();
}
}
AvatarTabButton {
id: directChatButton
@@ -151,7 +154,7 @@ QQC2.Control {
visible: true
Kirigami.Theme.colorSet: Kirigami.Theme.Button
Kirigami.Theme.inherit: false
color: root.connection.directChatsHaveHighlightNotifications || root.connection.directChatInvites ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
color: root.connection.directChatsHaveHighlightNotifications ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
radius: height / 2
}
@@ -159,20 +162,16 @@ QQC2.Control {
id: directChatNotificationCountTextMetrics
text: directChatNotificationCountLabel.text
}
Kirigami.Icon {
anchors.fill: parent
source: "list-add-symbolic"
visible: root.connection.directChatInvites && root.connection.directChatNotifications === 0
}
}
}
activeFocusOnTab: true
checked: RoomManager.currentSpace === "DM"
onSelected: RoomManager.currentSpace = "DM"
onSelected: {
RoomManager.currentSpace = "DM";
root.selectionChanged();
}
}
Repeater {

View File

@@ -122,18 +122,7 @@ Kirigami.Dialog {
text: i18n("Kick this user")
icon.name: "im-kick-user"
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user"
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.room.kickMember(root.user.id);
root.close();
}
}
@@ -161,18 +150,13 @@ Kirigami.Dialog {
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user"
(root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'BanSheet'), {
room: root.room,
userId: root.user.id
}, {
title: i18nc("@title:dialog", "Ban User"),
title: i18nc("@title", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
}
@@ -220,22 +204,17 @@ Kirigami.Dialog {
visible: root.room && (root.user.id === root.connection.localUserId || room.canSendState("redact"))
action: Kirigami.Action {
text: i18nc("@action:button", "Remove recent messages by this user")
text: i18n("Remove recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete"
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: root.room,
userId: root.user.id
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
}
}

View File

@@ -45,7 +45,6 @@ RowLayout {
text: i18n("Edit this account")
source: mediaId ? root.connection.makeMediaUrl("mxc://" + mediaId) : ""
name: root.connection.localUser.displayName
activeFocusOnTab: true

View File

@@ -375,7 +375,7 @@ void RoomManager::knockRoom(NeoChatConnection *account, const QString &roomAlias
account,
&NeoChatConnection::newRoom,
this,
[](Quotient::Room *room) {
[this](Quotient::Room *room) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18n("You requested to join '%1'", room -> name()));
},
Qt::SingleShotConnection);

View File

@@ -15,6 +15,10 @@ FormCard.FormComboBoxDelegate {
textRole: "display"
valueRole: "display"
model: ColorSchemer.model
Component.onCompleted: currentIndex = ColorSchemer.indexForCurrentScheme()
onCurrentValueChanged: ColorSchemer.apply(currentIndex);
Component.onCompleted: currentIndex = ColorSchemer.indexForScheme(NeoChatConfig.colorScheme)
onCurrentValueChanged: {
ColorSchemer.apply(currentIndex);
NeoChatConfig.colorScheme = ColorSchemer.nameForIndex(currentIndex);
NeoChatConfig.save();
}
}

View File

@@ -45,7 +45,7 @@ FormCard.FormCardPage {
id: minimizeDelegate
text: i18n("Minimize to system tray on startup")
checked: NeoChatConfig.minimizeToSystemTrayOnStartup
visible: Controller.supportSystemTray && !Kirigami.Settings.isMobile && NeoChatConfig.systemTray
visible: Controller.supportSystemTray && !Kirigami.Settings.isMobile
enabled: NeoChatConfig.systemTray && !NeoChatConfig.isMinimizeToSystemTrayOnStartupImmutable
onToggled: {
NeoChatConfig.minimizeToSystemTrayOnStartup = checked;
@@ -56,7 +56,6 @@ FormCard.FormCardPage {
FormCard.FormDelegateSeparator {
above: minimizeDelegate
below: automaticallyDelegate
visible: minimizeDelegate.visible
}
FormCard.FormCheckDelegate {

View File

@@ -3,7 +3,6 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -16,7 +15,7 @@ FormCard.FormCardPage {
required property NeoChatConnection connection
title: i18nc("@title", "Security & Safety")
title: i18nc("@title", "Security")
header: KirigamiComponents.Banner {
id: banner
@@ -25,39 +24,13 @@ FormCard.FormCardPage {
type: Kirigami.MessageType.Error
}
FormCard.FormHeader {
title: i18nc("@title:group", "Invitations")
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormButtonDelegate {
id: ignoredUsersDelegate
text: i18nc("@action:button", "Ignored Users")
icon.name: "im-invisible-user"
onClicked: root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(ignoredUsersDialogComponent, {}, {
title: i18nc("@title:window", "Ignored Users")
});
}
FormCard.FormDelegateSeparator {
above: ignoredUsersDelegate
below: hideImagesDelegate
}
FormCard.FormCheckDelegate {
id: hideImagesDelegate
text: i18nc("@label:checkbox", "Hide images and videos by default")
description: i18nc("@info", "When this option is enabled, images and videos are only shown after a button is clicked.")
checked: NeoChatConfig.hideImages
enabled: !NeoChatConfig.isHideImagesImmutable
onToggled: {
NeoChatConfig.hideImages = checked;
NeoChatConfig.save();
}
}
FormCard.FormDelegateSeparator {
above: hideImagesDelegate
below: rejectInvitationsDelegate
}
FormCard.FormCheckDelegate {
id: rejectInvitationsDelegate
text: i18nc("@option:check", "Reject invitations from unknown users")
description: connection.canCheckMutualRooms ? i18nc("@info", "If enabled, NeoChat will reject invitations from users you don't share a room with.") : i18nc("@info", "Your server does not support this setting.")
description: connection.canCheckMutualRooms ? i18n("If enabled, NeoChat will reject invitations from users you don't share a room with.") : i18n("Your server does not support this setting.")
checked: NeoChatConfig.rejectUnknownInvites
enabled: !NeoChatConfig.isRejectUnknownInvitesImmutable && connection.canCheckMutualRooms
onToggled: {
@@ -67,15 +40,42 @@ FormCard.FormCardPage {
}
}
FormCard.FormHeader {
title: i18nc("@title", "Encryption")
title: i18nc("@title:group", "Ignored Users")
}
FormCard.FormCard {
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Manage ignored users")
onClicked: root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(ignoredUsersDialogComponent, {}, {
title: i18nc("@title:window", "Ignored Users")
});
}
}
FormCard.FormHeader {
title: i18nc("@title", "Keys")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
text: connection.deviceKey
description: i18n("Device key")
}
FormCard.FormTextDelegate {
text: connection.encryptionKey
description: i18n("Encryption key")
}
FormCard.FormTextDelegate {
text: connection.deviceId
description: i18n("Device id")
}
}
FormCard.FormHeader {
visible: Controller.csSupported
title: i18nc("@title", "Encryption Keys")
}
FormCard.FormCard {
visible: Controller.csSupported
FormCard.FormButtonDelegate {
id: importKeysDelegate
text: i18nc("@action:button", "Import Keys")
description: i18nc("@info", "Import encryption keys from a backup.")
text: i18nc("@action:button", "Import Encryption Keys")
icon.name: "document-import"
onClicked: {
let dialog = root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ImportKeysDialog"), {
@@ -91,14 +91,8 @@ FormCard.FormCardPage {
banner.visible = false;
}
}
FormCard.FormDelegateSeparator {
above: importKeysDelegate
below: exportKeysDelegate
}
FormCard.FormButtonDelegate {
id: exportKeysDelegate
text: i18nc("@action:button", "Export Keys")
description: i18nc("@info", "Export this device's encryption keys.")
text: i18nc("@action:button", "Export Encryption Keys")
icon.name: "document-export"
onClicked: {
root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ExportKeysDialog"), {

View File

@@ -44,11 +44,10 @@ KirigamiSettings.ConfigurationView {
connection: root.connection
};
}
visible: root.connection !== null
},
KirigamiSettings.ConfigurationModule {
moduleId: "security"
text: i18nc("@title", "Security & Safety")
text: i18n("Security")
icon.name: "preferences-security"
page: () => Qt.createComponent("org.kde.neochat.settings", "NeoChatSecurityPage")
initialProperties: () => {
@@ -56,14 +55,12 @@ KirigamiSettings.ConfigurationView {
connection: root.connection
};
}
visible: root.connection !== null
},
KirigamiSettings.ConfigurationModule {
moduleId: "accounts"
text: i18n("Accounts")
icon.name: "preferences-system-users"
page: () => Qt.createComponent("org.kde.neochat.settings", "AccountsPage")
visible: root.connection !== null
},
KirigamiSettings.ConfigurationModule {
moduleId: "emoticons"
@@ -75,7 +72,6 @@ KirigamiSettings.ConfigurationView {
connection: root.connection
};
}
visible: root.connection !== null
},
KirigamiSettings.SpellcheckingConfigurationModule {},
KirigamiSettings.ConfigurationModule {
@@ -94,7 +90,6 @@ KirigamiSettings.ConfigurationView {
connection: root.connection
};
}
visible: root.connection !== null
},
KirigamiSettings.ConfigurationModule {
moduleId: "aboutNeochat"

View File

@@ -89,7 +89,7 @@ void SpaceHierarchyCache::addBatch(const QString &spaceId, Quotient::GetSpaceHie
group.sync();
const auto nextBatchToken = job->nextBatch();
if (!nextBatchToken.isEmpty() && nextBatchToken != *m_nextBatchTokens[spaceId] && m_connection) {
if (!nextBatchToken.isEmpty() && nextBatchToken != *m_nextBatchTokens[spaceId]) {
*m_nextBatchTokens[spaceId] = nextBatchToken;
auto nextJob = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId]);
connect(nextJob, &BaseJob::success, this, [this, nextJob, spaceId]() {

View File

@@ -459,7 +459,7 @@ QString TextHandler::cleanAttributes(const QString &tag, const QString &tagStrin
nextAttributeIndex += 1;
while (nextAttributeIndex < tagString.length()) {
nextSpaceIndex = tagString.indexOf(TextRegex::endAttributeType, nextAttributeIndex);
nextSpaceIndex = tagString.indexOf(TextRegex::endTagType, nextAttributeIndex);
if (nextSpaceIndex == -1) {
nextSpaceIndex = tagString.length();
}
@@ -505,7 +505,7 @@ QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagStr
nextAttributeIndex += 1;
while (nextAttributeIndex < tagString.length()) {
nextSpaceIndex = tagString.indexOf(TextRegex::endAttributeType, nextAttributeIndex);
nextSpaceIndex = tagString.indexOf(TextRegex::endTagType, nextAttributeIndex);
if (nextSpaceIndex == -1) {
nextSpaceIndex = tagString.length();
}
@@ -626,7 +626,7 @@ QString TextHandler::linkifyUrls(QString stringIn)
int skip = 0;
if (match.captured(0).size() > 0) {
if (stringIn.left(index).count(QStringLiteral("<code>")) == stringIn.left(index).count(QStringLiteral("</code>"))) {
auto replacement = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%1</a>").arg(match.captured(1));
auto replacement = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%1</a>").arg(match.captured(2));
stringIn = stringIn.replace(index, match.captured(0).size(), replacement);
} else {
skip = match.captured().length();

View File

@@ -43,6 +43,7 @@ RowLayout {
QQC2.AbstractButton {
id: nameButton
Layout.fillWidth: true
contentItem: QQC2.Label {
text: root.author.disambiguatedName
color: root.author.color
@@ -52,13 +53,6 @@ RowLayout {
}
Accessible.name: contentItem.text
onClicked: RoomManager.resolveResource(root.author.uri)
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
}
Item {
Layout.fillWidth: true
}
QQC2.Label {
id: timeLabel

Some files were not shown because too many files have changed in this diff Show More