diff --git a/.ci/macos/build.sh b/.ci/macos/build.sh
index c4c92cab..1db9f009 100755
--- a/.ci/macos/build.sh
+++ b/.ci/macos/build.sh
@@ -24,6 +24,7 @@ cmake -GNinja -S. -Bbuild \
-DCMAKE_INSTALL_PREFIX="nheko.temp" \
-DHUNTER_ROOT="../.hunter" \
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \
+ -DKDSingleApplication_STATIC=ON -DKDSingleApplication_EXAMPLES=OFF \
-DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \
-DQt6_DIR=${QT_BASEPATH}/lib/cmake \
-DCI_BUILD=ON
diff --git a/.ci/windows/build.bat b/.ci/windows/build.bat
index 70d72283..332ed4f9 100644
--- a/.ci/windows/build.bat
+++ b/.ci/windows/build.bat
@@ -15,6 +15,7 @@ echo %DATE%
call "C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Auxiliary/Build/vcvarsall.bat" x64
+set CMAKE_POLICY_VERSION_MINIMUM=3.5
cmake -G "Visual Studio 17 2022" -A x64 -S. -Bbuild -DHUNTER_ROOT="C:\hunter" -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON -DUSE_BUNDLED_KDSINGLEAPPLICATION=ON -DKDSingleApplication_STATIC=ON -DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release
cmake --build build --config Release -j %NUMBER_OF_PROCESSORS%
diff --git a/.clang-format b/.clang-format
index f26fc328..70bb3cae 100644
--- a/.clang-format
+++ b/.clang-format
@@ -13,6 +13,8 @@ KeepEmptyLinesAtTheStartOfBlocks: false
PointerAlignment: Right
Cpp11BracedListStyle: true
PenaltyReturnTypeOnItsOwnLine: 0
+StatementAttributeLikeMacros:
+ - emit
---
BasedOnStyle: WebKit
Language: ObjC
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c09836bd..724ee5d4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -98,7 +98,7 @@ pages:
- export LATEST_WINDOWS_NIGHTLY=$(curl "https://nheko.im/api/v4/projects/2/packages?package_name=windows-nightly&order_by=version&sort=desc" | jq -r '.[0].version')
#- export LATEST_WINDOWS=$(curl "https://nheko.im/api/v4/projects/2/packages?package_name=windows&order_by=version&sort=desc" | jq -r '.[0].version')
# hardcoded to avoid fuzzy matching
- - export LATEST_WINDOWS='0.12.0.35798'
+ - export LATEST_WINDOWS='0.12.0.38759'
- sed "s/0.12.1.0/${LATEST_WINDOWS_NIGHTLY}/g" -i resources/NhekoNightly.appinstaller
- sed "s/0.12.1.0/${LATEST_WINDOWS}/g" -i resources/Nheko.appinstaller
- mkdir public
@@ -450,8 +450,8 @@ linting:
before_script:
- apk update && apk add make git python3 py3-pip qt6-qtdeclarative-dev
# clang18 seems to mess with the emit keyword when using the `->` operator
- - apk add clang17-extra-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
- - export PATH="$PATH:/usr/lib/llvm17/bin/:/root/.local/bin"
+ - apk add clang-extra-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
+ - export PATH="$PATH:/usr/lib/llvm/bin/:/root/.local/bin"
- pip3 install --break-system-packages --user reuse
script:
- make lint
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4ff134c5..0976231b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,8 +25,8 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "macos deployment target")
option(HUNTER_ENABLED "Enable Hunter package manager" OFF)
include("cmake/HunterGate.cmake")
HunterGate(
- URL "https://github.com/cpp-pm/hunter/archive/v0.26.1.tar.gz"
- SHA1 "e41ac7a18c49b35ebac99ff2b5244317b2638a65"
+ URL "https://github.com/cpp-pm/hunter/archive/v0.26.6.tar.gz"
+ SHA1 "e70c29f878f5d5f5cdf1b9ccd628fb872e8624a8"
LOCAL
)
@@ -246,7 +246,11 @@ endif()
#
# Discover Qt dependencies.
#
+
find_package(Qt6 6.5 COMPONENTS Core Widgets Gui LinguistTools Svg Multimedia Qml QuickControls2 REQUIRED)
+if (Qt6Qml_VERSION VERSION_GREATER_EQUAL "6.10.0")
+ find_package(Qt6 REQUIRED COMPONENTS GuiPrivate QmlPrivate)
+endif()
find_package(Qt6DBus)
if(USE_BUNDLED_QTKEYCHAIN)
@@ -295,7 +299,8 @@ if(NOT MSVC)
-fsized-deallocation \
-fdiagnostics-color=always \
-Wunreachable-code \
- -Wno-attributes"
+ -Wno-attributes \
+ -Wno-error=unused-parameter"
)
if(NOT CMAKE_COMPILER_IS_GNUCXX)
# -Wshadow is buggy and broken in GCC, so do not enable it.
@@ -539,7 +544,9 @@ if(USE_BUNDLED_OLM)
Olm
GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git
GIT_TAG 3.2.16
- PATCH_COMMAND git apply ${CMAKE_CURRENT_SOURCE_DIR}/third_party/olm-patches/0001-fix-list-const-ptr.patch
+ PATCH_COMMAND git apply
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/olm-patches/0001-fix-list-const-ptr.patch
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/olm-patches/0002-fix-cmake-cmp0148.patch
UPDATE_DISCONNECTED 1
)
set(OLM_TESTS OFF CACHE INTERNAL "")
@@ -562,16 +569,12 @@ if(USE_BUNDLED_CMARK)
FetchContent_Declare(
cmark
GIT_REPOSITORY https://github.com/commonmark/cmark.git
- GIT_TAG 0.30.2
- CMAKE_ARGS "CMARK_STATIC=ON CMARK_SHARED=OFF CMARK_TESTS=OFF CMARK_TESTS=OFF"
+ GIT_TAG 0.31.1
+ CMAKE_ARGS "BUILD_TESTING=OFF"
)
FetchContent_MakeAvailable(cmark)
if (NOT TARGET cmark::cmark)
- if(MSVC)
- add_library(cmark::cmark ALIAS cmark)
- else()
- add_library(cmark::cmark ALIAS cmark_static)
- endif()
+ add_library(cmark::cmark ALIAS cmark)
endif()
else()
find_package(cmark REQUIRED 0.29.0)
@@ -619,7 +622,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
- GIT_TAG v0.10.1
+ GIT_TAG 873911e352a0845dfb178f77b1ddea796a5d3455
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -713,7 +716,9 @@ set_target_properties(nheko
#
# Add qml files
#
-
+if(QT_KNOWN_POLICY_QTP0004)
+ qt_policy(SET QTP0004 NEW)
+endif()
set(QML_SOURCES
resources/qml/Root.qml
resources/qml/ChatPage.qml
diff --git a/README.md b/README.md
index b4c6fa8e..a7dc62e2 100644
--- a/README.md
+++ b/README.md
@@ -343,6 +343,7 @@ sudo pacman -S qt6-base \
gcc \
fontconfig \
lmdb \
+ lmdbxx \
cmark \
qtkeychain-qt6
```
diff --git a/im.nheko.Nheko.yaml b/im.nheko.Nheko.yaml
index a773c5ba..76f15bb9 100644
--- a/im.nheko.Nheko.yaml
+++ b/im.nheko.Nheko.yaml
@@ -70,10 +70,12 @@ modules:
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMARK_TESTS=OFF
+ - -DBUILD_TESTING=OFF
+ - -DBUILD_SHARED_LIBS=OFF
sources:
- - sha256: bbcb8f8c03b5af33fcfcf11a74e9499f20a9043200b8552f78a6e8ba76e04d11
+ - sha256: 3da93db5469c30588cfeb283d9d62edfc6ded9eb0edc10a4f5bbfb7d722ea802
type: archive
- url: https://github.com/commonmark/cmark/archive/0.31.0.tar.gz
+ url: https://github.com/commonmark/cmark/archive/0.31.1.tar.gz
- name: fmt
buildsystem: cmake-ninja
config-opts:
@@ -211,8 +213,8 @@ modules:
- -DBUILD_SHARED_LIBS=OFF
buildsystem: cmake-ninja
sources:
- - commit: 15b43844f4ec27faa5f2ec92c4ded313206763aa
- tag: v0.10.1
+ - commit: 873911e352a0845dfb178f77b1ddea796a5d3455
+ #tag: v0.10.1
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git
- name: nheko
diff --git a/man/nheko.1.adoc b/man/nheko.1.adoc
index ac7dadff..a4bd0a65 100644
--- a/man/nheko.1.adoc
+++ b/man/nheko.1.adoc
@@ -7,7 +7,7 @@
== NAME
-nheko - Desktop client for Matrix using Qt and C++17
+nheko - Desktop client for Matrix using Qt and C++20
== SYNOPSIS
diff --git a/resources/icons/ui/alert.svg b/resources/icons/ui/alert.svg
new file mode 100644
index 00000000..7b730a59
--- /dev/null
+++ b/resources/icons/ui/alert.svg
@@ -0,0 +1 @@
+
diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts
index f06f278e..1cc35f80 100644
--- a/resources/langs/nheko_fr.ts
+++ b/resources/langs/nheko_fr.ts
@@ -17,7 +17,7 @@
You are screen sharing
- Vous êtes en train de partager votre écran.
+ Vous êtes en train de partager votre écran
@@ -312,7 +312,7 @@
Kicked user: %1
- L'utilisateur %1 a été expulsé.
+ L'utilisateur %1 a été expulsé
@@ -322,7 +322,7 @@
Banned user: %1
- L'utilisateur %1 a été banni.
+ L'utilisateur %1 a été banni
@@ -625,32 +625,32 @@ Eventuellement, vous pouvez fournir une explication de votre demande aux autres
Kick a user from the current room. Reason is optional. If user is left out, will try to kick the sender you are replying to.
-
+ Expulser un utilisateur de la salle actuelle. La raison est optionnelle. Si l'utilisateur est exclu, une tentative d'expulsion de l'utilisateur auquel vous êtes en train de répondre sera effectuée.
Ban a user from the current room. Reason is optional. If user is left out, will try to ban the sender you are replying to.
-
+ Bannir un utilisateur de la salle actuelle. La raison est optionnelle. Si l'utilisateur est exclu, une tentative de bannissement de l'utilisateur auquel vous êtes en train de répondre sera effectuée.
Unban a user in the current room. Reason is optional. If user is left out, will try to unban the sender you are replying to.
-
+ Annuler le banissement d'un utilisateur dans le salon actuel. La raison est optionnelle. Si l'utilisateur est exclu, une tentative d'annulation du bannissement de l'utilisateur auquel vous êtes en train de répondre sera effectuée.
Redact an event by event id or that you are replying to or all locally cached messages of a user.
-
+ Rédiger un événement grâce à un identifiant événement, ou celui auquel vous êtes en train de répondre, ou tous les messages de l'utilisateur mis en cache localement.
Block all invites from a user, a server, to a specific room or set the default.
-
+ Bloquer toutes les invitations en provenance d'un utilisateur ou d'un serveur pour un salon spécifique, ou bien définir le comportement par défaut.
Allow all invites from a user, a server, to a specific room or set the default.
-
+ Autoriser toutes les invitations en provenance d'un utilisateur ou d'un serveur pour un salon spécifique, ou bien définir le comportement par défaut.
@@ -750,12 +750,12 @@ Eventuellement, vous pouvez fournir une explication de votre demande aux autres
Send a message with a glitch effect.
-
+ Envoyer un message avec un effet de déformation.
Send a message that gradually glitches.
-
+ Envoyer un message qui se déforme progressivement.
@@ -2143,7 +2143,7 @@ Exemple : https://serveur.domaine.extension:8787
%1 replied with a spoiler.
Format a reply in a notification. %1 is the sender.
-
+ %1 a répondu avec un spoiler.
@@ -2250,7 +2250,7 @@ Exemple : https://serveur.domaine.extension:8787
User (%1)
- Utilisateur (%)
+ Utilisateur (%1)
@@ -3706,7 +3706,7 @@ Si vous choisissez de vérifier, vous aurez besoin de l'autre appareil. Si
Add or remove from community...
- Ajouter ou retirer de la communauté...
+ Ajouter ou retirer de la communauté...
@@ -4307,9 +4307,9 @@ Raison : %4
%n hour(s) later
-
-
-
+
+ %n heure plus tard
+ %n heures plus tard
@@ -5495,7 +5495,7 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l'applicati
Repeat File Password
-
+ Répéter le mot de passe du fichier
@@ -5686,7 +5686,7 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l'applicati
Message contains spoiler.
-
+ Le message contient un spoiler.
@@ -5755,13 +5755,13 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l'applicati
You sent a spoiler.
-
+ Vous avez envoyé un spoiler.
%1 sent a spoiler.
-
+ %1 a envoyé un spoiler.
@@ -5788,7 +5788,7 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l'applicati
* %1 spoils something.
-
+ * %1 a spoilé quelque chose.
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index c5c1689a..c79abbc2 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -57,6 +57,7 @@ Popup {
eventId: mid
userColor: TimelineManager.userColor(replyPreview.userId, palette.window)
maxWidth: parent.width
+ limitHeight: true
}
MatrixTextField {
id: roomTextInput
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 58ae90bb..4cc1445c 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -39,7 +39,7 @@ Rectangle {
anchors.fill: parent
spacing: 0
- visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
+ visible: room ? room.permissions.canSend(room.isEncrypted ? MtxEvent.Encrypted : MtxEvent.TextMessage) : false
ImageButton {
Layout.alignment: Qt.AlignBottom
@@ -170,15 +170,13 @@ Rectangle {
} else if (event.matches(StandardKey.SelectAll) && popup.opened) {
completer.completerName = "";
popup.close();
- } else if (event.matches(StandardKey.InsertLineSeparator)) {
- if (popup.opened)
- popup.close();
- if (Settings.invertEnterKey) {
- room.input.send();
- event.accepted = true;
- }
- } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
- if (popup.opened) {
+ } else if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) {
+ // Handling popup takes priority over newline and sending message.
+ if (popup.opened &&
+ (event.modifiers == Qt.NoModifier
+ || event.modifiers == Qt.ShiftModifier
+ || event.modifiers == Qt.ControlModifier)
+ ) {
var currentCompletion = completer.currentCompletion();
let userid = completer.currentUserid();
@@ -191,14 +189,26 @@ Rectangle {
console.log(userid);
room.input.addMention(userid, currentCompletion);
}
- event.accepted = true;
- return;
}
+ event.accepted = true;
}
- if (!Settings.invertEnterKey) {
+ // Send message Enter key combination event.
+ else if (Settings.sendMessageKey == 0 && event.modifiers == Qt.NoModifier
+ || Settings.sendMessageKey == 1 && event.modifiers == Qt.ShiftModifier
+ || Settings.sendMessageKey == 2 && event.modifiers == Qt.ControlModifier
+ ) {
room.input.send();
event.accepted = true;
}
+ // Add newline Enter key combination event.
+ else if (Settings.sendMessageKey == 0 && event.modifiers == Qt.ShiftModifier
+ || Settings.sendMessageKey == 1 && event.modifiers == Qt.NoModifier
+ || Settings.sendMessageKey == 2 && event.modifiers == Qt.ShiftModifier
+ ) {
+ messageInput.insert(messageInput.cursorPosition, "\n");
+ event.accepted = true;
+ }
+ // Any other Enter key combo is ignored here.
} else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) {
event.accepted = true;
if (popup.opened) {
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 8a457afb..c5377650 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -16,6 +16,7 @@ Item {
property int availableWidth: width
property int padding: Nheko.paddingMedium
property string searchString: ""
+ property bool filterByNotifications: false
property Room roommodel: room
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
@@ -60,7 +61,7 @@ Item {
boundsBehavior: Flickable.StopAtBounds
displayMarginBeginning: height / 4
displayMarginEnd: height / 4
- model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
+ model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent || filteredTimeline.filterByNotifications) ? filteredTimeline : room
//pixelAligned: true
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
@@ -145,6 +146,7 @@ Item {
id: filteredTimeline
filterByContent: chatRoot.searchString
+ filterByNotifications: chatRoot.filterByNotifications
filterByThread: room ? room.thread : ""
source: room
}
@@ -554,6 +556,8 @@ Item {
Component {
MenuItem {
text: qsTr("&Mark as read")
+
+ onTriggered: room.markEventAsRead(messageContextMenuC.eventId)
}
}
Component {
diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml
index 560cb133..722718bc 100644
--- a/resources/qml/TimelineBubbleMessageStyle.qml
+++ b/resources/qml/TimelineBubbleMessageStyle.qml
@@ -210,9 +210,10 @@ TimelineEvent {
AbstractButton {
id: replyRow
- visible: wrapper.reply
+ visible: wrapper.replyTo
+
+ leftPadding: Nheko.paddingSmall + 4
- height: replyLine.height
anchors.left: parent.left
anchors.right: parent.right
@@ -225,19 +226,7 @@ TimelineEvent {
cursorShape: Qt.PointingHandCursor
}
- contentItem: Row {
- id: replyRowLay
-
- spacing: Nheko.paddingSmall
-
- Rectangle {
- id: replyLine
- height: Math.min( wrapper.reply?.height, timelineView.height / 10) + Nheko.paddingSmall + replyUserButton.height
- color: replyRow.userColor
- width: 4
- }
-
- Column {
+ contentItem: Column {
spacing: 0
id: replyCol
@@ -247,7 +236,7 @@ TimelineEvent {
contentItem: Label {
id: userName_
- text: wrapper.reply?.userName ?? ''
+ text: wrapper.reply?.userName ?? 'missing name'
color: replyRow.userColor
textFormat: Text.RichText
width: wrapper.maxWidth
@@ -259,12 +248,20 @@ TimelineEvent {
replyUserButton,
wrapper.reply,
]
- }
}
background: Rectangle {
//width: replyRow.implicitContentWidth
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1))
+ Rectangle {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+
+ id: replyLine
+ color: replyRow.userColor
+ width: 4
+ }
}
onClicked: {
diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml
index 2bc0171a..49454ac0 100644
--- a/resources/qml/TimelineDefaultMessageStyle.qml
+++ b/resources/qml/TimelineDefaultMessageStyle.qml
@@ -192,9 +192,9 @@ TimelineEvent {
AbstractButton {
id: replyRow
- visible: wrapper.reply
+ visible: wrapper.replyTo
- height: replyLine.height
+ leftPadding: Nheko.paddingSmall + 4
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
@@ -205,19 +205,7 @@ TimelineEvent {
cursorShape: Qt.PointingHandCursor
}
- contentItem: Row {
- id: replyRowLay
-
- spacing: Nheko.paddingSmall
-
- Rectangle {
- id: replyLine
- height: Math.min( wrapper.reply?.height, timelineView.height / 10) + Nheko.paddingSmall + replyUserButton.height
- color: replyRow.userColor
- width: 4
- }
-
- Column {
+ contentItem: Column {
spacing: 0
id: replyCol
@@ -227,7 +215,7 @@ TimelineEvent {
contentItem: Label {
id: userName_
- text: wrapper.reply?.userName ?? ''
+ text: wrapper.reply?.userName ?? 'missing name'
color: replyRow.userColor
textFormat: Text.RichText
width: wrapper.maxWidth
@@ -239,12 +227,20 @@ TimelineEvent {
replyUserButton,
wrapper.reply,
]
- }
}
background: Rectangle {
- //width: replyRow.implicitContentWidth
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1))
+
+ Rectangle {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+
+ id: replyLine
+ color: replyRow.userColor
+ width: 4
+ }
}
onClicked: {
diff --git a/resources/qml/TimelineSectionHeader.qml b/resources/qml/TimelineSectionHeader.qml
index 20dbaf6a..2a95807c 100644
--- a/resources/qml/TimelineSectionHeader.qml
+++ b/resources/qml/TimelineSectionHeader.qml
@@ -97,7 +97,7 @@ Column {
sourceSize.height: height
permissions: room ? room.permissions : null
- visible: isAdmin || isModerator
+ visible: isAdmin || isModerator // implicitly includes creators as well
}
ToolTip.delay: Nheko.tooltipDelay
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 8e83cc1f..64493c4e 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -119,6 +119,7 @@ Item {
Layout.fillWidth: true
implicitHeight: msgView.height - typingIndicator.height
searchString: topBar.searchString
+ filterByNotifications: topBar.filterNotifications
}
Loader {
source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? (Qt.platform.os != "windows" ? "voip/VideoCall.qml" : "voip/VideoCallD3D11.qml") : ""
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 0bdd4ab8..cd20e94e 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -22,6 +22,7 @@ Pane {
property bool searchHasFocus: searchField.focus && searchField.enabled
property string searchString: ""
property bool showBackButton: false
+ property bool filterNotifications: false
property int trustlevel: room ? room.trustlevel : Crypto.Unverified
Layout.fillWidth: true
@@ -129,13 +130,30 @@ Pane {
selectByMouse: false
text: roomTopic
}
+ ImageButton {
+ id: notificationsButton
+
+ Layout.alignment: Qt.AlignRight
+ Layout.column: 3
+ Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+ Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+ Layout.row: 1
+ Layout.rowSpan: 2
+ ToolTip.text: qsTr("Show only notifications")
+ ToolTip.visible: hovered
+ image: ":/icons/icons/ui/alert.svg"
+
+ onClicked: {
+ topBar.filterNotifications = !topBar.filterNotifications
+ }
+ }
ImageButton {
id: pinButton
property bool pinsShown: !Settings.hiddenPins.includes(roomId)
Layout.alignment: Qt.AlignVCenter
- Layout.column: 3
+ Layout.column: 4
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
@@ -160,7 +178,7 @@ Pane {
}
AbstractButton {
id: memberButton
- Layout.column: 4
+ Layout.column: 5
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
@@ -200,7 +218,7 @@ Pane {
property bool searchActive: false
Layout.alignment: Qt.AlignVCenter
- Layout.column: 5
+ Layout.column: 6
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
@@ -224,7 +242,7 @@ Pane {
id: roomOptionsButton
Layout.alignment: Qt.AlignVCenter
- Layout.column: 6
+ Layout.column: 7
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
@@ -273,7 +291,7 @@ Pane {
id: pinnedMessages
Layout.column: 2
- Layout.columnSpan: 4
+ Layout.columnSpan: 5
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
Layout.row: 3
@@ -312,7 +330,7 @@ Pane {
ImageButton {
id: deletePinButton
- Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+ Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.preferredHeight: 16
Layout.preferredWidth: 16
ToolTip.text: qsTr("Unpin")
@@ -330,7 +348,7 @@ Pane {
id: widgets
Layout.column: 2
- Layout.columnSpan: 4
+ Layout.columnSpan: 5
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5)
Layout.row: 4
@@ -356,7 +374,7 @@ Pane {
id: searchField
Layout.column: 2
- Layout.columnSpan: 4
+ Layout.columnSpan: 5
Layout.fillWidth: true
Layout.row: 5
enabled: visible
@@ -378,6 +396,7 @@ Pane {
searchString = "";
searchButton.searchActive = false;
searchField.text = "";
+ filterNotifications = false;
}
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
diff --git a/resources/qml/components/PowerlevelIndicator.qml b/resources/qml/components/PowerlevelIndicator.qml
index 6a6d89af..01f3bc39 100644
--- a/resources/qml/components/PowerlevelIndicator.qml
+++ b/resources/qml/components/PowerlevelIndicator.qml
@@ -7,15 +7,16 @@ import QtQuick.Controls
import im.nheko
Image {
- required property int powerlevel
- required property var permissions
+ required property var powerlevel
+ required property Permissions permissions
+ readonly property bool isV12Creator: permissions ? permissions.creatorLevel() == powerlevel : false
readonly property bool isAdmin: permissions ? permissions.changeLevel(MtxEvent.PowerLevels) <= powerlevel : false
readonly property bool isModerator: permissions ? permissions.redactLevel() <= powerlevel : false
readonly property bool isDefault: permissions ? permissions.defaultLevel() <= powerlevel : false
readonly property string sourceUrl: {
- if (isAdmin)
+ if (isAdmin || isV12Creator)
return "image://colorimage/:/icons/icons/ui/ribbon_star.svg?";
else if (isModerator)
return "image://colorimage/:/icons/icons/ui/ribbon.svg?";
@@ -26,12 +27,15 @@ Image {
source: sourceUrl + (ma.hovered ? palette.highlight : palette.buttonText)
ToolTip.visible: ma.hovered
ToolTip.text: {
- if (isAdmin)
- return qsTr("Administrator: %1").arg(powerlevel);
+ let pl = powerlevel.toLocaleString(Qt.locale(), "f", 0);
+ if (isV12Creator)
+ return qsTr("Creator");
+ else if (isAdmin)
+ return qsTr("Administrator (%1)").arg(pl)
else if (isModerator)
- return qsTr("Moderator: %1").arg(powerlevel);
+ return qsTr("Moderator: %1").arg(pl);
else
- return qsTr("User: %1").arg(powerlevel);
+ return qsTr("User: %1").arg(pl);
}
HoverHandler {
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 7ee2a0a1..0bc2e4b9 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -5,6 +5,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Window
+import QtQuick.Layouts
import im.nheko
import "../"
@@ -21,7 +22,11 @@ AbstractButton {
property string userId: eventId ? room.dataById(eventId, Room.UserId, "") : ""
property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : ""
implicitHeight: replyContainer.height
- implicitWidth: replyContainer.implicitWidth
+ implicitWidth: replyContainer.implicitWidth + leftPadding + rightPadding
+
+ leftPadding: 4 + Nheko.paddingSmall
+ rightPadding: Nheko.paddingSmall
+
required property int maxWidth
property bool limitHeight: false
@@ -31,14 +36,14 @@ AbstractButton {
}
onClicked: {
- let link = reply.child.linkAt != undefined && reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight);
+ let link = timelineEvent.main.linkAt != undefined && timelineEvent.main.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight);
if (link) {
Nheko.openLink(link)
} else {
room.showEvent(r.eventId)
}
}
- onPressAndHold: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight), r.eventId)
+ onPressAndHold: replyContextMenu.show(timelineEvent.main.copyText, timelineEvent.main.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight), r.eventId)
contentItem: TimelineEvent {
id: timelineEvent
@@ -51,49 +56,37 @@ AbstractButton {
maxWidth: r.maxWidth
limitAsReply: r.limitHeight
- //height: replyContainer.implicitHeight
- data: Row {
+ data: Column {
id: replyContainer
-
- spacing: Nheko.paddingSmall
+ spacing: 0
clip: r.limitHeight
height: r.limitHeight ? Math.min( timelineEvent.main?.height, timelineView.height / 10) + Nheko.paddingSmall + usernameBtn.height : undefined
- Rectangle {
- id: colorline
+ AbstractButton {
+ id: usernameBtn
- width: 4
- height: content.height
-
- color: TimelineManager.userColor(r.userId, palette.base)
- }
-
- Column {
- id: content
- spacing: 0
-
- AbstractButton {
- id: usernameBtn
-
-
- contentItem: Label {
- id: userName_
- text: r.userName
- color: r.userColor
- textFormat: Text.RichText
- width: timelineEvent.main?.width
- }
- onClicked: room.openUserProfile(r.userId)
+ contentItem: Label {
+ id: userName_
+ // HACK: To ensure the username gets rendered in Qt 6.9.2,
+ // we need to always have some text in here. The name
+ // should never be empty, since it falls to the mxid, but
+ // if we have no text there, Qt culls the item, before we
+ // fill it...
+ text: r.userName || "."
+ color: r.userColor
+ textFormat: Text.RichText
+ width: timelineEvent.main?.width
}
-
- data: [
- usernameBtn, timelineEvent.main,
- ]
+ onClicked: room.openUserProfile(r.userId)
}
+ data: [
+ usernameBtn, timelineEvent.main,
+ ]
}
+
}
background: Rectangle {
@@ -103,6 +96,16 @@ AbstractButton {
property color userColor: TimelineManager.userColor(r.userId, palette.base)
property color bgColor: palette.base
color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1))
+
+ Rectangle {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+
+ id: colorline
+ color: backgroundItem.userColor
+ width: 4
+ }
}
}
diff --git a/resources/qml/dialogs/HiddenEventsDialog.qml b/resources/qml/dialogs/HiddenEventsDialog.qml
index a66a78f1..44e90a0c 100644
--- a/resources/qml/dialogs/HiddenEventsDialog.qml
+++ b/resources/qml/dialogs/HiddenEventsDialog.qml
@@ -112,6 +112,17 @@ ApplicationWindow {
checked: !hiddenEvents.hiddenEvents.includes(MtxEvent.Sticker)
onToggled: hiddenEvents.toggle(MtxEvent.Sticker)
}
+
+ MatrixText {
+ text: qsTr("Allowed server changes")
+ Layout.fillWidth: true
+ }
+
+ ToggleButton {
+ Layout.alignment: Qt.AlignRight
+ checked: !hiddenEvents.hiddenEvents.includes(MtxEvent.ServerAcl)
+ onToggled: hiddenEvents.toggle(MtxEvent.ServerAcl)
+ }
}
}
diff --git a/resources/qml/dialogs/PowerLevelEditor.qml b/resources/qml/dialogs/PowerLevelEditor.qml
index 17b19c25..694a259e 100644
--- a/resources/qml/dialogs/PowerLevelEditor.qml
+++ b/resources/qml/dialogs/PowerLevelEditor.qml
@@ -94,14 +94,17 @@ ApplicationWindow {
Text {
visible: !model.isType;
text: {
+ let pl = model.powerlevel.toLocaleString(Qt.locale(), "f", 0);
+ if (editingModel.creatorLevel == model.powerlevel)
+ return qsTr("Creator")
if (editingModel.adminLevel == model.powerlevel)
- return qsTr("Administrator (%1)").arg(model.powerlevel)
+ return qsTr("Administrator (%1)").arg(pl)
else if (editingModel.moderatorLevel == model.powerlevel)
- return qsTr("Moderator (%1)").arg(model.powerlevel)
+ return qsTr("Moderator (%1)").arg(pl)
else if (editingModel.defaultUserLevel == model.powerlevel)
- return qsTr("User (%1)").arg(model.powerlevel)
+ return qsTr("User (%1)").arg(pl)
else
- return qsTr("Custom (%1)").arg(model.powerlevel)
+ return qsTr("Custom (%1)").arg(pl)
}
color: palette.text
}
@@ -138,7 +141,7 @@ ApplicationWindow {
color: palette.text
- Keys.onPressed: {
+ Keys.onPressed: event => {
if (typeEntry.text.includes('.') && event.matches(StandardKey.InsertParagraphSeparator)) {
editingModel.types.add(typeEntry.index, typeEntry.text)
typeEntry.visible = false;
@@ -334,12 +337,17 @@ ApplicationWindow {
Text {
visible: !model.isUser;
text: {
+ let pl = model.powerlevel.toLocaleString(Qt.locale(), "f", 0);
+ if (editingModel.creatorLevel == model.powerlevel)
+ return qsTr("Creator")
if (editingModel.adminLevel == model.powerlevel)
- return qsTr("Administrator (%1)").arg(model.powerlevel)
+ return qsTr("Administrator (%1)").arg(pl)
else if (editingModel.moderatorLevel == model.powerlevel)
- return qsTr("Moderator (%1)").arg(model.powerlevel)
+ return qsTr("Moderator (%1)").arg(pl)
+ else if (editingModel.defaultUserLevel == model.powerlevel)
+ return qsTr("User (%1)").arg(pl)
else
- return qsTr("Custom (%1)").arg(model.powerlevel)
+ return qsTr("Custom (%1)").arg(pl)
}
color: palette.text
}
@@ -349,7 +357,7 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: 2
image: model.isUser ? ":/icons/icons/ui/dismiss.svg" : ":/icons/icons/ui/add-square-button.svg"
- visible: !model.isUser || model.removeable
+ visible: (!model.isUser || model.removeable) && model.powerlevel != editingModel.creatorLevel
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: model.isUser ? qsTr("Remove user") : qsTr("Add user")
diff --git a/resources/qml/dialogs/RoomSettingsDialog.qml b/resources/qml/dialogs/RoomSettingsDialog.qml
index 696cd9c9..ab24bbf2 100644
--- a/resources/qml/dialogs/RoomSettingsDialog.qml
+++ b/resources/qml/dialogs/RoomSettingsDialog.qml
@@ -304,13 +304,16 @@ ApplicationWindow {
}
ComboBox {
+ id: notificationsCombo
+ Layout.fillWidth: true
model: [qsTr("Muted"), qsTr("Mentions only"), qsTr("All messages")]
currentIndex: roomSettings.notifications
onActivated: (index) => {
roomSettings.changeNotifications(index);
}
- Layout.fillWidth: true
- WheelHandler{} // suppress scrolling changing values
+
+ // Disable built-in wheel handling unless focused
+ wheelEnabled: activeFocus
}
Label {
diff --git a/resources/qml/pages/UserSettingsPage.qml b/resources/qml/pages/UserSettingsPage.qml
index 30cfe230..8f51e668 100644
--- a/resources/qml/pages/UserSettingsPage.qml
+++ b/resources/qml/pages/UserSettingsPage.qml
@@ -89,7 +89,7 @@ Rectangle {
roleValue: UserSettingsModel.Toggle
ToggleButton {
checked: model.value
- onCheckedChanged: model.value = checked
+ onClicked: model.value = checked
enabled: model.enabled
}
}
@@ -100,10 +100,13 @@ Rectangle {
model: r.model.values
currentIndex: r.model.value
width: Math.min(implicitWidth, scroll.availableWidth - Nheko.paddingMedium)
- onCurrentIndexChanged: r.model.value = currentIndex
+ onActivated: {
+ r.model.value = currentIndex
+ }
implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted
- WheelHandler{} // suppress scrolling changing values
+ // Disable built-in wheel handling unless focused
+ wheelEnabled: activeFocus
}
}
DelegateChoice {
@@ -118,7 +121,7 @@ Rectangle {
onValueChanged: model.value = value
editable: true
- WheelHandler{} // suppress scrolling changing values
+ wheelEnabled: activeFocus
}
}
DelegateChoice {
@@ -135,7 +138,7 @@ Rectangle {
to: model.valueUpperBound * div
stepSize: model.valueStep * div
value: model.value * div
- onValueChanged: model.value = value/div
+ onValueModified: model.value = value/div
editable: true
property real realValue: value / div
@@ -153,7 +156,7 @@ Rectangle {
return Number.fromLocaleString(locale, text) * spinbox.div
}
- WheelHandler{} // suppress scrolling changing values
+ wheelEnabled: activeFocus
}
}
DelegateChoice {
@@ -272,6 +275,5 @@ Rectangle {
ToolTip.text: qsTr("Back")
onClicked: mainWindow.pop()
}
-
}
diff --git a/resources/res.qrc b/resources/res.qrc
index 642bc220..13d4c371 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -1,6 +1,7 @@
icons/ui/add-square-button.svg
+ icons/ui/alert.svg
icons/ui/angle-arrow-left.svg
icons/ui/attach.svg
icons/ui/ban.svg
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 84cad3ab..dac67ae4 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -2479,11 +2479,12 @@ try {
}
}
- updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString();
- updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString();
- updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
- updatedInfo.version = getRoomVersion(txn, statesdb).toStdString();
- updatedInfo.is_space = getRoomIsSpace(txn, statesdb);
+ updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString();
+ updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString();
+ updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+ updatedInfo.version = getRoomVersion(txn, statesdb).toStdString();
+ updatedInfo.is_space = getRoomIsSpace(txn, statesdb);
+ updatedInfo.is_tombstoned = getRoomIsTombstoned(txn, statesdb);
updatedInfo.notification_count = room.second.unread_notifications.notification_count;
updatedInfo.highlight_count = room.second.unread_notifications.highlight_count;
@@ -3623,7 +3624,7 @@ Cache::getRoomIsTombstoned(lmdb::txn &txn, lmdb::dbi &statesdb)
using namespace mtx::events::state;
std::string_view event;
- bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+ bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTombstone), event);
if (res) {
try {
@@ -4621,12 +4622,14 @@ Cache::updateSpaces(lmdb::txn &txn,
event.state_key.at(0) == '!') {
const std::string &space = event.state_key;
+ auto create = getStateEvent(txn, space)
+ .value_or(mtx::events::StateEvent{});
auto pls = getStateEvent(txn, space);
if (!pls)
continue;
- if (pls->content.user_level(event.sender) >=
+ if (pls->content.user_level(event.sender, create) >=
pls->content.state_level(space_event_type)) {
db->spacesChildren.put(txn, space, room);
db->spacesParents.put(txn, room, space);
@@ -4635,7 +4638,7 @@ Cache::updateSpaces(lmdb::txn &txn,
room,
space,
event.sender,
- pls->content.user_level(event.sender),
+ pls->content.user_level(event.sender, create),
pls->content.state_level(space_event_type));
}
}
@@ -4866,23 +4869,19 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes
int64_t min_event_level = std::numeric_limits::max();
int64_t user_level = std::numeric_limits::min();
- std::string_view event;
- bool res = db_.get(txn, to_string(EventType::RoomPowerLevels), event);
+ try {
+ StateEvent create = getStateEvent(txn, room_id)
+ .value_or(StateEvent{});
+ StateEvent pls =
+ getStateEvent(txn, room_id)
+ .value_or(StateEvent{});
- if (res) {
- try {
- StateEvent msg =
- nlohmann::json::parse(std::string_view(event.data(), event.size()))
- .get>();
+ user_level = pls.content.user_level(user_id, create);
- user_level = msg.content.user_level(user_id);
-
- for (const auto &ty : eventTypes)
- min_event_level =
- std::min(min_event_level, msg.content.state_level(to_string(ty)));
- } catch (const nlohmann::json::exception &e) {
- nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what());
- }
+ for (const auto &ty : eventTypes)
+ min_event_level = std::min(min_event_level, pls.content.state_level(to_string(ty)));
+ } catch (const nlohmann::json::exception &e) {
+ nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what());
}
return user_level >= min_event_level;
@@ -6410,6 +6409,7 @@ NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::JoinRules)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Name)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PinnedEvents)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PowerLevels)
+NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Create)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::ServerAcl)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Child)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Parent)
diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp
index 895cabbb..25cc556e 100644
--- a/src/CompletionProxyModel.cpp
+++ b/src/CompletionProxyModel.cpp
@@ -44,6 +44,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
auto string1 = sourceModel()
->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
.toString()
+ .normalized(QString::NormalizationForm_KD)
.toCaseFolded();
if (!string1.isEmpty()) {
trie_.insert(string1.toUcs4(), i);
@@ -53,6 +54,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
auto string2 = sourceModel()
->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
.toString()
+ .normalized(QString::NormalizationForm_KD)
.toCaseFolded();
if (!string2.isEmpty()) {
trie_.insert(string2.toUcs4(), i);
@@ -73,7 +75,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
&CompletionProxyModel::newSearchString,
this,
[this](const QString &s) {
- searchString_ = s.toCaseFolded();
+ searchString_ = s.normalized(QString::NormalizationForm_KD).toCaseFolded();
invalidate();
},
Qt::QueuedConnection);
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
index 1d939bfa..10487974 100644
--- a/src/MemberList.cpp
+++ b/src/MemberList.cpp
@@ -19,6 +19,9 @@ MemberListBackend::MemberListBackend(const QString &room_id, QObject *parent)
->getStateEvent(room_id_.toStdString())
.value_or(mtx::events::StateEvent{})
.content}
+ , create_{cache::client()
+ ->getStateEvent(room_id_.toStdString())
+ .value_or(mtx::events::StateEvent{})}
{
try {
info_ = cache::singleRoomInfo(room_id_.toStdString());
@@ -92,7 +95,7 @@ MemberListBackend::data(const QModelIndex &index, int role) const
}
case Powerlevel:
return static_cast(
- powerLevels_.user_level(m_memberList[index.row()].first.user_id.toStdString()));
+ powerLevels_.user_level(m_memberList[index.row()].first.user_id.toStdString(), create_));
default:
return {};
}
diff --git a/src/MemberList.h b/src/MemberList.h
index f1d39336..0ceaad5e 100644
--- a/src/MemberList.h
+++ b/src/MemberList.h
@@ -73,6 +73,7 @@ private:
bool loadingMoreMembers_{false};
mtx::events::state::PowerLevels powerLevels_;
+ mtx::events::StateEvent create_;
friend class MemberList;
};
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index d4dd2f16..ba4c4dd0 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -299,12 +299,10 @@ MxcImageProvider::download(const QString &id,
"/media_cache",
fileName);
QDir().mkpath(fileInfo.absolutePath());
+ QFile f(fileInfo.absoluteFilePath());
- if (fileInfo.exists()) {
+ if (fileInfo.exists() && f.open(QIODevice::ReadOnly)) {
if (encryptionInfo) {
- QFile f(fileInfo.absoluteFilePath());
- f.open(QIODevice::ReadOnly);
-
QByteArray fileData = f.readAll();
auto tempData = mtx::crypto::to_string(
mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value()));
diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp
index 2ecbdd53..cf8a6944 100644
--- a/src/PowerlevelsEditModels.cpp
+++ b/src/PowerlevelsEditModels.cpp
@@ -17,12 +17,15 @@
#include "Logging.h"
#include "MatrixClient.h"
-PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid,
- const mtx::events::state::PowerLevels &pl,
- QObject *parent)
+PowerlevelsTypeListModel::PowerlevelsTypeListModel(
+ const std::string &rid,
+ const mtx::events::state::PowerLevels &pl,
+ const mtx::events::StateEvent &create,
+ QObject *parent)
: QAbstractListModel(parent)
, room_id(rid)
, powerLevels_(pl)
+ , create_(create)
{
std::set seen_levels;
for (const auto &[type, level] : powerLevels_.events) {
@@ -40,6 +43,9 @@ PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid,
seen_levels.insert(level);
}
}
+ if (create_.content.room_version_creators_with_infinite_power()) {
+ seen_levels.insert(mtx::events::state::Creator);
+ }
for (const auto &level : {
powerLevels_.events_default,
@@ -354,12 +360,15 @@ PowerlevelsTypeListModel::moveRows(const QModelIndex &,
return true;
}
-PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid,
- const mtx::events::state::PowerLevels &pl,
- QObject *parent)
+PowerlevelsUserListModel::PowerlevelsUserListModel(
+ const std::string &rid,
+ const mtx::events::state::PowerLevels &pl,
+ const mtx::events::StateEvent &create,
+ QObject *parent)
: QAbstractListModel(parent)
, room_id(rid)
, powerLevels_(pl)
+ , create_(create)
{
std::set seen_levels;
for (const auto &[user, level] : powerLevels_.users) {
@@ -378,6 +387,16 @@ PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid,
}
}
+ if (create_.content.room_version_creators_with_infinite_power()) {
+ users.push_back(Entry{"", mtx::events::state::Creator});
+ seen_levels.insert(mtx::events::state::Creator);
+
+ users.push_back(Entry{create_.sender, mtx::events::state::Creator});
+ for (const auto &user : create.content.additional_creators) {
+ users.push_back(Entry{user, mtx::events::state::Creator});
+ }
+ }
+
for (const auto &level : {
powerLevels_.events_default,
powerLevels_.state_default,
@@ -408,7 +427,7 @@ PowerlevelsUserListModel::toUsers() const
{
std::map> m;
for (const auto &[key, pl] : std::as_const(users))
- if (key.size() > 0 && key.at(0) == '@')
+ if (key.size() > 0 && key.at(0) == '@' && pl != mtx::events::state::Creator)
m[key] = pl;
return m;
}
@@ -459,7 +478,7 @@ PowerlevelsUserListModel::data(const QModelIndex &index, int role) const
case IsUser:
return !user.mxid.empty();
case Moveable:
- return !user.mxid.empty();
+ return !user.mxid.empty() && user.pl != mtx::events::state::Creator;
case Removeable:
return !user.mxid.empty() && user.mxid.find('.') != std::string::npos;
}
@@ -554,7 +573,15 @@ PowerlevelsUserListModel::moveRows(const QModelIndex &,
if (users.at(sourceRow).mxid.empty())
return false;
- auto pl = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
+ if (users.at(sourceRow).pl == mtx::events::state::Creator)
+ return false;
+ if (users.at(destinationChild).pl == mtx::events::state::Creator)
+ return false;
+
+ auto pl = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
+ if (pl == mtx::events::state::Creator)
+ return false;
+
auto sourceItem = users.takeAt(sourceRow);
sourceItem.pl = pl;
@@ -577,9 +604,12 @@ PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *paren
->getStateEvent(room_id.toStdString())
.value_or(mtx::events::StateEvent{})
.content)
- , types_(room_id.toStdString(), powerLevels_, this)
- , users_(room_id.toStdString(), powerLevels_, this)
- , spaces_(room_id.toStdString(), powerLevels_, this)
+ , create_(cache::client()
+ ->getStateEvent(room_id.toStdString())
+ .value_or(mtx::events::StateEvent{}))
+ , types_(room_id.toStdString(), powerLevels_, create_, this)
+ , users_(room_id.toStdString(), powerLevels_, create_, this)
+ , spaces_(room_id.toStdString(), powerLevels_, create_, this)
, room_id_(room_id.toStdString())
{
connect(&types_,
@@ -678,16 +708,18 @@ samePl(const mtx::events::state::PowerLevels &a, const mtx::events::state::Power
b.redact);
}
-PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_id_,
- const mtx::events::state::PowerLevels &pl,
- QObject *parent)
+PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(
+ const std::string &room_id_,
+ const mtx::events::state::PowerLevels &pl,
+ const mtx::events::StateEvent &create,
+ QObject *parent)
: QAbstractListModel(parent)
, room_id(std::move(room_id_))
, oldPowerLevels_(std::move(pl))
{
beginResetModel();
- spaces.push_back(Entry{room_id, oldPowerLevels_, true});
+ spaces.push_back(Entry{room_id, oldPowerLevels_, create, true});
std::unordered_set visited;
@@ -703,10 +735,16 @@ PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_i
cache::client()->getStateEvent(s, space);
if (parent && parent->content.via && !parent->content.via->empty() &&
parent->content.canonical) {
- auto parentPl = cache::client()->getStateEvent(s);
+ auto childPl = cache::client()->getStateEvent(s);
+ auto childCreate =
+ cache::client()->getStateEvent(s).value_or(
+ mtx::events::StateEvent{});
- spaces.push_back(Entry{
- s, parentPl ? parentPl->content : mtx::events::state::PowerLevels{}, false});
+ spaces.push_back(
+ Entry{s,
+ childPl ? childPl->content : mtx::events::state::PowerLevels{},
+ childCreate,
+ false});
addChildren(s);
}
}
@@ -813,7 +851,7 @@ PowerlevelsSpacesListModel::data(QModelIndex const &index, int role) const
auto entry = spaces.at(row);
switch (role) {
case Roles::IsEditable:
- return entry.pl.user_level(http::client()->user_id().to_string()) >=
+ return entry.pl.user_level(http::client()->user_id().to_string(), entry.create) >=
entry.pl.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
case Roles::IsDifferentFromBase:
return !samePl(entry.pl, oldPowerLevels_);
diff --git a/src/PowerlevelsEditModels.h b/src/PowerlevelsEditModels.h
index 1fe075b7..edb3a821 100644
--- a/src/PowerlevelsEditModels.h
+++ b/src/PowerlevelsEditModels.h
@@ -29,9 +29,11 @@ public:
Removeable,
};
- explicit PowerlevelsTypeListModel(const std::string &room_id_,
- const mtx::events::state::PowerLevels &pl,
- QObject *parent = nullptr);
+ explicit PowerlevelsTypeListModel(
+ const std::string &room_id_,
+ const mtx::events::state::PowerLevels &pl,
+ const mtx::events::StateEvent &create,
+ QObject *parent = nullptr);
QHash roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast(types.size()); }
@@ -67,6 +69,7 @@ public:
std::string room_id;
QVector types;
mtx::events::state::PowerLevels powerLevels_;
+ mtx::events::StateEvent create_;
};
class PowerlevelsUserListModel final : public QAbstractListModel
@@ -88,9 +91,11 @@ public:
Removeable,
};
- explicit PowerlevelsUserListModel(const std::string &room_id_,
- const mtx::events::state::PowerLevels &pl,
- QObject *parent = nullptr);
+ explicit PowerlevelsUserListModel(
+ const std::string &room_id_,
+ const mtx::events::state::PowerLevels &pl,
+ const mtx::events::StateEvent &create,
+ QObject *parent = nullptr);
QHash roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast(users.size()); }
@@ -121,6 +126,7 @@ public:
std::string room_id;
QVector users;
mtx::events::state::PowerLevels powerLevels_;
+ mtx::events::StateEvent create_;
};
class PowerlevelsSpacesListModel final : public QAbstractListModel
@@ -147,9 +153,11 @@ public:
ApplyPermissions,
};
- explicit PowerlevelsSpacesListModel(const std::string &room_id_,
- const mtx::events::state::PowerLevels &pl,
- QObject *parent = nullptr);
+ explicit PowerlevelsSpacesListModel(
+ const std::string &room_id_,
+ const mtx::events::state::PowerLevels &pl,
+ const mtx::events::StateEvent &create,
+ QObject *parent = nullptr);
QHash roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast(spaces.size()); }
@@ -183,6 +191,7 @@ public:
std::string roomid;
mtx::events::state::PowerLevels pl;
+ mtx::events::StateEvent create;
bool apply = false;
};
@@ -203,6 +212,7 @@ class PowerlevelEditingModels final : public QObject
Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT)
Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT)
Q_PROPERTY(PowerlevelsSpacesListModel *spaces READ spaces CONSTANT)
+ Q_PROPERTY(qlonglong creatorLevel READ creatorLevel CONSTANT)
Q_PROPERTY(qlonglong adminLevel READ adminLevel NOTIFY adminLevelChanged)
Q_PROPERTY(qlonglong moderatorLevel READ moderatorLevel NOTIFY moderatorLevelChanged)
Q_PROPERTY(qlonglong defaultUserLevel READ defaultUserLevel NOTIFY defaultUserLevelChanged)
@@ -222,6 +232,7 @@ public:
PowerlevelsUserListModel *users() { return &users_; }
PowerlevelsTypeListModel *types() { return &types_; }
PowerlevelsSpacesListModel *spaces() { return &spaces_; }
+ qlonglong creatorLevel() const { return mtx::events::state::Creator; }
qlonglong adminLevel() const
{
return powerLevels_.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
@@ -235,6 +246,7 @@ public:
Q_INVOKABLE void addRole(int pl);
mtx::events::state::PowerLevels powerLevels_;
+ mtx::events::StateEvent create_;
PowerlevelsTypeListModel types_;
PowerlevelsUserListModel users_;
PowerlevelsSpacesListModel spaces_;
diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp
index 5fe6b4dc..4f38c63d 100644
--- a/src/TrayIcon.cpp
+++ b/src/TrayIcon.cpp
@@ -107,13 +107,18 @@ TrayIcon::TrayIcon(const QString &filename, QWindow *parent)
QMenu *menu = new QMenu();
setContextMenu(menu);
- viewAction_ = new QAction(tr("Show"), this);
- quitAction_ = new QAction(tr("Quit"), this);
+ toggleAction_ = new QAction(tr("Show"), this);
+ quitAction_ = new QAction(tr("Quit"), this);
- connect(viewAction_, &QAction::triggered, parent, &QWindow::show);
+ connect(parent, &QWindow::visibleChanged, toggleAction_, [=, this] {
+ toggleAction_->setText(tr(parent->isVisible() ? "Hide" : "Show"));
+ });
+ connect(toggleAction_, &QAction::triggered, parent, [=] {
+ parent->isVisible() ? parent->hide() : parent->show();
+ });
connect(quitAction_, &QAction::triggered, this, QApplication::quit);
- menu->addAction(viewAction_);
+ menu->addAction(toggleAction_);
menu->addAction(quitAction_);
QString toolTip = QLatin1String("nheko");
diff --git a/src/TrayIcon.h b/src/TrayIcon.h
index 7c0bc7b2..5ed0dad1 100644
--- a/src/TrayIcon.h
+++ b/src/TrayIcon.h
@@ -40,7 +40,7 @@ public slots:
void setUnreadCount(int count);
private:
- QAction *viewAction_;
+ QAction *toggleAction_;
QAction *quitAction_;
int previousCount = 0;
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 705a605b..db3bf7f1 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -25,10 +25,24 @@
#include "config/nheko.h"
+QStringList themes{
+ QStringLiteral("light"),
+ QStringLiteral("dark"),
+ QStringLiteral("system"),
+};
+
QSharedPointer UserSettings::instance_;
UserSettings::UserSettings()
{
+ if (settings.contains("user/invert_enter_key")) {
+ auto oldValue =
+ (settings.value("user/invert_enter_key", false).toBool() ? SendMessageKey::ShiftEnter
+ : SendMessageKey::Enter);
+ settings.setValue("user/send_message_key", static_cast(oldValue));
+ settings.remove("user/invert_enter_key");
+ }
+
connect(
QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); });
}
@@ -65,8 +79,13 @@ UserSettings::load(std::optional profile)
settings.value("user/timeline/message_hover_highlight", false).toBool();
enlargeEmojiOnlyMessages_ =
settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool();
- markdown_ = settings.value("user/markdown_enabled", true).toBool();
- invertEnterKey_ = settings.value("user/invert_enter_key", false).toBool();
+ markdown_ = settings.value("user/markdown_enabled", true).toBool();
+
+ auto sendMessageKey = settings.value("user/send_message_key", 0).toInt();
+ if (sendMessageKey < 0 || sendMessageKey > 2)
+ sendMessageKey = static_cast(SendMessageKey::Enter);
+ sendMessageKey_ = static_cast(sendMessageKey);
+
bubbles_ = settings.value("user/bubbles_enabled", false).toBool();
smallAvatars_ = settings.value("user/small_avatars_enabled", false).toBool();
animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool();
@@ -334,13 +353,12 @@ UserSettings::setMarkdown(bool state)
}
void
-UserSettings::setInvertEnterKey(bool state)
+UserSettings::setSendMessageKey(SendMessageKey key)
{
- if (state == invertEnterKey_)
+ if (key == sendMessageKey_)
return;
-
- invertEnterKey_ = state;
- emit invertEnterKeyChanged(state);
+ sendMessageKey_ = key;
+ emit sendMessageKeyChanged(key);
save();
}
@@ -640,7 +658,7 @@ UserSettings::setShowImage(ShowImage state)
void
UserSettings::setTheme(QString theme)
{
- if (theme == theme_)
+ if (theme == theme_ || !themes.contains(theme))
return;
theme_ = theme;
save();
@@ -930,7 +948,7 @@ UserSettings::save()
settings.setValue("group_view", groupView_);
settings.setValue("scrollbars_in_roomlist", scrollbarsInRoomlist_);
settings.setValue("markdown_enabled", markdown_);
- settings.setValue("invert_enter_key", invertEnterKey_);
+ settings.setValue("send_message_key", static_cast(sendMessageKey_));
settings.setValue("bubbles_enabled", bubbles_);
settings.setValue("small_avatars_enabled", smallAvatars_);
settings.setValue("animate_images_on_hover", animateImagesOnHover_);
@@ -1044,8 +1062,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr("Scrollbars in room list");
case Markdown:
return tr("Send messages as Markdown");
- case InvertEnterKey:
- return tr("Use shift+enter to send and enter to start a new line");
+ case SendMessageKey:
+ return tr("Send messages with a shortcut");
case Bubbles:
return tr("Enable message bubbles");
case SmallAvatars:
@@ -1182,12 +1200,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
} else if (role == Value) {
switch (index.row()) {
case Theme:
- return QStringList{
- QStringLiteral("light"),
- QStringLiteral("dark"),
- QStringLiteral("system"),
- }
- .indexOf(i->theme());
+ return themes.indexOf(i->theme());
case ScaleFactor:
return utils::scaleFactor();
case MessageHoverHighlight:
@@ -1204,8 +1217,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return i->scrollbarsInRoomlist();
case Markdown:
return i->markdown();
- case InvertEnterKey:
- return i->invertEnterKey();
+ case SendMessageKey:
+ return static_cast(i->sendMessageKey());
case Bubbles:
return i->bubbles();
case SmallAvatars:
@@ -1370,10 +1383,11 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr(
"Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
"text.");
- case InvertEnterKey:
+ case SendMessageKey:
return tr(
- "Invert the behavior of the enter key in the text input, making it send the message "
- "when shift+enter is pressed and starting a new line when enter is pressed.");
+ "Select what Enter key combination sends the message. Shift+Enter adds a new line, "
+ "unless it has been selected, in which case Enter adds a new line instead.\n\n"
+ "If an emoji picker or a mention picker is open, it is always handled first.");
case Bubbles:
return tr(
"Messages get a bubble background. This also triggers some layout changes (WIP).");
@@ -1541,6 +1555,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case CameraFrameRate:
case Ringtone:
case ShowImage:
+ case SendMessageKey:
return Options;
case TimelineMaxWidth:
case PrivacyScreenTimeout:
@@ -1555,7 +1570,6 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case GroupView:
case ScrollbarsInRoomlist:
case Markdown:
- case InvertEnterKey:
case Bubbles:
case SmallAvatars:
case AnimateImagesOnHover:
@@ -1674,6 +1688,12 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
tr("Only in private rooms"),
tr("Never"),
};
+ case SendMessageKey:
+ return QStringList{
+ tr("Enter"),
+ tr("Shift+Enter"),
+ tr("Ctrl+Enter"),
+ };
case Microphone:
return vecToList(CallDevices::instance().names(false, i->microphone().toStdString()));
case Camera:
@@ -1741,14 +1761,10 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
if (role == Value) {
switch (index.row()) {
case Theme: {
- if (value == 0) {
- i->setTheme("light");
- return true;
- } else if (value == 1) {
- i->setTheme("dark");
- return true;
- } else if (value == 2) {
- i->setTheme("system");
+ auto idx = value.toInt();
+
+ if (idx >= 0 && idx < themes.size()) {
+ i->setTheme(themes[idx]);
return true;
} else
return false;
@@ -1819,12 +1835,14 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
} else
return false;
}
- case InvertEnterKey: {
- if (value.userType() == QMetaType::Bool) {
- i->setInvertEnterKey(value.toBool());
- return true;
- } else
+ case SendMessageKey: {
+ auto newKey = value.toInt();
+ if (newKey < 0 ||
+ QMetaEnum::fromType().keyCount() <= newKey)
return false;
+
+ i->setSendMessageKey(static_cast(newKey));
+ return true;
}
case Bubbles: {
if (value.userType() == QMetaType::Bool) {
@@ -2309,8 +2327,8 @@ UserSettingsModel::UserSettingsModel(QObject *p)
connect(s.get(), &UserSettings::markdownChanged, this, [this]() {
emit dataChanged(index(Markdown), index(Markdown), {Value});
});
- connect(s.get(), &UserSettings::invertEnterKeyChanged, this, [this]() {
- emit dataChanged(index(InvertEnterKey), index(InvertEnterKey), {Value});
+ connect(s.get(), &UserSettings::sendMessageKeyChanged, this, [this]() {
+ emit dataChanged(index(SendMessageKey), index(SendMessageKey), {Value});
});
connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
emit dataChanged(index(Bubbles), index(Bubbles), {Value});
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 63a4d616..c1c198d2 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -29,8 +29,8 @@ class UserSettings final : public QObject
Q_PROPERTY(bool scrollbarsInRoomlist READ scrollbarsInRoomlist WRITE setScrollbarsInRoomlist
NOTIFY scrollbarsInRoomlistChanged)
Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
- Q_PROPERTY(
- bool invertEnterKey READ invertEnterKey WRITE setInvertEnterKey NOTIFY invertEnterKeyChanged)
+ Q_PROPERTY(SendMessageKey sendMessageKey READ sendMessageKey WRITE setSendMessageKey NOTIFY
+ sendMessageKeyChanged)
Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged)
Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
@@ -166,6 +166,14 @@ public:
};
Q_ENUM(ShowImage)
+ enum class SendMessageKey
+ {
+ Enter,
+ ShiftEnter,
+ CtrlEnter,
+ };
+ Q_ENUM(SendMessageKey)
+
void save();
void load(std::optional profile);
void applyTheme();
@@ -182,7 +190,7 @@ public:
void setGroupView(bool state);
void setScrollbarsInRoomlist(bool state);
void setMarkdown(bool state);
- void setInvertEnterKey(bool state);
+ void setSendMessageKey(SendMessageKey key);
void setBubbles(bool state);
void setSmallAvatars(bool state);
void setAnimateImagesOnHover(bool state);
@@ -255,7 +263,7 @@ public:
bool privacyScreen() const { return privacyScreen_; }
int privacyScreenTimeout() const { return privacyScreenTimeout_; }
bool markdown() const { return markdown_; }
- bool invertEnterKey() const { return invertEnterKey_; }
+ SendMessageKey sendMessageKey() const { return sendMessageKey_; }
bool bubbles() const { return bubbles_; }
bool smallAvatars() const { return smallAvatars_; }
bool animateImagesOnHover() const { return animateImagesOnHover_; }
@@ -328,7 +336,7 @@ signals:
void trayChanged(bool state);
void startInTrayChanged(bool state);
void markdownChanged(bool state);
- void invertEnterKeyChanged(bool state);
+ void sendMessageKeyChanged(SendMessageKey key);
void bubblesChanged(bool state);
void smallAvatarsChanged(bool state);
void animateImagesOnHoverChanged(bool state);
@@ -399,7 +407,7 @@ private:
bool groupView_;
bool scrollbarsInRoomlist_;
bool markdown_;
- bool invertEnterKey_;
+ SendMessageKey sendMessageKey_;
bool bubbles_;
bool smallAvatars_;
bool animateImagesOnHover_;
@@ -510,7 +518,7 @@ class UserSettingsModel : public QAbstractListModel
TypingNotifications,
ReadReceipts,
Markdown,
- InvertEnterKey,
+ SendMessageKey,
Bubbles,
SmallAvatars,
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 2bec9d36..4556b680 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -1453,6 +1453,9 @@ utils::roomVias(const std::string &roomid)
auto powerlevels =
cache::client()->getStateEvent(roomid).value_or(
mtx::events::StateEvent{});
+ auto create =
+ cache::client()->getStateEvent(roomid).value_or(
+ mtx::events::StateEvent{});
auto acls = cache::client()->getStateEvent(roomid);
std::vector allowedServers;
@@ -1501,6 +1504,19 @@ utils::roomVias(const std::string &roomid)
std::set users_with_high_pl_in_room;
// we should pick PL > 50, but imo that is broken, so we just pick users who have admins
// perm
+ if (create.content.room_version_creators_with_infinite_power()) {
+ {
+ auto user = create.sender;
+ auto host = mtx::identifiers::parse(user).hostname();
+ if (isHostAllowed(host))
+ users_with_high_pl.insert(user);
+ }
+ for (const auto &user : create.content.additional_creators) {
+ auto host = mtx::identifiers::parse(user).hostname();
+ if (isHostAllowed(host))
+ users_with_high_pl.insert(user);
+ }
+ }
for (const auto &user : powerlevels.content.users) {
if (user.second >= powerlevels.content.events_default &&
user.second >= powerlevels.content.state_default) {
@@ -1525,12 +1541,13 @@ utils::roomVias(const std::string &roomid)
});
// add the highest powerlevel user
- auto max_pl_user = std::max_element(
- users_with_high_pl_in_room.begin(),
- users_with_high_pl_in_room.end(),
- [&pl_content = powerlevels.content](const std::string &a, const std::string &b) {
- return pl_content.user_level(a) < pl_content.user_level(b);
- });
+ auto max_pl_user = std::max_element(users_with_high_pl_in_room.begin(),
+ users_with_high_pl_in_room.end(),
+ [&pl_content = powerlevels.content, &create](
+ const std::string &a, const std::string &b) {
+ return pl_content.user_level(a, create) <
+ pl_content.user_level(b, create);
+ });
if (max_pl_user != users_with_high_pl_in_room.end()) {
auto host =
mtx::identifiers::parse(*max_pl_user).hostname();
@@ -1705,11 +1722,15 @@ utils::updateSpaceVias()
auto spaceid = roomid.toStdString();
+ auto create = cache::client()->getStateEvent(spaceid).value_or(
+ mtx::events::StateEvent{});
+
if (auto pl = cache::client()
->getStateEvent(spaceid)
.value_or(mtx::events::StateEvent{})
.content;
- pl.user_level(us) < pl.state_level(to_string(mtx::events::EventType::SpaceChild)))
+ pl.user_level(us, create) <
+ pl.state_level(to_string(mtx::events::EventType::SpaceChild)))
continue;
auto children = cache::client()->getChildRoomIds(spaceid);
@@ -1748,12 +1769,16 @@ utils::updateSpaceVias()
parent->origin_server_ts < weekAgo &&
// ignore unset spaces
(parent->content.via && !parent->content.via->empty())) {
+ auto childCreate =
+ cache::client()->getStateEvent(spaceid).value_or(
+ mtx::events::StateEvent{});
+
if (auto pl =
cache::client()
->getStateEvent(childid)
.value_or(mtx::events::StateEvent{})
.content;
- pl.user_level(us) <
+ pl.user_level(us, childCreate) <
pl.state_level(to_string(mtx::events::EventType::SpaceParent)))
continue;
@@ -2041,11 +2066,15 @@ utils::removeExpiredEvents()
if (!asus->globalExpiry && !getExpEv(roomid))
continue;
+ auto create = cache::client()->getStateEvent(roomid).value_or(
+ mtx::events::StateEvent{});
+
if (auto pl = cache::client()
->getStateEvent(roomid)
.value_or(mtx::events::StateEvent{})
.content;
- pl.user_level(us) < pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) {
+ pl.user_level(us, create) <
+ pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) {
nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid);
continue;
}
diff --git a/src/dbus/NhekoDBusApi.cpp b/src/dbus/NhekoDBusApi.cpp
index c2c62eb7..1da550cf 100644
--- a/src/dbus/NhekoDBusApi.cpp
+++ b/src/dbus/NhekoDBusApi.cpp
@@ -170,6 +170,14 @@ setStatusMessage(const QString &message)
interface.call(QDBus::NoBlock, QStringLiteral("setStatusMessage"), message);
}
+void
+setTheme(const QString &theme)
+{
+ if (QDBusInterface interface{QStringLiteral(NHEKO_DBUS_SERVICE_NAME), QStringLiteral("/")};
+ interface.isValid())
+ interface.call(QDBus::NoBlock, QStringLiteral("setTheme"), theme);
+}
+
} // nheko::dbus
/**
diff --git a/src/dbus/NhekoDBusApi.h b/src/dbus/NhekoDBusApi.h
index 6acb2b65..74e6aeee 100644
--- a/src/dbus/NhekoDBusApi.h
+++ b/src/dbus/NhekoDBusApi.h
@@ -85,6 +85,9 @@ statusMessage();
//! Sets the user's status message (if supported by the homeserver).
void
setStatusMessage(const QString &message);
+//! Sets the current theme (supported values: "light", "dark" or "system")
+void
+setTheme(const QString &theme);
QDBusArgument &
operator<<(QDBusArgument &arg, const RoomInfoItem &item);
diff --git a/src/dbus/NhekoDBusBackend.cpp b/src/dbus/NhekoDBusBackend.cpp
index 898286f8..9831d5e6 100644
--- a/src/dbus/NhekoDBusBackend.cpp
+++ b/src/dbus/NhekoDBusBackend.cpp
@@ -11,6 +11,7 @@
#include "Logging.h"
#include "MainWindow.h"
#include "MxcImageProvider.h"
+#include "UserSettingsPage.h"
#include "timeline/RoomlistModel.h"
#include "timeline/TimelineModel.h"
@@ -112,6 +113,12 @@ NhekoDBusBackend::setStatusMessage(const QString &message)
ChatPage::instance()->setStatus(message);
}
+void
+NhekoDBusBackend::setTheme(const QString &theme)
+{
+ UserSettings::instance()->setTheme(theme);
+}
+
void
NhekoDBusBackend::bringWindowToTop() const
{
diff --git a/src/dbus/NhekoDBusBackend.h b/src/dbus/NhekoDBusBackend.h
index 79d396f8..66b239aa 100644
--- a/src/dbus/NhekoDBusBackend.h
+++ b/src/dbus/NhekoDBusBackend.h
@@ -40,6 +40,8 @@ public slots:
Q_SCRIPTABLE QString statusMessage() const;
//! Sets the user's status message.
Q_SCRIPTABLE void setStatusMessage(const QString &message);
+ //! Sets the current theme (supported values: "light", "dark" or "system")
+ Q_SCRIPTABLE void setTheme(const QString &theme);
private:
void bringWindowToTop() const;
diff --git a/src/encryption/Olm.cpp b/src/encryption/Olm.cpp
index d4074933..52fd3a53 100644
--- a/src/encryption/Olm.cpp
+++ b/src/encryption/Olm.cpp
@@ -398,17 +398,19 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
body[local_user][dev] = secretRequest;
}
- http::client()->send_to_device(
- http::client()->generate_txn_id(),
- body,
- [secret_name](mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->error("Failed to send request cancellation "
- "for secrect "
- "'{}'",
- secret_name);
- }
- });
+ if (!body.empty()) {
+ http::client()->send_to_device(
+ http::client()->generate_txn_id(),
+ body,
+ [secret_name](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error("Failed to send request cancellation "
+ "for secrect "
+ "'{}'",
+ secret_name);
+ }
+ });
+ }
nhlog::crypto()->info("Storing secret {}", secret_name);
cache::client()->storeSecret(secret_name, e->content.secret);
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 52e7fb77..bc086a14 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -166,6 +166,11 @@ EventStore::EventStore(std::string room_id, QObject *)
nhlog::ui()->debug("failing txn id '{}'", txn_id);
cache::client()->removePendingStatus(room_id_, txn_id);
current_txn_error_count = 0;
+
+ auto idx = idToIndex(txn_id);
+
+ if (idx)
+ emit dataChanged(*idx, *idx);
}
}
QTimer::singleShot(1000, this, [this]() {
diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp
index 2ef6e5cd..5e8bd169 100644
--- a/src/timeline/Permissions.cpp
+++ b/src/timeline/Permissions.cpp
@@ -4,6 +4,8 @@
#include "Permissions.h"
+#include
+
#include "Cache_p.h"
#include "MatrixClient.h"
#include "TimelineModel.h"
@@ -22,44 +24,53 @@ Permissions::invalidate()
->getStateEvent(roomId_.toStdString())
.value_or(mtx::events::StateEvent{})
.content;
+ create = cache::client()
+ ->getStateEvent(roomId_.toStdString())
+ .value_or(mtx::events::StateEvent{});
}
bool
Permissions::canInvite()
{
- return pl.user_level(http::client()->user_id().to_string()) >= pl.invite;
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.invite;
+ return plCheck;
}
bool
Permissions::canBan()
{
- return pl.user_level(http::client()->user_id().to_string()) >= pl.ban;
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.ban;
+ return plCheck;
}
bool
Permissions::canKick()
{
- return pl.user_level(http::client()->user_id().to_string()) >= pl.kick;
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.kick;
+ return plCheck;
}
bool
Permissions::canRedact()
{
- return pl.user_level(http::client()->user_id().to_string()) >= pl.redact;
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.redact;
+ return plCheck;
}
bool
Permissions::canChange(int eventType)
{
- return pl.user_level(http::client()->user_id().to_string()) >=
- pl.state_level(to_string(
- qml_mtx_events::fromRoomEventType(static_cast(eventType))));
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >=
+ pl.state_level(to_string(qml_mtx_events::fromRoomEventType(
+ static_cast(eventType))));
+ return plCheck;
}
bool
Permissions::canSend(int eventType)
{
- return pl.user_level(http::client()->user_id().to_string()) >=
- pl.event_level(to_string(
- qml_mtx_events::fromRoomEventType(static_cast(eventType))));
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >=
+ pl.event_level(to_string(qml_mtx_events::fromRoomEventType(
+ static_cast(eventType))));
+ return plCheck;
}
int
@@ -88,8 +99,9 @@ Permissions::sendLevel(int eventType)
bool
Permissions::canPingRoom()
{
- return pl.user_level(http::client()->user_id().to_string()) >=
- pl.notification_level(mtx::events::state::notification_keys::room);
+ const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >=
+ pl.notification_level(mtx::events::state::notification_keys::room);
+ return plCheck;
}
#include "moc_Permissions.cpp"
diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h
index 05513524..2829bedd 100644
--- a/src/timeline/Permissions.h
+++ b/src/timeline/Permissions.h
@@ -5,7 +5,10 @@
#pragma once
#include
+#include
+#include
+#include
#include
class TimelineModel;
@@ -13,6 +16,8 @@ class TimelineModel;
class Permissions final : public QObject
{
Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Only to be used to refer to C++ values")
public:
Permissions(QString roomId, QObject *parent = nullptr);
@@ -28,14 +33,20 @@ public:
Q_INVOKABLE int redactLevel();
Q_INVOKABLE int changeLevel(int eventType);
Q_INVOKABLE int sendLevel(int eventType);
+ Q_INVOKABLE qint64 creatorLevel() const { return mtx::events::state::Creator; }
Q_INVOKABLE bool canPingRoom();
void invalidate();
const mtx::events::state::PowerLevels &powerlevelEvent() const { return pl; };
+ const mtx::events::StateEvent &createEvent() const
+ {
+ return create;
+ };
private:
QString roomId_;
mtx::events::state::PowerLevels pl;
+ mtx::events::StateEvent create;
};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 823ffe20..ce2a8f84 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -977,6 +977,10 @@ FilteredRoomlistModel::getRoomById(const QString &id) const
void
FilteredRoomlistModel::updateHiddenTagsAndSpaces()
{
+#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ beginFilterChange();
+#endif
+
hiddenTags.clear();
hiddenSpaces.clear();
hideDMs = false;
@@ -991,7 +995,11 @@ FilteredRoomlistModel::updateHiddenTagsAndSpaces()
hideDMs = true;
}
+#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
+ endFilterChange();
+#else
invalidateFilter();
+#endif
}
bool
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index c3f485ef..7dfeb707 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -212,6 +212,10 @@ public slots:
void updateFilterTag(QString tagId)
{
+#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
+ beginFilterChange();
+#endif
+
if (tagId.startsWith(QLatin1String("tag:"))) {
filterType = FilterBy::Tag;
filterStr = tagId.mid(4);
@@ -227,7 +231,11 @@ public slots:
filterStr.clear();
}
+#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
+ endFilterChange();
+#else
invalidateFilter();
+#endif
}
void updateHiddenTagsAndSpaces();
diff --git a/src/timeline/TimelineFilter.cpp b/src/timeline/TimelineFilter.cpp
index 0833900e..b9e93859 100644
--- a/src/timeline/TimelineFilter.cpp
+++ b/src/timeline/TimelineFilter.cpp
@@ -41,7 +41,13 @@ TimelineFilter::startFiltering()
{
incrementalSearchIndex = 0;
emit isFilteringChanged();
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
+ endFilterChange();
+#else
invalidateFilter();
+#endif
+
beginResetModel();
endResetModel();
@@ -96,6 +102,10 @@ TimelineFilter::setThreadId(const QString &t)
{
nhlog::ui()->debug("Filtering by thread '{}'", t.toStdString());
if (this->threadId != t) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ beginFilterChange();
+#endif
+
this->threadId = t;
emit threadIdChanged();
@@ -104,11 +114,30 @@ TimelineFilter::setThreadId(const QString &t)
}
}
+void
+TimelineFilter::setFilterNotifications(bool filter)
+{
+ nhlog::ui()->debug("Filtering by notifications '{}'", filter);
+ if (this->filterByNotifications_ != filter) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ beginFilterChange();
+#endif
+ this->filterByNotifications_ = filter;
+
+ emit filterNotificationsChanged();
+ startFiltering();
+ fetchMore({});
+ }
+}
+
void
TimelineFilter::setContentFilter(const QString &c)
{
nhlog::ui()->debug("Filtering by content '{}'", c.toStdString());
if (this->contentFilter != c) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ beginFilterChange();
+#endif
this->contentFilter = c;
emit contentFilterChanged();
@@ -145,7 +174,8 @@ TimelineFilter::sourceDataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QVector &roles)
{
- if (!roles.contains(TimelineModel::Roles::Body) && !roles.contains(TimelineModel::ThreadId))
+ if (!roles.contains(TimelineModel::Roles::Body) && !roles.contains(TimelineModel::ThreadId) &&
+ !roles.contains(TimelineModel::Notificationlevel))
return;
if (auto s = source()) {
@@ -157,6 +187,10 @@ void
TimelineFilter::setSource(TimelineModel *s)
{
if (auto orig = this->source(); orig != s) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
+ beginFilterChange();
+#endif
+
cachedCount = 0;
incrementalSearchIndex = 0;
@@ -191,7 +225,12 @@ TimelineFilter::setSource(TimelineModel *s)
emit sourceChanged();
emit isFilteringChanged();
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
+ endFilterChange();
+#else
invalidateFilter();
+#endif
}
}
@@ -233,19 +272,27 @@ TimelineFilter::filterAcceptsRow(int source_row, const QModelIndex &) const
if (source_row > incrementalSearchIndex)
return false;
- if (threadId.isEmpty() && contentFilter.isEmpty())
+ if (threadId.isEmpty() && contentFilter.isEmpty() && !filterByNotifications_)
return true;
if (auto s = sourceModel()) {
auto idx = s->index(source_row, 0);
+
if (!contentFilter.isEmpty() && !s->data(idx, TimelineModel::Body)
.toString()
.contains(contentFilter, Qt::CaseInsensitive)) {
return false;
}
- if (threadId.isEmpty())
+ if (filterByNotifications_ && s->data(idx, TimelineModel::Notificationlevel)
+ .value() !=
+ qml_mtx_events::NotificationLevel::Highlight) {
+ return false;
+ }
+
+ if (threadId.isEmpty()) {
return true;
+ }
return s->data(idx, TimelineModel::EventId) == threadId ||
s->data(idx, TimelineModel::ThreadId) == threadId;
diff --git a/src/timeline/TimelineFilter.h b/src/timeline/TimelineFilter.h
index 336339e2..76d5bf52 100644
--- a/src/timeline/TimelineFilter.h
+++ b/src/timeline/TimelineFilter.h
@@ -18,6 +18,8 @@ class TimelineFilter : public QSortFilterProxyModel
QML_ELEMENT
Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged)
+ Q_PROPERTY(bool filterByNotifications READ filterByNotifications WRITE setFilterNotifications
+ NOTIFY filterNotificationsChanged)
Q_PROPERTY(QString filterByContent READ filterByContent WRITE setContentFilter NOTIFY
contentFilterChanged)
Q_PROPERTY(TimelineModel *source READ source WRITE setSource NOTIFY sourceChanged)
@@ -28,12 +30,14 @@ public:
explicit TimelineFilter(QObject *parent = nullptr);
QString filterByThread() const { return threadId; }
+ bool filterByNotifications() const { return filterByNotifications_; }
QString filterByContent() const { return contentFilter; }
TimelineModel *source() const;
int currentIndex() const;
bool isFiltering() const;
void setThreadId(const QString &t);
+ void setFilterNotifications(bool v);
void setContentFilter(const QString &t);
void setSource(TimelineModel *t);
void setCurrentIndex(int idx);
@@ -47,6 +51,7 @@ public:
signals:
void threadIdChanged();
+ void filterNotificationsChanged();
void contentFilterChanged();
void sourceChanged();
void currentIndexChanged();
@@ -67,4 +72,5 @@ private:
QString threadId, contentFilter;
int cachedCount = 0, incrementalSearchIndex = 0;
+ bool filterByNotifications_ = false;
};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index d5645ac4..b00d7d8f 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -605,8 +605,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
case UserName:
return QVariant(displayName(QString::fromStdString(acc::sender(event))));
case UserPowerlevel: {
- return static_cast(
- permissions_.powerlevelEvent().user_level(acc::sender(event)));
+ return static_cast(permissions_.powerlevelEvent().user_level(
+ acc::sender(event), permissions_.createEvent()));
}
case Day: {
@@ -1137,7 +1137,6 @@ TimelineModel::syncState(const mtx::responses::State &s)
avatarChanged = true;
nameChanged = true;
memberCountChanged = true;
-
} else if (std::holds_alternative>(e)) {
this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
emit encryptionChanged();
@@ -2422,6 +2421,7 @@ QString
TimelineModel::formatPowerLevelEvent(
const mtx::events::StateEvent &event) const
{
+ const auto create = permissions_.createEvent();
mtx::events::StateEvent const *prevEvent = nullptr;
if (!event.unsigned_data.replaces_state.empty()) {
auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id);
@@ -2441,15 +2441,15 @@ TimelineModel::formatPowerLevelEvent(
if (!prevEvent)
return tr("%1 has changed the room's permissions.").arg(sender_name);
- auto calc_affected = [&event,
- &prevEvent](int64_t newPowerlevelSetting) -> std::pair {
+ auto calc_affected =
+ [&event, &prevEvent, &create](int64_t newPowerlevelSetting) -> std::pair {
QStringList affected{};
auto numberOfAffected = 0;
// We do only compare to people with explicit PL. Usually others are not going to be
// affected either way and this is cheaper to iterate over.
for (auto const &[mxid, currentPowerlevel] : event.content.users) {
if (currentPowerlevel == newPowerlevelSetting &&
- prevEvent->content.user_level(mxid) < newPowerlevelSetting) {
+ prevEvent->content.user_level(mxid, create) < newPowerlevelSetting) {
numberOfAffected++;
if (numberOfAffected <= 2) {
affected.push_back(QString::fromStdString(mxid));
@@ -2626,24 +2626,25 @@ TimelineModel::formatPowerLevelEvent(
// Compare if a Powerlevel of a user changed
for (auto const &[mxid, powerlevel] : event.content.users) {
auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid)));
- if (prevEvent->content.user_level(mxid) != powerlevel) {
+ if (prevEvent->content.user_level(mxid, create) != powerlevel) {
if (powerlevel >= administrator_power_level) {
resultingMessage.append(tr("%1 has made %2 an administrator of this room.")
.arg(sender_name, nameOfChangedUser));
} else if (powerlevel >= moderator_power_level &&
- powerlevel > prevEvent->content.user_level(mxid)) {
+ powerlevel > prevEvent->content.user_level(mxid, create)) {
resultingMessage.append(tr("%1 has made %2 a moderator of this room.")
.arg(sender_name, nameOfChangedUser));
} else if (powerlevel >= moderator_power_level &&
- powerlevel < prevEvent->content.user_level(mxid)) {
+ powerlevel < prevEvent->content.user_level(mxid, create)) {
resultingMessage.append(tr("%1 has downgraded %2 to moderator of this room.")
.arg(sender_name, nameOfChangedUser));
} else {
- resultingMessage.append(tr("%1 has changed the powerlevel of %2 from %3 to %4.")
- .arg(sender_name,
- nameOfChangedUser,
- QString::number(prevEvent->content.user_level(mxid)),
- QString::number(powerlevel)));
+ resultingMessage.append(
+ tr("%1 has changed the powerlevel of %2 from %3 to %4.")
+ .arg(sender_name,
+ nameOfChangedUser,
+ QString::number(prevEvent->content.user_level(mxid, create)),
+ QString::number(powerlevel)));
}
}
}
@@ -3374,6 +3375,7 @@ TimelineModel::pushrulesRoomContext() const
cache::displayName(room_id_.toStdString(), http::client()->user_id().to_string()),
.member_count = cache::client()->memberCount(room_id_.toStdString()),
.power_levels = permissions_.powerlevelEvent(),
+ .create = permissions_.createEvent(),
};
}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index ad9f574e..db7cee53 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -332,6 +332,7 @@ public:
Q_INVOKABLE void openUserProfile(QString userid);
Q_INVOKABLE void unpin(const QString &id);
Q_INVOKABLE void pin(const QString &id);
+ Q_INVOKABLE void markEventAsRead(const QString &id) { this->readEvent(id.toStdString()); }
Q_INVOKABLE void showReadReceipts(const QString &id);
Q_INVOKABLE void redactEvent(const QString &id, const QString &reason = "");
Q_INVOKABLE void redactAllFromUser(const QString &userid, const QString &reason = "");
diff --git a/src/ui/EventExpiry.cpp b/src/ui/EventExpiry.cpp
index 8065c397..610cdd5c 100644
--- a/src/ui/EventExpiry.cpp
+++ b/src/ui/EventExpiry.cpp
@@ -28,7 +28,7 @@ EventExpiry::load()
if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry,
roomid_.toStdString())) {
auto h = std::get>(*temp);
+ mtx::events::account_data::nheko_extensions::EventExpiry>>(*temp);
this->event = std::move(h.content);
}
}
diff --git a/src/voip/CallDevices.cpp b/src/voip/CallDevices.cpp
index 90eef113..d42b378b 100644
--- a/src/voip/CallDevices.cpp
+++ b/src/voip/CallDevices.cpp
@@ -61,9 +61,7 @@ getFrameRate(const GValue *value)
void
addFrameRate(std::vector &rates, const FrameRate &rate)
{
- constexpr double minimumFrameRate = 15.0;
- if (static_cast(rate.first) / rate.second >= minimumFrameRate)
- rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second));
+ rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second));
}
void
diff --git a/third_party/olm-patches/0002-fix-cmake-cmp0148.patch b/third_party/olm-patches/0002-fix-cmake-cmp0148.patch
new file mode 100644
index 00000000..37518b99
--- /dev/null
+++ b/third_party/olm-patches/0002-fix-cmake-cmp0148.patch
@@ -0,0 +1,11 @@
+--- CMakeLists.txt.orig 2026-01-25 20:50:49.905592647 -0500
++++ CMakeLists.txt 2026-01-25 20:50:49.908925942 -0500
+@@ -1,4 +1,7 @@
+-cmake_minimum_required(VERSION 3.4)
++cmake_minimum_required(VERSION 3.5)
++if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.27")
++ cmake_policy(SET CMP0148 OLD)
++endif()
+
+ project(olm VERSION 3.2.16 LANGUAGES CXX C)
+