Compare commits

...

70 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
Joshua Goins
bff69e21ad Add icons for the menus in the message context menu
In Qt6 we can now assign icons to menus, so let's add what's missing.
2024-09-02 17:28:03 +00:00
Joshua Goins
e117cc0cfb Move the "Show User" action to DelegateContextMenu, add it for media
This was missing from the media context menu, and should be added. Since
it's shared between messages and files, it's now a common action.
2024-09-02 17:28:03 +00:00
Joshua Goins
3af1a88e05 Don't show the "Web Search" action in the media context menu
All this does is try to web search for the filename of the image, which
is useless. Let's hide it entirely in this case.
2024-09-02 17:28:03 +00:00
Joshua Goins
aa116a35f5 Web Shortcuts: kcmshell5 does not exist anymore on Plasma 6
This is hardcoded, but it's probably a safe assumption to think most
people running modern NeoChat are using Plasma 6 anyway.
2024-09-02 17:15:37 +00:00
Joshua Goins
ac232d7f55 Change the "configure room" button icon to something more fitting
This button doesn't actually configure anything, you can do plenty of
actions like "mark as read" and such. Since the button isn't solely for
configuration, we should use an overflow menu icon instead.
2024-09-02 17:02:36 +00:00
Joshua Goins
928911e33c Settings: Overhaul the Security page
This improves the organization of this page, which is starting to become
a bit of a mess. The "Hide images and video events" option is moved
here, and the page is rebranded accordingly for "Security & Safety".
Unnecessary headings are removed, and the ignored users button is moved
to the top of the page.

Explanations for the import/keys buttons are added. The key display
is removed as it's not useful for the user (because they don't know what
to do with it) nor developers (because you can't copy it.) We can add
it back somewhere else.

This has the added benefit of making the whole page fit in the default
settings window size too.
2024-09-02 16:13:12 +00:00
Joshua Goins
e3c30f5bb3 Settings: Hide "minimize to system tray" when system tray is disabled
We do this for the state events setting, so let's do it here too.
2024-09-02 16:01:51 +00:00
Tobias Fella
4b51855528 Add name for Avatar in RecommendedSpaceDialog 2024-09-02 17:26:14 +02:00
Tobias Fella
79c27db0a9 Improve InviteUserPage 2024-09-02 16:33:45 +02:00
Tobias Fella
78e42ab352 Make it possible to invite users that were previously in the room by command 2024-09-02 15:54:40 +02:00
Tobias Fella
93909c45ee Refactor dialogs for reporting, banning, and removing messages 2024-09-02 15:18:32 +02:00
Joshua Goins
eda0bf4b23 LocationChooser: Add a "Locate" button to locate yourself
The map centers on London by default, but for the other people living
outside it may find it hard to figure out where they are. This adds a
button that calls into QtPositioning to center the map over where you
are.
2024-09-02 13:09:21 +00:00
Joshua Goins
4f4b10e0b6 LocationChooser: Add a "Re-center" action to find the pinned location
It's somewhat easy to scroll or pan away from the pin you placed, so
let's add an action to re-center the map if you want to find it again.
2024-09-02 13:09:21 +00:00
Joshua Goins
2b374a8bec Add an icon when you only have invites but no direct messages
There's an edge case with the friends icon, where it will display a
blank circle if you only have pending invites but no actual direct
messages. Now an icon is added to make it clear there is pending invites
and it's not a visual bug.
2024-09-02 12:40:54 +00:00
Tobias Fella
7821de4a8d Don't end kick messages without a reason with a ":"
BUG: 492512
2024-09-02 14:39:17 +02:00
Tobias Fella
86b88c851f Close AccountSwitchDialog when logging in a new connection
Otherwise, the dialog is still opened after login has finished
2024-09-02 14:38:33 +02:00
Tobias Fella
1157882f1b Don't register ThreadChatBarModel as a QML_ELEMENT
It's not needed and doesn't work
2024-09-02 13:36:32 +02:00
Tobias Fella
594d1373c9 Cleanup 2024-09-02 13:30:15 +02:00
Tobias Fella
1e6948bbc7 Fix crash during logout 2024-09-02 12:49:38 +02:00
Tobias Fella
41845b97d5 Fix crash on logout 2024-09-02 12:45:11 +02:00
Tobias Fella
bb56f74622 Fix initials for avatar in UserInfo 2024-09-02 10:34:39 +02:00
l10n daemon script
a98f6ac331 GIT_SILENT Sync po/docbooks with svn 2024-09-02 01:26:52 +00:00
Joshua Goins
15d6287995 LocationHelper: Move clamp from zoomToFit to QML
The OSM plugin has a different zoom tolerance than what we're hardcoding
here. This fixes the map looking funky from being too zoomed while
trying to fit multiple location points at once.
2024-09-01 18:48:20 +00:00
Joshua Goins
0242ab72e8 Allow opening locations in your preferred map
Once someone shares a location with you, typically you want to open it
in your preferred mapping application or website. For example, being
sent a location to a restaurant and needing to route it via Google Maps.

Now in NeoChat you can click on the "Open Externally" button on a
location message. On KDE Plasma, the default application can be set
under System Settings. On Android, this URI is handled by Google Maps
and possibly others.
2024-09-01 18:23:49 +00:00
Joshua Goins
4b7cbf37d5 AuthorDelegate: Don't make empty space clickable 2024-09-01 15:55:51 +00:00
Joshua Goins
0ccfe7d991 AuthorDelegate: Add pointing hand cursor to indicate you can tap
It's not immediately obvious that you can press on this static text to
bring up the user's details. This isn't a problem with the avatar - for
example - because it has a pointing hand cursor. Let's do the same here.
2024-09-01 15:55:51 +00:00
Joshua Goins
ab5585cd06 Make the SectionDelegate look just a little bit better
This has insets on it - probably from qqc2-desktop-style - and makes it
look extremely bad when scrolling through messages as the background
size doesn't match. Now the insets are set to zero, except for topInset.
This is done to work around a visual bug where you can see a one-pixel
line right above the SectionDelegate when scrolling.
2024-09-01 11:42:17 -04:00
Joshua Goins
b25a0a7a4e Settings: Hide certain pages when we have no connection
This hides the "Notifications", "Security", "Accounts" and "Devices"
page from the settings if we have no connection. This can now happen
since the user is able to enter the full settings without being logged
in from the welcome page.
2024-09-01 14:21:14 +00:00
Joshua Goins
f574c12adc WelcomePage: Redesign to center the contents, other misc improvements
We now remove the header from the page, and replace it with a separator
(it still lives with an InlineMessage for error handling.) The contents
of this page are now centered, and the maximum width of the buttons are
reduced.

Along with that are two smaller misc improvements. One is that the
duplicate separator underneath "Register" is now gone. Another is that
the full settings page may now be opened from here, allowing users to
access more than proxy settings.
2024-09-01 14:21:14 +00:00
James Graham
05a2f03c18 Make use of the proposed new KColorScheme API
Make use of new API proposed in frameworks/kcolorscheme!12
2024-09-01 14:17:41 +00:00
l10n daemon script
cbc2e65856 GIT_SILENT Sync po/docbooks with svn 2024-09-01 01:26:36 +00:00
l10n daemon script
1df80a7d2a GIT_SILENT Sync po/docbooks with svn 2024-08-31 01:27:27 +00:00
l10n daemon script
050a014df7 GIT_SILENT Sync po/docbooks with svn 2024-08-30 01:35:35 +00:00
l10n daemon script
4775ae09b0 GIT_SILENT Sync po/docbooks with svn 2024-08-29 01:27:27 +00:00
l10n daemon script
8d4c3bb4fc GIT_SILENT Sync po/docbooks with svn 2024-08-28 01:38:01 +00:00
l10n daemon script
439260ff03 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-08-28 01:25:46 +00:00
James Graham
5b04ad6805 Minor fixes for MessageContentModel and ThreadModel 2024-08-27 15:58:27 +01:00
James Graham
6d77ed1e0e Create an LRU cache for linkpreviewers
Create an LRU cache for linkpreviewers to stop the storage growing continuously as new links are made.
2024-08-27 08:15:03 +00:00
l10n daemon script
cc7f783b50 GIT_SILENT Sync po/docbooks with svn 2024-08-27 01:29:27 +00:00
James Graham
1d7ed1983b Use the ChatBar Component for new thread messages
![image](/uploads/a24dea919e7f165d602991659f517c22/image.png){width=148 height=210}

Note: there is still an issue where after starting a new thread the threaded messages only appear after a restart as the root event needs re-downloading from the server to get the thread info added. My plan is to tackle this next.
2024-08-26 19:13:19 +00:00
Tobias Fella
c0151353c5 Add an option to hide images and videos by default
Implements #658
2024-08-26 17:42:29 +02:00
l10n daemon script
72994248fa GIT_SILENT Sync po/docbooks with svn 2024-08-26 01:35:00 +00:00
l10n daemon script
af0d3d5ee1 GIT_SILENT Sync po/docbooks with svn 2024-08-25 01:26:59 +00:00
114 changed files with 22586 additions and 17386 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.4")
set(KF_MIN_VERSION "6.5")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)

View File

@@ -107,8 +107,13 @@ 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,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,11 +242,9 @@ 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/ReportSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/RoomSearchPage.qml
qml/LocationChooser.qml
qml/TimelineView.qml
@@ -291,6 +289,7 @@ 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
@@ -308,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

@@ -139,8 +139,8 @@ void ChatBarCache::setThreadId(const QString &threadId)
if (m_threadId == threadId) {
return;
}
m_threadId = threadId;
Q_EMIT threadIdChanged();
const auto oldThreadId = std::exchange(m_threadId, threadId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
}
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());
m_threadId = QString();
const auto oldThreadId = std::exchange(m_threadId, QString());
m_attachmentPath = QString();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT threadIdChanged();
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
Q_EMIT attachmentPathChanged();
}

View File

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

View File

@@ -26,23 +26,10 @@ void ColorSchemer::apply(int idx)
c->activateScheme(c->model()->index(idx, 0));
}
void ColorSchemer::apply(const QString &name)
int ColorSchemer::indexForCurrentScheme()
{
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();
return -1;
// return c->indexForSchemeId(c->activeSchemeId()).row();
}
#include "moc_colorschemer.cpp"

View File

@@ -44,21 +44,11 @@ public:
Q_INVOKABLE void apply(int idx);
/**
* @brief Activates the KColorScheme with the given name.
* @brief Get the row for the current color scheme.
*
* @sa KColorScheme
*/
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;
Q_INVOKABLE int indexForCurrentScheme();
private:
KColorSchemeManager *c;

View File

@@ -439,4 +439,14 @@ 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,6 +116,9 @@ 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);
@@ -128,6 +131,7 @@ 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. */
Edit, /**< A text edit for editing a message. */
ChatBar, /**< 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,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()) {
@@ -366,9 +367,13 @@ 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");
}
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");
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);
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 std::clamp(z, 5.0, 18.0);
return z;
}
#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
FormCard.FormCardPage {
Kirigami.Page {
id: root
property bool showExisting: false
@@ -23,202 +23,237 @@ FormCard.FormCardPage {
signal connectionChosen
title: i18n("Welcome")
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
header: QQC2.Control {
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 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)
}
contentItem: ColumnLayout {
spacing: 0
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();
}
Kirigami.Separator {
Layout.fillWidth: true
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
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();
}
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
}
contentItem: Item {
ColumnLayout {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
FormCard.FormCard {
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
spacing: 0
Connections {
id: stepConnections
target: currentStep
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)
}
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();
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
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
background: null
contentItem: RowLayout {
spacing: 0
function onCloseDialog(): void {
root.closeDialog();
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();
}
}
}
}
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
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();
}
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
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");
}
}
}
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;
}
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
FormCard.FormDelegateSeparator {
below: continueButton
visible: root.currentStep.nextAction
}
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.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")
});
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()
}
}
}
}

View File

@@ -185,9 +185,6 @@ 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"));
@@ -307,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

@@ -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->members().contains(room->member(text))) {
if (room->joinedMemberIds().contains(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) {
m_room->getEvent(m_eventId);
intiializeEvent(m_room->getEvent(m_eventId));
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
}
@@ -140,6 +140,13 @@ 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();
});
@@ -182,14 +189,13 @@ 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.
if (m_eventId.isEmpty()) {
m_eventId = m_event->id();
if (m_eventId.isEmpty()) {
m_eventId = m_event->transactionId();
}
}
m_eventId = EventHandler::id(m_event.get());
auto senderId = m_event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
@@ -215,7 +221,7 @@ void MessageContentModel::setShowAuthor(bool showAuthor)
}
m_showAuthor = showAuthor;
if (m_event != nullptr && m_room->connection()->ignoredUsers().contains(m_event->senderId())) {
if (m_event != nullptr && m_room->connection()->isIgnored(m_event->senderId())) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
@@ -245,7 +251,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()->ignoredUsers().contains(m_event->senderId()))) {
if (m_notFound || (m_event && m_room->connection()->isIgnored(m_event->senderId()))) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
@@ -341,6 +347,12 @@ 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 {};
}
@@ -372,6 +384,7 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roles;
}
@@ -380,7 +393,7 @@ void MessageContentModel::resetModel()
beginResetModel();
m_components.clear();
if ((m_event && m_room->connection()->ignoredUsers().contains(m_event->senderId())) || m_notFound) {
if ((m_event && m_room->connection()->isIgnored(m_event->senderId())) || m_notFound) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
@@ -400,7 +413,7 @@ void MessageContentModel::resetModel()
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing)
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
{
Q_ASSERT(m_event != nullptr);
@@ -409,7 +422,7 @@ void MessageContentModel::resetContent(bool isEditing)
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing);
const auto newComponents = messageContentComponents(isEditing, isThreading);
if (newComponents.size() == 0) {
return;
}
@@ -418,7 +431,7 @@ void MessageContentModel::resetContent(bool isEditing)
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
{
QList<MessageComponent> newComponents;
@@ -438,7 +451,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*m_event.get())));
}
@@ -447,6 +460,11 @@ 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,6 +70,7 @@ public:
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
LinkPreviewerRole, /**< The link preview details. */
ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
@@ -133,8 +134,8 @@ private:
QList<MessageComponent> m_components;
void resetModel();
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();

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

@@ -9,12 +9,15 @@
#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);
@@ -25,7 +28,6 @@ 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();
}
}
@@ -38,7 +40,6 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
}
}
}
clearModels();
addModels();
});
@@ -46,6 +47,11 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
addModels();
}
QString ThreadModel::threadRootId() const
{
return m_threadRootId;
}
MessageContentModel *ThreadModel::threadRootContentModel() const
{
return m_threadRootContentModel.get();
@@ -77,7 +83,6 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
m_contentModels.push_back(new MessageContentModel(room, event.get()));
}
clearModels();
addModels();
const auto newNextBatch = m_currentJob->nextBatch();
@@ -103,10 +108,15 @@ 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();
@@ -120,6 +130,61 @@ 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,6 +21,56 @@
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
*
@@ -38,6 +88,8 @@ class ThreadModel : public QConcatenateTablesProxyModel
public:
explicit ThreadModel(const QString &threadRootId, NeoChatRoom *room);
QString threadRootId() const;
/**
* @brief The content model for the thread root event.
*/
@@ -70,6 +122,7 @@ 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->allHistoryLoaded()) {
if (m_room && 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("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"), this);
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), QStringList() << QStringLiteral("webshortcuts"), this);
job->exec();
#endif
}

View File

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

View File

@@ -3,6 +3,7 @@
#pragma once
#include <QCache>
#include <QObject>
#include <QQmlEngine>
@@ -13,10 +14,9 @@
#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;
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
QCache<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, 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

@@ -4,9 +4,34 @@
{
"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[x-test]": "xxTobias Fellaxx"
"Name[tr]": "Tobias Fella",
"Name[uk]": "Tobias Fella",
"Name[x-test]": "xxTobias Fellaxx",
"Name[zh_TW]": "Tobias Fella"
}
],
"Category": "Utilities",
@@ -15,6 +40,7 @@
"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",
@@ -42,9 +68,35 @@
"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: "list-remove-user"
icon.name: "im-kick-user"
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.window.overlay).open()
}

View File

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

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

@@ -70,6 +70,11 @@ 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
*/
@@ -82,16 +87,23 @@ Loader {
component RemoveMessageAction: Kirigami.Action {
visible: author.isLocalMember || currentRoom.canSendState("redact")
text: i18n("Remove")
text: i18nc("@action:button", "Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: currentRoom,
eventId: eventId
}, {
title: i18nc("@title", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
})
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);
});
}
}
component ReplyMessageAction: Kirigami.Action {
@@ -108,13 +120,28 @@ Loader {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: !author.isLocalMember
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReportSheet'), {
room: currentRoom,
eventId: eventId
}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
})
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)
}
}
Component {
@@ -128,6 +155,7 @@ Loader {
id: menuItem
visible: modelData.visible
title: modelData.text
icon: modelData.icon
Instantiator {
model: modelData.children
@@ -158,7 +186,8 @@ Loader {
QQC2.Menu {
id: webshortcutmenu
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
property bool isVisible: webshortcutmodel.enabled
icon.name: "search-symbolic"
property bool isVisible: webshortcutmodel.enabled && root.enableWebSearch
Component.onCompleted: {
webshortcutmenu.parent.visible = isVisible;
}

View File

@@ -32,6 +32,9 @@ DelegateContextMenu {
*/
required property var progressInfo
// Web search isn't useful for images
enableWebSearch: false
/**
* @brief The main list of menu item actions.
*
@@ -67,15 +70,23 @@ DelegateContextMenu {
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: currentRoom,
eventId: eventId
}, {
title: i18nc("@title", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
})
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 {}
]

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

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

View File

@@ -1,6 +1,8 @@
// 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
@@ -11,111 +13,77 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
Kirigami.ScrollablePage {
SearchPage {
id: root
property NeoChatRoom room
title: i18n("Invite a User")
title: i18nc("@title:dialog", "Invite a User")
actions: [
Kirigami.Action {
icon.name: "dialog-close"
text: i18nc("@action", "Cancel")
onTriggered: root.closeDialog()
}
]
header: RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
searchFieldPlaceholder: i18nc("@info:placeholder", "Find a user…")
noResultPlaceholderMessage: i18nc("@info:placeholder", "No users found")
Kirigami.SearchField {
id: identifierField
property bool isUserId: text.match(/@(.+):(.+)/g)
Layout.fillWidth: true
headerTrailing: QQC2.Button {
icon.name: "list-add"
display: QQC2.Button.IconOnly
enabled: root.model.searchText.match(/@(.+):(.+)/g) && !root.room.containsUser(root.model.searchText)
placeholderText: i18n("Find a user...")
onAccepted: userDictListModel.search()
}
text: i18nc("@action:button", "Invite this User")
QQC2.Button {
visible: identifierField.isUserId
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
text: i18n("Add")
highlighted: true
onClicked: {
room.inviteToRoom(identifierField.text);
}
}
onClicked: root.room.inviteToRoom(root.model.searchText);
}
ListView {
id: userDictListView
model: UserDirectoryListModel {
id: userDictListModel
clip: true
connection: root.room.connection
}
model: UserDirectoryListModel {
id: userDictListModel
modelDelegate: Delegates.RoundedItemDelegate {
id: delegate
connection: root.room.connection
searchText: identifierField.text
}
required property string userId
required property string displayName
required property url avatarUrl
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: displayName
visible: userDictListView.count < 1
contentItem: RowLayout {
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: delegate.avatarUrl
name: delegate.displayName
}
text: i18n("No users available")
}
Delegates.SubtitleContentItem {
itemDelegate: delegate
subtitle: delegate.userId
labelItem.textFormat: Text.PlainText
}
delegate: Delegates.RoundedItemDelegate {
id: delegate
QQC2.ToolButton {
id: inviteButton
required property string userId
required property string displayName
required property url avatarUrl
readonly property bool inRoom: root.room && root.room.containsUser(delegate.userId)
property bool inRoom: room && room.containsUser(userId)
icon.name: "document-send"
text: i18nc("@action:button", "Send invitation")
opacity: inRoom ? 0.5 : 1
enabled: !inRoom
text: displayName
contentItem: RowLayout {
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: delegate.avatarUrl
name: delegate.displayName
onClicked: {
inviteButton.enabled = false;
root.room.inviteToRoom(delegate.userId);
}
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
}
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
}
}
}

View File

@@ -30,9 +30,36 @@ 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
@@ -54,6 +81,15 @@ 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,7 +27,10 @@ Kirigami.Page {
let c = LocationHelper.center(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox));
return QtPositioning.coordinate(c.y, c.x);
}
map.zoomLevel: LocationHelper.zoomToFit(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox), mapView.width, mapView.height)
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);
}
MapItemView {
Component.onCompleted: mapView.map.addMapItemView(this)

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

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

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
// 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()
}
}
}
}

View File

@@ -1,58 +0,0 @@
// 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: "configure"
icon.name: "overflow-menu-symbolic"
onClicked: createRoomListContextMenu()
}
}

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
@@ -154,7 +151,7 @@ QQC2.Control {
visible: true
Kirigami.Theme.colorSet: Kirigami.Theme.Button
Kirigami.Theme.inherit: false
color: root.connection.directChatsHaveHighlightNotifications ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
color: root.connection.directChatsHaveHighlightNotifications || root.connection.directChatInvites ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
radius: height / 2
}
@@ -162,16 +159,20 @@ 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";
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();
}
}
@@ -150,13 +161,18 @@ Kirigami.Dialog {
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
(root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'BanSheet'), {
room: root.room,
userId: root.user.id
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"
}, {
title: i18nc("@title", "Ban User"),
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
}
@@ -204,17 +220,22 @@ Kirigami.Dialog {
visible: root.room && (root.user.id === root.connection.localUserId || room.canSendState("redact"))
action: Kirigami.Action {
text: i18n("Remove recent messages by this user")
text: i18nc("@action:button", "Remove recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: root.room,
userId: root.user.id
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"
}, {
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,6 +45,7 @@ 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,
[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

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

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
visible: Controller.supportSystemTray && !Kirigami.Settings.isMobile && NeoChatConfig.systemTray
enabled: NeoChatConfig.systemTray && !NeoChatConfig.isMinimizeToSystemTrayOnStartupImmutable
onToggled: {
NeoChatConfig.minimizeToSystemTrayOnStartup = checked;
@@ -56,6 +56,7 @@ FormCard.FormCardPage {
FormCard.FormDelegateSeparator {
above: minimizeDelegate
below: automaticallyDelegate
visible: minimizeDelegate.visible
}
FormCard.FormCheckDelegate {

View File

@@ -3,6 +3,7 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -15,7 +16,7 @@ FormCard.FormCardPage {
required property NeoChatConnection connection
title: i18nc("@title", "Security")
title: i18nc("@title", "Security & Safety")
header: KirigamiComponents.Banner {
id: banner
@@ -24,13 +25,39 @@ 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 ? i18n("If enabled, NeoChat will reject invitations from users you don't share a room with.") : i18n("Your server does not support this setting.")
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.")
checked: NeoChatConfig.rejectUnknownInvites
enabled: !NeoChatConfig.isRejectUnknownInvitesImmutable && connection.canCheckMutualRooms
onToggled: {
@@ -40,42 +67,15 @@ FormCard.FormCardPage {
}
}
FormCard.FormHeader {
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 {
title: i18nc("@title", "Encryption")
visible: Controller.csSupported
title: i18nc("@title", "Encryption Keys")
}
FormCard.FormCard {
visible: Controller.csSupported
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Import Encryption Keys")
id: importKeysDelegate
text: i18nc("@action:button", "Import Keys")
description: i18nc("@info", "Import encryption keys from a backup.")
icon.name: "document-import"
onClicked: {
let dialog = root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ImportKeysDialog"), {
@@ -91,8 +91,14 @@ FormCard.FormCardPage {
banner.visible = false;
}
}
FormCard.FormDelegateSeparator {
above: importKeysDelegate
below: exportKeysDelegate
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Export Encryption Keys")
id: exportKeysDelegate
text: i18nc("@action:button", "Export Keys")
description: i18nc("@info", "Export this device's encryption keys.")
icon.name: "document-export"
onClicked: {
root.QQC2.ApplicationWindow.window.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ExportKeysDialog"), {

View File

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

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