Compare commits

..

4 commits

Author SHA1 Message Date
7775068bf5 ui and group call shenanigans 2026-03-11 12:58:07 +02:00
586c3e3e3a devenv shenanigans 2026-03-11 00:44:53 +02:00
8dc75613c7 slop shenanigans 2026-03-11 00:44:29 +02:00
6a0852ee36 voice shenanigans 2026-03-11 00:41:46 +02:00
66 changed files with 1170 additions and 832 deletions

View file

@ -24,7 +24,6 @@ cmake -GNinja -S. -Bbuild \
-DCMAKE_INSTALL_PREFIX="nheko.temp" \ -DCMAKE_INSTALL_PREFIX="nheko.temp" \
-DHUNTER_ROOT="../.hunter" \ -DHUNTER_ROOT="../.hunter" \
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \ -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \
-DKDSingleApplication_STATIC=ON -DKDSingleApplication_EXAMPLES=OFF \
-DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \
-DQt6_DIR=${QT_BASEPATH}/lib/cmake \ -DQt6_DIR=${QT_BASEPATH}/lib/cmake \
-DCI_BUILD=ON -DCI_BUILD=ON

View file

@ -15,7 +15,6 @@ echo %DATE%
call "C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Auxiliary/Build/vcvarsall.bat" x64 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 -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% cmake --build build --config Release -j %NUMBER_OF_PROCESSORS%

View file

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

2
.envrc Normal file
View file

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

23
.gitignore vendored
View file

@ -128,3 +128,26 @@ package.dir
# Archives # Archives
*.bz2 *.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_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') #- 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 # hardcoded to avoid fuzzy matching
- export LATEST_WINDOWS='0.12.0.38759' - export LATEST_WINDOWS='0.12.0.35798'
- sed "s/0.12.1.0/${LATEST_WINDOWS_NIGHTLY}/g" -i resources/NhekoNightly.appinstaller - 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 - sed "s/0.12.1.0/${LATEST_WINDOWS}/g" -i resources/Nheko.appinstaller
- mkdir public - mkdir public
@ -450,8 +450,8 @@ linting:
before_script: before_script:
- apk update && apk add make git python3 py3-pip qt6-qtdeclarative-dev - apk update && apk add make git python3 py3-pip qt6-qtdeclarative-dev
# clang18 seems to mess with the emit keyword when using the `->` operator # clang18 seems to mess with the emit keyword when using the `->` operator
- apk add clang-extra-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main - apk add clang17-extra-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
- export PATH="$PATH:/usr/lib/llvm/bin/:/root/.local/bin" - export PATH="$PATH:/usr/lib/llvm17/bin/:/root/.local/bin"
- pip3 install --break-system-packages --user reuse - pip3 install --break-system-packages --user reuse
script: script:
- make lint - 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) option(HUNTER_ENABLED "Enable Hunter package manager" OFF)
include("cmake/HunterGate.cmake") include("cmake/HunterGate.cmake")
HunterGate( HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.26.6.tar.gz" URL "https://github.com/cpp-pm/hunter/archive/v0.26.1.tar.gz"
SHA1 "e70c29f878f5d5f5cdf1b9ccd628fb872e8624a8" SHA1 "e41ac7a18c49b35ebac99ff2b5244317b2638a65"
LOCAL LOCAL
) )
@ -246,12 +246,13 @@ endif()
# #
# Discover Qt dependencies. # Discover Qt dependencies.
# #
find_package(Qt6 6.5 COMPONENTS Core Widgets Gui LinguistTools Svg Multimedia Qml QuickControls2 REQUIRED) 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) find_package(Qt6DBus)
find_package(Qt6QmlPrivate REQUIRED NO_MODULE)
if(UNIX)
find_package(Qt6GuiPrivate REQUIRED NO_MODULE)
endif()
if(USE_BUNDLED_QTKEYCHAIN) if(USE_BUNDLED_QTKEYCHAIN)
include(FetchContent) include(FetchContent)
@ -299,8 +300,7 @@ if(NOT MSVC)
-fsized-deallocation \ -fsized-deallocation \
-fdiagnostics-color=always \ -fdiagnostics-color=always \
-Wunreachable-code \ -Wunreachable-code \
-Wno-attributes \ -Wno-attributes"
-Wno-error=unused-parameter"
) )
if(NOT CMAKE_COMPILER_IS_GNUCXX) if(NOT CMAKE_COMPILER_IS_GNUCXX)
# -Wshadow is buggy and broken in GCC, so do not enable it. # -Wshadow is buggy and broken in GCC, so do not enable it.
@ -544,9 +544,7 @@ if(USE_BUNDLED_OLM)
Olm Olm
GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git
GIT_TAG 3.2.16 GIT_TAG 3.2.16
PATCH_COMMAND git apply 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/0001-fix-list-const-ptr.patch
${CMAKE_CURRENT_SOURCE_DIR}/third_party/olm-patches/0002-fix-cmake-cmp0148.patch
UPDATE_DISCONNECTED 1 UPDATE_DISCONNECTED 1
) )
set(OLM_TESTS OFF CACHE INTERNAL "") set(OLM_TESTS OFF CACHE INTERNAL "")
@ -569,12 +567,16 @@ if(USE_BUNDLED_CMARK)
FetchContent_Declare( FetchContent_Declare(
cmark cmark
GIT_REPOSITORY https://github.com/commonmark/cmark.git GIT_REPOSITORY https://github.com/commonmark/cmark.git
GIT_TAG 0.31.1 GIT_TAG 0.30.2
CMAKE_ARGS "BUILD_TESTING=OFF" CMAKE_ARGS "CMARK_STATIC=ON CMARK_SHARED=OFF CMARK_TESTS=OFF CMARK_TESTS=OFF"
) )
FetchContent_MakeAvailable(cmark) FetchContent_MakeAvailable(cmark)
if (NOT TARGET cmark::cmark) if (NOT TARGET cmark::cmark)
add_library(cmark::cmark ALIAS cmark) if(MSVC)
add_library(cmark::cmark ALIAS cmark)
else()
add_library(cmark::cmark ALIAS cmark_static)
endif()
endif() endif()
else() else()
find_package(cmark REQUIRED 0.29.0) find_package(cmark REQUIRED 0.29.0)
@ -622,7 +624,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 873911e352a0845dfb178f77b1ddea796a5d3455 GIT_TAG v0.10.1
) )
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -716,9 +718,7 @@ set_target_properties(nheko
# #
# Add qml files # Add qml files
# #
if(QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif()
set(QML_SOURCES set(QML_SOURCES
resources/qml/Root.qml resources/qml/Root.qml
resources/qml/ChatPage.qml resources/qml/ChatPage.qml

81
PLAN-calls.md Normal file
View file

@ -0,0 +1,81 @@
# 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,7 +343,6 @@ sudo pacman -S qt6-base \
gcc \ gcc \
fontconfig \ fontconfig \
lmdb \ lmdb \
lmdbxx \
cmark \ cmark \
qtkeychain-qt6 qtkeychain-qt6
``` ```
@ -418,6 +417,51 @@ cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build 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.: To use bundled dependencies you can use hunter, i.e.:
```bash ```bash

65
devenv.lock Normal file
View file

@ -0,0 +1,65 @@
{
"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
}

129
devenv.nix Normal file
View file

@ -0,0 +1,129 @@
{ 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
'';
}

12
devenv.yaml Normal file
View file

@ -0,0 +1,12 @@
# 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,12 +70,10 @@ modules:
config-opts: config-opts:
- -DCMAKE_BUILD_TYPE=Release - -DCMAKE_BUILD_TYPE=Release
- -DCMARK_TESTS=OFF - -DCMARK_TESTS=OFF
- -DBUILD_TESTING=OFF
- -DBUILD_SHARED_LIBS=OFF
sources: sources:
- sha256: 3da93db5469c30588cfeb283d9d62edfc6ded9eb0edc10a4f5bbfb7d722ea802 - sha256: bbcb8f8c03b5af33fcfcf11a74e9499f20a9043200b8552f78a6e8ba76e04d11
type: archive type: archive
url: https://github.com/commonmark/cmark/archive/0.31.1.tar.gz url: https://github.com/commonmark/cmark/archive/0.31.0.tar.gz
- name: fmt - name: fmt
buildsystem: cmake-ninja buildsystem: cmake-ninja
config-opts: config-opts:
@ -213,8 +211,8 @@ modules:
- -DBUILD_SHARED_LIBS=OFF - -DBUILD_SHARED_LIBS=OFF
buildsystem: cmake-ninja buildsystem: cmake-ninja
sources: sources:
- commit: 873911e352a0845dfb178f77b1ddea796a5d3455 - commit: 15b43844f4ec27faa5f2ec92c4ded313206763aa
#tag: v0.10.1 tag: v0.10.1
type: git type: git
url: https://github.com/Nheko-Reborn/mtxclient.git url: https://github.com/Nheko-Reborn/mtxclient.git
- name: nheko - name: nheko

View file

@ -7,7 +7,7 @@
== NAME == NAME
nheko - Desktop client for Matrix using Qt and C++20 nheko - Desktop client for Matrix using Qt and C++17
== SYNOPSIS == SYNOPSIS

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 494 B

View file

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

View file

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

View file

@ -39,7 +39,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
visible: room ? room.permissions.canSend(room.isEncrypted ? MtxEvent.Encrypted : MtxEvent.TextMessage) : false visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
ImageButton { ImageButton {
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
@ -170,13 +170,15 @@ Rectangle {
} else if (event.matches(StandardKey.SelectAll) && popup.opened) { } else if (event.matches(StandardKey.SelectAll) && popup.opened) {
completer.completerName = ""; completer.completerName = "";
popup.close(); popup.close();
} else if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { } else if (event.matches(StandardKey.InsertLineSeparator)) {
// Handling popup takes priority over newline and sending message. if (popup.opened)
if (popup.opened && popup.close();
(event.modifiers == Qt.NoModifier if (Settings.invertEnterKey) {
|| event.modifiers == Qt.ShiftModifier room.input.send();
|| event.modifiers == Qt.ControlModifier) event.accepted = true;
) { }
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
if (popup.opened) {
var currentCompletion = completer.currentCompletion(); var currentCompletion = completer.currentCompletion();
let userid = completer.currentUserid(); let userid = completer.currentUserid();
@ -189,26 +191,14 @@ Rectangle {
console.log(userid); console.log(userid);
room.input.addMention(userid, currentCompletion); room.input.addMention(userid, currentCompletion);
} }
event.accepted = true;
return;
} }
event.accepted = true;
} }
// Send message Enter key combination event. if (!Settings.invertEnterKey) {
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(); room.input.send();
event.accepted = true; 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)) { } else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) {
event.accepted = true; event.accepted = true;
if (popup.opened) { if (popup.opened) {

View file

@ -16,7 +16,6 @@ Item {
property int availableWidth: width property int availableWidth: width
property int padding: Nheko.paddingMedium property int padding: Nheko.paddingMedium
property string searchString: "" property string searchString: ""
property bool filterByNotifications: false
property Room roommodel: room property Room roommodel: room
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
@ -61,7 +60,7 @@ Item {
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
displayMarginBeginning: height / 4 displayMarginBeginning: height / 4
displayMarginEnd: height / 4 displayMarginEnd: height / 4
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent || filteredTimeline.filterByNotifications) ? filteredTimeline : room model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
//pixelAligned: true //pixelAligned: true
spacing: 2 spacing: 2
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
@ -146,7 +145,6 @@ Item {
id: filteredTimeline id: filteredTimeline
filterByContent: chatRoot.searchString filterByContent: chatRoot.searchString
filterByNotifications: chatRoot.filterByNotifications
filterByThread: room ? room.thread : "" filterByThread: room ? room.thread : ""
source: room source: room
} }
@ -556,8 +554,6 @@ Item {
Component { Component {
MenuItem { MenuItem {
text: qsTr("&Mark as read") text: qsTr("&Mark as read")
onTriggered: room.markEventAsRead(messageContextMenuC.eventId)
} }
} }
Component { Component {

View file

@ -210,10 +210,9 @@ TimelineEvent {
AbstractButton { AbstractButton {
id: replyRow id: replyRow
visible: wrapper.replyTo visible: wrapper.reply
leftPadding: Nheko.paddingSmall + 4
height: replyLine.height
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -226,7 +225,19 @@ TimelineEvent {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
contentItem: Column { 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 {
spacing: 0 spacing: 0
id: replyCol id: replyCol
@ -236,7 +247,7 @@ TimelineEvent {
contentItem: Label { contentItem: Label {
id: userName_ id: userName_
text: wrapper.reply?.userName ?? 'missing name' text: wrapper.reply?.userName ?? ''
color: replyRow.userColor color: replyRow.userColor
textFormat: Text.RichText textFormat: Text.RichText
width: wrapper.maxWidth width: wrapper.maxWidth
@ -248,20 +259,12 @@ TimelineEvent {
replyUserButton, replyUserButton,
wrapper.reply, wrapper.reply,
] ]
}
} }
background: Rectangle { background: Rectangle {
//width: replyRow.implicitContentWidth //width: replyRow.implicitContentWidth
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) 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: { onClicked: {

View file

@ -192,9 +192,9 @@ TimelineEvent {
AbstractButton { AbstractButton {
id: replyRow id: replyRow
visible: wrapper.replyTo visible: wrapper.reply
leftPadding: Nheko.paddingSmall + 4 height: replyLine.height
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
@ -205,7 +205,19 @@ TimelineEvent {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
contentItem: Column { 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 {
spacing: 0 spacing: 0
id: replyCol id: replyCol
@ -215,7 +227,7 @@ TimelineEvent {
contentItem: Label { contentItem: Label {
id: userName_ id: userName_
text: wrapper.reply?.userName ?? 'missing name' text: wrapper.reply?.userName ?? ''
color: replyRow.userColor color: replyRow.userColor
textFormat: Text.RichText textFormat: Text.RichText
width: wrapper.maxWidth width: wrapper.maxWidth
@ -227,20 +239,12 @@ TimelineEvent {
replyUserButton, replyUserButton,
wrapper.reply, wrapper.reply,
] ]
}
} }
background: Rectangle { background: Rectangle {
//width: replyRow.implicitContentWidth
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) 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: { onClicked: {

View file

@ -97,7 +97,7 @@ Column {
sourceSize.height: height sourceSize.height: height
permissions: room ? room.permissions : null permissions: room ? room.permissions : null
visible: isAdmin || isModerator // implicitly includes creators as well visible: isAdmin || isModerator
} }
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay

View file

@ -119,7 +119,6 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: msgView.height - typingIndicator.height implicitHeight: msgView.height - typingIndicator.height
searchString: topBar.searchString searchString: topBar.searchString
filterByNotifications: topBar.filterNotifications
} }
Loader { Loader {
source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? (Qt.platform.os != "windows" ? "voip/VideoCall.qml" : "voip/VideoCallD3D11.qml") : "" source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? (Qt.platform.os != "windows" ? "voip/VideoCall.qml" : "voip/VideoCallD3D11.qml") : ""

View file

@ -22,7 +22,6 @@ Pane {
property bool searchHasFocus: searchField.focus && searchField.enabled property bool searchHasFocus: searchField.focus && searchField.enabled
property string searchString: "" property string searchString: ""
property bool showBackButton: false property bool showBackButton: false
property bool filterNotifications: false
property int trustlevel: room ? room.trustlevel : Crypto.Unverified property int trustlevel: room ? room.trustlevel : Crypto.Unverified
Layout.fillWidth: true Layout.fillWidth: true
@ -130,30 +129,13 @@ Pane {
selectByMouse: false selectByMouse: false
text: roomTopic 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 { ImageButton {
id: pinButton id: pinButton
property bool pinsShown: !Settings.hiddenPins.includes(roomId) property bool pinsShown: !Settings.hiddenPins.includes(roomId)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.column: 4 Layout.column: 3
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1 Layout.row: 1
@ -178,7 +160,7 @@ Pane {
} }
AbstractButton { AbstractButton {
id: memberButton id: memberButton
Layout.column: 5 Layout.column: 4
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1 Layout.row: 1
@ -218,7 +200,7 @@ Pane {
property bool searchActive: false property bool searchActive: false
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.column: 6 Layout.column: 5
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1 Layout.row: 1
@ -242,7 +224,7 @@ Pane {
id: roomOptionsButton id: roomOptionsButton
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.column: 7 Layout.column: 6
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1 Layout.row: 1
@ -291,7 +273,7 @@ Pane {
id: pinnedMessages id: pinnedMessages
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 5 Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4) Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
Layout.row: 3 Layout.row: 3
@ -330,7 +312,7 @@ Pane {
ImageButton { ImageButton {
id: deletePinButton id: deletePinButton
Layout.alignment: Qt.AlignTop | Qt.AlignRight Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredHeight: 16 Layout.preferredHeight: 16
Layout.preferredWidth: 16 Layout.preferredWidth: 16
ToolTip.text: qsTr("Unpin") ToolTip.text: qsTr("Unpin")
@ -348,7 +330,7 @@ Pane {
id: widgets id: widgets
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 5 Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5) Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5)
Layout.row: 4 Layout.row: 4
@ -374,7 +356,7 @@ Pane {
id: searchField id: searchField
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 5 Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
Layout.row: 5 Layout.row: 5
enabled: visible enabled: visible
@ -396,7 +378,6 @@ Pane {
searchString = ""; searchString = "";
searchButton.searchActive = false; searchButton.searchActive = false;
searchField.text = ""; searchField.text = "";
filterNotifications = false;
} }
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu

View file

@ -7,16 +7,15 @@ import QtQuick.Controls
import im.nheko import im.nheko
Image { Image {
required property var powerlevel required property int powerlevel
required property Permissions permissions required property var permissions
readonly property bool isV12Creator: permissions ? permissions.creatorLevel() == powerlevel : false
readonly property bool isAdmin: permissions ? permissions.changeLevel(MtxEvent.PowerLevels) <= 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 isModerator: permissions ? permissions.redactLevel() <= powerlevel : false
readonly property bool isDefault: permissions ? permissions.defaultLevel() <= powerlevel : false readonly property bool isDefault: permissions ? permissions.defaultLevel() <= powerlevel : false
readonly property string sourceUrl: { readonly property string sourceUrl: {
if (isAdmin || isV12Creator) if (isAdmin)
return "image://colorimage/:/icons/icons/ui/ribbon_star.svg?"; return "image://colorimage/:/icons/icons/ui/ribbon_star.svg?";
else if (isModerator) else if (isModerator)
return "image://colorimage/:/icons/icons/ui/ribbon.svg?"; return "image://colorimage/:/icons/icons/ui/ribbon.svg?";
@ -27,15 +26,12 @@ Image {
source: sourceUrl + (ma.hovered ? palette.highlight : palette.buttonText) source: sourceUrl + (ma.hovered ? palette.highlight : palette.buttonText)
ToolTip.visible: ma.hovered ToolTip.visible: ma.hovered
ToolTip.text: { ToolTip.text: {
let pl = powerlevel.toLocaleString(Qt.locale(), "f", 0); if (isAdmin)
if (isV12Creator) return qsTr("Administrator: %1").arg(powerlevel);
return qsTr("Creator");
else if (isAdmin)
return qsTr("Administrator (%1)").arg(pl)
else if (isModerator) else if (isModerator)
return qsTr("Moderator: %1").arg(pl); return qsTr("Moderator: %1").arg(powerlevel);
else else
return qsTr("User: %1").arg(pl); return qsTr("User: %1").arg(powerlevel);
} }
HoverHandler { HoverHandler {

View file

@ -5,7 +5,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Window import QtQuick.Window
import QtQuick.Layouts
import im.nheko import im.nheko
import "../" import "../"
@ -22,11 +21,7 @@ AbstractButton {
property string userId: eventId ? room.dataById(eventId, Room.UserId, "") : "" property string userId: eventId ? room.dataById(eventId, Room.UserId, "") : ""
property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : "" property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : ""
implicitHeight: replyContainer.height implicitHeight: replyContainer.height
implicitWidth: replyContainer.implicitWidth + leftPadding + rightPadding implicitWidth: replyContainer.implicitWidth
leftPadding: 4 + Nheko.paddingSmall
rightPadding: Nheko.paddingSmall
required property int maxWidth required property int maxWidth
property bool limitHeight: false property bool limitHeight: false
@ -36,14 +31,14 @@ AbstractButton {
} }
onClicked: { onClicked: {
let link = timelineEvent.main.linkAt != undefined && timelineEvent.main.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight); let link = reply.child.linkAt != undefined && reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight);
if (link) { if (link) {
Nheko.openLink(link) Nheko.openLink(link)
} else { } else {
room.showEvent(r.eventId) room.showEvent(r.eventId)
} }
} }
onPressAndHold: replyContextMenu.show(timelineEvent.main.copyText, timelineEvent.main.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight), r.eventId) onPressAndHold: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight), r.eventId)
contentItem: TimelineEvent { contentItem: TimelineEvent {
id: timelineEvent id: timelineEvent
@ -56,37 +51,49 @@ AbstractButton {
maxWidth: r.maxWidth maxWidth: r.maxWidth
limitAsReply: r.limitHeight limitAsReply: r.limitHeight
data: Column { //height: replyContainer.implicitHeight
data: Row {
id: replyContainer id: replyContainer
spacing: 0
spacing: Nheko.paddingSmall
clip: r.limitHeight clip: r.limitHeight
height: r.limitHeight ? Math.min( timelineEvent.main?.height, timelineView.height / 10) + Nheko.paddingSmall + usernameBtn.height : undefined height: r.limitHeight ? Math.min( timelineEvent.main?.height, timelineView.height / 10) + Nheko.paddingSmall + usernameBtn.height : undefined
AbstractButton { Rectangle {
id: usernameBtn id: colorline
contentItem: Label { width: 4
id: userName_ height: content.height
// HACK: To ensure the username gets rendered in Qt 6.9.2,
// we need to always have some text in here. The name color: TimelineManager.userColor(r.userId, palette.base)
// 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
}
onClicked: room.openUserProfile(r.userId)
} }
data: [ Column {
usernameBtn, timelineEvent.main, 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)
}
data: [
usernameBtn, timelineEvent.main,
]
}
}
} }
background: Rectangle { background: Rectangle {
@ -96,16 +103,6 @@ AbstractButton {
property color userColor: TimelineManager.userColor(r.userId, palette.base) property color userColor: TimelineManager.userColor(r.userId, palette.base)
property color bgColor: palette.base property color bgColor: palette.base
color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1)) 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,17 +112,6 @@ ApplicationWindow {
checked: !hiddenEvents.hiddenEvents.includes(MtxEvent.Sticker) checked: !hiddenEvents.hiddenEvents.includes(MtxEvent.Sticker)
onToggled: hiddenEvents.toggle(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,17 +94,14 @@ ApplicationWindow {
Text { Text {
visible: !model.isType; visible: !model.isType;
text: { text: {
let pl = model.powerlevel.toLocaleString(Qt.locale(), "f", 0);
if (editingModel.creatorLevel == model.powerlevel)
return qsTr("Creator")
if (editingModel.adminLevel == model.powerlevel) if (editingModel.adminLevel == model.powerlevel)
return qsTr("Administrator (%1)").arg(pl) return qsTr("Administrator (%1)").arg(model.powerlevel)
else if (editingModel.moderatorLevel == model.powerlevel) else if (editingModel.moderatorLevel == model.powerlevel)
return qsTr("Moderator (%1)").arg(pl) return qsTr("Moderator (%1)").arg(model.powerlevel)
else if (editingModel.defaultUserLevel == model.powerlevel) else if (editingModel.defaultUserLevel == model.powerlevel)
return qsTr("User (%1)").arg(pl) return qsTr("User (%1)").arg(model.powerlevel)
else else
return qsTr("Custom (%1)").arg(pl) return qsTr("Custom (%1)").arg(model.powerlevel)
} }
color: palette.text color: palette.text
} }
@ -141,7 +138,7 @@ ApplicationWindow {
color: palette.text color: palette.text
Keys.onPressed: event => { Keys.onPressed: {
if (typeEntry.text.includes('.') && event.matches(StandardKey.InsertParagraphSeparator)) { if (typeEntry.text.includes('.') && event.matches(StandardKey.InsertParagraphSeparator)) {
editingModel.types.add(typeEntry.index, typeEntry.text) editingModel.types.add(typeEntry.index, typeEntry.text)
typeEntry.visible = false; typeEntry.visible = false;
@ -337,17 +334,12 @@ ApplicationWindow {
Text { Text {
visible: !model.isUser; visible: !model.isUser;
text: { text: {
let pl = model.powerlevel.toLocaleString(Qt.locale(), "f", 0);
if (editingModel.creatorLevel == model.powerlevel)
return qsTr("Creator")
if (editingModel.adminLevel == model.powerlevel) if (editingModel.adminLevel == model.powerlevel)
return qsTr("Administrator (%1)").arg(pl) return qsTr("Administrator (%1)").arg(model.powerlevel)
else if (editingModel.moderatorLevel == model.powerlevel) else if (editingModel.moderatorLevel == model.powerlevel)
return qsTr("Moderator (%1)").arg(pl) return qsTr("Moderator (%1)").arg(model.powerlevel)
else if (editingModel.defaultUserLevel == model.powerlevel)
return qsTr("User (%1)").arg(pl)
else else
return qsTr("Custom (%1)").arg(pl) return qsTr("Custom (%1)").arg(model.powerlevel)
} }
color: palette.text color: palette.text
} }
@ -357,7 +349,7 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Layout.rightMargin: 2 Layout.rightMargin: 2
image: model.isUser ? ":/icons/icons/ui/dismiss.svg" : ":/icons/icons/ui/add-square-button.svg" image: model.isUser ? ":/icons/icons/ui/dismiss.svg" : ":/icons/icons/ui/add-square-button.svg"
visible: (!model.isUser || model.removeable) && model.powerlevel != editingModel.creatorLevel visible: !model.isUser || model.removeable
hoverEnabled: true hoverEnabled: true
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: model.isUser ? qsTr("Remove user") : qsTr("Add user") ToolTip.text: model.isUser ? qsTr("Remove user") : qsTr("Add user")

View file

@ -304,16 +304,13 @@ ApplicationWindow {
} }
ComboBox { ComboBox {
id: notificationsCombo
Layout.fillWidth: true
model: [qsTr("Muted"), qsTr("Mentions only"), qsTr("All messages")] model: [qsTr("Muted"), qsTr("Mentions only"), qsTr("All messages")]
currentIndex: roomSettings.notifications currentIndex: roomSettings.notifications
onActivated: (index) => { onActivated: (index) => {
roomSettings.changeNotifications(index); roomSettings.changeNotifications(index);
} }
Layout.fillWidth: true
// Disable built-in wheel handling unless focused WheelHandler{} // suppress scrolling changing values
wheelEnabled: activeFocus
} }
Label { Label {

View file

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

View file

@ -89,7 +89,7 @@ Rectangle {
roleValue: UserSettingsModel.Toggle roleValue: UserSettingsModel.Toggle
ToggleButton { ToggleButton {
checked: model.value checked: model.value
onClicked: model.value = checked onCheckedChanged: model.value = checked
enabled: model.enabled enabled: model.enabled
} }
} }
@ -100,13 +100,10 @@ Rectangle {
model: r.model.values model: r.model.values
currentIndex: r.model.value currentIndex: r.model.value
width: Math.min(implicitWidth, scroll.availableWidth - Nheko.paddingMedium) width: Math.min(implicitWidth, scroll.availableWidth - Nheko.paddingMedium)
onActivated: { onCurrentIndexChanged: r.model.value = currentIndex
r.model.value = currentIndex
}
implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted
// Disable built-in wheel handling unless focused WheelHandler{} // suppress scrolling changing values
wheelEnabled: activeFocus
} }
} }
DelegateChoice { DelegateChoice {
@ -121,7 +118,7 @@ Rectangle {
onValueChanged: model.value = value onValueChanged: model.value = value
editable: true editable: true
wheelEnabled: activeFocus WheelHandler{} // suppress scrolling changing values
} }
} }
DelegateChoice { DelegateChoice {
@ -138,7 +135,7 @@ Rectangle {
to: model.valueUpperBound * div to: model.valueUpperBound * div
stepSize: model.valueStep * div stepSize: model.valueStep * div
value: model.value * div value: model.value * div
onValueModified: model.value = value/div onValueChanged: model.value = value/div
editable: true editable: true
property real realValue: value / div property real realValue: value / div
@ -156,7 +153,7 @@ Rectangle {
return Number.fromLocaleString(locale, text) * spinbox.div return Number.fromLocaleString(locale, text) * spinbox.div
} }
wheelEnabled: activeFocus WheelHandler{} // suppress scrolling changing values
} }
} }
DelegateChoice { DelegateChoice {
@ -275,5 +272,6 @@ Rectangle {
ToolTip.text: qsTr("Back") ToolTip.text: qsTr("Back")
onClicked: mainWindow.pop() onClicked: mainWindow.pop()
} }
} }

View file

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

View file

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

View file

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

View file

@ -2479,12 +2479,11 @@ try {
} }
} }
updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString();
updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString();
updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); updatedInfo.version = getRoomVersion(txn, statesdb).toStdString();
updatedInfo.is_space = getRoomIsSpace(txn, statesdb); updatedInfo.is_space = getRoomIsSpace(txn, statesdb);
updatedInfo.is_tombstoned = getRoomIsTombstoned(txn, statesdb);
updatedInfo.notification_count = room.second.unread_notifications.notification_count; updatedInfo.notification_count = room.second.unread_notifications.notification_count;
updatedInfo.highlight_count = room.second.unread_notifications.highlight_count; updatedInfo.highlight_count = room.second.unread_notifications.highlight_count;
@ -3624,7 +3623,7 @@ Cache::getRoomIsTombstoned(lmdb::txn &txn, lmdb::dbi &statesdb)
using namespace mtx::events::state; using namespace mtx::events::state;
std::string_view event; std::string_view event;
bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTombstone), event); bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
if (res) { if (res) {
try { try {
@ -4622,14 +4621,12 @@ Cache::updateSpaces(lmdb::txn &txn,
event.state_key.at(0) == '!') { event.state_key.at(0) == '!') {
const std::string &space = event.state_key; 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); auto pls = getStateEvent<mtx::events::state::PowerLevels>(txn, space);
if (!pls) if (!pls)
continue; continue;
if (pls->content.user_level(event.sender, create) >= if (pls->content.user_level(event.sender) >=
pls->content.state_level(space_event_type)) { pls->content.state_level(space_event_type)) {
db->spacesChildren.put(txn, space, room); db->spacesChildren.put(txn, space, room);
db->spacesParents.put(txn, room, space); db->spacesParents.put(txn, room, space);
@ -4638,7 +4635,7 @@ Cache::updateSpaces(lmdb::txn &txn,
room, room,
space, space,
event.sender, event.sender,
pls->content.user_level(event.sender, create), pls->content.user_level(event.sender),
pls->content.state_level(space_event_type)); pls->content.state_level(space_event_type));
} }
} }
@ -4869,19 +4866,23 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
int64_t min_event_level = std::numeric_limits<int64_t>::max(); int64_t min_event_level = std::numeric_limits<int64_t>::max();
int64_t user_level = std::numeric_limits<int64_t>::min(); int64_t user_level = std::numeric_limits<int64_t>::min();
try { std::string_view event;
StateEvent<Create> create = getStateEvent<mtx::events::state::Create>(txn, room_id) bool res = db_.get(txn, to_string(EventType::RoomPowerLevels), event);
.value_or(StateEvent<Create>{});
StateEvent<PowerLevels> pls =
getStateEvent<mtx::events::state::PowerLevels>(txn, room_id)
.value_or(StateEvent<PowerLevels>{});
user_level = pls.content.user_level(user_id, create); if (res) {
try {
StateEvent<PowerLevels> msg =
nlohmann::json::parse(std::string_view(event.data(), event.size()))
.get<StateEvent<PowerLevels>>();
for (const auto &ty : eventTypes) user_level = msg.content.user_level(user_id);
min_event_level = std::min(min_event_level, pls.content.state_level(to_string(ty)));
} catch (const nlohmann::json::exception &e) { for (const auto &ty : eventTypes)
nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); 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());
}
} }
return user_level >= min_event_level; return user_level >= min_event_level;
@ -6409,7 +6410,6 @@ 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::Name)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PinnedEvents) 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::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::ServerAcl)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Child) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Child)
NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Parent) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Parent)

View file

@ -44,7 +44,6 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
auto string1 = sourceModel() auto string1 = sourceModel()
->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
.toString() .toString()
.normalized(QString::NormalizationForm_KD)
.toCaseFolded(); .toCaseFolded();
if (!string1.isEmpty()) { if (!string1.isEmpty()) {
trie_.insert<ElementRank::first>(string1.toUcs4(), i); trie_.insert<ElementRank::first>(string1.toUcs4(), i);
@ -54,7 +53,6 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
auto string2 = sourceModel() auto string2 = sourceModel()
->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
.toString() .toString()
.normalized(QString::NormalizationForm_KD)
.toCaseFolded(); .toCaseFolded();
if (!string2.isEmpty()) { if (!string2.isEmpty()) {
trie_.insert<ElementRank::first>(string2.toUcs4(), i); trie_.insert<ElementRank::first>(string2.toUcs4(), i);
@ -75,7 +73,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
&CompletionProxyModel::newSearchString, &CompletionProxyModel::newSearchString,
this, this,
[this](const QString &s) { [this](const QString &s) {
searchString_ = s.normalized(QString::NormalizationForm_KD).toCaseFolded(); searchString_ = s.toCaseFolded();
invalidate(); invalidate();
}, },
Qt::QueuedConnection); Qt::QueuedConnection);

View file

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

View file

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

View file

@ -299,10 +299,12 @@ MxcImageProvider::download(const QString &id,
"/media_cache", "/media_cache",
fileName); fileName);
QDir().mkpath(fileInfo.absolutePath()); QDir().mkpath(fileInfo.absolutePath());
QFile f(fileInfo.absoluteFilePath());
if (fileInfo.exists() && f.open(QIODevice::ReadOnly)) { if (fileInfo.exists()) {
if (encryptionInfo) { if (encryptionInfo) {
QFile f(fileInfo.absoluteFilePath());
f.open(QIODevice::ReadOnly);
QByteArray fileData = f.readAll(); QByteArray fileData = f.readAll();
auto tempData = mtx::crypto::to_string( auto tempData = mtx::crypto::to_string(
mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value())); mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value()));

View file

@ -17,15 +17,12 @@
#include "Logging.h" #include "Logging.h"
#include "MatrixClient.h" #include "MatrixClient.h"
PowerlevelsTypeListModel::PowerlevelsTypeListModel( PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid,
const std::string &rid, const mtx::events::state::PowerLevels &pl,
const mtx::events::state::PowerLevels &pl, QObject *parent)
const mtx::events::StateEvent<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, room_id(rid) , room_id(rid)
, powerLevels_(pl) , powerLevels_(pl)
, create_(create)
{ {
std::set<mtx::events::state::power_level_t> seen_levels; std::set<mtx::events::state::power_level_t> seen_levels;
for (const auto &[type, level] : powerLevels_.events) { for (const auto &[type, level] : powerLevels_.events) {
@ -43,9 +40,6 @@ PowerlevelsTypeListModel::PowerlevelsTypeListModel(
seen_levels.insert(level); seen_levels.insert(level);
} }
} }
if (create_.content.room_version_creators_with_infinite_power()) {
seen_levels.insert(mtx::events::state::Creator);
}
for (const auto &level : { for (const auto &level : {
powerLevels_.events_default, powerLevels_.events_default,
@ -360,15 +354,12 @@ PowerlevelsTypeListModel::moveRows(const QModelIndex &,
return true; return true;
} }
PowerlevelsUserListModel::PowerlevelsUserListModel( PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid,
const std::string &rid, const mtx::events::state::PowerLevels &pl,
const mtx::events::state::PowerLevels &pl, QObject *parent)
const mtx::events::StateEvent<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, room_id(rid) , room_id(rid)
, powerLevels_(pl) , powerLevels_(pl)
, create_(create)
{ {
std::set<mtx::events::state::power_level_t> seen_levels; std::set<mtx::events::state::power_level_t> seen_levels;
for (const auto &[user, level] : powerLevels_.users) { for (const auto &[user, level] : powerLevels_.users) {
@ -387,16 +378,6 @@ PowerlevelsUserListModel::PowerlevelsUserListModel(
} }
} }
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 : { for (const auto &level : {
powerLevels_.events_default, powerLevels_.events_default,
powerLevels_.state_default, powerLevels_.state_default,
@ -427,7 +408,7 @@ PowerlevelsUserListModel::toUsers() const
{ {
std::map<std::string, mtx::events::state::power_level_t, std::less<>> m; std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
for (const auto &[key, pl] : std::as_const(users)) for (const auto &[key, pl] : std::as_const(users))
if (key.size() > 0 && key.at(0) == '@' && pl != mtx::events::state::Creator) if (key.size() > 0 && key.at(0) == '@')
m[key] = pl; m[key] = pl;
return m; return m;
} }
@ -478,7 +459,7 @@ PowerlevelsUserListModel::data(const QModelIndex &index, int role) const
case IsUser: case IsUser:
return !user.mxid.empty(); return !user.mxid.empty();
case Moveable: case Moveable:
return !user.mxid.empty() && user.pl != mtx::events::state::Creator; return !user.mxid.empty();
case Removeable: case Removeable:
return !user.mxid.empty() && user.mxid.find('.') != std::string::npos; return !user.mxid.empty() && user.mxid.find('.') != std::string::npos;
} }
@ -573,15 +554,7 @@ PowerlevelsUserListModel::moveRows(const QModelIndex &,
if (users.at(sourceRow).mxid.empty()) if (users.at(sourceRow).mxid.empty())
return false; return false;
if (users.at(sourceRow).pl == mtx::events::state::Creator) auto pl = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
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); auto sourceItem = users.takeAt(sourceRow);
sourceItem.pl = pl; sourceItem.pl = pl;
@ -604,12 +577,9 @@ PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *paren
->getStateEvent<mtx::events::state::PowerLevels>(room_id.toStdString()) ->getStateEvent<mtx::events::state::PowerLevels>(room_id.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content) .content)
, create_(cache::client() , types_(room_id.toStdString(), powerLevels_, this)
->getStateEvent<mtx::events::state::Create>(room_id.toStdString()) , users_(room_id.toStdString(), powerLevels_, this)
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{})) , spaces_(room_id.toStdString(), powerLevels_, this)
, 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()) , room_id_(room_id.toStdString())
{ {
connect(&types_, connect(&types_,
@ -708,18 +678,16 @@ samePl(const mtx::events::state::PowerLevels &a, const mtx::events::state::Power
b.redact); b.redact);
} }
PowerlevelsSpacesListModel::PowerlevelsSpacesListModel( PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_id_,
const std::string &room_id_, const mtx::events::state::PowerLevels &pl,
const mtx::events::state::PowerLevels &pl, QObject *parent)
const mtx::events::StateEvent<mtx::events::state::Create> &create,
QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, room_id(std::move(room_id_)) , room_id(std::move(room_id_))
, oldPowerLevels_(std::move(pl)) , oldPowerLevels_(std::move(pl))
{ {
beginResetModel(); beginResetModel();
spaces.push_back(Entry{room_id, oldPowerLevels_, create, true}); spaces.push_back(Entry{room_id, oldPowerLevels_, true});
std::unordered_set<std::string> visited; std::unordered_set<std::string> visited;
@ -735,16 +703,10 @@ PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(
cache::client()->getStateEvent<mtx::events::state::space::Parent>(s, space); cache::client()->getStateEvent<mtx::events::state::space::Parent>(s, space);
if (parent && parent->content.via && !parent->content.via->empty() && if (parent && parent->content.via && !parent->content.via->empty() &&
parent->content.canonical) { parent->content.canonical) {
auto childPl = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s); auto parentPl = 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( spaces.push_back(Entry{
Entry{s, s, parentPl ? parentPl->content : mtx::events::state::PowerLevels{}, false});
childPl ? childPl->content : mtx::events::state::PowerLevels{},
childCreate,
false});
addChildren(s); addChildren(s);
} }
} }
@ -851,7 +813,7 @@ PowerlevelsSpacesListModel::data(QModelIndex const &index, int role) const
auto entry = spaces.at(row); auto entry = spaces.at(row);
switch (role) { switch (role) {
case Roles::IsEditable: case Roles::IsEditable:
return entry.pl.user_level(http::client()->user_id().to_string(), entry.create) >= return entry.pl.user_level(http::client()->user_id().to_string()) >=
entry.pl.state_level(to_string(mtx::events::EventType::RoomPowerLevels)); entry.pl.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
case Roles::IsDifferentFromBase: case Roles::IsDifferentFromBase:
return !samePl(entry.pl, oldPowerLevels_); return !samePl(entry.pl, oldPowerLevels_);

View file

@ -29,11 +29,9 @@ public:
Removeable, Removeable,
}; };
explicit PowerlevelsTypeListModel( explicit PowerlevelsTypeListModel(const std::string &room_id_,
const std::string &room_id_, const mtx::events::state::PowerLevels &pl,
const mtx::events::state::PowerLevels &pl, QObject *parent = nullptr);
const mtx::events::StateEvent<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(types.size()); } int rowCount(const QModelIndex &) const override { return static_cast<int>(types.size()); }
@ -69,7 +67,6 @@ public:
std::string room_id; std::string room_id;
QVector<Entry> types; QVector<Entry> types;
mtx::events::state::PowerLevels powerLevels_; mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> create_;
}; };
class PowerlevelsUserListModel final : public QAbstractListModel class PowerlevelsUserListModel final : public QAbstractListModel
@ -91,11 +88,9 @@ public:
Removeable, Removeable,
}; };
explicit PowerlevelsUserListModel( explicit PowerlevelsUserListModel(const std::string &room_id_,
const std::string &room_id_, const mtx::events::state::PowerLevels &pl,
const mtx::events::state::PowerLevels &pl, QObject *parent = nullptr);
const mtx::events::StateEvent<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(users.size()); } int rowCount(const QModelIndex &) const override { return static_cast<int>(users.size()); }
@ -126,7 +121,6 @@ public:
std::string room_id; std::string room_id;
QVector<Entry> users; QVector<Entry> users;
mtx::events::state::PowerLevels powerLevels_; mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> create_;
}; };
class PowerlevelsSpacesListModel final : public QAbstractListModel class PowerlevelsSpacesListModel final : public QAbstractListModel
@ -153,11 +147,9 @@ public:
ApplyPermissions, ApplyPermissions,
}; };
explicit PowerlevelsSpacesListModel( explicit PowerlevelsSpacesListModel(const std::string &room_id_,
const std::string &room_id_, const mtx::events::state::PowerLevels &pl,
const mtx::events::state::PowerLevels &pl, QObject *parent = nullptr);
const mtx::events::StateEvent<mtx::events::state::Create> &create,
QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &) const override { return static_cast<int>(spaces.size()); } int rowCount(const QModelIndex &) const override { return static_cast<int>(spaces.size()); }
@ -191,7 +183,6 @@ public:
std::string roomid; std::string roomid;
mtx::events::state::PowerLevels pl; mtx::events::state::PowerLevels pl;
mtx::events::StateEvent<mtx::events::state::Create> create;
bool apply = false; bool apply = false;
}; };
@ -212,7 +203,6 @@ class PowerlevelEditingModels final : public QObject
Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT) Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT)
Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT) Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT)
Q_PROPERTY(PowerlevelsSpacesListModel *spaces READ spaces 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 adminLevel READ adminLevel NOTIFY adminLevelChanged)
Q_PROPERTY(qlonglong moderatorLevel READ moderatorLevel NOTIFY moderatorLevelChanged) Q_PROPERTY(qlonglong moderatorLevel READ moderatorLevel NOTIFY moderatorLevelChanged)
Q_PROPERTY(qlonglong defaultUserLevel READ defaultUserLevel NOTIFY defaultUserLevelChanged) Q_PROPERTY(qlonglong defaultUserLevel READ defaultUserLevel NOTIFY defaultUserLevelChanged)
@ -232,7 +222,6 @@ public:
PowerlevelsUserListModel *users() { return &users_; } PowerlevelsUserListModel *users() { return &users_; }
PowerlevelsTypeListModel *types() { return &types_; } PowerlevelsTypeListModel *types() { return &types_; }
PowerlevelsSpacesListModel *spaces() { return &spaces_; } PowerlevelsSpacesListModel *spaces() { return &spaces_; }
qlonglong creatorLevel() const { return mtx::events::state::Creator; }
qlonglong adminLevel() const qlonglong adminLevel() const
{ {
return powerLevels_.state_level(to_string(mtx::events::EventType::RoomPowerLevels)); return powerLevels_.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
@ -246,7 +235,6 @@ public:
Q_INVOKABLE void addRole(int pl); Q_INVOKABLE void addRole(int pl);
mtx::events::state::PowerLevels powerLevels_; mtx::events::state::PowerLevels powerLevels_;
mtx::events::StateEvent<mtx::events::state::Create> create_;
PowerlevelsTypeListModel types_; PowerlevelsTypeListModel types_;
PowerlevelsUserListModel users_; PowerlevelsUserListModel users_;
PowerlevelsSpacesListModel spaces_; PowerlevelsSpacesListModel spaces_;

View file

@ -107,18 +107,13 @@ TrayIcon::TrayIcon(const QString &filename, QWindow *parent)
QMenu *menu = new QMenu(); QMenu *menu = new QMenu();
setContextMenu(menu); setContextMenu(menu);
toggleAction_ = new QAction(tr("Show"), this); viewAction_ = new QAction(tr("Show"), this);
quitAction_ = new QAction(tr("Quit"), this); quitAction_ = new QAction(tr("Quit"), this);
connect(parent, &QWindow::visibleChanged, toggleAction_, [=, this] { connect(viewAction_, &QAction::triggered, parent, &QWindow::show);
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); connect(quitAction_, &QAction::triggered, this, QApplication::quit);
menu->addAction(toggleAction_); menu->addAction(viewAction_);
menu->addAction(quitAction_); menu->addAction(quitAction_);
QString toolTip = QLatin1String("nheko"); QString toolTip = QLatin1String("nheko");

View file

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

View file

@ -25,24 +25,10 @@
#include "config/nheko.h" #include "config/nheko.h"
QStringList themes{
QStringLiteral("light"),
QStringLiteral("dark"),
QStringLiteral("system"),
};
QSharedPointer<UserSettings> UserSettings::instance_; QSharedPointer<UserSettings> UserSettings::instance_;
UserSettings::UserSettings() 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( connect(
QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); }); QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); });
} }
@ -79,13 +65,8 @@ UserSettings::load(std::optional<QString> profile)
settings.value("user/timeline/message_hover_highlight", false).toBool(); settings.value("user/timeline/message_hover_highlight", false).toBool();
enlargeEmojiOnlyMessages_ = enlargeEmojiOnlyMessages_ =
settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool(); settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool();
markdown_ = settings.value("user/markdown_enabled", true).toBool(); markdown_ = settings.value("user/markdown_enabled", true).toBool();
invertEnterKey_ = settings.value("user/invert_enter_key", false).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(); bubbles_ = settings.value("user/bubbles_enabled", false).toBool();
smallAvatars_ = settings.value("user/small_avatars_enabled", false).toBool(); smallAvatars_ = settings.value("user/small_avatars_enabled", false).toBool();
animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool(); animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool();
@ -353,12 +334,13 @@ UserSettings::setMarkdown(bool state)
} }
void void
UserSettings::setSendMessageKey(SendMessageKey key) UserSettings::setInvertEnterKey(bool state)
{ {
if (key == sendMessageKey_) if (state == invertEnterKey_)
return; return;
sendMessageKey_ = key;
emit sendMessageKeyChanged(key); invertEnterKey_ = state;
emit invertEnterKeyChanged(state);
save(); save();
} }
@ -658,7 +640,7 @@ UserSettings::setShowImage(ShowImage state)
void void
UserSettings::setTheme(QString theme) UserSettings::setTheme(QString theme)
{ {
if (theme == theme_ || !themes.contains(theme)) if (theme == theme_)
return; return;
theme_ = theme; theme_ = theme;
save(); save();
@ -948,7 +930,7 @@ UserSettings::save()
settings.setValue("group_view", groupView_); settings.setValue("group_view", groupView_);
settings.setValue("scrollbars_in_roomlist", scrollbarsInRoomlist_); settings.setValue("scrollbars_in_roomlist", scrollbarsInRoomlist_);
settings.setValue("markdown_enabled", markdown_); settings.setValue("markdown_enabled", markdown_);
settings.setValue("send_message_key", static_cast<int>(sendMessageKey_)); settings.setValue("invert_enter_key", invertEnterKey_);
settings.setValue("bubbles_enabled", bubbles_); settings.setValue("bubbles_enabled", bubbles_);
settings.setValue("small_avatars_enabled", smallAvatars_); settings.setValue("small_avatars_enabled", smallAvatars_);
settings.setValue("animate_images_on_hover", animateImagesOnHover_); settings.setValue("animate_images_on_hover", animateImagesOnHover_);
@ -1062,8 +1044,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr("Scrollbars in room list"); return tr("Scrollbars in room list");
case Markdown: case Markdown:
return tr("Send messages as Markdown"); return tr("Send messages as Markdown");
case SendMessageKey: case InvertEnterKey:
return tr("Send messages with a shortcut"); return tr("Use shift+enter to send and enter to start a new line");
case Bubbles: case Bubbles:
return tr("Enable message bubbles"); return tr("Enable message bubbles");
case SmallAvatars: case SmallAvatars:
@ -1200,7 +1182,12 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
} else if (role == Value) { } else if (role == Value) {
switch (index.row()) { switch (index.row()) {
case Theme: case Theme:
return themes.indexOf(i->theme()); return QStringList{
QStringLiteral("light"),
QStringLiteral("dark"),
QStringLiteral("system"),
}
.indexOf(i->theme());
case ScaleFactor: case ScaleFactor:
return utils::scaleFactor(); return utils::scaleFactor();
case MessageHoverHighlight: case MessageHoverHighlight:
@ -1217,8 +1204,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return i->scrollbarsInRoomlist(); return i->scrollbarsInRoomlist();
case Markdown: case Markdown:
return i->markdown(); return i->markdown();
case SendMessageKey: case InvertEnterKey:
return static_cast<int>(i->sendMessageKey()); return i->invertEnterKey();
case Bubbles: case Bubbles:
return i->bubbles(); return i->bubbles();
case SmallAvatars: case SmallAvatars:
@ -1383,11 +1370,10 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr( return tr(
"Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " "Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
"text."); "text.");
case SendMessageKey: case InvertEnterKey:
return tr( return tr(
"Select what Enter key combination sends the message. Shift+Enter adds a new line, " "Invert the behavior of the enter key in the text input, making it send the message "
"unless it has been selected, in which case Enter adds a new line instead.\n\n" "when shift+enter is pressed and starting a new line when enter is pressed.");
"If an emoji picker or a mention picker is open, it is always handled first.");
case Bubbles: case Bubbles:
return tr( return tr(
"Messages get a bubble background. This also triggers some layout changes (WIP)."); "Messages get a bubble background. This also triggers some layout changes (WIP).");
@ -1555,7 +1541,6 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case CameraFrameRate: case CameraFrameRate:
case Ringtone: case Ringtone:
case ShowImage: case ShowImage:
case SendMessageKey:
return Options; return Options;
case TimelineMaxWidth: case TimelineMaxWidth:
case PrivacyScreenTimeout: case PrivacyScreenTimeout:
@ -1570,6 +1555,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case GroupView: case GroupView:
case ScrollbarsInRoomlist: case ScrollbarsInRoomlist:
case Markdown: case Markdown:
case InvertEnterKey:
case Bubbles: case Bubbles:
case SmallAvatars: case SmallAvatars:
case AnimateImagesOnHover: case AnimateImagesOnHover:
@ -1688,12 +1674,6 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
tr("Only in private rooms"), tr("Only in private rooms"),
tr("Never"), tr("Never"),
}; };
case SendMessageKey:
return QStringList{
tr("Enter"),
tr("Shift+Enter"),
tr("Ctrl+Enter"),
};
case Microphone: case Microphone:
return vecToList(CallDevices::instance().names(false, i->microphone().toStdString())); return vecToList(CallDevices::instance().names(false, i->microphone().toStdString()));
case Camera: case Camera:
@ -1761,10 +1741,14 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
if (role == Value) { if (role == Value) {
switch (index.row()) { switch (index.row()) {
case Theme: { case Theme: {
auto idx = value.toInt(); if (value == 0) {
i->setTheme("light");
if (idx >= 0 && idx < themes.size()) { return true;
i->setTheme(themes[idx]); } else if (value == 1) {
i->setTheme("dark");
return true;
} else if (value == 2) {
i->setTheme("system");
return true; return true;
} else } else
return false; return false;
@ -1835,14 +1819,12 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
} else } else
return false; return false;
} }
case SendMessageKey: { case InvertEnterKey: {
auto newKey = value.toInt(); if (value.userType() == QMetaType::Bool) {
if (newKey < 0 || i->setInvertEnterKey(value.toBool());
QMetaEnum::fromType<UserSettings::SendMessageKey>().keyCount() <= newKey) return true;
} else
return false; return false;
i->setSendMessageKey(static_cast<UserSettings::SendMessageKey>(newKey));
return true;
} }
case Bubbles: { case Bubbles: {
if (value.userType() == QMetaType::Bool) { if (value.userType() == QMetaType::Bool) {
@ -2327,8 +2309,8 @@ UserSettingsModel::UserSettingsModel(QObject *p)
connect(s.get(), &UserSettings::markdownChanged, this, [this]() { connect(s.get(), &UserSettings::markdownChanged, this, [this]() {
emit dataChanged(index(Markdown), index(Markdown), {Value}); emit dataChanged(index(Markdown), index(Markdown), {Value});
}); });
connect(s.get(), &UserSettings::sendMessageKeyChanged, this, [this]() { connect(s.get(), &UserSettings::invertEnterKeyChanged, this, [this]() {
emit dataChanged(index(SendMessageKey), index(SendMessageKey), {Value}); emit dataChanged(index(InvertEnterKey), index(InvertEnterKey), {Value});
}); });
connect(s.get(), &UserSettings::bubblesChanged, this, [this]() { connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
emit dataChanged(index(Bubbles), index(Bubbles), {Value}); 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 Q_PROPERTY(bool scrollbarsInRoomlist READ scrollbarsInRoomlist WRITE setScrollbarsInRoomlist
NOTIFY scrollbarsInRoomlistChanged) NOTIFY scrollbarsInRoomlistChanged)
Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
Q_PROPERTY(SendMessageKey sendMessageKey READ sendMessageKey WRITE setSendMessageKey NOTIFY Q_PROPERTY(
sendMessageKeyChanged) bool invertEnterKey READ invertEnterKey WRITE setInvertEnterKey NOTIFY invertEnterKeyChanged)
Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged) Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged) Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged)
Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
@ -166,14 +166,6 @@ public:
}; };
Q_ENUM(ShowImage) Q_ENUM(ShowImage)
enum class SendMessageKey
{
Enter,
ShiftEnter,
CtrlEnter,
};
Q_ENUM(SendMessageKey)
void save(); void save();
void load(std::optional<QString> profile); void load(std::optional<QString> profile);
void applyTheme(); void applyTheme();
@ -190,7 +182,7 @@ public:
void setGroupView(bool state); void setGroupView(bool state);
void setScrollbarsInRoomlist(bool state); void setScrollbarsInRoomlist(bool state);
void setMarkdown(bool state); void setMarkdown(bool state);
void setSendMessageKey(SendMessageKey key); void setInvertEnterKey(bool state);
void setBubbles(bool state); void setBubbles(bool state);
void setSmallAvatars(bool state); void setSmallAvatars(bool state);
void setAnimateImagesOnHover(bool state); void setAnimateImagesOnHover(bool state);
@ -263,7 +255,7 @@ public:
bool privacyScreen() const { return privacyScreen_; } bool privacyScreen() const { return privacyScreen_; }
int privacyScreenTimeout() const { return privacyScreenTimeout_; } int privacyScreenTimeout() const { return privacyScreenTimeout_; }
bool markdown() const { return markdown_; } bool markdown() const { return markdown_; }
SendMessageKey sendMessageKey() const { return sendMessageKey_; } bool invertEnterKey() const { return invertEnterKey_; }
bool bubbles() const { return bubbles_; } bool bubbles() const { return bubbles_; }
bool smallAvatars() const { return smallAvatars_; } bool smallAvatars() const { return smallAvatars_; }
bool animateImagesOnHover() const { return animateImagesOnHover_; } bool animateImagesOnHover() const { return animateImagesOnHover_; }
@ -336,7 +328,7 @@ signals:
void trayChanged(bool state); void trayChanged(bool state);
void startInTrayChanged(bool state); void startInTrayChanged(bool state);
void markdownChanged(bool state); void markdownChanged(bool state);
void sendMessageKeyChanged(SendMessageKey key); void invertEnterKeyChanged(bool state);
void bubblesChanged(bool state); void bubblesChanged(bool state);
void smallAvatarsChanged(bool state); void smallAvatarsChanged(bool state);
void animateImagesOnHoverChanged(bool state); void animateImagesOnHoverChanged(bool state);
@ -407,7 +399,7 @@ private:
bool groupView_; bool groupView_;
bool scrollbarsInRoomlist_; bool scrollbarsInRoomlist_;
bool markdown_; bool markdown_;
SendMessageKey sendMessageKey_; bool invertEnterKey_;
bool bubbles_; bool bubbles_;
bool smallAvatars_; bool smallAvatars_;
bool animateImagesOnHover_; bool animateImagesOnHover_;
@ -518,7 +510,7 @@ class UserSettingsModel : public QAbstractListModel
TypingNotifications, TypingNotifications,
ReadReceipts, ReadReceipts,
Markdown, Markdown,
SendMessageKey, InvertEnterKey,
Bubbles, Bubbles,
SmallAvatars, SmallAvatars,

View file

@ -1453,9 +1453,6 @@ utils::roomVias(const std::string &roomid)
auto powerlevels = auto powerlevels =
cache::client()->getStateEvent<mtx::events::state::PowerLevels>(roomid).value_or( cache::client()->getStateEvent<mtx::events::state::PowerLevels>(roomid).value_or(
mtx::events::StateEvent<mtx::events::state::PowerLevels>{}); 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); auto acls = cache::client()->getStateEvent<mtx::events::state::ServerAcl>(roomid);
std::vector<QRegularExpression> allowedServers; std::vector<QRegularExpression> allowedServers;
@ -1504,19 +1501,6 @@ utils::roomVias(const std::string &roomid)
std::set<std::string> users_with_high_pl_in_room; 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 // we should pick PL > 50, but imo that is broken, so we just pick users who have admins
// perm // 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) { for (const auto &user : powerlevels.content.users) {
if (user.second >= powerlevels.content.events_default && if (user.second >= powerlevels.content.events_default &&
user.second >= powerlevels.content.state_default) { user.second >= powerlevels.content.state_default) {
@ -1541,13 +1525,12 @@ utils::roomVias(const std::string &roomid)
}); });
// add the highest powerlevel user // add the highest powerlevel user
auto max_pl_user = std::max_element(users_with_high_pl_in_room.begin(), auto max_pl_user = std::max_element(
users_with_high_pl_in_room.end(), users_with_high_pl_in_room.begin(),
[&pl_content = powerlevels.content, &create]( users_with_high_pl_in_room.end(),
const std::string &a, const std::string &b) { [&pl_content = powerlevels.content](const std::string &a, const std::string &b) {
return pl_content.user_level(a, create) < return pl_content.user_level(a) < pl_content.user_level(b);
pl_content.user_level(b, create); });
});
if (max_pl_user != users_with_high_pl_in_room.end()) { if (max_pl_user != users_with_high_pl_in_room.end()) {
auto host = auto host =
mtx::identifiers::parse<mtx::identifiers::User>(*max_pl_user).hostname(); mtx::identifiers::parse<mtx::identifiers::User>(*max_pl_user).hostname();
@ -1722,15 +1705,11 @@ utils::updateSpaceVias()
auto spaceid = roomid.toStdString(); 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() if (auto pl = cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(spaceid) ->getStateEvent<mtx::events::state::PowerLevels>(spaceid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content; .content;
pl.user_level(us, create) < pl.user_level(us) < pl.state_level(to_string(mtx::events::EventType::SpaceChild)))
pl.state_level(to_string(mtx::events::EventType::SpaceChild)))
continue; continue;
auto children = cache::client()->getChildRoomIds(spaceid); auto children = cache::client()->getChildRoomIds(spaceid);
@ -1769,16 +1748,12 @@ utils::updateSpaceVias()
parent->origin_server_ts < weekAgo && parent->origin_server_ts < weekAgo &&
// ignore unset spaces // ignore unset spaces
(parent->content.via && !parent->content.via->empty())) { (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 = if (auto pl =
cache::client() cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(childid) ->getStateEvent<mtx::events::state::PowerLevels>(childid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content; .content;
pl.user_level(us, childCreate) < pl.user_level(us) <
pl.state_level(to_string(mtx::events::EventType::SpaceParent))) pl.state_level(to_string(mtx::events::EventType::SpaceParent)))
continue; continue;
@ -2066,15 +2041,11 @@ utils::removeExpiredEvents()
if (!asus->globalExpiry && !getExpEv(roomid)) if (!asus->globalExpiry && !getExpEv(roomid))
continue; 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() if (auto pl = cache::client()
->getStateEvent<mtx::events::state::PowerLevels>(roomid) ->getStateEvent<mtx::events::state::PowerLevels>(roomid)
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content; .content;
pl.user_level(us, create) < pl.user_level(us) < pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) {
pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) {
nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid); nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid);
continue; continue;
} }

View file

@ -170,14 +170,6 @@ setStatusMessage(const QString &message)
interface.call(QDBus::NoBlock, QStringLiteral("setStatusMessage"), 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 } // nheko::dbus
/** /**

View file

@ -85,9 +85,6 @@ statusMessage();
//! Sets the user's status message (if supported by the homeserver). //! Sets the user's status message (if supported by the homeserver).
void void
setStatusMessage(const QString &message); setStatusMessage(const QString &message);
//! Sets the current theme (supported values: "light", "dark" or "system")
void
setTheme(const QString &theme);
QDBusArgument & QDBusArgument &
operator<<(QDBusArgument &arg, const RoomInfoItem &item); operator<<(QDBusArgument &arg, const RoomInfoItem &item);

View file

@ -11,7 +11,6 @@
#include "Logging.h" #include "Logging.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "MxcImageProvider.h" #include "MxcImageProvider.h"
#include "UserSettingsPage.h"
#include "timeline/RoomlistModel.h" #include "timeline/RoomlistModel.h"
#include "timeline/TimelineModel.h" #include "timeline/TimelineModel.h"
@ -113,12 +112,6 @@ NhekoDBusBackend::setStatusMessage(const QString &message)
ChatPage::instance()->setStatus(message); ChatPage::instance()->setStatus(message);
} }
void
NhekoDBusBackend::setTheme(const QString &theme)
{
UserSettings::instance()->setTheme(theme);
}
void void
NhekoDBusBackend::bringWindowToTop() const NhekoDBusBackend::bringWindowToTop() const
{ {

View file

@ -40,8 +40,6 @@ public slots:
Q_SCRIPTABLE QString statusMessage() const; Q_SCRIPTABLE QString statusMessage() const;
//! Sets the user's status message. //! Sets the user's status message.
Q_SCRIPTABLE void setStatusMessage(const QString &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: private:
void bringWindowToTop() const; void bringWindowToTop() const;

View file

@ -398,19 +398,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
body[local_user][dev] = secretRequest; body[local_user][dev] = secretRequest;
} }
if (!body.empty()) { http::client()->send_to_device<mtx::events::msg::SecretRequest>(
http::client()->send_to_device<mtx::events::msg::SecretRequest>( http::client()->generate_txn_id(),
http::client()->generate_txn_id(), body,
body, [secret_name](mtx::http::RequestErr err) {
[secret_name](mtx::http::RequestErr err) { if (err) {
if (err) { nhlog::net()->error("Failed to send request cancellation "
nhlog::net()->error("Failed to send request cancellation " "for secrect "
"for secrect " "'{}'",
"'{}'", secret_name);
secret_name); }
} });
});
}
nhlog::crypto()->info("Storing secret {}", secret_name); nhlog::crypto()->info("Storing secret {}", secret_name);
cache::client()->storeSecret(secret_name, e->content.secret); cache::client()->storeSecret(secret_name, e->content.secret);

View file

@ -166,11 +166,6 @@ EventStore::EventStore(std::string room_id, QObject *)
nhlog::ui()->debug("failing txn id '{}'", txn_id); nhlog::ui()->debug("failing txn id '{}'", txn_id);
cache::client()->removePendingStatus(room_id_, txn_id); cache::client()->removePendingStatus(room_id_, txn_id);
current_txn_error_count = 0; current_txn_error_count = 0;
auto idx = idToIndex(txn_id);
if (idx)
emit dataChanged(*idx, *idx);
} }
} }
QTimer::singleShot(1000, this, [this]() { QTimer::singleShot(1000, this, [this]() {

View file

@ -4,8 +4,6 @@
#include "Permissions.h" #include "Permissions.h"
#include <algorithm>
#include "Cache_p.h" #include "Cache_p.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "TimelineModel.h" #include "TimelineModel.h"
@ -24,53 +22,44 @@ Permissions::invalidate()
->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString()) ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content; .content;
create = cache::client()
->getStateEvent<mtx::events::state::Create>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::Create>{});
} }
bool bool
Permissions::canInvite() Permissions::canInvite()
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.invite; return pl.user_level(http::client()->user_id().to_string()) >= pl.invite;
return plCheck;
} }
bool bool
Permissions::canBan() Permissions::canBan()
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.ban; return pl.user_level(http::client()->user_id().to_string()) >= pl.ban;
return plCheck;
} }
bool bool
Permissions::canKick() Permissions::canKick()
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.kick; return pl.user_level(http::client()->user_id().to_string()) >= pl.kick;
return plCheck;
} }
bool bool
Permissions::canRedact() Permissions::canRedact()
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= pl.redact; return pl.user_level(http::client()->user_id().to_string()) >= pl.redact;
return plCheck;
} }
bool bool
Permissions::canChange(int eventType) Permissions::canChange(int eventType)
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= return pl.user_level(http::client()->user_id().to_string()) >=
pl.state_level(to_string(qml_mtx_events::fromRoomEventType( pl.state_level(to_string(
static_cast<qml_mtx_events::EventType>(eventType)))); qml_mtx_events::fromRoomEventType(static_cast<qml_mtx_events::EventType>(eventType))));
return plCheck;
} }
bool bool
Permissions::canSend(int eventType) Permissions::canSend(int eventType)
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= return pl.user_level(http::client()->user_id().to_string()) >=
pl.event_level(to_string(qml_mtx_events::fromRoomEventType( pl.event_level(to_string(
static_cast<qml_mtx_events::EventType>(eventType)))); qml_mtx_events::fromRoomEventType(static_cast<qml_mtx_events::EventType>(eventType))));
return plCheck;
} }
int int
@ -99,9 +88,8 @@ Permissions::sendLevel(int eventType)
bool bool
Permissions::canPingRoom() Permissions::canPingRoom()
{ {
const bool plCheck = pl.user_level(http::client()->user_id().to_string(), create) >= return pl.user_level(http::client()->user_id().to_string()) >=
pl.notification_level(mtx::events::state::notification_keys::room); pl.notification_level(mtx::events::state::notification_keys::room);
return plCheck;
} }
#include "moc_Permissions.cpp" #include "moc_Permissions.cpp"

View file

@ -5,10 +5,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QQmlEngine>
#include <mtx/events.hpp>
#include <mtx/events/create.hpp>
#include <mtx/events/power_levels.hpp> #include <mtx/events/power_levels.hpp>
class TimelineModel; class TimelineModel;
@ -16,8 +13,6 @@ class TimelineModel;
class Permissions final : public QObject class Permissions final : public QObject
{ {
Q_OBJECT Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Only to be used to refer to C++ values")
public: public:
Permissions(QString roomId, QObject *parent = nullptr); Permissions(QString roomId, QObject *parent = nullptr);
@ -33,20 +28,14 @@ public:
Q_INVOKABLE int redactLevel(); Q_INVOKABLE int redactLevel();
Q_INVOKABLE int changeLevel(int eventType); Q_INVOKABLE int changeLevel(int eventType);
Q_INVOKABLE int sendLevel(int eventType); Q_INVOKABLE int sendLevel(int eventType);
Q_INVOKABLE qint64 creatorLevel() const { return mtx::events::state::Creator; }
Q_INVOKABLE bool canPingRoom(); Q_INVOKABLE bool canPingRoom();
void invalidate(); void invalidate();
const mtx::events::state::PowerLevels &powerlevelEvent() const { return pl; }; const mtx::events::state::PowerLevels &powerlevelEvent() const { return pl; };
const mtx::events::StateEvent<mtx::events::state::Create> &createEvent() const
{
return create;
};
private: private:
QString roomId_; QString roomId_;
mtx::events::state::PowerLevels pl; mtx::events::state::PowerLevels pl;
mtx::events::StateEvent<mtx::events::state::Create> create;
}; };

View file

@ -977,10 +977,6 @@ FilteredRoomlistModel::getRoomById(const QString &id) const
void void
FilteredRoomlistModel::updateHiddenTagsAndSpaces() FilteredRoomlistModel::updateHiddenTagsAndSpaces()
{ {
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
beginFilterChange();
#endif
hiddenTags.clear(); hiddenTags.clear();
hiddenSpaces.clear(); hiddenSpaces.clear();
hideDMs = false; hideDMs = false;
@ -995,11 +991,7 @@ FilteredRoomlistModel::updateHiddenTagsAndSpaces()
hideDMs = true; hideDMs = true;
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
endFilterChange();
#else
invalidateFilter(); invalidateFilter();
#endif
} }
bool bool

View file

@ -212,10 +212,6 @@ public slots:
void updateFilterTag(QString tagId) void updateFilterTag(QString tagId)
{ {
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
#endif
if (tagId.startsWith(QLatin1String("tag:"))) { if (tagId.startsWith(QLatin1String("tag:"))) {
filterType = FilterBy::Tag; filterType = FilterBy::Tag;
filterStr = tagId.mid(4); filterStr = tagId.mid(4);
@ -231,11 +227,7 @@ public slots:
filterStr.clear(); filterStr.clear();
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
endFilterChange();
#else
invalidateFilter(); invalidateFilter();
#endif
} }
void updateHiddenTagsAndSpaces(); void updateHiddenTagsAndSpaces();

View file

@ -41,13 +41,7 @@ TimelineFilter::startFiltering()
{ {
incrementalSearchIndex = 0; incrementalSearchIndex = 0;
emit isFilteringChanged(); emit isFilteringChanged();
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
endFilterChange();
#else
invalidateFilter(); invalidateFilter();
#endif
beginResetModel(); beginResetModel();
endResetModel(); endResetModel();
@ -102,10 +96,6 @@ TimelineFilter::setThreadId(const QString &t)
{ {
nhlog::ui()->debug("Filtering by thread '{}'", t.toStdString()); nhlog::ui()->debug("Filtering by thread '{}'", t.toStdString());
if (this->threadId != t) { if (this->threadId != t) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
beginFilterChange();
#endif
this->threadId = t; this->threadId = t;
emit threadIdChanged(); emit threadIdChanged();
@ -114,30 +104,11 @@ 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 void
TimelineFilter::setContentFilter(const QString &c) TimelineFilter::setContentFilter(const QString &c)
{ {
nhlog::ui()->debug("Filtering by content '{}'", c.toStdString()); nhlog::ui()->debug("Filtering by content '{}'", c.toStdString());
if (this->contentFilter != c) { if (this->contentFilter != c) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
beginFilterChange();
#endif
this->contentFilter = c; this->contentFilter = c;
emit contentFilterChanged(); emit contentFilterChanged();
@ -174,8 +145,7 @@ TimelineFilter::sourceDataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight, const QModelIndex &bottomRight,
const QVector<int> &roles) 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; return;
if (auto s = source()) { if (auto s = source()) {
@ -187,10 +157,6 @@ void
TimelineFilter::setSource(TimelineModel *s) TimelineFilter::setSource(TimelineModel *s)
{ {
if (auto orig = this->source(); orig != s) { if (auto orig = this->source(); orig != s) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
#endif
cachedCount = 0; cachedCount = 0;
incrementalSearchIndex = 0; incrementalSearchIndex = 0;
@ -225,12 +191,7 @@ TimelineFilter::setSource(TimelineModel *s)
emit sourceChanged(); emit sourceChanged();
emit isFilteringChanged(); emit isFilteringChanged();
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
endFilterChange();
#else
invalidateFilter(); invalidateFilter();
#endif
} }
} }
@ -272,27 +233,19 @@ TimelineFilter::filterAcceptsRow(int source_row, const QModelIndex &) const
if (source_row > incrementalSearchIndex) if (source_row > incrementalSearchIndex)
return false; return false;
if (threadId.isEmpty() && contentFilter.isEmpty() && !filterByNotifications_) if (threadId.isEmpty() && contentFilter.isEmpty())
return true; return true;
if (auto s = sourceModel()) { if (auto s = sourceModel()) {
auto idx = s->index(source_row, 0); auto idx = s->index(source_row, 0);
if (!contentFilter.isEmpty() && !s->data(idx, TimelineModel::Body) if (!contentFilter.isEmpty() && !s->data(idx, TimelineModel::Body)
.toString() .toString()
.contains(contentFilter, Qt::CaseInsensitive)) { .contains(contentFilter, Qt::CaseInsensitive)) {
return false; return false;
} }
if (filterByNotifications_ && s->data(idx, TimelineModel::Notificationlevel) if (threadId.isEmpty())
.value<qml_mtx_events::NotificationLevel>() !=
qml_mtx_events::NotificationLevel::Highlight) {
return false;
}
if (threadId.isEmpty()) {
return true; return true;
}
return s->data(idx, TimelineModel::EventId) == threadId || return s->data(idx, TimelineModel::EventId) == threadId ||
s->data(idx, TimelineModel::ThreadId) == threadId; s->data(idx, TimelineModel::ThreadId) == threadId;

View file

@ -18,8 +18,6 @@ class TimelineFilter : public QSortFilterProxyModel
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged) 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 Q_PROPERTY(QString filterByContent READ filterByContent WRITE setContentFilter NOTIFY
contentFilterChanged) contentFilterChanged)
Q_PROPERTY(TimelineModel *source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(TimelineModel *source READ source WRITE setSource NOTIFY sourceChanged)
@ -30,14 +28,12 @@ public:
explicit TimelineFilter(QObject *parent = nullptr); explicit TimelineFilter(QObject *parent = nullptr);
QString filterByThread() const { return threadId; } QString filterByThread() const { return threadId; }
bool filterByNotifications() const { return filterByNotifications_; }
QString filterByContent() const { return contentFilter; } QString filterByContent() const { return contentFilter; }
TimelineModel *source() const; TimelineModel *source() const;
int currentIndex() const; int currentIndex() const;
bool isFiltering() const; bool isFiltering() const;
void setThreadId(const QString &t); void setThreadId(const QString &t);
void setFilterNotifications(bool v);
void setContentFilter(const QString &t); void setContentFilter(const QString &t);
void setSource(TimelineModel *t); void setSource(TimelineModel *t);
void setCurrentIndex(int idx); void setCurrentIndex(int idx);
@ -51,7 +47,6 @@ public:
signals: signals:
void threadIdChanged(); void threadIdChanged();
void filterNotificationsChanged();
void contentFilterChanged(); void contentFilterChanged();
void sourceChanged(); void sourceChanged();
void currentIndexChanged(); void currentIndexChanged();
@ -72,5 +67,4 @@ private:
QString threadId, contentFilter; QString threadId, contentFilter;
int cachedCount = 0, incrementalSearchIndex = 0; 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: case UserName:
return QVariant(displayName(QString::fromStdString(acc::sender(event)))); return QVariant(displayName(QString::fromStdString(acc::sender(event))));
case UserPowerlevel: { case UserPowerlevel: {
return static_cast<qlonglong>(permissions_.powerlevelEvent().user_level( return static_cast<qlonglong>(
acc::sender(event), permissions_.createEvent())); permissions_.powerlevelEvent().user_level(acc::sender(event)));
} }
case Day: { case Day: {
@ -1137,6 +1137,7 @@ TimelineModel::syncState(const mtx::responses::State &s)
avatarChanged = true; avatarChanged = true;
nameChanged = true; nameChanged = true;
memberCountChanged = true; memberCountChanged = true;
} else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
emit encryptionChanged(); emit encryptionChanged();
@ -2421,7 +2422,6 @@ QString
TimelineModel::formatPowerLevelEvent( TimelineModel::formatPowerLevelEvent(
const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const 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; mtx::events::StateEvent<mtx::events::state::PowerLevels> const *prevEvent = nullptr;
if (!event.unsigned_data.replaces_state.empty()) { if (!event.unsigned_data.replaces_state.empty()) {
auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id);
@ -2441,15 +2441,15 @@ TimelineModel::formatPowerLevelEvent(
if (!prevEvent) if (!prevEvent)
return tr("%1 has changed the room's permissions.").arg(sender_name); return tr("%1 has changed the room's permissions.").arg(sender_name);
auto calc_affected = auto calc_affected = [&event,
[&event, &prevEvent, &create](int64_t newPowerlevelSetting) -> std::pair<QStringList, int> { &prevEvent](int64_t newPowerlevelSetting) -> std::pair<QStringList, int> {
QStringList affected{}; QStringList affected{};
auto numberOfAffected = 0; auto numberOfAffected = 0;
// We do only compare to people with explicit PL. Usually others are not going to be // 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. // affected either way and this is cheaper to iterate over.
for (auto const &[mxid, currentPowerlevel] : event.content.users) { for (auto const &[mxid, currentPowerlevel] : event.content.users) {
if (currentPowerlevel == newPowerlevelSetting && if (currentPowerlevel == newPowerlevelSetting &&
prevEvent->content.user_level(mxid, create) < newPowerlevelSetting) { prevEvent->content.user_level(mxid) < newPowerlevelSetting) {
numberOfAffected++; numberOfAffected++;
if (numberOfAffected <= 2) { if (numberOfAffected <= 2) {
affected.push_back(QString::fromStdString(mxid)); affected.push_back(QString::fromStdString(mxid));
@ -2626,25 +2626,24 @@ TimelineModel::formatPowerLevelEvent(
// Compare if a Powerlevel of a user changed // Compare if a Powerlevel of a user changed
for (auto const &[mxid, powerlevel] : event.content.users) { for (auto const &[mxid, powerlevel] : event.content.users) {
auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid))); auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid)));
if (prevEvent->content.user_level(mxid, create) != powerlevel) { if (prevEvent->content.user_level(mxid) != powerlevel) {
if (powerlevel >= administrator_power_level) { if (powerlevel >= administrator_power_level) {
resultingMessage.append(tr("%1 has made %2 an administrator of this room.") resultingMessage.append(tr("%1 has made %2 an administrator of this room.")
.arg(sender_name, nameOfChangedUser)); .arg(sender_name, nameOfChangedUser));
} else if (powerlevel >= moderator_power_level && } else if (powerlevel >= moderator_power_level &&
powerlevel > prevEvent->content.user_level(mxid, create)) { powerlevel > prevEvent->content.user_level(mxid)) {
resultingMessage.append(tr("%1 has made %2 a moderator of this room.") resultingMessage.append(tr("%1 has made %2 a moderator of this room.")
.arg(sender_name, nameOfChangedUser)); .arg(sender_name, nameOfChangedUser));
} else if (powerlevel >= moderator_power_level && } else if (powerlevel >= moderator_power_level &&
powerlevel < prevEvent->content.user_level(mxid, create)) { powerlevel < prevEvent->content.user_level(mxid)) {
resultingMessage.append(tr("%1 has downgraded %2 to moderator of this room.") resultingMessage.append(tr("%1 has downgraded %2 to moderator of this room.")
.arg(sender_name, nameOfChangedUser)); .arg(sender_name, nameOfChangedUser));
} else { } else {
resultingMessage.append( resultingMessage.append(tr("%1 has changed the powerlevel of %2 from %3 to %4.")
tr("%1 has changed the powerlevel of %2 from %3 to %4.") .arg(sender_name,
.arg(sender_name, nameOfChangedUser,
nameOfChangedUser, QString::number(prevEvent->content.user_level(mxid)),
QString::number(prevEvent->content.user_level(mxid, create)), QString::number(powerlevel)));
QString::number(powerlevel)));
} }
} }
} }
@ -3375,7 +3374,6 @@ TimelineModel::pushrulesRoomContext() const
cache::displayName(room_id_.toStdString(), http::client()->user_id().to_string()), cache::displayName(room_id_.toStdString(), http::client()->user_id().to_string()),
.member_count = cache::client()->memberCount(room_id_.toStdString()), .member_count = cache::client()->memberCount(room_id_.toStdString()),
.power_levels = permissions_.powerlevelEvent(), .power_levels = permissions_.powerlevelEvent(),
.create = permissions_.createEvent(),
}; };
} }

View file

@ -332,7 +332,6 @@ public:
Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void openUserProfile(QString userid);
Q_INVOKABLE void unpin(const QString &id); Q_INVOKABLE void unpin(const QString &id);
Q_INVOKABLE void pin(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 showReadReceipts(const QString &id);
Q_INVOKABLE void redactEvent(const QString &id, const QString &reason = ""); Q_INVOKABLE void redactEvent(const QString &id, const QString &reason = "");
Q_INVOKABLE void redactAllFromUser(const QString &userid, 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, if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry,
roomid_.toStdString())) { roomid_.toStdString())) {
auto h = std::get<mtx::events::AccountDataEvent< 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); this->event = std::move(h.content);
} }
} }

View file

@ -61,7 +61,9 @@ getFrameRate(const GValue *value)
void void
addFrameRate(std::vector<std::string> &rates, const FrameRate &rate) addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
{ {
rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); 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));
} }
void void

View file

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

View file

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

View file

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

View file

@ -1,11 +0,0 @@
--- 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)