Compare commits
4 commits
master
...
group-voic
| Author | SHA1 | Date | |
|---|---|---|---|
| 7775068bf5 | |||
| 586c3e3e3a | |||
| 8dc75613c7 | |||
| 6a0852ee36 |
66 changed files with 1170 additions and 832 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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%
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
2
.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
eval "$(devenv direnvrc)"
|
||||||
|
use devenv
|
||||||
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
81
PLAN-calls.md
Normal 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.
|
||||||
46
README.md
46
README.md
|
|
@ -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
65
devenv.lock
Normal 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
129
devenv.nix
Normal 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
12
devenv.yaml
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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'utilisateur %1 a été expulsé</translation>
|
<translation>L'utilisateur %1 a été 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'utilisateur %1 a été banni</translation>
|
<translation>L'utilisateur %1 a été 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'utilisateur est exclu, une tentative d'expulsion de l'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'utilisateur est exclu, une tentative de bannissement de l'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'un utilisateur dans le salon actuel. La raison est optionnelle. Si l'utilisateur est exclu, une tentative d'annulation du bannissement de l'utilisateur auquel vous êtes en train de répondre sera effectuée.</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'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'un utilisateur ou d'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'un utilisateur ou d'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 : 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 : 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'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'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'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'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'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"/>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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") : ""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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', '')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
|
|
|
||||||
|
|
@ -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_);
|
||||||
|
|
|
||||||
|
|
@ -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_;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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]() {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = "");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue