Compare commits

..

49 commits

Author SHA1 Message Date
Nicolas Werner
15322555e8
Limit forward completer height
Fixes #2009
2026-02-23 20:43:24 +01:00
DeepBlueV7.X
b083dc801c
Merge pull request #2012 from TheTaktik/fix/1831-pipewire-screencast-crash
fix: Remove 15fps limit for screensharing sources
2026-02-23 00:09:06 +01:00
Florian Olk
95532442ca fix: Remove 15fps limit for screensharing to prevent variable fps sources from crashing 2026-02-22 19:36:05 +01:00
Nicolas Werner
b1c387e03c
Fix tombstones and create events getting mixed up 2026-02-22 17:17:43 +01:00
Nicolas Werner
5894e32482
Fix rooms not getting marked as tombstoned on sync 2026-02-22 14:04:16 +01:00
Nicolas Werner
58e23302d0
Register permissions as a qml type 2026-02-21 01:46:49 +01:00
Nicolas Werner
51da48c706
Fix reply popup rendering on newer qt 2026-02-21 00:43:33 +01:00
Nicolas Werner
014d70fd64
Fix failed send indicator not updating automatically 2026-02-20 17:45:00 +01:00
Nicolas Werner
f91427e653
Merge branch 'master' of github.com:Nheko-Reborn/nheko 2026-02-20 01:33:15 +01:00
Nicolas Werner
e3bc058845
Refactor v12 support to use new user_level helper from mtxclient 2026-02-20 01:32:37 +01:00
Nicolas Werner
3184ab464c
Merge remote-tracking branch 'nep/v12' 2026-02-19 01:21:55 +01:00
DeepBlueV7.X
abb2325a99
Merge pull request #2003 from Integral-Tech/fix-toggle-action
Update `toggleAction_` text when parent window visibility changes
2026-02-17 01:20:06 +01:00
Nicolas Werner
ee62c9990d
Fix mark event as read command 2026-02-17 01:18:21 +01:00
Integral
b5ce330c82 Update toggleAction_ text when parent window visibility changes
Currently, the `toggleAction_` text is only updated when the
parent window visibility changes via the tray icon menu. If the
visibility changes through other means, the action text becomes out
of sync.

Connect parent window visibility changes to `toggleAction_` to keep
the text in sync.
2026-02-12 14:38:49 +08:00
Nicolas Werner
89e06f32dd
Allow hiding ACL events
fixes #2006
2026-02-11 23:01:01 +01:00
Nicolas Werner
597aa36f28
Work around hunter libevent using older cmake version 2026-02-08 23:11:08 +01:00
Nicolas Werner
ee5fbe2927
Bump hunter version 2026-02-08 22:50:41 +01:00
Nicolas Werner
c17734c7a0
Fix newer clang format versions 2026-02-08 19:35:46 +01:00
Shane Jaroch
3a707a5ee5
Merge pull request #1988 from gamesguru/fix/settings-comboBox-no-autocapture-scroll
prevent comboBox & spinner from capturing mouse scroll unless active
2026-02-08 19:31:13 +01:00
DeepBlueV7.X
2ece2aee9f
Merge pull request #2001 from Integral-Tech/improve-tray-show
Change "Show" to "Hide" in tray icon menu when the window is visible
2026-02-08 19:27:15 +01:00
DeepBlueV7.X
e13c7449a4
Merge pull request #1960 from direc85/send-message-key-selection
Add a setting to send messages with Enter, Shift+Enter or Ctrl+Enter
2026-02-08 19:25:56 +01:00
DeepBlueV7.X
16c8d95208
Merge pull request #1964 from RICCIARDI-Adrien/update_french_translations
Update the french translations.
2026-02-08 19:23:50 +01:00
Integral
3d9e14e001
Change "Show" to "Hide" in tray icon menu when the window is visible
This change updates the tray icon menu action to reflect the current
state of the window.

Closes: #2000
2026-02-01 21:39:15 +08:00
Sofia/Nep
d9aa04778a
Revert "Remove unnecessary references, not harmful in the absence of RVO but redundant due to move semantics"
This reverts commit b0d09926a1.

Misguided original change.
2026-01-26 16:39:20 -03:00
DeepBlueV7.X
9650c5f4be
Merge pull request #1992 from Penguin-Guru/patch-1
Update nheko.1.adoc
2026-01-25 11:02:40 +01:00
Penguin-Guru
4effdc6955
Update nheko.1.adoc
Bumped C++ version to match references elsewhere.
2026-01-22 17:47:09 -08:00
Nicolas Werner
5b065f353c
Fix matching strings with accents without typing them 2025-12-26 18:48:40 +01:00
Nicolas Werner
93ce60d6f1
Qt 6.10 compat 2025-12-26 18:34:30 +01:00
Nicolas Werner
1bd2970c4d
Don't send empty SecretRequest cancellation if there are only 2 devices 2025-11-03 03:55:23 +01:00
RICCIARDI-Adrien
c33f7fde6d Update the french translations. 2025-10-15 21:30:31 +02:00
Matti Viljanen
451e88fe72 Add a setting to send messages with Enter, Shift+Enter or Ctrl+Enter
Previously the option was just `invertEnterKey` boolean, which didn't
allow any flexibility, so I replaced it with a three-choice option:
Enter, Shift+Enter and Ctrl+Enter being the send message choices.
Add newline combos are Shift+Enter, Enter and Shift+Enter respectively.

I ended up fixing the emoji/mention pop-up behavior as a side product.
If any of the three combos are pressed, the pop-up is handled and
the event is accepted. This makes it impossible to accidentally send the
message if a pop-up is open.

If an Enter combo didn't match, it's passed to the next event handler.

The old `invertEnterKey` is migrated to the new `sendMessageKey`,
so this change doesn't change the existing preference.
2025-09-28 10:05:00 +03:00
Sofia/Nep
b0d09926a1
Remove unnecessary references, not harmful in the absence of RVO but redundant due to move semantics 2025-09-22 16:03:41 -03:00
Sofia/Nep
4a85031516
Improved v12 support 2025-09-22 14:04:35 -03:00
Nicolas Werner
2769642d3c
Fix most reply rendering issues with qt 6.9.2 2025-09-14 23:43:20 +02:00
Nicolas Werner
53cd31d181
Allow filtering the timeline for notifications 2025-09-13 01:01:43 +02:00
DeepBlueV7.X
5b025fa2b0
Merge pull request #1947 from heirecka/fix-build-with-qt-6.10
Search for private modules with Qt 6.10
2025-09-04 22:18:39 +00:00
Heiko Becker
af2ca72030 Search for private modules with Qt 6.10
Usage of private Qt modules requires a call to

`find_package(Qt6 COMPONENTS FooPrivate)` since 6.10 [1].

The  build complains about

'CMake Error at CMakeLists.txt:909 (target_link_libraries):
  Target "nheko" links to:

    Qt::QmlPrivate

    but the target was not found.'

otherwise.

[1] https://doc-snapshots.qt.io/qt6-dev/whatsnew610.html#build-system-changes
2025-09-04 17:34:03 +02:00
DeepBlueV7.X
5d05753eeb
Merge pull request #1942 from alwayshopeless/patch-1
Update README.md
2025-08-26 00:18:14 +00:00
alwayshopeless
896e44d507
Update README.md
Added missing library for installation in the guide for Arch-based(Manjaro)
2025-08-24 18:15:17 +03:00
DeepBlueV7.X
ba9fab78d5
Merge pull request #1937 from Integral-Tech/fix-binding-loop
Fix binding loop warnings in UserSettingsPage
2025-08-24 09:31:31 +00:00
DeepBlueV7.X
c7f191519c
Merge pull request #1935 from weeman1337/setTheme-dbus-api
Add `setTheme` to the D-Bus API
2025-08-24 09:28:54 +00:00
Integral
f1d88ea0a3
Fix binding loop warnings in UserSettingsPage 2025-08-20 22:55:33 +08:00
weeman
ffaa12cc19
Add setTheme D-Bus API
Signed-off-by: weeman <weeman@frankfurt.ccc.de>
2025-08-18 18:12:22 +02:00
weeman
398cef5f8f
Extract list of valid themes
Signed-off-by: weeman <weeman@frankfurt.ccc.de>
2025-08-17 13:28:22 +02:00
Nicolas Werner
f59f77a21e
Fix sending encrypted messages in encrypted rooms when messages are disallowed
Fixes #1933
2025-08-16 11:29:26 +02:00
Nicolas Werner
2bc2dfb64c
Fix kdsingleapplication linking on macos (second take)
CMAKE_ARGS don't work in FetchContent
2025-08-11 10:49:21 +02:00
Nicolas Werner
9c017ba1e2
Fix kdsingleapplication linking on macos 2025-08-11 10:29:20 +02:00
Nicolas Werner
a13ea11e57
Fix cmark loading on macos 2025-08-11 09:16:49 +02:00
Nicolas Werner
a7bc00d9a5
Bump windows latest version 2025-08-10 17:49:13 +02:00
66 changed files with 832 additions and 1170 deletions

View file

@ -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

View file

@ -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%

View file

@ -13,6 +13,8 @@ KeepEmptyLinesAtTheStartOfBlocks: false
PointerAlignment: Right
Cpp11BracedListStyle: true
PenaltyReturnTypeOnItsOwnLine: 0
StatementAttributeLikeMacros:
- emit
---
BasedOnStyle: WebKit
Language: ObjC

2
.envrc
View file

@ -1,2 +0,0 @@
eval "$(devenv direnvrc)"
use devenv

23
.gitignore vendored
View file

@ -128,26 +128,3 @@ package.dir
# Archives
*.bz2
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml
codex-resume*.sh

View file

@ -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

View file

@ -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,13 +246,12 @@ endif()
#
# Discover Qt dependencies.
#
find_package(Qt6 6.5 COMPONENTS Core Widgets Gui LinguistTools Svg Multimedia Qml QuickControls2 REQUIRED)
find_package(Qt6DBus)
find_package(Qt6QmlPrivate REQUIRED NO_MODULE)
if(UNIX)
find_package(Qt6GuiPrivate REQUIRED NO_MODULE)
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)
include(FetchContent)
@ -300,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.
@ -544,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 "")
@ -567,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)
@ -624,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 "")
@ -718,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

View file

@ -1,81 +0,0 @@
# Stabilize Existing VoIP Call Flow and Add Non-Fatal Diagnostics
## Summary
Tighten the existing Matrix call flow in the current VoIP stack, focusing on real correctness issues already present in src/voip/CallManager.cpp and src/voip/WebRTCSession.cpp, and
add structured logging around non-fatal failures so future call bugs are diagnosable without crashing or silently retrying forever.
This plan targets:
- Core invite/answer/candidate/select-answer stability
- Inbound renegotiation support for m.call.negotiate
- Logging of non-critical exceptions and ignored/invalid call events
- No user-visible feature expansion beyond making current calls more reliable
## Implementation Changes
- Fix the CallSelectAnswer rejection lookup bug in CallManager::handleEvent(const RoomEvent<CallSelectAnswer>&) by changing the std::find(begin, begin, ...) calls to search through
end(). This is a concrete logic bug that currently prevents expected branch behavior.
- Add strict call-id and state guards across inbound handlers in CallManager and log every early-return branch that currently drops events silently. Include at least:
- mismatched call_id
- wrong local webrtc::State
- duplicate answers/select-answer/reject
- self-echo cases
- candidates received before a call is initialized
- Implement WebRTCSession::acceptNegotiation(const std::string &) instead of the current unconditional false. Use the same parsing/validation style as acceptOffer:
- require an active session, not DISCONNECTED
- parse SDP as a remote offer
- validate required audio media and optional VP8 video media
- update remote video direction flags
- call set-remote-description
- create and send an answer through the existing answerCreated path
- keep wire format unchanged; do not add local renegotiation initiation in this pass
- Extend CallManager::handleEvent(const RoomEvent<CallNegotiate>&) so renegotiation only applies to the active call, logs dropped mismatches, and drains queued ICE candidates only
after renegotiation is accepted.
- Harden acceptICECandidates handling:
- log when candidates are ignored because the session is not ready
- keep buffering only in manager-side inbound invite/renegotiation waiting states
- avoid silent no-ops when candidates arrive for the wrong call or after teardown
- Improve failure handling around setup paths:
- log TURN retrieval failures in retrieveTurnServer() with the request error and retry interval
- log why createOffer, acceptOffer, acceptAnswer, and renegotiation fail, including current state and call id where available
- log pipeline creation failures with the missing device/element context instead of only generic “Problem setting up call”
- Add non-critical exception logging around call orchestration paths that touch cache/state but should not kill the process:
- syncEvent dispatch
- inbound call event handlers that query room info/members
- outbound invite setup that reads room/member data
- TURN response parsing/conversion
Catch std::exception and log with nhlog at warn or error depending on whether the call can continue; rethrow nothing for these paths. Add a final catch (...) only where needed
to prevent silent termination, with a clear “unknown exception” log line.
- Keep user notifications mostly unchanged. Prefer logs for diagnostics and only surface UI notifications for existing fatal outcomes such as setup failure, ICE failure, reject,
timeout, or answered elsewhere.
## Public Interfaces / Behavior
- No QML property or signal changes.
- No Matrix event schema changes.
- Existing CallAnswer, CallCandidates, CallNegotiate, and CallSelectAnswer messages remain the transport surface.
- Behavioral change: inbound m.call.negotiate will now be handled instead of always failing.
- Behavioral change: previously silent dropped call events and recoverable exceptions will now emit logs with call id, party id, and local state.
## Test Plan
- Add focused unit-style coverage where practical for pure call-manager logic, or otherwise add regression tests around isolated helper logic if the repo lacks VoIP integration
tests.
- Verify manually or with targeted harness coverage:
- outbound call reaches OFFERSENT, receives answer, transitions to CONNECTED
- inbound call invite buffers ICE before accept, then drains after accept
- duplicate CallAnswer and CallSelectAnswer are ignored with logs, not state corruption
- CallSelectAnswer rejection-party matching now works after the std::find fix
- incoming CallNegotiate on the active call produces a local answer instead of immediate failure
- CallNegotiate for a different call_id is ignored with a diagnostic log
- TURN retrieval failure logs retry details without crashing
- exceptions thrown from room/member lookup in call paths are logged and do not abort the process
- Run a build check after the changes. If the environment still lacks cmake, document that verification gap and at minimum run the strongest available static inspection.
## Assumptions
- Use existing nhlog logging categories rather than raw printf; “prints” here means persistent diagnostic logging.
- Keep the existing user-facing notification policy unless a failure is already fatal to the active call.
- Renegotiation scope is inbound-only for now because the current codebase already receives m.call.negotiate but does not expose a local renegotiation initiator.
- No attempt will be made in this pass to redesign the entire call state machine or add automatic media-session recovery after teardown.

View file

@ -343,6 +343,7 @@ sudo pacman -S qt6-base \
gcc \
fontconfig \
lmdb \
lmdbxx \
cmark \
qtkeychain-qt6
```
@ -417,51 +418,6 @@ cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build
```
#### direnv + devenv
If you want a reproducible Nix-based development shell for building and smoke-testing the local
binary, this repo now includes `devenv.nix` and `.envrc`.
Requirements:
- `nix` with the `nix-command` and `flakes` features enabled
- `devenv` 2.x
- `direnv` with its shell hook enabled in your shell rc file
- network access on the first run so `devenv` can resolve the `devenv.yaml` nixpkgs input and
fetch any missing Nix store paths
- acceptance of the current `olm` insecurity override shipped in `devenv.nix`, since nheko still
depends on `libolm` and recent nixpkgs revisions block it by default
Usage:
```bash
direnv allow
```
If you don't want automatic shell activation, you can enter the same environment manually:
```bash
devenv shell
```
Inside the shell, use the provided helper commands:
```bash
configure-nheko
build-nheko
test-nheko
run-nheko
```
`test-nheko` builds the project, runs `ctest --output-on-failure`, and then performs a headless
smoke check by running the built `nheko` binary with `--help`.
You can also run the same test flow through `devenv` directly:
```bash
devenv test
```
To use bundled dependencies you can use hunter, i.e.:
```bash

View file

@ -1,65 +0,0 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1773179136,
"narHash": "sha256-aOxRGdpITEvasogdZXGqlSrr8pJQ+QGiRxpzmT3i/UQ=",
"owner": "cachix",
"repo": "devenv",
"rev": "169c048513ca064c02a4233ace0ecbdd935d4f80",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"nixpkgs": {
"inputs": {
"nixpkgs-src": "nixpkgs-src"
},
"locked": {
"lastModified": 1772749504,
"narHash": "sha256-eqtQIz0alxkQPym+Zh/33gdDjkkch9o6eHnMPnXFXN0=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "08543693199362c1fddb8f52126030d0d374ba2e",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs-src": {
"flake": false,
"locked": {
"lastModified": 1772173633,
"narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,129 +0,0 @@
{ pkgs, lib, config, inputs, ... }:
let
qtPackages = with pkgs.qt6Packages; [
qtbase
qtdeclarative
qtimageformats
qtkeychain
qtmultimedia
qtsvg
qttools
qtwayland
qt-jdenticon
];
gstreamerPackages = [
pkgs.gst_all_1.gstreamer
pkgs.gst_all_1.gst-plugins-base
(pkgs.gst_all_1.gst-plugins-good.override { qt6Support = true; })
pkgs.gst_all_1.gst-plugins-bad
];
cmakePackages = [
pkgs.cmark
pkgs.coeurl
pkgs.curl
pkgs.kdsingleapplication
pkgs.libevent
pkgs.libsecret
pkgs.lmdb
pkgs.lmdbxx
pkgs.mtxclient
pkgs.nlohmann_json
pkgs.olm
pkgs.re2
pkgs.spdlog
] ++ qtPackages ++ gstreamerPackages ++ [ pkgs.libnice ];
runtimePackages = [
pkgs.pipewire
];
qtPluginPath = lib.makeSearchPath "lib/qt-6/plugins" qtPackages;
qmlImportPath = lib.makeSearchPath "lib/qt-6/qml" qtPackages;
gstreamerPluginPath = lib.makeSearchPath "lib/gstreamer-1.0" gstreamerPackages;
runtimeLibraryPath = lib.makeLibraryPath runtimePackages;
cmakePrefixPath =
lib.concatStringsSep ":" (
(map (pkg: "${lib.getDev pkg}/lib/cmake") cmakePackages)
++ (map (pkg: "${lib.getDev pkg}") cmakePackages)
);
in
{
packages = [
pkgs.asciidoc
pkgs.cmake
pkgs.direnv
pkgs.gcc
pkgs.git
pkgs.gnumake
pkgs.gdb
pkgs.ninja
pkgs.pkg-config
pkgs.pipewire
pkgs.qt6Packages.wrapQtAppsHook
] ++ cmakePackages;
env = {
CMAKE_GENERATOR = "Ninja";
CMAKE_PREFIX_PATH = cmakePrefixPath;
GST_PLUGIN_SYSTEM_PATH_1_0 = gstreamerPluginPath;
LD_LIBRARY_PATH = runtimeLibraryPath;
NHEKO_BUILD_DIR = "build";
QT_PLUGIN_PATH = qtPluginPath;
QML2_IMPORT_PATH = qmlImportPath;
};
scripts."configure-nheko".exec = ''
cmake -S . -B "$NHEKO_BUILD_DIR" \
-G "$CMAKE_GENERATOR" \
-DCMAKE_BUILD_TYPE="''${CMAKE_BUILD_TYPE:-RelWithDebInfo}" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
'';
scripts."build-nheko".exec = ''
if [ ! -f "$NHEKO_BUILD_DIR/CMakeCache.txt" ]; then
configure-nheko
fi
cmake --build "$NHEKO_BUILD_DIR" --parallel "''${NIX_BUILD_CORES:-$(nproc)}"
'';
scripts."test-nheko".exec = ''
build-nheko
ctest --test-dir "$NHEKO_BUILD_DIR" --output-on-failure
binary="$(find "$NHEKO_BUILD_DIR" -maxdepth 4 -type f -name nheko -executable | head -n 1)"
if [ -z "$binary" ]; then
echo "Unable to locate the built nheko binary under $NHEKO_BUILD_DIR" >&2
exit 1
fi
QT_QPA_PLATFORM=offscreen "$binary" --help >/dev/null
'';
scripts."run-nheko".exec = ''
build-nheko
binary="$(find "$NHEKO_BUILD_DIR" -maxdepth 4 -type f -name nheko -executable | head -n 1)"
if [ -z "$binary" ]; then
echo "Unable to locate the built nheko binary under $NHEKO_BUILD_DIR" >&2
exit 1
fi
exec "$binary" "$@"
'';
enterShell = ''
echo "nheko devenv ready"
echo " configure-nheko Configure the CMake build tree"
echo " build-nheko Build the nheko binary"
echo " test-nheko Run ctest and a headless --help smoke test"
echo " run-nheko Launch the built client"
'';
enterTest = ''
test-nheko
'';
}

View file

@ -1,12 +0,0 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
nixpkgs:
permittedInsecurePackages:
- "olm-3.2.16"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
<svg width="32" height="32" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.25 1.25 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5ZM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.006-.147ZM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.004-.225A5.988 5.988 0 0 0 12 3.496Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 494 B

View file

@ -17,7 +17,7 @@
<message>
<location line="+73"/>
<source>You are screen sharing</source>
<translation>Vous êtes en train de partager votre écran.</translation>
<translation>Vous êtes en train de partager votre écran</translation>
</message>
<message>
<location line="+17"/>
@ -312,7 +312,7 @@
<message>
<location line="+32"/>
<source>Kicked user: %1</source>
<translation>L&apos;utilisateur %1 a é expulsé.</translation>
<translation>L&apos;utilisateur %1 a é expulsé</translation>
</message>
<message>
<location line="+26"/>
@ -322,7 +322,7 @@
<message>
<location line="+3"/>
<source>Banned user: %1</source>
<translation>L&apos;utilisateur %1 a é banni.</translation>
<translation>L&apos;utilisateur %1 a é banni</translation>
</message>
<message>
<location line="+8"/>
@ -625,32 +625,32 @@ Eventuellement, vous pouvez fournir une explication de votre demande aux autres
<message>
<location line="+2"/>
<source>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.</source>
<translation type="unfinished"></translation>
<translation>Expulser un utilisateur de la salle actuelle. La raison est optionnelle. Si l&apos;utilisateur est exclu, une tentative d&apos;expulsion de l&apos;utilisateur auquel vous êtes en train de répondre sera effectuée.</translation>
</message>
<message>
<location line="+3"/>
<source>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.</source>
<translation type="unfinished"></translation>
<translation>Bannir un utilisateur de la salle actuelle. La raison est optionnelle. Si l&apos;utilisateur est exclu, une tentative de bannissement de l&apos;utilisateur auquel vous êtes en train de répondre sera effectuée.</translation>
</message>
<message>
<location line="+3"/>
<source>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.</source>
<translation type="unfinished"></translation>
<translation>Annuler le banissement d&apos;un utilisateur dans le salon actuel. La raison est optionnelle. Si l&apos;utilisateur est exclu, une tentative d&apos;annulation du bannissement de l&apos;utilisateur auquel vous êtes en train de répondre sera effectuée.</translation>
</message>
<message>
<location line="+3"/>
<source>Redact an event by event id or that you are replying to or all locally cached messages of a user.</source>
<translation type="unfinished"></translation>
<translation>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&apos;utilisateur mis en cache localement.</translation>
</message>
<message>
<location line="+56"/>
<source>Block all invites from a user, a server, to a specific room or set the default.</source>
<translation type="unfinished"></translation>
<translation>Bloquer toutes les invitations en provenance d&apos;un utilisateur ou d&apos;un serveur pour un salon spécifique, ou bien définir le comportement par défaut.</translation>
</message>
<message>
<location line="+3"/>
<source>Allow all invites from a user, a server, to a specific room or set the default.</source>
<translation type="unfinished"></translation>
<translation>Autoriser toutes les invitations en provenance d&apos;un utilisateur ou d&apos;un serveur pour un salon spécifique, ou bien définir le comportement par défaut.</translation>
</message>
<message>
<location line="-56"/>
@ -750,12 +750,12 @@ Eventuellement, vous pouvez fournir une explication de votre demande aux autres
<message>
<location line="+2"/>
<source>Send a message with a glitch effect.</source>
<translation type="unfinished"></translation>
<translation>Envoyer un message avec un effet de déformation.</translation>
</message>
<message>
<location line="+2"/>
<source>Send a message that gradually glitches.</source>
<translation type="unfinished"></translation>
<translation>Envoyer un message qui se déforme progressivement.</translation>
</message>
<message>
<location line="+2"/>
@ -2143,7 +2143,7 @@ Exemple&#xa0;: https://serveur.domaine.extension:8787</translation>
<location line="+12"/>
<source>%1 replied with a spoiler.</source>
<comment>Format a reply in a notification. %1 is the sender.</comment>
<translation type="unfinished"></translation>
<translation>%1 a répondu avec un spoiler.</translation>
</message>
<message>
<location line="+10"/>
@ -2250,7 +2250,7 @@ Exemple&#xa0;: https://serveur.domaine.extension:8787</translation>
<message>
<location line="-238"/>
<source>User (%1)</source>
<translation>Utilisateur (%)</translation>
<translation>Utilisateur (%1)</translation>
</message>
<message>
<location line="+2"/>
@ -3706,7 +3706,7 @@ Si vous choisissez de vérifier, vous aurez besoin de l&apos;autre appareil. Si
<message>
<location filename="../qml/components/SpaceMenu.qml" line="+16"/>
<source>Add or remove from community...</source>
<translation type="unfinished">Ajouter ou retirer de la communauté...</translation>
<translation>Ajouter ou retirer de la communauté...</translation>
</message>
</context>
<context>
@ -4307,9 +4307,9 @@ Raison : %4</translation>
<message numerus="yes">
<location line="+115"/>
<source>%n hour(s) later</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
<translation>
<numerusform>%n heure plus tard</numerusform>
<numerusform>%n heures plus tard</numerusform>
</translation>
</message>
<message>
@ -5495,7 +5495,7 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l&apos;applicati
<message>
<location line="+13"/>
<source>Repeat File Password</source>
<translation type="unfinished"></translation>
<translation>Répéter le mot de passe du fichier</translation>
</message>
<message>
<location line="+1"/>
@ -5686,7 +5686,7 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l&apos;applicati
<message>
<location filename="../../src/notifications/ManagerMac.cpp" line="-12"/>
<source>Message contains spoiler.</source>
<translation type="unfinished"></translation>
<translation>Le message contient un spoiler.</translation>
</message>
</context>
<context>
@ -5755,13 +5755,13 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l&apos;applicati
<location line="+6"/>
<location line="+26"/>
<source>You sent a spoiler.</source>
<translation type="unfinished"></translation>
<translation>Vous avez envoyé un spoiler.</translation>
</message>
<message>
<location line="-23"/>
<location line="+26"/>
<source>%1 sent a spoiler.</source>
<translation type="unfinished"></translation>
<translation>%1 a envoyé un spoiler.</translation>
</message>
<message>
<location line="-20"/>
@ -5788,7 +5788,7 @@ Cette fonctionnalité prendra effet au prochain redémarrage de l&apos;applicati
<message>
<location line="+23"/>
<source>* %1 spoils something.</source>
<translation type="unfinished"></translation>
<translation>* %1 a spoilé quelque chose.</translation>
</message>
<message>
<location line="+8"/>

View file

@ -57,6 +57,7 @@ Popup {
eventId: mid
userColor: TimelineManager.userColor(replyPreview.userId, palette.window)
maxWidth: parent.width
limitHeight: true
}
MatrixTextField {
id: roomTextInput

View file

@ -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) {

View file

@ -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 {

View file

@ -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: {

View file

@ -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: {

View file

@ -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

View file

@ -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") : ""

View file

@ -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

View file

@ -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 {

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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")

View file

@ -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 {

View file

@ -183,7 +183,7 @@ Menu {
height: stickerDim
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
/* font.family: Settings.emojiFont != "" ? Settings.emojiFont : undefined */
font.family: Settings.emojiFont != "" ? Settings.emojiFont : undefined
font.pixelSize: 36
text: del.modelData.unicode.replace('\ufe0f', '')
}

View file

@ -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()
}
}

View file

@ -218,7 +218,7 @@ Rectangle {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
buttonTextColor: "#000000"
image: CallManager.isMicMuted ? ":/icons/icons/ui/microphone-mute.svg" : ":/icons/icons/ui/microphone-unmute.svg"
image: CallManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.svg" : ":/icons/icons/ui/microphone-mute.svg"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: CallManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")

View file

@ -36,7 +36,7 @@ Popup {
Layout.leftMargin: 8
Label {
text: qsTr("Place a call in %1?").arg(room.roomName)
text: qsTr("Place a call to %1?").arg(room.roomName)
color: palette.windowText
}

View file

@ -1,6 +1,7 @@
<RCC>
<qresource prefix="/icons">
<file>icons/ui/add-square-button.svg</file>
<file>icons/ui/alert.svg</file>
<file>icons/ui/angle-arrow-left.svg</file>
<file>icons/ui/attach.svg</file>
<file>icons/ui/ban.svg</file>

View file

@ -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<mtx::events::state::Create>(txn, space)
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{});
auto pls = getStateEvent<mtx::events::state::PowerLevels>(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<mtx::events::EventType> &eventTypes
int64_t min_event_level = std::numeric_limits<int64_t>::max();
int64_t user_level = std::numeric_limits<int64_t>::min();
std::string_view event;
bool res = db_.get(txn, to_string(EventType::RoomPowerLevels), event);
try {
StateEvent<Create> create = getStateEvent<mtx::events::state::Create>(txn, room_id)
.value_or(StateEvent<Create>{});
StateEvent<PowerLevels> pls =
getStateEvent<mtx::events::state::PowerLevels>(txn, room_id)
.value_or(StateEvent<PowerLevels>{});
if (res) {
try {
StateEvent<PowerLevels> msg =
nlohmann::json::parse(std::string_view(event.data(), event.size()))
.get<StateEvent<PowerLevels>>();
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)

View file

@ -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<ElementRank::first>(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<ElementRank::first>(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);

View file

@ -19,6 +19,9 @@ MemberListBackend::MemberListBackend(const QString &room_id, QObject *parent)
->getStateEvent<mtx::events::state::PowerLevels>(room_id_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content}
, create_{cache::client()
->getStateEvent<mtx::events::state::Create>(room_id_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{})}
{
try {
info_ = cache::singleRoomInfo(room_id_.toStdString());
@ -92,7 +95,7 @@ MemberListBackend::data(const QModelIndex &index, int role) const
}
case Powerlevel:
return static_cast<qlonglong>(
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 {};
}

View file

@ -73,6 +73,7 @@ private:
bool loadingMoreMembers_{false};
mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> create_;
friend class MemberList;
};

View file

@ -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()));

View file

@ -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<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent)
, room_id(rid)
, powerLevels_(pl)
, create_(create)
{
std::set<mtx::events::state::power_level_t> 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<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent)
, room_id(rid)
, powerLevels_(pl)
, create_(create)
{
std::set<mtx::events::state::power_level_t> 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<std::string, mtx::events::state::power_level_t, std::less<>> 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<mtx::events::state::PowerLevels>(room_id.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content)
, types_(room_id.toStdString(), powerLevels_, this)
, users_(room_id.toStdString(), powerLevels_, this)
, spaces_(room_id.toStdString(), powerLevels_, this)
, create_(cache::client()
->getStateEvent<mtx::events::state::Create>(room_id.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{}))
, 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<mtx::events::state::Create> &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<std::string> visited;
@ -703,10 +735,16 @@ PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_i
cache::client()->getStateEvent<mtx::events::state::space::Parent>(s, space);
if (parent && parent->content.via && !parent->content.via->empty() &&
parent->content.canonical) {
auto parentPl = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s);
auto childPl = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s);
auto childCreate =
cache::client()->getStateEvent<mtx::events::state::Create>(s).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
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_);

View file

@ -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<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(types.size()); }
@ -67,6 +69,7 @@ public:
std::string room_id;
QVector<Entry> types;
mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> 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<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(users.size()); }
@ -121,6 +126,7 @@ public:
std::string room_id;
QVector<Entry> users;
mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> 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<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(spaces.size()); }
@ -183,6 +191,7 @@ public:
std::string roomid;
mtx::events::state::PowerLevels pl;
mtx::events::StateEvent<mtx::events::state::Create> 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<mtx::events::state::Create> create_;
PowerlevelsTypeListModel types_;
PowerlevelsUserListModel users_;
PowerlevelsSpacesListModel spaces_;

View file

@ -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");

View file

@ -40,7 +40,7 @@ public slots:
void setUnreadCount(int count);
private:
QAction *viewAction_;
QAction *toggleAction_;
QAction *quitAction_;
int previousCount = 0;

View file

@ -25,10 +25,24 @@
#include "config/nheko.h"
QStringList themes{
QStringLiteral("light"),
QStringLiteral("dark"),
QStringLiteral("system"),
};
QSharedPointer<UserSettings> 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<int>(oldValue));
settings.remove("user/invert_enter_key");
}
connect(
QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); });
}
@ -65,8 +79,13 @@ UserSettings::load(std::optional<QString> 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<int>(SendMessageKey::Enter);
sendMessageKey_ = static_cast<SendMessageKey>(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<int>(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<int>(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<UserSettings::SendMessageKey>().keyCount() <= newKey)
return false;
i->setSendMessageKey(static_cast<UserSettings::SendMessageKey>(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});

View file

@ -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<QString> 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,

View file

@ -1453,6 +1453,9 @@ utils::roomVias(const std::string &roomid)
auto powerlevels =
cache::client()->getStateEvent<mtx::events::state::PowerLevels>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::PowerLevels>{});
auto create =
cache::client()->getStateEvent<mtx::events::state::Create>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
auto acls = cache::client()->getStateEvent<mtx::events::state::ServerAcl>(roomid);
std::vector<QRegularExpression> allowedServers;
@ -1501,6 +1504,19 @@ utils::roomVias(const std::string &roomid)
std::set<std::string> 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<mtx::identifiers::User>(user).hostname();
if (isHostAllowed(host))
users_with_high_pl.insert(user);
}
for (const auto &user : create.content.additional_creators) {
auto host = mtx::identifiers::parse<mtx::identifiers::User>(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<mtx::identifiers::User>(*max_pl_user).hostname();
@ -1705,11 +1722,15 @@ utils::updateSpaceVias()
auto spaceid = roomid.toStdString();
auto create = cache::client()->getStateEvent<mtx::events::state::Create>(spaceid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
if (auto pl = cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(spaceid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.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<mtx::events::state::Create>(spaceid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
if (auto pl =
cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(childid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.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<mtx::events::state::Create>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::Create>{});
if (auto pl = cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(roomid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.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;
}

View file

@ -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
/**

View file

@ -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);

View file

@ -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
{

View file

@ -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;

View file

@ -398,17 +398,19 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
body[local_user][dev] = secretRequest;
}
http::client()->send_to_device<mtx::events::msg::SecretRequest>(
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<mtx::events::msg::SecretRequest>(
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);

View file

@ -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]() {

View file

@ -4,6 +4,8 @@
#include "Permissions.h"
#include <algorithm>
#include "Cache_p.h"
#include "MatrixClient.h"
#include "TimelineModel.h"
@ -22,44 +24,53 @@ Permissions::invalidate()
->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content;
create = cache::client()
->getStateEvent<mtx::events::state::Create>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{});
}
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<qml_mtx_events::EventType>(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<qml_mtx_events::EventType>(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<qml_mtx_events::EventType>(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<qml_mtx_events::EventType>(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"

View file

@ -5,7 +5,10 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <mtx/events.hpp>
#include <mtx/events/create.hpp>
#include <mtx/events/power_levels.hpp>
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<mtx::events::state::Create> &createEvent() const
{
return create;
};
private:
QString roomId_;
mtx::events::state::PowerLevels pl;
mtx::events::StateEvent<mtx::events::state::Create> create;
};

View file

@ -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

View file

@ -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();

View file

@ -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<int> &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>() !=
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;

View file

@ -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;
};

View file

@ -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<qlonglong>(
permissions_.powerlevelEvent().user_level(acc::sender(event)));
return static_cast<qlonglong>(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<StateEvent<state::Encryption>>(e)) {
this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
emit encryptionChanged();
@ -2422,6 +2421,7 @@ QString
TimelineModel::formatPowerLevelEvent(
const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const
{
const auto create = permissions_.createEvent();
mtx::events::StateEvent<mtx::events::state::PowerLevels> 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<QStringList, int> {
auto calc_affected =
[&event, &prevEvent, &create](int64_t newPowerlevelSetting) -> std::pair<QStringList, int> {
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(),
};
}

View file

@ -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 = "");

View file

@ -28,7 +28,7 @@ EventExpiry::load()
if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry,
roomid_.toStdString())) {
auto h = std::get<mtx::events::AccountDataEvent<
mtx::events::account_data::nheko_extensions::EventExpiry>>(*temp);
mtx::events::account_data::nheko_extensions::EventExpiry>>(*temp);
this->event = std::move(h.content);
}
}

View file

@ -61,9 +61,7 @@ getFrameRate(const GValue *value)
void
addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
{
constexpr double minimumFrameRate = 15.0;
if (static_cast<double>(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

View file

@ -7,7 +7,6 @@
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <QAudioOutput>
#include <QGuiApplication>
@ -57,67 +56,6 @@ typedef RTCSessionDescriptionInit SDO;
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer);
const char *
callStateString(webrtc::State state)
{
switch (state) {
case webrtc::State::DISCONNECTED:
return "DISCONNECTED";
case webrtc::State::ICEFAILED:
return "ICEFAILED";
case webrtc::State::INITIATING:
return "INITIATING";
case webrtc::State::INITIATED:
return "INITIATED";
case webrtc::State::OFFERSENT:
return "OFFERSENT";
case webrtc::State::ANSWERSENT:
return "ANSWERSENT";
case webrtc::State::CONNECTING:
return "CONNECTING";
case webrtc::State::CONNECTED:
return "CONNECTED";
}
return "UNKNOWN";
}
void
logCallException(const char *context,
const QString &roomid,
const std::string &callid,
const std::exception &e)
{
nhlog::ui()->warn("WebRTC: {} failed: {} (room id: {}, call id: {})",
context,
e.what(),
roomid.isEmpty() ? "<none>" : roomid.toStdString(),
callid.empty() ? "<none>" : callid);
}
void
logUnknownCallException(const char *context, const QString &roomid, const std::string &callid)
{
nhlog::ui()->warn("WebRTC: {} failed with an unknown exception (room id: {}, call id: {})",
context,
roomid.isEmpty() ? "<none>" : roomid.toStdString(),
callid.empty() ? "<none>" : callid);
}
std::string
requestErrorString(const mtx::http::RequestErr &err)
{
if (!err)
return "unknown request error";
std::string details = "status=" + std::to_string(static_cast<int>(err->status_code));
if (!err->matrix_error.error.empty())
details += ", matrix_error=" + err->matrix_error.error;
if (!err->parse_error.empty())
details += ", parse_error=" + err->parse_error;
return details;
}
}
CallManager *
@ -207,18 +145,6 @@ CallManager::CallManager(QObject *parent)
CallCandidates{callid_, partyid_, candidates, callPartyVersion_});
});
connect(
&session_,
&WebRTCSession::negotiationCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending negotiation answer", callid_);
emit newMessage(
roomid_, CallNegotiate{callid_, partyid_, timeoutms_, SDO{sdp, SDO::Type::Answer}});
emit newMessage(roomid_,
CallCandidates{callid_, partyid_, candidates, callPartyVersion_});
});
connect(&session_,
&WebRTCSession::newICECandidate,
this,
@ -232,25 +158,19 @@ CallManager::CallManager(QObject *parent)
connect(
this, &CallManager::turnServerRetrieved, this, [this](const mtx::responses::TurnServer &res) {
try {
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
nhlog::net()->info("username: {}", res.username);
nhlog::net()->info("ttl: {} seconds", res.ttl);
for (const auto &u : res.uris)
nhlog::net()->info("uri: {}", u);
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
nhlog::net()->info("username: {}", res.username);
nhlog::net()->info("ttl: {} seconds", res.ttl);
for (const auto &u : res.uris)
nhlog::net()->info("uri: {}", u);
// Request new credentials close to expiry
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
turnURIs_ = getTurnURIs(res);
uint32_t ttl = std::max(res.ttl, std::uint32_t{3600});
if (res.ttl < 3600)
nhlog::net()->warn("Setting ttl to 1 hour");
turnServerTimer_.setInterval(std::chrono::seconds(ttl) * 10 / 9);
} catch (const std::exception &e) {
nhlog::net()->warn("Failed to process TURN server response: {}", e.what());
} catch (...) {
nhlog::net()->warn("Failed to process TURN server response: unknown exception");
}
// Request new credentials close to expiry
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
turnURIs_ = getTurnURIs(res);
uint32_t ttl = std::max(res.ttl, std::uint32_t{3600});
if (res.ttl < 3600)
nhlog::net()->warn("Setting ttl to 1 hour");
turnServerTimer_.setInterval(std::chrono::seconds(ttl) * 10 / 9);
});
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
@ -317,130 +237,90 @@ void
CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
{
if (isOnCall() || isOnCallOnOtherDevice()) {
nhlog::ui()->debug("WebRTC: refusing outbound invite in state {} (call id: {}, other device call id: {})",
callStateString(session_.state()),
callid_,
isOnCallOnOtherDevice_);
if (isOnCallOnOtherDevice_ != "")
emit ChatPage::instance()->showNotification(
QStringLiteral("User is already in a call"));
return;
}
try {
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
callType_ = callType;
roomid_ = roomid;
generateCallID();
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
if (members.empty()) {
nhlog::ui()->warn("WebRTC: no room members available for outbound invite in room {}",
roomid.toStdString());
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
clear();
return;
}
std::string errorMessage;
const bool isRoomCall = roomInfo.member_count > 2;
const RoomMember *callee = nullptr;
if (roomInfo.member_count == 1)
callee = &members.front();
else if (roomInfo.member_count == 2)
callee =
members.front().user_id == utils::localUser() ? &members.back() : &members.front();
if (!isRoomCall && !callee) {
nhlog::ui()->warn("WebRTC: failed to resolve callee for outbound invite in room {}",
roomid.toStdString());
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
clear();
return;
}
callType_ = callType;
roomid_ = roomid;
generateCallID();
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
const RoomMember *callee;
if (roomInfo.member_count == 1)
callee = &members.front();
else if (roomInfo.member_count == 2)
callee = members.front().user_id == utils::localUser() ? &members.back() : &members.front();
else {
emit ChatPage::instance()->showNotification(
QStringLiteral("Calls are limited to rooms with less than two members"));
return;
}
#ifdef GSTREAMER_AVAILABLE
if (callType == CallType::SCREEN) {
if (screenShareType_ == ScreenShareType::X11 ||
screenShareType_ == ScreenShareType::D3D11) {
if (windows_.empty() || windowIndex >= windows_.size()) {
nhlog::ui()->error("WebRTC: window index out of range");
clear();
return;
}
} else {
ScreenCastPortal &sc_portal = ScreenCastPortal::instance();
if (sc_portal.getStream() == nullptr) {
nhlog::ui()->error("xdg-desktop-portal stream not started");
clear();
return;
}
if (callType == CallType::SCREEN) {
if (screenShareType_ == ScreenShareType::X11 ||
screenShareType_ == ScreenShareType::D3D11) {
if (windows_.empty() || windowIndex >= windows_.size()) {
nhlog::ui()->error("WebRTC: window index out of range");
return;
}
} else {
ScreenCastPortal &sc_portal = ScreenCastPortal::instance();
if (sc_portal.getStream() == nullptr) {
nhlog::ui()->error("xdg-desktop-portal stream not started");
return;
}
}
}
#endif
if (haveCallInvite_) {
nhlog::ui()->debug("WebRTC: discarding outbound call for inbound call; localUser is polite party");
if ((isRoomCall && roomid_ == roomid) || (!isRoomCall && callParty_ == callee->user_id)) {
if (callType == callType_)
acceptInvite();
else {
emit ChatPage::instance()->showNotification(
QStringLiteral("Can't place call. Call types do not match"));
emit newMessage(
roomid_,
CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy});
}
} else {
if (haveCallInvite_) {
nhlog::ui()->debug("WebRTC: Discarding outbound call for inbound call. "
"localUser is polite party");
if (callParty_ == callee->user_id) {
if (callType == callType_)
acceptInvite();
else {
emit ChatPage::instance()->showNotification(
QStringLiteral("Already on a call with a different user"));
QStringLiteral("Can't place call. Call types do not match"));
emit newMessage(
roomid_,
CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy});
}
return;
}
session_.setTurnServers(turnURIs_);
std::string strCallType = callType_ == CallType::VOICE
? "voice"
: (callType_ == CallType::VIDEO ? "video" : "screen");
nhlog::ui()->debug("WebRTC: call id: {} - creating {} {}invite",
callid_,
strCallType,
isRoomCall ? "room-wide " : "");
if (isRoomCall) {
callParty_ = utils::localUser();
callPartyDisplayName_ =
roomInfo.name.empty() ? roomid : QString::fromStdString(roomInfo.name);
} else {
callParty_ = callee->user_id;
callPartyDisplayName_ =
callee->display_name.isEmpty() ? callee->user_id : callee->display_name;
emit ChatPage::instance()->showNotification(
QStringLiteral("Already on a call with a different user"));
emit newMessage(
roomid_,
CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy});
}
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
invitee_ = isRoomCall ? std::string{} : callParty_.toStdString();
emit newInviteState();
playRingtone(QUrl(QStringLiteral("qrc:/media/media/ringback.ogg")), true);
return;
}
uint32_t shareWindowId =
callType == CallType::SCREEN &&
(screenShareType_ == ScreenShareType::X11 || screenShareType_ == ScreenShareType::D3D11)
? windows_[windowIndex].second
: 0;
if (!session_.createOffer(callType, screenShareType_, shareWindowId)) {
nhlog::ui()->warn("WebRTC: call id: {} - failed to create offer in state {}",
callid_,
callStateString(session_.state()));
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
endCall();
}
} catch (const std::exception &e) {
logCallException("sendInvite", roomid, callid_, e);
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
endCall();
} catch (...) {
logUnknownCallException("sendInvite", roomid, callid_);
session_.setTurnServers(turnURIs_);
std::string strCallType =
callType_ == CallType::VOICE ? "voice" : (callType_ == CallType::VIDEO ? "video" : "screen");
nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType);
callParty_ = callee->user_id;
callPartyDisplayName_ = callee->display_name.isEmpty() ? callee->user_id : callee->display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
invitee_ = callParty_.toStdString();
emit newInviteState();
playRingtone(QUrl(QStringLiteral("qrc:/media/media/ringback.ogg")), true);
uint32_t shareWindowId =
callType == CallType::SCREEN &&
(screenShareType_ == ScreenShareType::X11 || screenShareType_ == ScreenShareType::D3D11)
? windows_[windowIndex].second
: 0;
if (!session_.createOffer(callType, screenShareType_, shareWindowId)) {
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
endCall();
}
@ -486,17 +366,11 @@ void
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
{
#ifdef GSTREAMER_AVAILABLE
try {
if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
handleEvent<CallNegotiate>(event) || handleEvent<CallSelectAnswer>(event) ||
handleEvent<CallAnswer>(event) || handleEvent<CallReject>(event) ||
handleEvent<CallHangUp>(event))
return;
} catch (const std::exception &e) {
logCallException("syncEvent", roomid_, callid_, e);
} catch (...) {
logUnknownCallException("syncEvent", roomid_, callid_);
}
if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
handleEvent<CallNegotiate>(event) || handleEvent<CallSelectAnswer>(event) ||
handleEvent<CallAnswer>(event) || handleEvent<CallReject>(event) ||
handleEvent<CallHangUp>(event))
return;
#else
(void)event;
#endif
@ -531,11 +405,8 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
callInviteEvent.sender,
callInviteEvent.content.party_id);
if (callInviteEvent.content.call_id.empty()) {
nhlog::ui()->debug("WebRTC: ignoring CallInvite without call id from {}",
callInviteEvent.sender);
if (callInviteEvent.content.call_id.empty())
return;
}
if (callInviteEvent.sender == utils::localUser().toStdString()) {
if (callInviteEvent.content.party_id == partyid_)
@ -550,133 +421,103 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
}
}
try {
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
callPartyVersion_ = callInviteEvent.content.version;
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
callPartyVersion_ = callInviteEvent.content.version;
const QString &ringtone = UserSettings::instance()->ringtone();
bool sharesRoom = true;
const QString &ringtone = UserSettings::instance()->ringtone();
bool sharesRoom = true;
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
auto callerIt = std::find_if(members.begin(), members.end(), [&](const RoomMember &member) {
return member.user_id.toStdString() == callInviteEvent.sender;
});
if (callerIt == members.end()) {
nhlog::ui()->warn("WebRTC: ignoring CallInvite from unknown member {} in room {}",
callInviteEvent.sender,
callInviteEvent.room_id);
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
const RoomMember &caller =
*std::find_if(members.begin(), members.end(), [&](RoomMember member) {
return member.user_id.toStdString() == callInviteEvent.sender;
});
if (isOnCall() || isOnCallOnOtherDevice()) {
if (isOnCallOnOtherDevice_ != "")
return;
if (callParty_.toStdString() == callInviteEvent.sender) {
if (session_.state() == webrtc::State::OFFERSENT) {
if (callid_ > callInviteEvent.content.call_id) {
endCall();
callParty_ = caller.user_id;
callPartyDisplayName_ =
caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
haveCallInvite_ = true;
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
inviteSDP_ = callInviteEvent.content.offer.sdp;
emit newInviteState();
acceptInvite();
}
return;
} else if (session_.state() < webrtc::State::OFFERSENT)
endCall();
else
return;
} else
return;
}
if (callPartyVersion_ == "0") {
if (roomInfo.member_count != 2) {
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
partyid_,
callPartyVersion_,
CallHangUp::Reason::InviteTimeOut});
return;
}
const RoomMember &caller = *callerIt;
} else {
if (caller.user_id == utils::localUser() &&
callInviteEvent.content.party_id == partyid_) // remote echo
return;
if (isOnCall() || isOnCallOnOtherDevice()) {
if (isOnCallOnOtherDevice_ != "") {
nhlog::ui()->debug("WebRTC: ignoring CallInvite while another device is active");
return;
}
if (callParty_.toStdString() == callInviteEvent.sender) {
if (session_.state() == webrtc::State::OFFERSENT) {
if (callid_ > callInviteEvent.content.call_id) {
endCall();
callParty_ = caller.user_id;
callPartyDisplayName_ =
caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
haveCallInvite_ = true;
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
inviteSDP_ = callInviteEvent.content.offer.sdp;
emit newInviteState();
acceptInvite();
} else {
nhlog::ui()->debug(
"WebRTC: ignoring polite glare invite with newer local call id {}",
callid_);
}
return;
} else if (session_.state() < webrtc::State::OFFERSENT)
endCall();
else {
nhlog::ui()->debug("WebRTC: ignoring CallInvite in active state {}",
callStateString(session_.state()));
return;
}
} else {
nhlog::ui()->debug("WebRTC: ignoring CallInvite from {} while in call with {}",
callInviteEvent.sender,
callParty_.toStdString());
return;
}
}
if (callPartyVersion_ == "0") {
if (roomInfo.member_count != 2) {
nhlog::ui()->debug("WebRTC: rejecting v0 CallInvite in room with {} members",
roomInfo.member_count);
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
partyid_,
callPartyVersion_,
CallHangUp::Reason::InviteTimeOut});
return;
if (roomInfo.member_count == 2 || // general call in room with two members
(roomInfo.member_count == 1 &&
partyid_ !=
callInviteEvent.content.party_id) || // self call, ring if not the same party_id
callInviteEvent.content.invitee == "" || // empty, meant for everyone
callInviteEvent.content.invitee ==
utils::localUser().toStdString()) // meant specifically for local user
{
if (roomInfo.member_count > 2) {
// check if shares room
sharesRoom = checkSharesRoom(QString::fromStdString(callInviteEvent.room_id),
callInviteEvent.content.invitee);
}
} else {
if (caller.user_id == utils::localUser() &&
callInviteEvent.content.party_id == partyid_) {
nhlog::ui()->debug("WebRTC: ignoring echoed CallInvite from this device");
return;
}
if (roomInfo.member_count == 2 ||
(roomInfo.member_count == 1 && partyid_ != callInviteEvent.content.party_id) ||
callInviteEvent.content.invitee == "" ||
callInviteEvent.content.invitee == utils::localUser().toStdString()) {
if (roomInfo.member_count > 2) {
sharesRoom = checkSharesRoom(QString::fromStdString(callInviteEvent.room_id),
callInviteEvent.content.invitee);
}
} else {
nhlog::ui()->debug("WebRTC: rejecting CallInvite not addressed to this device/user");
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
partyid_,
callPartyVersion_,
CallHangUp::Reason::InviteTimeOut});
return;
}
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
partyid_,
callPartyVersion_,
CallHangUp::Reason::InviteTimeOut});
return;
}
if (ringtone != QLatin1String("Mute") && sharesRoom)
playRingtone(ringtone == QLatin1String("Default")
? QUrl(QStringLiteral("qrc:/media/media/ring.ogg"))
: QUrl::fromLocalFile(ringtone),
true);
callParty_ = caller.user_id;
callPartyDisplayName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
haveCallInvite_ = true;
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
inviteSDP_ = callInviteEvent.content.offer.sdp;
emit newInviteState();
} catch (const std::exception &e) {
logCallException("handleEvent(CallInvite)",
QString::fromStdString(callInviteEvent.room_id),
callInviteEvent.content.call_id,
e);
} catch (...) {
logUnknownCallException("handleEvent(CallInvite)",
QString::fromStdString(callInviteEvent.room_id),
callInviteEvent.content.call_id);
}
// ring if not mute or does not have direct message room
if (ringtone != QLatin1String("Mute") && sharesRoom)
playRingtone(ringtone == QLatin1String("Default")
? QUrl(QStringLiteral("qrc:/media/media/ring.ogg"))
: QUrl::fromLocalFile(ringtone),
true);
callParty_ = caller.user_id;
callPartyDisplayName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
haveCallInvite_ = true;
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
inviteSDP_ = callInviteEvent.content.offer.sdp;
emit newInviteState();
}
void
@ -684,10 +525,8 @@ CallManager::acceptInvite()
{
// if call was accepted/rejected elsewhere and m.call.select_answer is
// received before acceptInvite
if (!haveCallInvite_) {
nhlog::ui()->debug("WebRTC: acceptInvite ignored without a pending invite");
if (!haveCallInvite_)
return;
}
stopRingtone();
std::string errorMessage;
@ -702,9 +541,6 @@ CallManager::acceptInvite()
session_.setTurnServers(turnURIs_);
if (!session_.acceptOffer(inviteSDP_)) {
nhlog::ui()->warn("WebRTC: call id: {} - failed to accept offer in state {}",
callid_,
callStateString(session_.state()));
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
hangUp();
return;
@ -734,11 +570,8 @@ void
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
{
if (callCandidatesEvent.sender == utils::localUser().toStdString() &&
callCandidatesEvent.content.party_id == partyid_) {
nhlog::ui()->debug("WebRTC: ignoring echoed CallCandidates for call id {}",
callCandidatesEvent.content.call_id);
callCandidatesEvent.content.party_id == partyid_)
return;
}
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from ({}, {})",
callCandidatesEvent.content.call_id,
callCandidatesEvent.sender,
@ -750,16 +583,9 @@ CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
else {
// CallInvite has been received and we're awaiting localUser to accept or
// reject the call
nhlog::ui()->debug("WebRTC: queueing {} ICE candidates for pending invite {}",
callCandidatesEvent.content.candidates.size(),
callCandidatesEvent.content.call_id);
for (const auto &c : callCandidatesEvent.content.candidates)
remoteICECandidates_.push_back(c);
}
} else {
nhlog::ui()->debug("WebRTC: ignoring CallCandidates for inactive call id {} (active: {})",
callCandidatesEvent.content.call_id,
callid_);
}
}
@ -770,11 +596,8 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
callAnswerEvent.content.call_id,
callAnswerEvent.sender,
callAnswerEvent.content.party_id);
if (answerSelected_) {
nhlog::ui()->debug("WebRTC: ignoring duplicate CallAnswer for call id {}",
callAnswerEvent.content.call_id);
if (answerSelected_)
return;
}
if (callAnswerEvent.sender == utils::localUser().toStdString() &&
callid_ == callAnswerEvent.content.call_id) {
@ -799,19 +622,9 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.answer.sdp)) {
nhlog::ui()->warn("WebRTC: call id: {} - failed to accept answer in state {}",
callid_,
callStateString(session_.state()));
emit ChatPage::instance()->showNotification(QStringLiteral("Problem setting up call."));
hangUp();
return;
}
} else {
nhlog::ui()->debug("WebRTC: ignoring CallAnswer for inactive call id {} (active: {}, state: {})",
callAnswerEvent.content.call_id,
callid_,
callStateString(session_.state()));
return;
}
emit newMessage(
roomid_,
@ -832,11 +645,6 @@ CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
if (callid_ == callHangUpEvent.content.call_id ||
isOnCallOnOtherDevice_ == callHangUpEvent.content.call_id)
endCall();
else
nhlog::ui()->debug("WebRTC: ignoring CallHangUp for inactive call id {} (active: {}, other device: {})",
callHangUpEvent.content.call_id,
callid_,
isOnCallOnOtherDevice_);
}
void
@ -849,7 +657,7 @@ CallManager::handleEvent(const RoomEvent<CallSelectAnswer> &callSelectAnswerEven
if (callSelectAnswerEvent.sender == utils::localUser().toStdString()) {
if (callSelectAnswerEvent.content.party_id != partyid_) {
if (std::find(rejectCallPartyIDs_.begin(),
rejectCallPartyIDs_.end(),
rejectCallPartyIDs_.begin(),
callSelectAnswerEvent.content.selected_party_id) !=
rejectCallPartyIDs_.end())
endCall();
@ -867,7 +675,7 @@ CallManager::handleEvent(const RoomEvent<CallSelectAnswer> &callSelectAnswerEven
if (callSelectAnswerEvent.content.selected_party_id != partyid_) {
bool endAllCalls = false;
if (std::find(rejectCallPartyIDs_.begin(),
rejectCallPartyIDs_.end(),
rejectCallPartyIDs_.begin(),
callSelectAnswerEvent.content.selected_party_id) !=
rejectCallPartyIDs_.end())
endAllCalls = true;
@ -878,10 +686,6 @@ CallManager::handleEvent(const RoomEvent<CallSelectAnswer> &callSelectAnswerEven
endCall(endAllCalls);
} else if (session_.state() == webrtc::State::DISCONNECTED)
endCall();
} else {
nhlog::ui()->debug("WebRTC: ignoring CallSelectAnswer for inactive call id {} (active: {})",
callSelectAnswerEvent.content.call_id,
callid_);
}
}
@ -892,11 +696,8 @@ CallManager::handleEvent(const RoomEvent<CallReject> &callRejectEvent)
callRejectEvent.content.call_id,
callRejectEvent.sender,
callRejectEvent.content.party_id);
if (answerSelected_) {
nhlog::ui()->debug("WebRTC: ignoring CallReject after answer selection for call id {}",
callRejectEvent.content.call_id);
if (answerSelected_)
return;
}
rejectCallPartyIDs_.push_back(callRejectEvent.content.party_id);
// check remote echo
@ -918,10 +719,6 @@ CallManager::handleEvent(const RoomEvent<CallReject> &callRejectEvent)
callid_, partyid_, callPartyVersion_, callRejectEvent.content.party_id});
endCall();
}
} else {
nhlog::ui()->debug("WebRTC: ignoring CallReject for inactive call id {} (active: {})",
callRejectEvent.content.call_id,
callid_);
}
}
@ -933,24 +730,8 @@ CallManager::handleEvent(const RoomEvent<CallNegotiate> &callNegotiateEvent)
callNegotiateEvent.sender,
callNegotiateEvent.content.party_id);
if (callNegotiateEvent.sender == utils::localUser().toStdString() &&
callNegotiateEvent.content.party_id == partyid_) {
nhlog::ui()->debug("WebRTC: ignoring echoed CallNegotiate for call id {}",
callNegotiateEvent.content.call_id);
return;
}
if (callNegotiateEvent.content.call_id != callid_) {
nhlog::ui()->debug("WebRTC: ignoring CallNegotiate for inactive call id {} (active: {})",
callNegotiateEvent.content.call_id,
callid_);
return;
}
std::string negotiationSDP_ = callNegotiateEvent.content.description.sdp;
if (!session_.acceptNegotiation(negotiationSDP_)) {
nhlog::ui()->warn("WebRTC: call id: {} - failed to accept renegotiation in state {}",
callid_,
callStateString(session_.state()));
emit ChatPage::instance()->showNotification(QStringLiteral("Problem accepting new SDP"));
hangUp();
return;
@ -1067,8 +848,6 @@ CallManager::retrieveTurnServer()
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
if (err) {
turnServerTimer_.setInterval(5000);
nhlog::net()->warn("Failed to retrieve TURN server, retrying in 5s: {}",
requestErrorString(err));
return;
}
emit turnServerRetrieved(res);

View file

@ -206,9 +206,6 @@ iceGatheringStateChanged(GstElement *webrtc,
if (WebRTCSession::instance().isOffering()) {
emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
emit WebRTCSession::instance().stateChanged(State::OFFERSENT);
} else if (WebRTCSession::instance().isNegotiating()) {
emit WebRTCSession::instance().negotiationCreated(localsdp_, localcandidates_);
WebRTCSession::instance().finishNegotiation();
} else {
emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
emit WebRTCSession::instance().stateChanged(State::ANSWERSENT);
@ -768,12 +765,6 @@ WebRTCSession::createOffer(CallType callType,
ScreenShareType screenShareType,
uint32_t shareWindowId)
{
if (state_ != State::DISCONNECTED) {
nhlog::ui()->warn("WebRTC: createOffer ignored in state {}",
static_cast<int>(state_));
return false;
}
clear();
isOffering_ = true;
callType_ = callType;
@ -792,11 +783,8 @@ bool
WebRTCSession::acceptOffer(const std::string &sdp)
{
nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp);
if (state_ != State::DISCONNECTED) {
nhlog::ui()->warn("WebRTC: acceptOffer ignored in state {}",
static_cast<int>(state_));
if (state_ != State::DISCONNECTED)
return false;
}
clear();
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
@ -851,61 +839,17 @@ bool
WebRTCSession::acceptNegotiation(const std::string &sdp)
{
nhlog::ui()->debug("WebRTC: received negotiation offer:\n{}", sdp);
if (state_ < State::INITIATED) {
nhlog::ui()->warn("WebRTC: acceptNegotiation ignored in state {}",
static_cast<int>(state_));
if (state_ == State::DISCONNECTED)
return false;
}
if (!pipe_ || !webrtc_) {
nhlog::ui()->error("WebRTC: acceptNegotiation called without an active pipeline");
return false;
}
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
if (!offer)
return false;
int opusPayloadType;
bool recvOnly;
bool sendOnly;
if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) {
if (opusPayloadType == -1) {
nhlog::ui()->error("WebRTC: remote negotiation offer - no opus encoding");
gst_webrtc_session_description_free(offer);
return false;
}
} else {
nhlog::ui()->error("WebRTC: remote negotiation offer - no audio media");
gst_webrtc_session_description_free(offer);
return false;
}
int unusedPayloadType;
bool isVideo = getMediaAttributes(
offer->sdp, "video", "vp8", unusedPayloadType, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_);
if (isVideo && unusedPayloadType == -1) {
nhlog::ui()->error("WebRTC: remote negotiation offer - no vp8 encoding");
gst_webrtc_session_description_free(offer);
return false;
}
localsdp_.clear();
localcandidates_.clear();
isNegotiating_ = true;
GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
gst_webrtc_session_description_free(offer);
return true;
return false;
}
bool
WebRTCSession::acceptAnswer(const std::string &sdp)
{
nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
if (state_ != State::OFFERSENT) {
nhlog::ui()->warn("WebRTC: acceptAnswer ignored in state {}", static_cast<int>(state_));
if (state_ != State::OFFERSENT)
return false;
}
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
if (!answer) {
@ -929,19 +873,14 @@ void
WebRTCSession::acceptICECandidates(
const std::vector<mtx::events::voip::CallCandidates::Candidate> &candidates)
{
if (state_ < State::INITIATED || !webrtc_) {
nhlog::ui()->debug("WebRTC: ignoring {} remote ICE candidates in state {}",
candidates.size(),
static_cast<int>(state_));
return;
}
for (const auto &c : candidates) {
nhlog::ui()->debug("WebRTC: remote candidate: (m-line:{}):{}",
c.sdpMLineIndex,
c.candidate);
if (!c.candidate.empty()) {
g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
if (state_ >= State::INITIATED) {
for (const auto &c : candidates) {
nhlog::ui()->debug(
"WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
if (!c.candidate.empty()) {
g_signal_emit_by_name(
webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
}
}
}
}
@ -949,10 +888,8 @@ WebRTCSession::acceptICECandidates(
bool
WebRTCSession::startPipeline(int opusPayloadType, int vp8PayloadType)
{
if (state_ != State::DISCONNECTED) {
nhlog::ui()->warn("WebRTC: startPipeline ignored in state {}", static_cast<int>(state_));
if (state_ != State::DISCONNECTED)
return false;
}
emit stateChanged(State::INITIATING);
@ -1019,10 +956,8 @@ bool
WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType)
{
GstDevice *device = devices_.audioDevice();
if (!device) {
nhlog::ui()->error("WebRTC: no audio input device available");
if (!device)
return false;
}
GstElement *source = gst_device_create_element(device, nullptr);
GstElement *volume = gst_element_factory_make("volume", "srclevel");
@ -1104,10 +1039,8 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType)
std::pair<int, int> resolution;
std::pair<int, int> frameRate;
GstDevice *device = devices_.videoDevice(resolution, frameRate);
if (!device) {
nhlog::ui()->error("WebRTC: no video input device available");
if (!device)
return false;
}
GstElement *camera = gst_device_create_element(device, nullptr);
GstCaps *caps = gst_caps_new_simple("video/x-raw",
@ -1372,7 +1305,6 @@ WebRTCSession::clear()
{
callType_ = webrtc::CallType::VOICE;
isOffering_ = false;
isNegotiating_ = false;
isRemoteVideoRecvOnly_ = false;
isRemoteVideoSendOnly_ = false;
videoItem_ = nullptr;

View file

@ -70,8 +70,6 @@ public:
webrtc::State state() const { return state_; }
bool haveLocalPiP() const;
bool isOffering() const { return isOffering_; }
bool isNegotiating() const { return isNegotiating_; }
void finishNegotiation() { isNegotiating_ = false; }
bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; }
bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; }
@ -96,8 +94,6 @@ signals:
const std::vector<mtx::events::voip::CallCandidates::Candidate> &);
void answerCreated(const std::string &sdp,
const std::vector<mtx::events::voip::CallCandidates::Candidate> &);
void negotiationCreated(const std::string &sdp,
const std::vector<mtx::events::voip::CallCandidates::Candidate> &);
void newICECandidate(const mtx::events::voip::CallCandidates::Candidate &);
void stateChanged(webrtc::State);
@ -115,7 +111,6 @@ private:
webrtc::ScreenShareType screenShareType_ = webrtc::ScreenShareType::X11;
webrtc::State state_ = webrtc::State::DISCONNECTED;
bool isOffering_ = false;
bool isNegotiating_ = false;
bool isRemoteVideoRecvOnly_ = false;
bool isRemoteVideoSendOnly_ = false;
QQuickItem *videoItem_ = nullptr;

View file

@ -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)