Compare commits

...

27 Commits

Author SHA1 Message Date
James Graham
45c46ddcbb Tobias' fix 2024-09-12 09:12:53 +01:00
James Graham
31d83ac0e3 Hack to see if kquickimageeditor is also the problem 2024-09-12 08:12:31 +01:00
James Graham
909eec30d2 Temp disable color scheme so CI builds 2024-09-11 08:51:59 +01:00
James Graham
dbed3e99c2 Create a mobile version of FileDelegateContextMenu with no purpose import 2024-09-11 08:33:49 +01:00
l10n daemon script
4d0db0b5c2 GIT_SILENT Sync po/docbooks with svn 2024-09-10 01:26:40 +00:00
l10n daemon script
6e49aaf17b GIT_SILENT Sync po/docbooks with svn 2024-09-09 01:28:45 +00:00
Joshua Goins
1092d75f2e Remove vestigial references to window geometry
These weren't removed in d165cd955d
because I forgot.
2024-09-08 07:32:00 +00:00
Joshua Goins
8059c3797d Remove unused lambda capture variables
This removes two compile time warnings.
2024-09-08 07:24:24 +00:00
Joshua Goins
1302d62ad9 SpaceDrawer: Remove seemingly non-existent signal call 2024-09-08 07:23:58 +00:00
Joshua Goins
8eaae4034d Shorten the error message passive notification
The default timeout is a bit long, "short" is 3 seconds shorter than the
default. For  long-term network errors, we have a banner telling you so
anyway. This should hopefully reduce the notification spam when you have
temporary network dropouts.
2024-09-08 07:23:30 +00:00
l10n daemon script
354e3414a1 GIT_SILENT Sync po/docbooks with svn 2024-09-08 01:28:05 +00:00
James Graham
fc24beae6d Use im-kick-user consistently for logout.
I went with im-kick-user as it's fully red at all sizes.

BUG: 491355
2024-09-07 12:41:26 +00:00
Claire Elford
923cc67b55 Add test for missing character before a Matrix ID 2024-09-07 10:19:12 +00:00
Claire Elford
30e24069bc Fix missing character before a Matrix ID
When you send messages like "a @blankeclair:catgirl.cloud b" or
"]#rainversewiki:catgirl.cloud", they would be rendered like
"a@blankeclair:catgirl.cloud b" and "#rainversewiki:catgirl.cloud"
respectively. This commit fixes that by not matching the character before the
MXID in the regex.
2024-09-07 10:19:12 +00:00
Claire Elford
f7533a454c Fix increasing font size of certain emojis
Before this commit, NeoChat has two methods of detecting whether or not a piece
of text was an emoji. One is through a regex, and the other is by using the ICU
library. The two methods are used in different parts of the code.

This commit removes the regex detector and instead uses ICU for all the places
where NeoChat needs to figure out whether or not a string is an emoji. This
fixes increasing the font size for messages that only consist of emoji when
certain emoji are used that the regex did not handle (such as the transgender
symbol and transgender flag emojis).
2024-09-07 09:49:27 +00:00
Claire Elford
ab4e1a86dc Fix parsing self-closing tags with no space (such as <br/>)
If there was no space between the tag name and the slash of a self-closing tag,
the code assumes that the tag name is "br/". This commit adds the slash as a
character to close a tag on, so that "<br/>" is treated as a self-closing "br".

BUG: 487377
2024-09-07 12:50:18 +10:00
l10n daemon script
d28c2ed113 GIT_SILENT Sync po/docbooks with svn 2024-09-07 01:27:17 +00:00
Heiko Becker
3a467328f5 GIT_SILENT Update Appstream for new release
(cherry picked from commit b6dac3bbdf)
2024-09-07 00:48:52 +02:00
Tobias Fella
979d83cb01 Remove calls from tests 2024-09-06 08:21:06 +00:00
Joshua Goins
d165cd955d Use the new KConfig WindowStateSaver
This removes some NeoChat-specific code we have for saving/restoring the
window.
2024-09-06 08:21:06 +00:00
l10n daemon script
6eb770343e GIT_SILENT Sync po/docbooks with svn 2024-09-06 01:34:07 +00:00
James Graham
54be52b855 Fix default permissions settings
Make sure that if default permissions or basic permissons are not present in the power level event that they are set properly when changed rather than in the event section.

Also define some of the commonly used strings

BUG: 491371
2024-09-05 13:48:42 +00:00
Tobias Fella
d201333409 Don't consider events that change membership to be renames
Otherwise, the "Show rename events" flag affects the visibility of events where we don't expect it
2024-09-05 13:27:52 +02:00
Tobias Fella
3db8b4cd17 Ask for a reason when kicking a user 2024-09-05 13:22:23 +02:00
l10n daemon script
0e246a00bc GIT_SILENT Sync po/docbooks with svn 2024-09-05 01:26:38 +00:00
l10n daemon script
e638fa8929 GIT_SILENT Sync po/docbooks with svn 2024-09-04 01:26:36 +00:00
l10n daemon script
cdc982ad91 GIT_SILENT Sync po/docbooks with svn 2024-09-03 01:26:14 +00:00
69 changed files with 18572 additions and 15369 deletions

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.5")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)

View File

@@ -46,6 +46,7 @@ private Q_SLOTS:
void sendCustomEmojiCode_data();
void sendCustomEmojiCode();
void receiveSpacelessSelfClosingTag();
void receiveStripReply();
void receivePlainTextIn();
@@ -252,6 +253,19 @@ 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(
@@ -449,6 +463,9 @@ 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);
@@ -462,6 +479,9 @@ 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()
@@ -536,7 +556,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"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like<br/>"), {}},
MessageComponent{
MessageComponentType::Code,
QStringLiteral(

View File

@@ -17,7 +17,6 @@ class WindowControllerTest : public QObject
private Q_SLOTS:
void nullWindow();
void geometry();
void showAndRaise();
void toggle();
@@ -30,32 +29,10 @@ 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,6 +429,7 @@
<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

@@ -242,6 +242,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/FileDelegateContextMenuMobile.qml
qml/MessageSourceSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RoomSearchPage.qml
@@ -306,7 +307,7 @@ add_subdirectory(devtools)
add_subdirectory(login)
add_subdirectory(chatbar)
if(UNIX)
if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
else()
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES

View File

@@ -28,7 +28,8 @@ void ColorSchemer::apply(int idx)
int ColorSchemer::indexForCurrentScheme()
{
return c->indexForSchemeId(c->activeSchemeId()).row();
return -1;
// return c->indexForSchemeId(c->activeSchemeId()).row();
}
#include "moc_colorschemer.cpp"

View File

@@ -154,7 +154,8 @@ 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->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
} else if (roomMemberEvent->isRename() && roomMemberEvent->prevContent() && roomMemberEvent->prevContent()->membership == roomMemberEvent->membership()
&& !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {

View File

@@ -122,7 +122,7 @@ Kirigami.Page {
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "edit-delete-remove"
icon.name: "im-kick-user"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text

View File

@@ -304,7 +304,6 @@ int main(int argc, char *argv[])
QWindow *window = windowFromEngine(&engine);
WindowController::instance().setWindow(window);
WindowController::instance().restoreGeometry();
return app.exec();
}

View File

@@ -2,20 +2,40 @@
// 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 = {
QStringLiteral("users_default"),
QStringLiteral("state_default"),
QStringLiteral("events_default"),
QStringLiteral("invite"),
QStringLiteral("kick"),
QStringLiteral("ban"),
QStringLiteral("redact"),
UsersDefaultKey,
StateDefaultKey,
EventsDefaultKey,
};
static const QStringList basicPermissions = {
InviteKey,
KickKey,
BanKey,
RedactKey,
};
static const QStringList knownPermissions = {
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
QStringLiteral("m.room.power_levels"),
@@ -33,14 +53,14 @@ static const QStringList defaultPermissions = {
};
// Alternate name text for default permissions.
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")},
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")},
{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")},
@@ -58,10 +78,10 @@ static const QHash<QString, KLazyLocalizedString> defaultPermissionNames = {
};
// Subtitles for the default values.
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")},
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")},
};
// Permissions that should use the event default.
@@ -70,6 +90,7 @@ static const QStringList eventPermissions = {
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
};
};
PermissionsModel::PermissionsModel(QObject *parent)
: QAbstractListModel(parent)
@@ -109,6 +130,8 @@ 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)) {
@@ -131,17 +154,17 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
const auto permission = m_permissions.value(index.row());
if (role == NameRole) {
if (defaultPermissionNames.keys().contains(permission)) {
return defaultPermissionNames.value(permission).toString();
if (permissionNames.keys().contains(permission)) {
return permissionNames.value(permission).toString();
}
return permission;
}
if (role == SubtitleRole) {
if (permission.startsWith(QLatin1String("m.")) && defaultPermissionNames.keys().contains(permission)) {
if (knownPermissions.contains(permission) && permissionNames.keys().contains(permission)) {
return permission;
}
if (defaultSubtitles.contains(permission)) {
return defaultSubtitles.value(permission).toString();
if (permissionSubtitles.contains(permission)) {
return permissionSubtitles.value(permission).toString();
}
return QString();
}
@@ -166,11 +189,10 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
return QString();
}
if (role == IsDefaultValueRole) {
return permission.contains(QLatin1String("default"));
return defaultPermissions.contains(permission);
}
if (role == IsBasicPermissionRole) {
return permission == QStringLiteral("invite") || permission == QStringLiteral("kick") || permission == QStringLiteral("ban")
|| permission == QStringLiteral("redact");
return basicPermissions.contains(permission);
}
return {};
}
@@ -201,19 +223,19 @@ std::optional<int> PermissionsModel::powerLevel(const QString &permission) const
}
if (const auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>()) {
if (permission == QStringLiteral("ban")) {
if (permission == BanKey) {
return currentPowerLevelEvent->ban();
} else if (permission == QStringLiteral("kick")) {
} else if (permission == KickKey) {
return currentPowerLevelEvent->kick();
} else if (permission == QStringLiteral("invite")) {
} else if (permission == InviteKey) {
return currentPowerLevelEvent->invite();
} else if (permission == QStringLiteral("redact")) {
} else if (permission == RedactKey) {
return currentPowerLevelEvent->redact();
} else if (permission == QStringLiteral("users_default")) {
} else if (permission == UsersDefaultKey) {
return currentPowerLevelEvent->usersDefault();
} else if (permission == QStringLiteral("state_default")) {
} else if (permission == StateDefaultKey) {
return currentPowerLevelEvent->stateDefault();
} else if (permission == QStringLiteral("events_default")) {
} else if (permission == EventsDefaultKey) {
return currentPowerLevelEvent->eventsDefault();
} else if (eventPermissions.contains(permission)) {
return currentPowerLevelEvent->powerLevelForEvent(permission);
@@ -241,6 +263,9 @@ 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,15 +2,10 @@
// 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>
@@ -154,30 +149,6 @@ 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();
@@ -191,7 +162,7 @@ QString ReactionModel::reactionText(QString text) const
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
}
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
return Utils::isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}
#include "moc_reactionmodel.cpp"

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, roomMemberEvent, showNotification] {
connect(job, &BaseJob::result, this, [this, job, showNotification] {
QJsonObject replyData = job->jsonData();
if (replyData.contains(QStringLiteral("joined"))) {
const bool inAnyOfOurRooms = !replyData[QStringLiteral("joined")].toArray().isEmpty();

View File

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

View File

@@ -38,20 +38,20 @@ ColumnLayout {
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: root.attachmentPath
}
}
// Component {
// id: imageEditorPage
// ImageEditorPage {
// imagePath: root.attachmentPath
// }
// }
onClicked: {
let imageEditor = applicationWindow().pageStack.pushDialogLayer(imageEditorPage);
imageEditor.newPathChanged.connect(function (newPath) {
applicationWindow().pageStack.layers.pop();
root.attachmentPath = newPath;
});
}
// onClicked: {
// let imageEditor = applicationWindow().pageStack.pushDialogLayer(imageEditorPage);
// imageEditor.newPathChanged.connect(function (newPath) {
// applicationWindow().pageStack.layers.pop();
// root.attachmentPath = newPath;
// });
// }
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}

View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import Qt.labs.platform
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief The menu for media messages.
*
* This component just overloads the actions and nested actions of the base menu
* to what is required for a media item.
*
* @sa DelegateContextMenu
*/
DelegateContextMenu {
id: root
/**
* @brief The MIME type of the media.
*/
property string mimeType
/**
* @brief Progress info when downloading files.
*
* @sa Quotient::FileTransferInfo
*/
required property var progressInfo
// Web search isn't useful for images
enableWebSearch: false
/**
* @brief The main list of menu item actions.
*
* Each action will be instantiated as a single line in the menu.
*/
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("Open Externally")
icon.name: "document-open"
onTriggered: {
currentRoom.openEventMediaExternally(root.eventId);
}
},
Kirigami.Action {
text: i18n("Save As")
icon.name: "document-save"
onTriggered: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
dialog.open();
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId);
}
},
DelegateContextMenu.ReplyMessageAction {},
Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: {
currentRoom.copyEventMedia(root.eventId);
}
},
Kirigami.Action {
visible: author.id === currentRoom.localMember.id || currentRoom.canSendState("redact")
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);
});
}
},
DelegateContextMenu.ReportMessageAction {},
DelegateContextMenu.ShowUserAction {},
DelegateContextMenu.ViewSourceAction {}
]
/**
* @brief The list of menu item actions that have sub-actions.
*
* Each action will be instantiated as a single line that opens a sub menu.
*/
property list<Kirigami.Action> nestedActions: [
]
Component {
id: saveAsDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: NeoChatConfig.lastSaveDirectory.length > 0 ? NeoChatConfig.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
if (!currentFile) {
return;
}
NeoChatConfig.lastSaveDirectory = folder;
NeoChatConfig.save();
currentRoom.downloadFile(eventId, currentFile);
}
}
}
}

View File

@@ -6,6 +6,7 @@ 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
@@ -68,36 +69,8 @@ Kirigami.ApplicationWindow {
}
}
// 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();
}
KConfig.WindowStateSaver {
configGroupName: "Window"
}
QuickSwitcher {
@@ -198,11 +171,9 @@ Kirigami.ApplicationWindow {
if (ShareHandler.text && root.connection) {
root.handleShare()
}
if (NeoChatConfig.minimizeToSystemTrayOnStartup && !Kirigami.Settings.isMobile && Controller.supportSystemTray && NeoChatConfig.systemTray) {
restoreWindowGeometryConnections.enabled = true; // To restore window size and position
} else {
const hasSystemTray = Controller.supportSystemTray && NeoChatConfig.systemTray;
if (Kirigami.Settings.isMobile || !(hasSystemTray && NeoChatConfig.minimizeToSystemTrayOnStartup)) {
visible = true;
saveWindowGeometryConnections.enabled = true;
}
}
Connections {
@@ -276,22 +247,7 @@ Kirigami.ApplicationWindow {
target: Controller
function onErrorOccured(error, detail) {
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;
showPassiveNotification(detail.length > 0 ? i18n("%1: %2", error, detail) : error, "short");
}
}

View File

@@ -281,15 +281,27 @@ Kirigami.Page {
}
function onShowFileMenu(eventId, author, messageComponentType, plainText, mimeType, progressInfo, isThread) {
const contextMenu = fileDelegateContextMenu.createObject(root, {
author: author,
eventId: eventId,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo,
isThread: isThread
});
contextMenu.open();
if (Kirigami.Settings.isMobile) {
const contextMenu = fileDelegateContextMenuMobile.createObject(root, {
author: author,
eventId: eventId,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo,
isThread: isThread
});
contextMenu.open();
} else {
const contextMenu = fileDelegateContextMenu.createObject(root, {
author: author,
eventId: eventId,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo,
isThread: isThread
});
contextMenu.open();
}
}
function onShowMaximizedMedia(index) {
@@ -327,6 +339,13 @@ Kirigami.Page {
}
}
Component {
id: fileDelegateContextMenuMobile
FileDelegateContextMenuMobile {
connection: root.connection
}
}
Component {
id: maximizeComponent
NeochatMaximizeComponent {

View File

@@ -119,10 +119,7 @@ QQC2.Control {
activeFocusOnTab: true
checked: RoomManager.currentSpace.length === 0
onSelected: {
RoomManager.currentSpace = "";
root.selectionChanged();
}
onSelected: RoomManager.currentSpace = ""
}
AvatarTabButton {
id: directChatButton
@@ -175,10 +172,7 @@ QQC2.Control {
activeFocusOnTab: true
checked: RoomManager.currentSpace === "DM"
onSelected: {
RoomManager.currentSpace = "DM";
root.selectionChanged();
}
onSelected: RoomManager.currentSpace = "DM"
}
Repeater {

View File

@@ -122,7 +122,18 @@ Kirigami.Dialog {
text: i18n("Kick this user")
icon.name: "im-kick-user"
onTriggered: {
root.room.kickMember(root.user.id);
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.close();
}
}

View File

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

View File

@@ -217,12 +217,12 @@ FormCard.FormCardPage {
visible: colorSchemeDelegate.visible
}
Loader {
id: colorSchemeDelegate
visible: item !== null
sourceComponent: Qt.createComponent('org.kde.neochat.settings', 'ColorScheme')
Layout.fillWidth: true
}
// Loader {
// id: colorSchemeDelegate
// visible: item !== null
// sourceComponent: Qt.createComponent('org.kde.neochat.settings', 'ColorScheme')
// Layout.fillWidth: true
// }
}
FormCard.FormCard {

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::endTagType, nextAttributeIndex);
nextSpaceIndex = tagString.indexOf(TextRegex::endAttributeType, 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::endTagType, nextAttributeIndex);
nextSpaceIndex = tagString.indexOf(TextRegex::endAttributeType, 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(2));
auto replacement = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%1</a>").arg(match.captured(1));
stringIn = stringIn.replace(index, match.captured(0).size(), replacement);
} else {
skip = match.captured().length();

View File

@@ -25,16 +25,6 @@ TextEdit {
*/
property bool isReply: false
/**
* @brief Regex for detecting a message with a single emoji.
*/
readonly property var isEmojiRegex: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
/**
* @brief Whether the message is an emoji
*/
readonly property var isEmoji: isEmojiRegex.test(display)
/**
* @brief Regex for detecting a message with a spoiler.
*/
@@ -113,8 +103,8 @@ a{
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font {
pointSize: !root.isReply && root.isEmoji ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
family: root.isEmoji ? 'emoji' : Kirigami.Theme.defaultFont.family
pointSize: !root.isReply && QmlUtils.isEmoji(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
family: QmlUtils.isEmoji(display) ? 'emoji' : Kirigami.Theme.defaultFont.family
}
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true

View File

@@ -3,12 +3,24 @@
#include "utils.h"
#ifdef HAVE_ICU
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#endif
#include <Quotient/connection.h>
#include <QJsonDocument>
using namespace Quotient;
bool QmlUtils::isEmoji(const QString &text)
{
return Utils::isEmoji(text);
}
bool QmlUtils::isValidJson(const QByteArray &json)
{
return !QJsonDocument::fromJson(json).isNull();
@@ -26,4 +38,28 @@ QColor QmlUtils::getUserColor(qreal hueF)
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
}
bool Utils::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
}
#include "moc_utils.cpp"

View File

@@ -30,6 +30,7 @@ public:
return _instance;
}
Q_INVOKABLE bool isEmoji(const QString &text);
Q_INVOKABLE bool isValidJson(const QByteArray &json);
Q_INVOKABLE QString escapeString(const QString &string);
Q_INVOKABLE QColor getUserColor(qreal hueF);
@@ -53,11 +54,14 @@ inline QColor getUserColor(qreal hueF)
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
}
bool isEmoji(const QString &text);
}
namespace TextRegex
{
static const QRegularExpression endTagType{QStringLiteral("(>| )")};
static const QRegularExpression endTagType{QStringLiteral("[> /]")};
static const QRegularExpression endAttributeType{QStringLiteral("[> ]")};
static const QRegularExpression attributeData{QStringLiteral("['\"](.*?)['\"]")};
static const QRegularExpression removeReply{QStringLiteral("> <.*?>.*?\\n\\n"), QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReply{QStringLiteral("<mx-reply>.*?</mx-reply>"), QRegularExpression::DotMatchesEverythingOption};
@@ -76,6 +80,6 @@ static const QRegularExpression
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression emailAddress(QStringLiteral(R"(<a.*?<\/a>(*SKIP)(*F)|\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
static const QRegularExpression mxId(QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
static const QRegularExpression mxId(QStringLiteral(R"((?<=^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
}

View File

@@ -40,26 +40,6 @@ QWindow *WindowController::window() const
return m_window;
}
void WindowController::restoreGeometry()
{
const auto stateConfig = KSharedConfig::openStateConfig();
const KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
KWindowConfig::restoreWindowSize(m_window, windowGroup);
KWindowConfig::restoreWindowPosition(m_window, windowGroup);
}
void WindowController::saveGeometry()
{
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
KWindowConfig::saveWindowPosition(m_window, windowGroup);
KWindowConfig::saveWindowSize(m_window, windowGroup);
stateConfig->sync();
}
void WindowController::showAndRaiseWindow(const QString &startupId)
{
if (m_window == nullptr) {
@@ -67,7 +47,6 @@ void WindowController::showAndRaiseWindow(const QString &startupId)
}
if (!m_window->isVisible()) {
m_window->show();
restoreGeometry();
}
#ifdef HAVE_WINDOWSYSTEM

View File

@@ -41,16 +41,6 @@ public:
*/
QWindow *window() const;
/**
* @brief Restore any saved window geometry if available.
*/
void restoreGeometry();
/**
* @brief Save the current window geometry.
*/
Q_INVOKABLE void saveGeometry();
/**
* @brief Show the window and raise to the top.
*/