mirror of
				https://git.jami.net/savoirfairelinux/jami-client-qt.git
				synced 2025-11-04 08:10:18 +08:00 
			
		
		
		
	Compare commits
	
		
			73 Commits
		
	
	
		
			nightly/20
			...
			nightly/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 73aeb02ebd | |||
| 9d91317089 | |||
| 474bc5f6a4 | |||
| f5b64e955b | |||
| b88627d125 | |||
| 200978a044 | |||
| a673ff9890 | |||
| 7803dd0991 | |||
| a8a736bc8c | |||
| ff7acf9932 | |||
| afde816b23 | |||
| 0745c3b798 | |||
| 1376ee1f4b | |||
| 2b03107bd5 | |||
| cd1ab0ed12 | |||
| a13c6ae0e7 | |||
| 1ef9a85148 | |||
| 072eafbaf4 | |||
| 201f3182ca | |||
| 23130a5752 | |||
| f28d47bc51 | |||
| ee7818eefb | |||
| f25e66aa6a | |||
| 79b19aec01 | |||
| 4c92cb9936 | |||
| 1c81553245 | |||
| 5c2fec53da | |||
| f706abe5a6 | |||
| 610c27f751 | |||
| 6d20d3b515 | |||
| a0b583aa8d | |||
| 3855a5e951 | |||
| 6689bce782 | |||
| 860ddf22b6 | |||
| ef716d657d | |||
| b0fe0251d1 | |||
| 1ec2d5f27b | |||
| 23316993e5 | |||
| d42fe78676 | |||
| 78724c2a7b | |||
| e14fbe9437 | |||
| 82c63d5a89 | |||
| a72af9cba5 | |||
| d7c642a2fe | |||
| 08f3339693 | |||
| 402515365d | |||
| df102068bc | |||
| d40e884a1f | |||
| 5371dac882 | |||
| 0f62829588 | |||
| 39da97396c | |||
| 406edda453 | |||
| bbbeda6a26 | |||
| 6b3efff7cc | |||
| 3531b8b354 | |||
| 487446cbc3 | |||
| d5349490f5 | |||
| 7650f45d6f | |||
| a98f6ca4e3 | |||
| 0b96cf5f1f | |||
| 07e0b10478 | |||
| b38e216721 | |||
| 91f32f2421 | |||
| 06c3ffa6ce | |||
| ae53d92c2e | |||
| 97e477416a | |||
| 3d3b4612df | |||
| 7060afe467 | |||
| f56026439a | |||
| 0a24bec5ec | |||
| 38b7880d5f | |||
| 71a88b75ab | |||
| 37e1780762 | 
							
								
								
									
										2
									
								
								3rdparty/md4c
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								3rdparty/md4c
									
									
									
									
										vendored
									
									
								
							 Submodule 3rdparty/md4c updated: e9ff661ff8...ad8d41127b
									
								
							@ -33,7 +33,6 @@ include(${PROJECT_SOURCE_DIR}/extras/build/cmake/extra_tools.cmake)
 | 
			
		||||
 | 
			
		||||
option(WITH_DAEMON_SUBMODULE "Build with daemon submodule" ON)
 | 
			
		||||
option(JAMICORE_AS_SUBDIR "Build Jami-core as a subdir dependency" OFF)
 | 
			
		||||
option(ENABLE_TESTS "Build with tests" OFF)
 | 
			
		||||
option(WITH_WEBENGINE "Build with WebEngine" ON)
 | 
			
		||||
option(ENABLE_LIBWRAP "Enable libwrap (single process mode)" ON)
 | 
			
		||||
if(NOT (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
 | 
			
		||||
@ -88,7 +87,6 @@ list(APPEND QWINDOWKIT_OPTIONS
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if(WIN32)
 | 
			
		||||
  list(APPEND QWINDOWKIT_PATCHES ${EXTRA_PATCHES_DIR}/0002-workaround-right-margin.patch)
 | 
			
		||||
  list(APPEND QWINDOWKIT_OPTIONS QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS OFF)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
@ -141,6 +139,11 @@ else()
 | 
			
		||||
  find_package(QT NAMES Qt6 REQUIRED)
 | 
			
		||||
endif()
 | 
			
		||||
if (${QT_VERSION_MINOR} GREATER_EQUAL ${QT6_MINVER_MINOR})
 | 
			
		||||
  # Enforce a minimum Qt version of 6.6.2 for the Windows build
 | 
			
		||||
  # https://github.com/stdware/qwindowkit/issues/23
 | 
			
		||||
  if(MSVC AND ${QT_VERSION_MINOR} EQUAL 6 AND ${QT_VERSION_PATCH} LESS 2)
 | 
			
		||||
    message(FATAL_ERROR "Qt 6.6.2 or higher is required. Found ${QT_VERSION}")
 | 
			
		||||
  endif()
 | 
			
		||||
  # Qt version is 6.6 or higher
 | 
			
		||||
  message(STATUS "Found a suitable Qt version ${QT_VERSION}")
 | 
			
		||||
else()
 | 
			
		||||
@ -303,6 +306,7 @@ set(COMMON_SOURCES
 | 
			
		||||
  ${APP_SRC_DIR}/pluginversionmanager.cpp)
 | 
			
		||||
 | 
			
		||||
set(COMMON_HEADERS
 | 
			
		||||
  ${APP_SRC_DIR}/global.h
 | 
			
		||||
  ${APP_SRC_DIR}/avatarimageprovider.h
 | 
			
		||||
  ${APP_SRC_DIR}/networkmanager.h
 | 
			
		||||
  ${APP_SRC_DIR}/smartlistmodel.h
 | 
			
		||||
@ -451,10 +455,12 @@ elseif (NOT APPLE)
 | 
			
		||||
    ${APP_SRC_DIR}/xrectsel.c
 | 
			
		||||
    ${APP_SRC_DIR}/connectivitymonitor.cpp
 | 
			
		||||
    ${APP_SRC_DIR}/dbuserrorhandler.cpp
 | 
			
		||||
    ${APP_SRC_DIR}/appversionmanager.cpp)
 | 
			
		||||
    ${APP_SRC_DIR}/appversionmanager.cpp
 | 
			
		||||
    ${APP_SRC_DIR}/screencastportal.cpp)
 | 
			
		||||
  list(APPEND COMMON_HEADERS
 | 
			
		||||
    ${APP_SRC_DIR}/xrectsel.h
 | 
			
		||||
    ${APP_SRC_DIR}/dbuserrorhandler.h)
 | 
			
		||||
    ${APP_SRC_DIR}/dbuserrorhandler.h
 | 
			
		||||
    ${APP_SRC_DIR}/screencastportal.h)
 | 
			
		||||
  list(APPEND QT_MODULES DBus)
 | 
			
		||||
 | 
			
		||||
  find_package(PkgConfig REQUIRED)
 | 
			
		||||
@ -469,6 +475,11 @@ elseif (NOT APPLE)
 | 
			
		||||
    add_definitions(${GIO_CFLAGS})
 | 
			
		||||
  endif()
 | 
			
		||||
 | 
			
		||||
  pkg_check_modules(GIOUNIX REQUIRED gio-unix-2.0)
 | 
			
		||||
  if(GIOUNIX_FOUND)
 | 
			
		||||
    add_definitions(${GIOUNIX_CFLAGS})
 | 
			
		||||
  endif()
 | 
			
		||||
 | 
			
		||||
  pkg_check_modules(LIBNM libnm)
 | 
			
		||||
  if(LIBNM_FOUND)
 | 
			
		||||
    add_definitions(-DUSE_LIBNM)
 | 
			
		||||
@ -580,6 +591,7 @@ include_directories(
 | 
			
		||||
if(ENABLE_LIBWRAP)
 | 
			
		||||
  list(APPEND COMMON_HEADERS
 | 
			
		||||
    ${LIBCLIENT_SRC_DIR}/qtwrapper/instancemanager_wrap.h)
 | 
			
		||||
  add_definitions(-DENABLE_LIBWRAP=true)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
# SFPM
 | 
			
		||||
@ -588,6 +600,7 @@ add_subdirectory(3rdparty/SortFilterProxyModel)
 | 
			
		||||
set(SFPM_OBJECTS $<TARGET_OBJECTS:SortFilterProxyModel>)
 | 
			
		||||
 | 
			
		||||
# md4c
 | 
			
		||||
set(BUILD_MD2HTML_EXECUTABLE OFF CACHE BOOL "Don't build md2html executable" FORCE)
 | 
			
		||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Don't build shared md4c library" FORCE)
 | 
			
		||||
add_subdirectory(3rdparty/md4c EXCLUDE_FROM_ALL)
 | 
			
		||||
list(APPEND CLIENT_LINK_DIRS ${MD4C_BINARY_DIR}/src)
 | 
			
		||||
@ -869,7 +882,7 @@ qt_import_qml_plugins(${PROJECT_NAME})
 | 
			
		||||
qt_finalize_executable(${PROJECT_NAME})
 | 
			
		||||
 | 
			
		||||
# tests
 | 
			
		||||
if(ENABLE_TESTS)
 | 
			
		||||
if(BUILD_TESTING)
 | 
			
		||||
  message("Add Jami tests")
 | 
			
		||||
  add_subdirectory(${TESTS_DIR})
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								INSTALL.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								INSTALL.md
									
									
									
									
									
								
							@ -131,6 +131,11 @@ Notes:
 | 
			
		||||
 | 
			
		||||
- `--global-install` to install client-qt globally under /usr/local
 | 
			
		||||
- `--prefix` to change the destination of the install.
 | 
			
		||||
+ For developers:
 | 
			
		||||
    + `--asan` add address sanitizer on the binary
 | 
			
		||||
    + `--debug` enable debug symbols
 | 
			
		||||
    + `--testing` will build the tests for both the daemon and client
 | 
			
		||||
    + `--no-libwrap` will build the DBUS version.
 | 
			
		||||
 | 
			
		||||
## Build only the client
 | 
			
		||||
 | 
			
		||||
@ -207,7 +212,7 @@ Only 64-bit MSVC build can be compiled.
 | 
			
		||||
 | 
			
		||||
  |                      | Qt Version |
 | 
			
		||||
  | -------------------- | ---------- |
 | 
			
		||||
  | Minimum requirement: | 6.6.1      |
 | 
			
		||||
  | Minimum requirement: | 6.6.2      |
 | 
			
		||||
 | 
			
		||||
- Install [Python3](https://www.python.org/downloads/) for Windows
 | 
			
		||||
 | 
			
		||||
@ -233,7 +238,7 @@ Only 64-bit MSVC build can be compiled.
 | 
			
		||||
- Using a new **Non-Elevated Command Prompt**
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
    python build.py --install --qt <path-to-qt-bin-folder> (e.g. C:/Qt/6.6.1/msvc2019_64)
 | 
			
		||||
    python build.py --install --qt <path-to-qt-bin-folder> (e.g. C:/Qt/6.6.2/msvc2019_64)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> **SDK** Note:
 | 
			
		||||
@ -276,7 +281,7 @@ Once the build has finished, you should then be able to use the Visual Studio So
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
    python extras\scripts\build-windows.py --init
 | 
			
		||||
    python extras\scripts\build-windows.py --qt <path-to-qt-bin-folder> (e.g. C:/Qt/6.6.1/msvc2019_64)
 | 
			
		||||
    python extras\scripts\build-windows.py --qt <path-to-qt-bin-folder> (e.g. C:/Qt/6.6.2/msvc2019_64)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Building On MacOS
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								build.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								build.py
									
									
									
									
									
								
							@ -389,6 +389,8 @@ def run_install(args):
 | 
			
		||||
        install_args.append('-u')
 | 
			
		||||
    if args.debug:
 | 
			
		||||
        install_args.append('-d')
 | 
			
		||||
    if args.testing:
 | 
			
		||||
        install_args.append('-t')
 | 
			
		||||
    if args.asan:
 | 
			
		||||
        install_args.append('-A')
 | 
			
		||||
    if args.no_libwrap:
 | 
			
		||||
@ -397,6 +399,8 @@ def run_install(args):
 | 
			
		||||
        install_args.append('-w')
 | 
			
		||||
    if args.arch:
 | 
			
		||||
        install_args += ('-a', args.arch)
 | 
			
		||||
    if args.extra_cmake_flags:
 | 
			
		||||
        install_args += ('-D', args.extra_cmake_flags)
 | 
			
		||||
 | 
			
		||||
    if args.distribution == OSX_DISTRIBUTION_NAME:
 | 
			
		||||
        # The `universal_newlines` parameter has been renamed to `text` in
 | 
			
		||||
@ -725,6 +729,9 @@ def parse_args():
 | 
			
		||||
                    default=True, action='store_false')
 | 
			
		||||
    ap.add_argument('--qt', type=str,
 | 
			
		||||
                    help='Use the Qt path supplied')
 | 
			
		||||
    ap.add_argument('--testing', dest='testing',
 | 
			
		||||
                    default=False, action='store_true',
 | 
			
		||||
                    help='Enable testing for both client and daemon')
 | 
			
		||||
    ap.add_argument('--no-libwrap', dest='no_libwrap',
 | 
			
		||||
                    default=False, action='store_true',
 | 
			
		||||
                    help='Disable libwrap. Also set --disable-shared option to daemon configure')
 | 
			
		||||
@ -740,6 +747,9 @@ def parse_args():
 | 
			
		||||
    ap.add_argument('--pywinmake', dest='pywinmake',
 | 
			
		||||
                    default=False, action='store_true',
 | 
			
		||||
                    help='Build Jami for Windows using pywinmake')
 | 
			
		||||
    # Allow supplying extra congifure flags to the client cmake.
 | 
			
		||||
    ap.add_argument('--extra-cmake-flags', type=str,
 | 
			
		||||
                    help='Extra flags to pass to the client cmake')
 | 
			
		||||
 | 
			
		||||
    dist = choose_distribution()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								daemon
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								daemon
									
									
									
									
									
								
							 Submodule daemon updated: 302a4322b1...59a8e41ca8
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
FROM ubuntu:20.04
 | 
			
		||||
FROM ubuntu:22.04
 | 
			
		||||
 | 
			
		||||
ENV DEBIAN_FRONTEND noninteractive
 | 
			
		||||
ENV QT_QUICK_BACKEND software
 | 
			
		||||
@ -10,7 +10,7 @@ RUN apt-get update && \
 | 
			
		||||
 | 
			
		||||
RUN apt install gnupg dirmngr ca-certificates curl --no-install-recommends
 | 
			
		||||
RUN curl -s https://dl.jami.net/public-key.gpg | tee /usr/share/keyrings/jami-archive-keyring.gpg > /dev/null
 | 
			
		||||
RUN sh -c "echo 'deb [signed-by=/usr/share/keyrings/jami-archive-keyring.gpg] https://dl.jami.net/internal/ubuntu_20.04/ jami main' > /etc/apt/sources.list.d/jami.list"
 | 
			
		||||
RUN sh -c "echo 'deb [signed-by=/usr/share/keyrings/jami-archive-keyring.gpg] https://dl.jami.net/internal/ubuntu_22.04/ jami main' > /etc/apt/sources.list.d/jami.list"
 | 
			
		||||
RUN apt-get update && apt-get install libqt-jami -y
 | 
			
		||||
 | 
			
		||||
RUN apt-get install -y -o Acquire::Retries=10 \
 | 
			
		||||
@ -51,6 +51,7 @@ RUN apt-get install -y -o Acquire::Retries=10 \
 | 
			
		||||
        libswscale-dev \
 | 
			
		||||
        libavdevice-dev \
 | 
			
		||||
        libopus-dev \
 | 
			
		||||
        libpipewire-0.3-dev \
 | 
			
		||||
        libudev-dev \
 | 
			
		||||
        libgsm1-dev \
 | 
			
		||||
        libjsoncpp-dev \
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								extras/ci/client-qt-gnulinux/Jenkinsfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								extras/ci/client-qt-gnulinux/Jenkinsfile
									
									
									
									
										vendored
									
									
								
							@ -113,7 +113,7 @@ pipeline {
 | 
			
		||||
                                    cd ${dockerTopDir}
 | 
			
		||||
                                    ./build.py --install --qt /usr/lib/libqt-jami/
 | 
			
		||||
                                    cd build
 | 
			
		||||
                                    cmake .. -DENABLE_TESTS=True
 | 
			
		||||
                                    cmake .. -DBUILD_TESTING=True
 | 
			
		||||
                                    make -j${cpuCount}
 | 
			
		||||
                                """)
 | 
			
		||||
                                // Run tests
 | 
			
		||||
 | 
			
		||||
@ -100,6 +100,7 @@ RUN dnf install -y \
 | 
			
		||||
        cmake \
 | 
			
		||||
        fmt-devel \
 | 
			
		||||
        python3-html5lib \
 | 
			
		||||
        cups-devel
 | 
			
		||||
        cups-devel \
 | 
			
		||||
        pipewire-devel
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
 | 
			
		||||
CMD ["/opt/build-package-rpm.sh"]
 | 
			
		||||
@ -28,4 +28,10 @@ ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
 | 
			
		||||
RUN /opt/install-cmake.sh
 | 
			
		||||
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/build-package-debian.sh /opt/build-package-debian.sh
 | 
			
		||||
 | 
			
		||||
# Setting this variable so that FFmpeg gets built without pipewiregrab
 | 
			
		||||
# (see daemon/contrib/bootstrap and daemon/contrib/src/ffmpeg/rules.mak)
 | 
			
		||||
# We rely on PipeWire for screen sharing on Wayland, but the version available on Debian 11 is too old.
 | 
			
		||||
ENV DISABLE_PIPEWIRE=true
 | 
			
		||||
 | 
			
		||||
CMD ["/opt/build-package-debian.sh"]
 | 
			
		||||
 | 
			
		||||
@ -98,6 +98,7 @@ RUN dnf install -y \
 | 
			
		||||
        clang \
 | 
			
		||||
        cmake \
 | 
			
		||||
        fmt-devel \
 | 
			
		||||
        pipewire-devel \
 | 
			
		||||
        cups-devel #Chromium for Qt
 | 
			
		||||
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
 | 
			
		||||
 | 
			
		||||
@ -98,7 +98,8 @@ RUN dnf install -y \
 | 
			
		||||
        cmake \
 | 
			
		||||
        fmt-devel \
 | 
			
		||||
        python3-html5lib \
 | 
			
		||||
        cups-devel
 | 
			
		||||
        cups-devel \
 | 
			
		||||
        pipewire-devel
 | 
			
		||||
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,8 @@ RUN dnf install -y \
 | 
			
		||||
        cmake \
 | 
			
		||||
        fmt-devel \
 | 
			
		||||
        python3.10 \
 | 
			
		||||
        cups-devel
 | 
			
		||||
        cups-devel \
 | 
			
		||||
        pipewire-devel
 | 
			
		||||
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,8 @@ RUN zypper --non-interactive install -y \
 | 
			
		||||
        gstreamer-plugins-bad-devel \
 | 
			
		||||
        gstreamer-plugins-base-devel \
 | 
			
		||||
        cmake \
 | 
			
		||||
        wget
 | 
			
		||||
        wget \
 | 
			
		||||
        pipewire-devel
 | 
			
		||||
 | 
			
		||||
# openSUSE Leap 15.4 comes with Python 3.6 by default,
 | 
			
		||||
# but we need at least 3.7 to compile Qt 6.6.1
 | 
			
		||||
@ -112,4 +113,10 @@ ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-r
 | 
			
		||||
 | 
			
		||||
ENV CC=gcc
 | 
			
		||||
ENV CXX=g++
 | 
			
		||||
 | 
			
		||||
# Setting this variable so that FFmpeg gets built without pipewiregrab
 | 
			
		||||
# (see daemon/contrib/bootstrap and daemon/contrib/src/ffmpeg/rules.mak)
 | 
			
		||||
# We rely on PipeWire for screen sharing on Wayland, but the version available on openSUSE Leap 15.4 is too old.
 | 
			
		||||
ENV DISABLE_PIPEWIRE=true
 | 
			
		||||
 | 
			
		||||
CMD ["/opt/build-package-rpm.sh"]
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,8 @@ RUN zypper --non-interactive install -y \
 | 
			
		||||
        gstreamer-plugins-bad-devel \
 | 
			
		||||
        gstreamer-plugins-base-devel \
 | 
			
		||||
        cmake \
 | 
			
		||||
        wget
 | 
			
		||||
        wget \
 | 
			
		||||
        pipewire-devel
 | 
			
		||||
 | 
			
		||||
# openSUSE Leap 15.5 comes with Python 3.6 by default,
 | 
			
		||||
# but we need at least 3.7 to compile Qt 6.6.1
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
ARG RISK=edge
 | 
			
		||||
ARG UBUNTU=focal
 | 
			
		||||
ARG UBUNTU=jammy
 | 
			
		||||
 | 
			
		||||
FROM ubuntu:$UBUNTU as builder
 | 
			
		||||
ARG RISK
 | 
			
		||||
@ -69,7 +69,11 @@ COPY --from=builder /snap/snapcraft /snap/snapcraft
 | 
			
		||||
COPY --from=builder /snap/bin/snapcraft /snap/bin/snapcraft
 | 
			
		||||
 | 
			
		||||
# Generate locale and install dependencies.
 | 
			
		||||
RUN apt-get update && apt-get dist-upgrade --yes && apt-get install --yes snapd sudo apt-transport-https locales && locale-gen en_US.UTF-8
 | 
			
		||||
RUN apt-get update && apt-get dist-upgrade --yes && apt-get install --yes snapd sudo apt-transport-https locales wget && locale-gen en_US.UTF-8
 | 
			
		||||
 | 
			
		||||
# Install CMake 3.21 for Qt 6
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
 | 
			
		||||
RUN /opt/install-cmake.sh
 | 
			
		||||
 | 
			
		||||
# Set the proper environment.
 | 
			
		||||
ENV LANG="en_US.UTF-8"
 | 
			
		||||
 | 
			
		||||
@ -33,4 +33,10 @@ ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
 | 
			
		||||
RUN /opt/install-cmake.sh
 | 
			
		||||
 | 
			
		||||
ADD extras/packaging/gnu-linux/scripts/build-package-debian.sh /opt/build-package-debian.sh
 | 
			
		||||
 | 
			
		||||
# Setting this variable so that FFmpeg gets built without pipewiregrab
 | 
			
		||||
# (see daemon/contrib/bootstrap and daemon/contrib/src/ffmpeg/rules.mak)
 | 
			
		||||
# We rely on PipeWire for screen sharing on Wayland, but the version available on Ubuntu 20.04 is too old.
 | 
			
		||||
ENV DISABLE_PIPEWIRE=true
 | 
			
		||||
 | 
			
		||||
CMD ["/opt/build-package-debian.sh"]
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,8 @@ Build-Depends: debhelper (>= 9),
 | 
			
		||||
               libvdpau-dev,
 | 
			
		||||
               libssl-dev,
 | 
			
		||||
               libargon2-dev | libargon2-0-dev,
 | 
			
		||||
# TODO: remove libpipewire-0.2-dev once we stop supporting Ubuntu 20.04
 | 
			
		||||
               libpipewire-0.3-dev | libpipewire-0.2-dev,
 | 
			
		||||
# other
 | 
			
		||||
               nasm,
 | 
			
		||||
               yasm,
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ BuildRequires: libuuid-devel
 | 
			
		||||
BuildRequires: libva-devel
 | 
			
		||||
BuildRequires: libvdpau-devel
 | 
			
		||||
BuildRequires: pcre-devel
 | 
			
		||||
BuildRequires: pipewire-devel
 | 
			
		||||
BuildRequires: uuid-devel
 | 
			
		||||
BuildRequires: yaml-cpp-devel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -168,7 +168,7 @@ package-repositories:
 | 
			
		||||
    components: [main]
 | 
			
		||||
    suites: [jami]
 | 
			
		||||
    key-id: A295D773307D25A33AE72F2F64CD5FA175348F84
 | 
			
		||||
    url: https://dl.jami.net/nightly/ubuntu_20.04/
 | 
			
		||||
    url: https://dl.jami.net/nightly/ubuntu_22.04/
 | 
			
		||||
 | 
			
		||||
parts:
 | 
			
		||||
  desktop-launch:
 | 
			
		||||
@ -304,6 +304,7 @@ parts:
 | 
			
		||||
    - libswscale-dev
 | 
			
		||||
    - libva-dev
 | 
			
		||||
    - libvdpau-dev
 | 
			
		||||
    - libpipewire-0.3-dev
 | 
			
		||||
    - libargon2-0-dev # opendht
 | 
			
		||||
    - libexpat1-dev
 | 
			
		||||
    - libjsoncpp-dev
 | 
			
		||||
@ -326,7 +327,7 @@ parts:
 | 
			
		||||
    - libegl1
 | 
			
		||||
    - libgbm1
 | 
			
		||||
    - libgudev-1.0-0
 | 
			
		||||
    - libjsoncpp1
 | 
			
		||||
    - libjsoncpp25
 | 
			
		||||
    - libllvm12
 | 
			
		||||
    - libminizip1
 | 
			
		||||
    - libnm0
 | 
			
		||||
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
From ca2be6466c150d1b82a646d97b27df35b45d90f1 Mon Sep 17 00:00:00 2001
 | 
			
		||||
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
 | 
			
		||||
Date: Tue, 9 Jan 2024 15:25:19 -0500
 | 
			
		||||
Subject: [PATCH] workaround right margin
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 src/core/contexts/win32windowcontext.cpp | 11 +++++++++++
 | 
			
		||||
 1 file changed, 11 insertions(+)
 | 
			
		||||
 | 
			
		||||
diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp
 | 
			
		||||
index 3f6623e..9ee7752 100644
 | 
			
		||||
--- a/src/core/contexts/win32windowcontext.cpp
 | 
			
		||||
+++ b/src/core/contexts/win32windowcontext.cpp
 | 
			
		||||
@@ -402,6 +402,17 @@ namespace QWK {
 | 
			
		||||
                     return true;
 | 
			
		||||
                 }
 | 
			
		||||
             }
 | 
			
		||||
+
 | 
			
		||||
+#if !QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS)
 | 
			
		||||
+            if (msg->message == WM_MOVE || msg->message == WM_SIZE ||
 | 
			
		||||
+                (msg->message == WM_IME_SETCONTEXT && (GetForegroundWindow() == msg->hwnd))) {
 | 
			
		||||
+                static const auto flags = SWP_FRAMECHANGED | SWP_NOMOVE |
 | 
			
		||||
+                                          SWP_NOSIZE | SWP_NOZORDER |
 | 
			
		||||
+                                          SWP_NOOWNERZORDER;
 | 
			
		||||
+                SetWindowPos(msg->hwnd, NULL, 0, 0, 0, 0, flags);
 | 
			
		||||
+            }
 | 
			
		||||
+#endif
 | 
			
		||||
+
 | 
			
		||||
             return false;
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
2.7.4
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ mutually exclusive required arguments:
 | 
			
		||||
  -z, --zip         Build portable archive
 | 
			
		||||
 | 
			
		||||
examples:
 | 
			
		||||
1.  build.py --qt=C:/Qt/6.6.1/msvc2019_64  # Build the app using a specific Qt
 | 
			
		||||
1.  build.py --qt=C:/Qt/6.6.2/msvc2019_64  # Build the app using a specific Qt
 | 
			
		||||
2.  build.py --init pack --msi             # Build the app and an MSI installer
 | 
			
		||||
3.  build.py --init --tests                # Build the app and run tests
 | 
			
		||||
    build.py pack --zip --skip-build       # Generate a 7z archive of the app
 | 
			
		||||
@ -280,7 +280,7 @@ def build(config_str, qt_dir, tests):
 | 
			
		||||
        "-DCMAKE_INSTALL_PREFIX=" + os.getcwd(),
 | 
			
		||||
        "-DCMAKE_SYSTEM_VERSION=" + WIN_SDK_VERSION,
 | 
			
		||||
        "-DCMAKE_BUILD_TYPE=" + "Release",
 | 
			
		||||
        "-DENABLE_TESTS=" + str(tests).lower(),
 | 
			
		||||
        "-DBUILD_TESTING=" + str(tests).lower(),
 | 
			
		||||
        "-DBETA=" + str((0, 1)[config_str == "Beta"]),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,8 @@ export OSTYPE
 | 
			
		||||
  # -W: disable libwrap and shared library
 | 
			
		||||
  # -w: do not use Qt WebEngine
 | 
			
		||||
  # -a: arch to build
 | 
			
		||||
  # -A: enable AddressSanitizer
 | 
			
		||||
  # -D: extra CMake flags for the client
 | 
			
		||||
 | 
			
		||||
set -ex
 | 
			
		||||
 | 
			
		||||
@ -44,9 +46,11 @@ priv_install=true
 | 
			
		||||
enable_libwrap=true
 | 
			
		||||
enable_webengine=true
 | 
			
		||||
asan=
 | 
			
		||||
extra_cmake_flags=''
 | 
			
		||||
arch=''
 | 
			
		||||
enable_testing=false
 | 
			
		||||
 | 
			
		||||
while getopts gsc:dQ:P:p:uWwa:A OPT; do
 | 
			
		||||
while getopts gsc:dQ:P:p:uWwa:AtD: OPT; do
 | 
			
		||||
  case "$OPT" in
 | 
			
		||||
    g)
 | 
			
		||||
      global='true'
 | 
			
		||||
@ -81,6 +85,12 @@ while getopts gsc:dQ:P:p:uWwa:A OPT; do
 | 
			
		||||
    A)
 | 
			
		||||
      asan='true'
 | 
			
		||||
    ;;
 | 
			
		||||
    t)
 | 
			
		||||
      enable_testing='true'
 | 
			
		||||
    ;;
 | 
			
		||||
    D)
 | 
			
		||||
      extra_cmake_flags="${OPTARG}"
 | 
			
		||||
    ;;
 | 
			
		||||
    \?)
 | 
			
		||||
      exit 1
 | 
			
		||||
    ;;
 | 
			
		||||
@ -196,6 +206,12 @@ if [ "${asan}" = "true" ]; then
 | 
			
		||||
    client_cmake_flags+=(-DENABLE_ASAN=true)
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ "${enable_testing}" = "true" ]; then
 | 
			
		||||
    client_cmake_flags+=(-DBUILD_TESTING=On)
 | 
			
		||||
else
 | 
			
		||||
    client_cmake_flags+=(-DBUILD_TESTING=Off)
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
			
		||||
    #detect arch for macos
 | 
			
		||||
    CMAKE_OSX_ARCHITECTURES="arm64"
 | 
			
		||||
@ -220,6 +236,11 @@ else
 | 
			
		||||
                         -DWITH_DAEMON_SUBMODULE=true)
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Add extra flags for the client
 | 
			
		||||
if [ -n "${extra_cmake_flags}" ]; then
 | 
			
		||||
    client_cmake_flags+=(${extra_cmake_flags})
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo "info: Configuring $client client with flags: ${client_cmake_flags[*]}"
 | 
			
		||||
cmake .. "${client_cmake_flags[@]}"
 | 
			
		||||
make -j"${proc}" V=1
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								resources/icons/Receive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								resources/icons/Receive.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
<svg id="Receive" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 12 12">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <clipPath id="clip-path">
 | 
			
		||||
      <rect id="Rectangle_429" data-name="Rectangle 429" width="12" height="12" fill="none"/>
 | 
			
		||||
    </clipPath>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <g id="Group_225" data-name="Group 225" clip-path="url(#clip-path)">
 | 
			
		||||
    <path id="Path_333" data-name="Path 333" d="M6.43,8.784,3.007,5.362,4.06,4.309l2.37,2.37,4.314-4.314A5.966,5.966,0,0,0,6,0c-.032,0-.061.008-.094.01A5.98,5.98,0,0,0,.094,5.074,5.911,5.911,0,0,0,0,6a5.911,5.911,0,0,0,.094.926A5.98,5.98,0,0,0,5.906,11.99c.032,0,.061.01.094.01a6,6,0,0,0,5.533-8.32Z" fill="#60c880"/>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 702 B  | 
							
								
								
									
										3
									
								
								resources/icons/incoming-call.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								resources/icons/incoming-call.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="22.08" height="22.08" viewBox="0 0 22.08 22.08">
 | 
			
		||||
  <path id="noun-arrow-1167262" d="M35.45,26.488l-4.476,4.476V18.9H28.916V30.964l-4.476-4.476L23,27.955,29.945,34.9l6.971-6.945Z" transform="translate(8.913 -29.202) rotate(45)" stroke="#000" stroke-width="0.5"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 316 B  | 
							
								
								
									
										8
									
								
								resources/icons/missed-incoming-call.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								resources/icons/missed-incoming-call.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8.814" viewBox="0 0 16 8.814">
 | 
			
		||||
  <g id="noun-missed-3555066" transform="translate(-23.455 -28)">
 | 
			
		||||
    <g id="Group_82" data-name="Group 82" transform="translate(23.455 28)">
 | 
			
		||||
      <path id="Path_289" data-name="Path 289" d="M37.727,37.615a2.761,2.761,0,0,1-1.964-.815L31.17,32.211l1.782-1.782,4.589,4.593a.268.268,0,0,0,.368,0l5.852-5.852,1.782,1.782L39.691,36.8A2.761,2.761,0,0,1,37.727,37.615Z" transform="translate(-29.543 -28.802)"/>
 | 
			
		||||
      <path id="Path_290" data-name="Path 290" d="M28.518,35.555H26v-6.3A1.259,1.259,0,0,1,27.259,28h6.3v2.518H28.518Z" transform="translate(-26 -28)"/>
 | 
			
		||||
    </g>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 672 B  | 
							
								
								
									
										8
									
								
								resources/icons/missed-outgoing-call.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								resources/icons/missed-outgoing-call.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8.814" viewBox="0 0 16 8.814">
 | 
			
		||||
  <g id="noun-missed-3555066" transform="translate(-0.001)">
 | 
			
		||||
    <g id="Group_82" data-name="Group 82" transform="translate(0.001)">
 | 
			
		||||
      <path id="Path_289" data-name="Path 289" d="M38.986,37.615A2.761,2.761,0,0,0,40.95,36.8l4.593-4.589-1.782-1.782-4.589,4.593a.268.268,0,0,1-.368,0L32.952,29.17,31.17,30.952,37.022,36.8A2.761,2.761,0,0,0,38.986,37.615Z" transform="translate(-31.17 -28.802)"/>
 | 
			
		||||
      <path id="Path_290" data-name="Path 290" d="M31.036,35.555h2.518v-6.3A1.259,1.259,0,0,0,32.3,28H26v2.518h5.036Z" transform="translate(-17.555 -28)"/>
 | 
			
		||||
    </g>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 666 B  | 
							
								
								
									
										3
									
								
								resources/icons/outgoing-call.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								resources/icons/outgoing-call.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="22.08" height="22.08" viewBox="0 0 22.08 22.08">
 | 
			
		||||
  <path id="noun-arrow-1167262" d="M12.45,7.589,7.974,12.064V0H5.916V12.064L1.44,7.588,0,9.055,6.945,16l6.971-6.945Z" transform="translate(10.267 21.654) rotate(-135)" stroke="#000" stroke-width="0.5"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 306 B  | 
@ -1,5 +1,6 @@
 | 
			
		||||
<h4 align="left"><span style="font-weight:600"> Created by</span></h4>
 | 
			
		||||
<p>Adrien Béraud<br>
 | 
			
		||||
<p>Abhishek Ojha<br>
 | 
			
		||||
Adrien Béraud<br>
 | 
			
		||||
Albert Babí<br>
 | 
			
		||||
Alexandre Lision<br>
 | 
			
		||||
Alexandr Sergheev<br>
 | 
			
		||||
@ -25,6 +26,7 @@ Emma Falkiewitz<br>
 | 
			
		||||
Emmanuel Lepage-Vallée<br>
 | 
			
		||||
Fadi Shehadeh<br>
 | 
			
		||||
Franck Laurent<br>
 | 
			
		||||
François-Simon Fauteux-Chapleau<br>
 | 
			
		||||
Frédéric Guimont<br>
 | 
			
		||||
Guillaume Heller<br>
 | 
			
		||||
Guillaume Roguez<br>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										207
									
								
								src/app/ComponentTestWindow.qml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								src/app/ComponentTestWindow.qml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,207 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 Savoir-faire Linux Inc.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Qt.labs.qmlmodels
 | 
			
		||||
 | 
			
		||||
import net.jami.Models 1.1
 | 
			
		||||
import net.jami.Adapters 1.1
 | 
			
		||||
import net.jami.Enums 1.1
 | 
			
		||||
import net.jami.Helpers 1.1
 | 
			
		||||
import net.jami.Constants 1.1
 | 
			
		||||
 | 
			
		||||
import "mainview"
 | 
			
		||||
import "mainview/components"
 | 
			
		||||
import "wizardview"
 | 
			
		||||
import "commoncomponents"
 | 
			
		||||
 | 
			
		||||
// A window into which we can load a QML file for testing.
 | 
			
		||||
ApplicationWindow {
 | 
			
		||||
    id: appWindow
 | 
			
		||||
    visible: true
 | 
			
		||||
    width: testWidth || loader.implicitWidth || 800
 | 
			
		||||
    height: testHeight || loader.implicitHeight || 600
 | 
			
		||||
    title: testComponentURI
 | 
			
		||||
 | 
			
		||||
    // WARNING: The following currently must be maintained in tandem with MainApplicationWindow.qml
 | 
			
		||||
    // Used to manage full screen mode and save/restore window geometry.
 | 
			
		||||
    readonly property bool useFrameless: false
 | 
			
		||||
    property bool isRTL: UtilsAdapter.isRTL
 | 
			
		||||
    LayoutMirroring.enabled: isRTL
 | 
			
		||||
    LayoutMirroring.childrenInherit: isRTL
 | 
			
		||||
    property LayoutManager layoutManager: LayoutManager {
 | 
			
		||||
        appContainer: null
 | 
			
		||||
    }
 | 
			
		||||
    // Used to manage dynamic view loading and unloading.
 | 
			
		||||
    property ViewManager viewManager: ViewManager {}
 | 
			
		||||
    // Used to manage the view stack and the current view.
 | 
			
		||||
    property ViewCoordinator viewCoordinator: ViewCoordinator {}
 | 
			
		||||
 | 
			
		||||
    Loader {
 | 
			
		||||
        id: loader
 | 
			
		||||
 | 
			
		||||
        source: Qt.resolvedUrl(testComponentURI)
 | 
			
		||||
        onStatusChanged: {
 | 
			
		||||
            console.log("Status changed to:", loader.status)
 | 
			
		||||
            if (loader.status == Loader.Error || loader.status == Loader.Null) {
 | 
			
		||||
                console.error("Couldn't load component:", source)
 | 
			
		||||
                Qt.exit(1);
 | 
			
		||||
            } else if (loader.status == Loader.Ready) {
 | 
			
		||||
                console.info("Loaded component:", source);
 | 
			
		||||
                // If any of the dimensions are not set, set them to the appWindow's dimensions
 | 
			
		||||
                item.width = item.width || Qt.binding(() => appWindow.width);
 | 
			
		||||
                item.height = item.height || Qt.binding(() => appWindow.height);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Closing this window should always exit the application.
 | 
			
		||||
    onClosing: Qt.quit()
 | 
			
		||||
 | 
			
		||||
    // A window to modify properties for Jamified components.
 | 
			
		||||
    // Sometimes we need to modify properties including current conversation ID, account ID, etc.
 | 
			
		||||
    // This window should have a simple layout: a list of editable parameters within a scroll view.
 | 
			
		||||
    Window {
 | 
			
		||||
        id: configTool
 | 
			
		||||
        width: 400
 | 
			
		||||
        height: 400
 | 
			
		||||
        title: "Config tool"
 | 
			
		||||
 | 
			
		||||
        visible: true
 | 
			
		||||
        // Cannot be closed.
 | 
			
		||||
        flags: Qt.SplashScreen
 | 
			
		||||
 | 
			
		||||
        // Anchor the window to the right of the parent window.
 | 
			
		||||
        x: appWindow.x + appWindow.width
 | 
			
		||||
        y: appWindow.y
 | 
			
		||||
 | 
			
		||||
        color: "lightgray"
 | 
			
		||||
 | 
			
		||||
        Page {
 | 
			
		||||
            anchors.fill: parent
 | 
			
		||||
            header: Control {
 | 
			
		||||
                contentItem: Text {
 | 
			
		||||
                    horizontalAlignment: Text.AlignHCenter
 | 
			
		||||
                    text: "Config tool"
 | 
			
		||||
                }
 | 
			
		||||
                background: Rectangle { color: configTool.color }
 | 
			
		||||
            }
 | 
			
		||||
            contentItem: Control {
 | 
			
		||||
                background: Rectangle { color: Qt.lighter(configTool.color, 1.1) }
 | 
			
		||||
                padding: 10
 | 
			
		||||
                contentItem: ListView {
 | 
			
		||||
                    // Declare types of controls. TODO: add as needed.
 | 
			
		||||
                    Component {
 | 
			
		||||
                        id: checkComponent
 | 
			
		||||
                        CheckBox {
 | 
			
		||||
                            text: label
 | 
			
		||||
                            onCheckedChanged: checkChangedCb(checked)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Component {
 | 
			
		||||
                        id: comboComponent
 | 
			
		||||
                        Control {
 | 
			
		||||
                            contentItem: RowLayout {
 | 
			
		||||
                                Text { text: label }
 | 
			
		||||
                                ComboBox {
 | 
			
		||||
                                    id: comboBox
 | 
			
		||||
                                    displayText: CurrentConversation.title || "undefined"
 | 
			
		||||
                                    model: getDataModel()
 | 
			
		||||
                                    delegate: ItemDelegate {
 | 
			
		||||
                                        highlighted: comboBox.highlightedIndex === index
 | 
			
		||||
                                        width: parent.width
 | 
			
		||||
                                        text: JamiQmlUtils.getModelData(comboBox.model, index, displayRole)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    onCurrentIndexChanged: onIndexChanged(model, currentIndex)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    spacing: 5
 | 
			
		||||
                    model: ListModel {
 | 
			
		||||
                        ListElement {
 | 
			
		||||
                            label: "Conversation ID"
 | 
			
		||||
                            type: "combobox"
 | 
			
		||||
                            getDataModel: () => ConversationsAdapter.convListProxyModel
 | 
			
		||||
                            displayRole: ConversationList.Title
 | 
			
		||||
                            onIndexChanged: function(model, index) {
 | 
			
		||||
                                const convUid = JamiQmlUtils.getModelData(model, index, ConversationList.UID);
 | 
			
		||||
                                LRCInstance.selectConversation(convUid);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        ListElement {
 | 
			
		||||
                            label: "Force local preview"
 | 
			
		||||
                            type: "checkbox"
 | 
			
		||||
                            value: false
 | 
			
		||||
                            checkChangedCb: function(checked) {
 | 
			
		||||
                                // Find any child component of type `LocalVideo` and start it.
 | 
			
		||||
                                const localVideo = findChild(loader.item, LocalVideo, "type");
 | 
			
		||||
                                if (localVideo) {
 | 
			
		||||
                                    if (checked) {
 | 
			
		||||
                                        localVideo.startWithId(VideoDevices.getDefaultDevice());
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        localVideo.startWithId("");
 | 
			
		||||
                                    }
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    console.error("LocalVideo not found");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    delegate: DelegateChooser {
 | 
			
		||||
                        role: "type"
 | 
			
		||||
                        DelegateChoice {
 | 
			
		||||
                            roleValue: "checkbox"
 | 
			
		||||
                            delegate: checkComponent
 | 
			
		||||
                        }
 | 
			
		||||
                        DelegateChoice {
 | 
			
		||||
                            roleValue: "combobox"
 | 
			
		||||
                            delegate: comboComponent
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // From TestCase.qml, refactored to find a child by type or name.
 | 
			
		||||
    function findChild(parent, searchValue, searchBy = "name") {
 | 
			
		||||
        if (!parent || parent.children === undefined) {
 | 
			
		||||
            console.error("No children found");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        // Search directly under the given parent
 | 
			
		||||
        for (var i = 0; i < parent.children.length; ++i) {
 | 
			
		||||
            var child = parent.children[i];
 | 
			
		||||
            var match = false;
 | 
			
		||||
            if (searchBy === "name" && child.objectName === searchValue) {
 | 
			
		||||
                match = true;
 | 
			
		||||
            } else if (searchBy === "type" && child instanceof searchValue) {
 | 
			
		||||
                match = true;
 | 
			
		||||
            }
 | 
			
		||||
            if (match) return child;
 | 
			
		||||
        }
 | 
			
		||||
        // Recursively search in child objects
 | 
			
		||||
        for (i = 0; i < parent.children.length; ++i) {
 | 
			
		||||
            var found = findChild(parent.children[i], searchValue, searchBy);
 | 
			
		||||
            if (found) return found;
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -41,6 +41,20 @@ QtObject {
 | 
			
		||||
    // Used to store if a OngoingCallPage component is fullscreened.
 | 
			
		||||
    property bool isCallFullscreen: false
 | 
			
		||||
 | 
			
		||||
    // QWK: Provide spacing for widgets that may be occluded by the system buttons.
 | 
			
		||||
    property QtObject qwkSystemButtonSpacing: QtObject {
 | 
			
		||||
        id: qwkSystemButtonSpacing
 | 
			
		||||
        readonly property bool isMacOS: Qt.platform.os.toString() === "osx"
 | 
			
		||||
        // macOS buttons are on the left.
 | 
			
		||||
        readonly property real left: {
 | 
			
		||||
            appWindow.useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0
 | 
			
		||||
        }
 | 
			
		||||
        // Windows and Linux buttons are on the right.
 | 
			
		||||
        readonly property real right: {
 | 
			
		||||
            appWindow.useFrameless && !isMacOS && !root.isFullscreen ? sysBtnsLoader.width + 24 : 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Restore a visible windowed mode.
 | 
			
		||||
    function restoreApp() {
 | 
			
		||||
        if (isHidden) {
 | 
			
		||||
 | 
			
		||||
@ -41,14 +41,11 @@ import QWindowKit
 | 
			
		||||
ApplicationWindow {
 | 
			
		||||
    id: appWindow
 | 
			
		||||
 | 
			
		||||
    readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow)
 | 
			
		||||
    property bool isRTL: UtilsAdapter.isRTL
 | 
			
		||||
 | 
			
		||||
    LayoutMirroring.enabled: isRTL
 | 
			
		||||
    LayoutMirroring.childrenInherit: isRTL
 | 
			
		||||
 | 
			
		||||
    // This needs to be set from the start.
 | 
			
		||||
    readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow)
 | 
			
		||||
 | 
			
		||||
    onActiveFocusItemChanged: {
 | 
			
		||||
        focusOverlay.margin = -5;
 | 
			
		||||
        if (activeFocusItem && ((activeFocusItem.focusReason === Qt.TabFocusReason) || (activeFocusItem.focusReason === Qt.BacktabFocusReason))) {
 | 
			
		||||
@ -94,16 +91,10 @@ ApplicationWindow {
 | 
			
		||||
        id: layoutManager
 | 
			
		||||
        appContainer: fullscreenContainer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Used to manage dynamic view loading and unloading.
 | 
			
		||||
    ViewManager {
 | 
			
		||||
        id: viewManager
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    property ViewManager viewManager: ViewManager {}
 | 
			
		||||
    // Used to manage the view stack and the current view.
 | 
			
		||||
    ViewCoordinator {
 | 
			
		||||
        id: viewCoordinator
 | 
			
		||||
    }
 | 
			
		||||
    property ViewCoordinator viewCoordinator: ViewCoordinator {}
 | 
			
		||||
 | 
			
		||||
    // Used to prevent the window from being visible until the
 | 
			
		||||
    // window geometry has been restored and the view stack has
 | 
			
		||||
@ -234,17 +225,6 @@ ApplicationWindow {
 | 
			
		||||
        anchors.fill: parent
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // QWK: Provide spacing for widgets that may be occluded by the system buttons.
 | 
			
		||||
    QtObject {
 | 
			
		||||
        id: qwkSystemButtonSpacing
 | 
			
		||||
        readonly property bool isMacOS: Qt.platform.os.toString() === "osx"
 | 
			
		||||
        readonly property bool isFullscreen: layoutManager.isFullScreen
 | 
			
		||||
        // macOS buttons are on the left.
 | 
			
		||||
        readonly property real left: useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0
 | 
			
		||||
        // Windows and Linux buttons are on the right.
 | 
			
		||||
        readonly property real right: useFrameless && !isMacOS && !isFullscreen ? sysBtnsLoader.width + 24 : 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // QWK: Window Title bar
 | 
			
		||||
    Item {
 | 
			
		||||
        id: titleBar
 | 
			
		||||
@ -289,6 +269,12 @@ ApplicationWindow {
 | 
			
		||||
            raise();
 | 
			
		||||
            layoutManager.restoreApp();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function onCurrentAccountRemoved() {
 | 
			
		||||
            if (UtilsAdapter.getAccountListSize() === 0) {
 | 
			
		||||
                viewCoordinator.present("WizardView");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Connections {
 | 
			
		||||
 | 
			
		||||
@ -227,7 +227,7 @@ AccountAdapter::createJAMSAccount(const QVariantMap& settings)
 | 
			
		||||
        &lrcInstance_->accountModel(),
 | 
			
		||||
        &lrc::api::AccountModel::accountAdded,
 | 
			
		||||
        [this](const QString& accountId) {
 | 
			
		||||
            if (!lrcInstance_->accountModel().getAccountList().size())
 | 
			
		||||
            if (!lrcInstance_->accountModel().getAccountCount())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            Utils::oneShotConnect(&lrcInstance_->accountModel(),
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ int
 | 
			
		||||
AccountListModel::rowCount(const QModelIndex& parent) const
 | 
			
		||||
{
 | 
			
		||||
    if (!parent.isValid() && lrcInstance_) {
 | 
			
		||||
        return lrcInstance_->accountModel().getAccountList().size();
 | 
			
		||||
        return lrcInstance_->accountModel().getAccountCount();
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
@ -71,7 +71,7 @@ AccountListModel::data(const QModelIndex& index, int role) const
 | 
			
		||||
void
 | 
			
		||||
AccountListModel::updateNotifications()
 | 
			
		||||
{
 | 
			
		||||
    for (int i = 0; i < lrcInstance_->accountModel().getAccountList().size(); ++i) {
 | 
			
		||||
    for (int i = 0; i < lrcInstance_->accountModel().getAccountCount(); ++i) {
 | 
			
		||||
        QModelIndex modelIndex = QAbstractListModel::index(i, 0);
 | 
			
		||||
        Q_EMIT dataChanged(modelIndex, modelIndex, {Role::NotificationCount});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,6 @@ extern const QString defaultDownloadPath;
 | 
			
		||||
    X(DownloadPath, defaultDownloadPath) \
 | 
			
		||||
    X(ScreenshotPath, {}) \
 | 
			
		||||
    X(EnableNotifications, true) \
 | 
			
		||||
    X(EnableTypingIndicator, true) \
 | 
			
		||||
    X(EnableReadReceipt, true) \
 | 
			
		||||
    X(AcceptTransferBelow, 20) \
 | 
			
		||||
    X(AutoAcceptFiles, true) \
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,11 @@
 | 
			
		||||
#include "api/devicemodel.h"
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
#include "screencastportal.h"
 | 
			
		||||
#include "xrectsel.h"
 | 
			
		||||
#ifndef ENABLE_LIBWRAP
 | 
			
		||||
#include <sys/prctl.h>
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <QtConcurrent/QtConcurrent>
 | 
			
		||||
@ -58,6 +62,12 @@ AvAdapter::AvAdapter(LRCInstance* instance, QObject* parent)
 | 
			
		||||
            &lrc::api::AVModel::onRendererFpsChange,
 | 
			
		||||
            this,
 | 
			
		||||
            &AvAdapter::updateRenderersFPSInfo);
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
    connect(&lrcInstance_->behaviorController(),
 | 
			
		||||
            &BehaviorController::callStatusChanged,
 | 
			
		||||
            this,
 | 
			
		||||
            &AvAdapter::onCallStatusChanged);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// The top left corner of primary screen is (0, 0).
 | 
			
		||||
@ -119,6 +129,93 @@ AvAdapter::shareEntireScreen(int screenNumber)
 | 
			
		||||
        ->addMedia(callId, resource, lrc::api::CallModel::MediaRequestType::SCREENSHARING);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
static std::map<QString, std::unique_ptr<ScreenCastPortal>> callPortal;
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AvAdapter::onCallStatusChanged(const QString& accountId, const QString& callId)
 | 
			
		||||
{
 | 
			
		||||
    auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
 | 
			
		||||
    auto& callModel = accInfo.callModel;
 | 
			
		||||
    const auto call = callModel->getCall(callId);
 | 
			
		||||
 | 
			
		||||
    if (call.status == lrc::api::call::Status::ENDED) {
 | 
			
		||||
        closePortal(callId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AvAdapter::closePortal(const QString& callId)
 | 
			
		||||
{
 | 
			
		||||
    if (callPortal.count(callId)) {
 | 
			
		||||
        lrcInstance_->avModel().stopPreview(callPortal[callId]->videoInputId);
 | 
			
		||||
        callPortal.erase(callId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AvAdapter::shareWayland(bool entireScreen)
 | 
			
		||||
{
 | 
			
		||||
    QString callId = lrcInstance_->getCurrentCallId();
 | 
			
		||||
    closePortal(callId);
 | 
			
		||||
 | 
			
		||||
    PortalCaptureType captureType = entireScreen ? PortalCaptureType::SCREEN
 | 
			
		||||
                                                 : PortalCaptureType::WINDOW;
 | 
			
		||||
    auto portal = std::make_unique<ScreenCastPortal>(captureType);
 | 
			
		||||
 | 
			
		||||
    int err = portal->getPipewireFd();
 | 
			
		||||
    if (err == EACCES) {
 | 
			
		||||
        qInfo() << "Can't share screen: permission denied";
 | 
			
		||||
        return;
 | 
			
		||||
    } else if (err != 0) {
 | 
			
		||||
        qWarning() << "Failed to get PipeWire fd. Error code:" << err;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    QString resource = QString("%1%2pipewire pid:%3 fd:%4 node:%5")
 | 
			
		||||
                           .arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
 | 
			
		||||
                           .arg(libjami::Media::VideoProtocolPrefix::SEPARATOR)
 | 
			
		||||
                           .arg(getpid())
 | 
			
		||||
                           .arg(portal->pipewireFd)
 | 
			
		||||
                           .arg(portal->pipewireNode);
 | 
			
		||||
#ifndef ENABLE_LIBWRAP
 | 
			
		||||
    // If the daemon is running as a separate process, then it can't directly use the
 | 
			
		||||
    // PipeWire file descriptor opened by the client, so it will attempt to duplicate
 | 
			
		||||
    // it using the pidfd_getfd system call. This requires the daemon process to have
 | 
			
		||||
    // ptrace permission on the client process. On some systems, this will be true by
 | 
			
		||||
    // default (as long as the client and daemon processes have the same uid), but it
 | 
			
		||||
    // may not be if the Yama Linux Security Module is used. The call to prctl below
 | 
			
		||||
    // will grant permission if the Yama LSM is enabled and set to mode 1.
 | 
			
		||||
    //
 | 
			
		||||
    // References:
 | 
			
		||||
    // https://man7.org/linux/man-pages/man2/pidfd_getfd.2.html
 | 
			
		||||
    // https://man7.org/linux/man-pages/man2/prctl.2.html
 | 
			
		||||
    // https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/LSM/Yama.rst
 | 
			
		||||
    prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);
 | 
			
		||||
#endif
 | 
			
		||||
    // We open the video input here (instead of letting the daemon do it) to ensure
 | 
			
		||||
    // that the daemon doesn't try to restart it while we still need it, since this
 | 
			
		||||
    // would require getting a new file descriptor for PipeWire.
 | 
			
		||||
    portal->videoInputId = lrcInstance_->avModel().startPreview(resource);
 | 
			
		||||
 | 
			
		||||
    callPortal[callId] = std::move(portal);
 | 
			
		||||
    muteCamera_ = !isCapturing();
 | 
			
		||||
    lrcInstance_->getCurrentCallModel()
 | 
			
		||||
        ->addMedia(callId, resource, lrc::api::CallModel::MediaRequestType::SCREENSHARING);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AvAdapter::shareEntireScreenWayland()
 | 
			
		||||
{
 | 
			
		||||
    shareWayland(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AvAdapter::shareWindowWayland()
 | 
			
		||||
{
 | 
			
		||||
    shareWayland(false);
 | 
			
		||||
}
 | 
			
		||||
#endif // Q_OS_LINUX
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AvAdapter::shareAllScreens()
 | 
			
		||||
{
 | 
			
		||||
@ -204,10 +301,14 @@ AvAdapter::shareFile(const QString& filePath)
 | 
			
		||||
                              &lrc::api::AVModel::fileOpened,
 | 
			
		||||
                              this,
 | 
			
		||||
                              [this, callId, filePath, resource](bool hasAudio, bool hasVideo) {
 | 
			
		||||
                                    lrcInstance_->avModel().setAutoRestart(resource, true);
 | 
			
		||||
                                    lrcInstance_->getCurrentCallModel()
 | 
			
		||||
                                        ->addMedia(callId, filePath, lrc::api::CallModel::MediaRequestType::FILESHARING, false, hasAudio);
 | 
			
		||||
                                    lrcInstance_->avModel().pausePlayer(resource, false);
 | 
			
		||||
                                  lrcInstance_->avModel().setAutoRestart(resource, true);
 | 
			
		||||
                                  lrcInstance_->getCurrentCallModel()
 | 
			
		||||
                                      ->addMedia(callId,
 | 
			
		||||
                                                 filePath,
 | 
			
		||||
                                                 lrc::api::CallModel::MediaRequestType::FILESHARING,
 | 
			
		||||
                                                 false,
 | 
			
		||||
                                                 hasAudio);
 | 
			
		||||
                                  lrcInstance_->avModel().pausePlayer(resource, false);
 | 
			
		||||
                              });
 | 
			
		||||
 | 
			
		||||
        lrcInstance_->avModel().createMediaPlayer(resource);
 | 
			
		||||
@ -307,6 +408,9 @@ void
 | 
			
		||||
AvAdapter::stopSharing(const QString& source)
 | 
			
		||||
{
 | 
			
		||||
    auto callId = lrcInstance_->getCurrentCallId();
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
    closePortal(callId);
 | 
			
		||||
#endif
 | 
			
		||||
    if (!source.isEmpty() && !callId.isEmpty()) {
 | 
			
		||||
        if (source.startsWith(libjami::Media::VideoProtocolPrefix::DISPLAY)) {
 | 
			
		||||
            qDebug() << "Stopping display: " << source;
 | 
			
		||||
 | 
			
		||||
@ -69,9 +69,18 @@ protected:
 | 
			
		||||
     */
 | 
			
		||||
    Q_INVOKABLE bool hasCamera() const;
 | 
			
		||||
 | 
			
		||||
    // Share the screen specificed by screen number.
 | 
			
		||||
    // Share the screen specificed by screen number (all platforms except Wayland).
 | 
			
		||||
    Q_INVOKABLE void shareEntireScreen(int screenNumber);
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
    // Share a screen on Wayland.
 | 
			
		||||
    // Sharing a screen on Wayland requires getting permission from the user. The logic for
 | 
			
		||||
    // this is handled by the ScreenCastPortal class using xdg-desktop-portal.
 | 
			
		||||
    // The choice of screen is also handled by xdg-desktop-portal, which is why we don't need
 | 
			
		||||
    // an argument for it (whereas we do on other platforms, cf. shareEntireScreen above).
 | 
			
		||||
    Q_INVOKABLE void shareEntireScreenWayland();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    // Share the all screens connected.
 | 
			
		||||
    Q_INVOKABLE void shareAllScreens();
 | 
			
		||||
 | 
			
		||||
@ -87,9 +96,18 @@ protected:
 | 
			
		||||
    // Select screen area to display (from all screens).
 | 
			
		||||
    Q_INVOKABLE void shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned height);
 | 
			
		||||
 | 
			
		||||
    // Select window to display.
 | 
			
		||||
    // Select window to display (all platforms except Wayland).
 | 
			
		||||
    Q_INVOKABLE void shareWindow(const QString& windowProcessId, const QString& windowId);
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
    // Share a window on Wayland.
 | 
			
		||||
    // Sharing a window on Wayland requires getting permission from the user. The logic for
 | 
			
		||||
    // this is handled by the ScreenCastPortal class using xdg-desktop-portal.
 | 
			
		||||
    // The choice of window is also handled by xdg-desktop-portal, which is why we don't need
 | 
			
		||||
    // arguments for it (whereas we do on other platforms, cf. shareWindow above).
 | 
			
		||||
    Q_INVOKABLE void shareWindowWayland();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    // Returns the screensharing resource
 | 
			
		||||
    Q_INVOKABLE QString getSharingResource(int screenId = -2,
 | 
			
		||||
                                           const QString& windowProcessId = "",
 | 
			
		||||
@ -121,11 +139,25 @@ private Q_SLOTS:
 | 
			
		||||
    void onAudioDeviceEvent();
 | 
			
		||||
    void onRendererStarted(const QString& id, const QSize& size);
 | 
			
		||||
    void onRendererStopped(const QString& id);
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
    // This function needs to be called whenever a screen/window share stops on Wayland.
 | 
			
		||||
    // Failure to do so can cause subsequent sharing attempts to fail.
 | 
			
		||||
    void closePortal(const QString& callId);
 | 
			
		||||
 | 
			
		||||
    // On Wayland, we need to be informed of call status changes so that we can call
 | 
			
		||||
    // closePortal if a call ends while a screen/window share was in progress.
 | 
			
		||||
    void onCallStatusChanged(const QString& accountId, const QString& callId);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    // Get screens arrangement rect relative to primary screen.
 | 
			
		||||
    const QRect getAllScreensBoundingRect();
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
    // Used internally by shareEntireScreenWayland and shareWindowWayland
 | 
			
		||||
    void shareWayland(bool entireScreen);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    // Get the screen number
 | 
			
		||||
    int getScreenNumber(int screenId = 0) const;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,52 +25,74 @@
 | 
			
		||||
 | 
			
		||||
#include <QImage>
 | 
			
		||||
 | 
			
		||||
class AvatarImageProvider : public QuickImageProviderBase
 | 
			
		||||
class AsyncAvatarImageResponseRunnable : public AsyncImageResponseRunnable
 | 
			
		||||
{
 | 
			
		||||
    Q_OBJECT
 | 
			
		||||
public:
 | 
			
		||||
    AvatarImageProvider(LRCInstance* instance = nullptr)
 | 
			
		||||
        : QuickImageProviderBase(QQuickImageProvider::Image,
 | 
			
		||||
                                 QQmlImageProviderBase::ForceAsynchronousImageLoading,
 | 
			
		||||
                                 instance)
 | 
			
		||||
    AsyncAvatarImageResponseRunnable(const QString& id,
 | 
			
		||||
                                     const QSize& requestedSize,
 | 
			
		||||
                                     LRCInstance* lrcInstance)
 | 
			
		||||
        : AsyncImageResponseRunnable(id, requestedSize, lrcInstance)
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
    QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override
 | 
			
		||||
    void run() override
 | 
			
		||||
    {
 | 
			
		||||
        Q_UNUSED(size)
 | 
			
		||||
 | 
			
		||||
        if (requestedSize == QSize(0, 0)) {
 | 
			
		||||
            qWarning() << Q_FUNC_INFO << "Image request has no dimensions";
 | 
			
		||||
            return {};
 | 
			
		||||
        // For avatar images, the requested size should be a square. Anything else
 | 
			
		||||
        // is a request made prior to an aspect ratio guard calculation.
 | 
			
		||||
        if (requestedSize_ == QSize(0, 0) || requestedSize_.width() != requestedSize_.height()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // the first string is the item uri and the second is a uid
 | 
			
		||||
        // that is used for trigger a reload of the underlying image
 | 
			
		||||
        // data and can be discarded at this point
 | 
			
		||||
        auto idInfo = id.split("_");
 | 
			
		||||
        auto idInfo = id_.split("_");
 | 
			
		||||
 | 
			
		||||
        if (idInfo.size() < 2) {
 | 
			
		||||
            qWarning() << Q_FUNC_INFO << "Missing element(s) in the image url";
 | 
			
		||||
            return {};
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const auto& imageId = idInfo.at(1);
 | 
			
		||||
        if (!imageId.size()) {
 | 
			
		||||
            qWarning() << Q_FUNC_INFO << "Missing id in the image url";
 | 
			
		||||
            return {};
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        QImage image;
 | 
			
		||||
        const auto& type = idInfo.at(0);
 | 
			
		||||
 | 
			
		||||
        if (type == "conversation") {
 | 
			
		||||
            if (imageId == "temp")
 | 
			
		||||
                return Utils::tempConversationAvatar(requestedSize);
 | 
			
		||||
            return Utils::conversationAvatar(lrcInstance_, imageId, requestedSize);
 | 
			
		||||
                image = Utils::tempConversationAvatar(requestedSize_);
 | 
			
		||||
            else
 | 
			
		||||
                image = Utils::conversationAvatar(lrcInstance_, imageId, requestedSize_);
 | 
			
		||||
        } else if (type == "account") {
 | 
			
		||||
            image = Utils::accountPhoto(lrcInstance_, imageId, requestedSize_);
 | 
			
		||||
        } else if (type == "contact") {
 | 
			
		||||
            image = Utils::contactPhoto(lrcInstance_, imageId, requestedSize_);
 | 
			
		||||
        } else {
 | 
			
		||||
            qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url";
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (type == "account")
 | 
			
		||||
            return Utils::accountPhoto(lrcInstance_, imageId, requestedSize);
 | 
			
		||||
        if (type == "contact")
 | 
			
		||||
            return Utils::contactPhoto(lrcInstance_, imageId, requestedSize);
 | 
			
		||||
 | 
			
		||||
        qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url";
 | 
			
		||||
        return {};
 | 
			
		||||
        Q_EMIT done(image);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class AvatarImageProvider : public AsyncImageProviderBase
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
    AvatarImageProvider(LRCInstance* instance = nullptr)
 | 
			
		||||
        : AsyncImageProviderBase(instance)
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
    QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override
 | 
			
		||||
    {
 | 
			
		||||
        auto response = new AsyncImageResponse<AsyncAvatarImageResponseRunnable>(id,
 | 
			
		||||
                                                                                 requestedSize,
 | 
			
		||||
                                                                                 &pool_,
 | 
			
		||||
                                                                                 lrcInstance_);
 | 
			
		||||
        return response;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -33,8 +33,7 @@ AvatarRegistry::AvatarRegistry(LRCInstance* instance, QObject* parent)
 | 
			
		||||
    connect(&lrcInstance_->accountModel(),
 | 
			
		||||
            &AccountModel::profileUpdated,
 | 
			
		||||
            this,
 | 
			
		||||
            &AvatarRegistry::addOrUpdateImage,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
            &AvatarRegistry::addOrUpdateImage);
 | 
			
		||||
 | 
			
		||||
    connect(lrcInstance_, &LRCInstance::base64SwarmAvatarChanged, this, [&] {
 | 
			
		||||
        addOrUpdateImage("temp");
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ public:
 | 
			
		||||
    Q_INVOKABLE QString getUid(const QString& id);
 | 
			
		||||
 | 
			
		||||
    // add or update a specific image in the cache
 | 
			
		||||
    QString addOrUpdateImage(const QString& id);
 | 
			
		||||
    Q_SLOT QString addOrUpdateImage(const QString& id);
 | 
			
		||||
 | 
			
		||||
Q_SIGNALS:
 | 
			
		||||
    void avatarUidChanged(const QString& id);
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,9 @@
 | 
			
		||||
#include "bannedlistmodel.h"
 | 
			
		||||
 | 
			
		||||
#include "lrcinstance.h"
 | 
			
		||||
#include "global.h"
 | 
			
		||||
 | 
			
		||||
#include <api/contact.h>
 | 
			
		||||
 | 
			
		||||
BannedListModel::BannedListModel(QObject* parent)
 | 
			
		||||
    : AbstractListModelBase(parent)
 | 
			
		||||
@ -106,7 +109,13 @@ void
 | 
			
		||||
BannedListModel::reset()
 | 
			
		||||
{
 | 
			
		||||
    beginResetModel();
 | 
			
		||||
    bannedlist_ = lrcInstance_->getCurrentAccountInfo().contactModel->getBannedContacts();
 | 
			
		||||
    auto contactModel = lrcInstance_->getCurrentContactModel();
 | 
			
		||||
    if (!contactModel) {
 | 
			
		||||
        C_DBG << "Contact model is not available.";
 | 
			
		||||
        bannedlist_.clear();
 | 
			
		||||
    } else {
 | 
			
		||||
        bannedlist_ = contactModel->getBannedContacts();
 | 
			
		||||
    }
 | 
			
		||||
    endResetModel();
 | 
			
		||||
    set_count(rowCount());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,8 @@
 | 
			
		||||
 | 
			
		||||
#include "calloverlaymodel.h"
 | 
			
		||||
 | 
			
		||||
#include "global.h"
 | 
			
		||||
 | 
			
		||||
#include <QEvent>
 | 
			
		||||
#include <QMouseEvent>
 | 
			
		||||
#include <QQuickWindow>
 | 
			
		||||
@ -360,23 +362,37 @@ CallOverlayModel::clearControls()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
CallOverlayModel::registerFilter(QQuickWindow* object, QQuickItem* item)
 | 
			
		||||
CallOverlayModel::registerFilter(QObject* object, QQuickItem* item)
 | 
			
		||||
{
 | 
			
		||||
    if (!object || !item || watchedItems_.contains(item))
 | 
			
		||||
    QQuickWindow* window = qobject_cast<QQuickWindow*>(object);
 | 
			
		||||
    if (!window || !item) {
 | 
			
		||||
        C_WARN << "Attempting to register an invalid object or item" << object << item;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (watchedItems_.contains(item)) {
 | 
			
		||||
        C_DBG << "Item already registered" << item;
 | 
			
		||||
    }
 | 
			
		||||
    watchedItems_.push_back(item);
 | 
			
		||||
    if (watchedItems_.size() == 1)
 | 
			
		||||
        object->installEventFilter(this);
 | 
			
		||||
    if (watchedItems_.size() == 1) {
 | 
			
		||||
        window->installEventFilter(this);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
CallOverlayModel::unregisterFilter(QQuickWindow* object, QQuickItem* item)
 | 
			
		||||
CallOverlayModel::unregisterFilter(QObject* object, QQuickItem* item)
 | 
			
		||||
{
 | 
			
		||||
    if (!object || !item || !watchedItems_.contains(item))
 | 
			
		||||
    QQuickWindow* window = qobject_cast<QQuickWindow*>(object);
 | 
			
		||||
    if (!window || !item) {
 | 
			
		||||
        C_WARN << "Attempting to unregister an invalid object or item" << object << item;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!watchedItems_.contains(item)) {
 | 
			
		||||
        C_DBG << "Item not registered" << item;
 | 
			
		||||
    }
 | 
			
		||||
    watchedItems_.removeOne(item);
 | 
			
		||||
    if (watchedItems_.size() == 0)
 | 
			
		||||
        object->removeEventFilter(this);
 | 
			
		||||
    if (watchedItems_.size() == 0) {
 | 
			
		||||
        window->removeEventFilter(this);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool
 | 
			
		||||
 | 
			
		||||
@ -137,8 +137,8 @@ public:
 | 
			
		||||
    Q_INVOKABLE QVariant overflowHiddenModel();
 | 
			
		||||
    Q_INVOKABLE QVariant pendingConferenceesModel();
 | 
			
		||||
 | 
			
		||||
    Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item);
 | 
			
		||||
    Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item);
 | 
			
		||||
    Q_INVOKABLE void registerFilter(QObject* object, QQuickItem* item);
 | 
			
		||||
    Q_INVOKABLE void unregisterFilter(QObject* object, QQuickItem* item);
 | 
			
		||||
    bool eventFilter(QObject* object, QEvent* event) override;
 | 
			
		||||
 | 
			
		||||
Q_SIGNALS:
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Qt5Compat.GraphicalEffects
 | 
			
		||||
import net.jami.Models 1.1
 | 
			
		||||
import net.jami.Adapters 1.1
 | 
			
		||||
import net.jami.Constants 1.1
 | 
			
		||||
@ -25,8 +26,10 @@ import net.jami.Constants 1.1
 | 
			
		||||
SBSMessageBase {
 | 
			
		||||
    id: root
 | 
			
		||||
 | 
			
		||||
    property var confId: ConfId
 | 
			
		||||
    property var currentCallId: CurrentCall.id
 | 
			
		||||
    component JoinCallButton: MaterialButton {
 | 
			
		||||
        visible: root.isActive
 | 
			
		||||
        visible: root.isActive && root.currentCallId !== root.confId
 | 
			
		||||
        toolTipText: JamiStrings.joinCall
 | 
			
		||||
        color: JamiTheme.blackColor
 | 
			
		||||
        background.opacity: hovered ? 0.2 : 0.1
 | 
			
		||||
@ -47,13 +50,15 @@ SBSMessageBase {
 | 
			
		||||
 | 
			
		||||
    bubble.border.color: CurrentConversation.color
 | 
			
		||||
    bubble.border.width: root.isActive ? 1.5 : 0
 | 
			
		||||
    bubble.color: JamiTheme.messageInBgColor
 | 
			
		||||
    bubble.opacity: 0.6
 | 
			
		||||
 | 
			
		||||
    Connections {
 | 
			
		||||
        target: CurrentConversation
 | 
			
		||||
        enabled: root.isActive
 | 
			
		||||
 | 
			
		||||
        function onActiveCallsChanged() {
 | 
			
		||||
            root.isActive = LRCInstance.indexOfActiveCall(ConfId, ActionUri, DeviceId) !== -1;
 | 
			
		||||
            root.isActive = LRCInstance.indexOfActiveCall(root.confId, ActionUri, DeviceId) !== -1;
 | 
			
		||||
            if (root.isActive) {
 | 
			
		||||
                bubble.mask.border.color = CurrentConversation.color;
 | 
			
		||||
                bubble.mask.border.width = 1.5;
 | 
			
		||||
@ -62,10 +67,10 @@ SBSMessageBase {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    property bool isActive: LRCInstance.indexOfActiveCall(ConfId, ActionUri, DeviceId) !== -1
 | 
			
		||||
    visible: isActive || ConfId === "" || Duration > 0
 | 
			
		||||
    property bool isActive: LRCInstance.indexOfActiveCall(root.confId, ActionUri, DeviceId) !== -1
 | 
			
		||||
    visible: isActive || root.confId === "" || Duration > 0
 | 
			
		||||
 | 
			
		||||
    property var baseColor: isOutgoing? CurrentConversation.color : JamiTheme.messageInBgColor
 | 
			
		||||
    property var baseColor: JamiTheme.messageInBgColor
 | 
			
		||||
 | 
			
		||||
    innerContent.children: [
 | 
			
		||||
        RowLayout {
 | 
			
		||||
@ -74,22 +79,60 @@ SBSMessageBase {
 | 
			
		||||
            spacing: 10
 | 
			
		||||
            visible: root.visible
 | 
			
		||||
 | 
			
		||||
            Label {
 | 
			
		||||
                id: callLabel
 | 
			
		||||
            Image {
 | 
			
		||||
                id: statusIcon
 | 
			
		||||
                Layout.leftMargin: 8
 | 
			
		||||
                width: 10
 | 
			
		||||
                height: 10
 | 
			
		||||
                verticalAlignment: Qt.AlignVCenter
 | 
			
		||||
                visible: !root.isActive
 | 
			
		||||
 | 
			
		||||
                source: {
 | 
			
		||||
                    if (root.isOutgoing) {
 | 
			
		||||
                        if (Duration > 0)
 | 
			
		||||
                            return "qrc:/icons/outgoing-call.svg";
 | 
			
		||||
                        else
 | 
			
		||||
                            return "qrc:/icons/missed-outgoing-call.svg";
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if (Duration > 0)
 | 
			
		||||
                            return "qrc:/icons/incoming-call.svg";
 | 
			
		||||
                        else
 | 
			
		||||
                            return "qrc:/icons/missed-incoming-call.svg";
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                layer {
 | 
			
		||||
                    enabled: true
 | 
			
		||||
                    effect: ColorOverlay {
 | 
			
		||||
                        color: {
 | 
			
		||||
                            if (Duration > 0)
 | 
			
		||||
                                return UtilsAdapter.luma(root.baseColor) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
 | 
			
		||||
                            return JamiTheme.redColor
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            TextEdit {
 | 
			
		||||
                id: callLabel
 | 
			
		||||
                objectName: "callLabel"
 | 
			
		||||
 | 
			
		||||
                topPadding: 8
 | 
			
		||||
                bottomPadding: 8
 | 
			
		||||
 | 
			
		||||
                Layout.margins: 8
 | 
			
		||||
                Layout.fillWidth: true
 | 
			
		||||
                Layout.rightMargin: root.isActive ? 0 : root.timeWidth + 16
 | 
			
		||||
                Layout.leftMargin: root.isActive ? 10 : 8
 | 
			
		||||
                Layout.rightMargin: root.isActive && root.currentCallId !== root.confId ? 0 : root.timeWidth + 16
 | 
			
		||||
                Layout.leftMargin: root.isActive ? 10 : -5 /* spacing is 10 and we want 5px with icon */
 | 
			
		||||
 | 
			
		||||
                text: {
 | 
			
		||||
                    if (root.isActive)
 | 
			
		||||
                        return JamiStrings.startedACall;
 | 
			
		||||
                    return Body;
 | 
			
		||||
                }
 | 
			
		||||
                verticalAlignment: Qt.AlignVCenter
 | 
			
		||||
                horizontalAlignment: Qt.AlignHCenter
 | 
			
		||||
 | 
			
		||||
                font.pointSize: JamiTheme.mediumFontSize
 | 
			
		||||
                font.pointSize: JamiTheme.smallFontSize
 | 
			
		||||
                font.hintingPreference: Font.PreferNoHinting
 | 
			
		||||
                renderType: Text.NativeRendering
 | 
			
		||||
                textFormat: Text.MarkdownText
 | 
			
		||||
@ -99,20 +142,22 @@ SBSMessageBase {
 | 
			
		||||
 | 
			
		||||
            JoinCallButton {
 | 
			
		||||
                id: joinCallInAudio
 | 
			
		||||
                objectName: "joinCallInAudio"
 | 
			
		||||
                Layout.topMargin: 4
 | 
			
		||||
                Layout.bottomMargin: 4
 | 
			
		||||
 | 
			
		||||
                text: JamiStrings.joinInAudio
 | 
			
		||||
                onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, ConfId, true)
 | 
			
		||||
                onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, root.confId, true)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            JoinCallButton {
 | 
			
		||||
                id: joinCallInVideo
 | 
			
		||||
                objectName: "joinCallInVideo"
 | 
			
		||||
                text: JamiStrings.joinInVideo
 | 
			
		||||
                Layout.topMargin: 4
 | 
			
		||||
                Layout.bottomMargin: 4
 | 
			
		||||
 | 
			
		||||
                onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, ConfId)
 | 
			
		||||
                onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, root.confId)
 | 
			
		||||
                Layout.rightMargin: 4
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -125,7 +170,7 @@ SBSMessageBase {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    Component.onCompleted: {
 | 
			
		||||
        bubble.timestampItem.visible = !root.isActive;
 | 
			
		||||
        bubble.timestampItem.visible = !root.isActive || root.currentCallId === root.confId;
 | 
			
		||||
        opacity = 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -40,19 +40,20 @@ Loader {
 | 
			
		||||
    property int seq: MsgSeq.single
 | 
			
		||||
    property string author: Author
 | 
			
		||||
    property string body: Body
 | 
			
		||||
    property var transferStatus: Status
 | 
			
		||||
    property int transferStatus: Status
 | 
			
		||||
    onTransferStatusChanged: {
 | 
			
		||||
        if (transferStatus === Interaction.Status.TRANSFER_FINISHED) {
 | 
			
		||||
            mediaInfo = MessagesAdapter.getMediaInfo(root.body);
 | 
			
		||||
            if (Object.keys(mediaInfo).length !== 0 && WITH_WEBENGINE) {
 | 
			
		||||
                sourceComponent = localMediaMsgComp;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        sourceComponent = dataTransferMsgComp;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    width: ListView.view ? ListView.view.width : 0
 | 
			
		||||
 | 
			
		||||
    sourceComponent: {
 | 
			
		||||
        if (root.transferStatus === Interaction.Status.TRANSFER_FINISHED) {
 | 
			
		||||
            mediaInfo = MessagesAdapter.getMediaInfo(root.body)
 | 
			
		||||
            if (Object.keys(mediaInfo).length !== 0 && WITH_WEBENGINE)
 | 
			
		||||
                return localMediaMsgComp
 | 
			
		||||
        }
 | 
			
		||||
        return dataTransferMsgComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    opacity: 0
 | 
			
		||||
    Behavior on opacity { NumberAnimation { duration: 100 } }
 | 
			
		||||
    onLoaded: opacity = 1
 | 
			
		||||
@ -294,8 +295,6 @@ Loader {
 | 
			
		||||
                        return avComp
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    Component {
 | 
			
		||||
                        id: avComp
 | 
			
		||||
 | 
			
		||||
@ -304,7 +303,7 @@ Loader {
 | 
			
		||||
                                var qml = WITH_WEBENGINE ?
 | 
			
		||||
                                            "qrc:/webengine/MediaPreviewBase.qml" :
 | 
			
		||||
                                            "qrc:/nowebengine/MediaPreviewBase.qml"
 | 
			
		||||
                                setSource( qml, { isVideo: mediaInfo.isVideo, html:mediaInfo.html } )
 | 
			
		||||
                                setSource( qml, { isVideo: mediaInfo.isVideo, html: mediaInfo.html } )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@ -383,9 +382,11 @@ Loader {
 | 
			
		||||
                            antialiasing: true
 | 
			
		||||
                            autoTransform: true
 | 
			
		||||
                            asynchronous: true
 | 
			
		||||
                            source: Body !== undefined ? UtilsAdapter.urlFromLocalPath(Body) : ''
 | 
			
		||||
 | 
			
		||||
                            Component.onCompleted: localMediaMsgItem.bubble.imgSource = source
 | 
			
		||||
                            Component.onCompleted: {
 | 
			
		||||
                                source = UtilsAdapter.urlFromLocalPath(Body);
 | 
			
		||||
                                localMediaMsgItem.bubble.imgSource = source;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // The sourceSize represents the maximum source dimensions.
 | 
			
		||||
                            // This should not be a dynamic binding, as property changes
 | 
			
		||||
@ -401,7 +402,6 @@ Loader {
 | 
			
		||||
                                if (img.status == Image.Ready && aspectRatio) {
 | 
			
		||||
                                    height = Qt.binding(() => JamiQmlUtils.clamp(idealWidth / aspectRatio, 64, 256))
 | 
			
		||||
                                    width = Qt.binding(() => height * aspectRatio)
 | 
			
		||||
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -23,19 +23,26 @@ import net.jami.Adapters 1.1
 | 
			
		||||
VideoView {
 | 
			
		||||
    id: root
 | 
			
		||||
 | 
			
		||||
    property bool visibilityCondition: true
 | 
			
		||||
 | 
			
		||||
    crop: true
 | 
			
		||||
    visible: isRendering && visibilityCondition
 | 
			
		||||
 | 
			
		||||
    function startWithId(id, force = false) {
 | 
			
		||||
        if (id !== undefined && id.length === 0) {
 | 
			
		||||
            VideoDevices.stopDevice(rendererId);
 | 
			
		||||
            rendererId = id;
 | 
			
		||||
        } else {
 | 
			
		||||
            const forceRestart = rendererId === id;
 | 
			
		||||
            if (!forceRestart) {
 | 
			
		||||
                // Stop previous device
 | 
			
		||||
                VideoDevices.stopDevice(rendererId);
 | 
			
		||||
            }
 | 
			
		||||
            rendererId = VideoDevices.startDevice(id, forceRestart);
 | 
			
		||||
            stop();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const forceRestart = rendererId === id || force;
 | 
			
		||||
        if (!forceRestart) {
 | 
			
		||||
            // Stop previous device
 | 
			
		||||
            VideoDevices.stopDevice(rendererId);
 | 
			
		||||
        }
 | 
			
		||||
        rendererId = VideoDevices.startDevice(id, forceRestart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function stop() {
 | 
			
		||||
        VideoDevices.stopDevice(rendererId);
 | 
			
		||||
        rendererId = "";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,17 @@ Rectangle {
 | 
			
		||||
    property int status: Account.Status.REGISTERED
 | 
			
		||||
    property int size: 15
 | 
			
		||||
 | 
			
		||||
    MaterialToolTip {
 | 
			
		||||
        visible: text !== "" && hoverHandler.hovered
 | 
			
		||||
        delay: Qt.styleHints.mousePressAndHoldInterval
 | 
			
		||||
        text: status === 2 ? qsTr("Connected") : status === 1 ? qsTr("Available") : ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    HoverHandler {
 | 
			
		||||
        id: hoverHandler
 | 
			
		||||
        target: parent
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    width: size
 | 
			
		||||
    height: size
 | 
			
		||||
    radius: size * 0.5
 | 
			
		||||
@ -41,6 +52,10 @@ Rectangle {
 | 
			
		||||
            return JamiTheme.presenceGreen;
 | 
			
		||||
        else if (status === Account.Status.TRYING)
 | 
			
		||||
            return JamiTheme.unPresenceOrange;
 | 
			
		||||
        else if (status === 2)
 | 
			
		||||
            return JamiTheme.presenceGreen;
 | 
			
		||||
        else if (status === 1)
 | 
			
		||||
            return JamiTheme.unPresenceOrange;
 | 
			
		||||
        return JamiTheme.notificationRed;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -98,10 +98,18 @@ Control {
 | 
			
		||||
            Layout.fillHeight: true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Label {
 | 
			
		||||
            id: username
 | 
			
		||||
            text: UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author)
 | 
			
		||||
 | 
			
		||||
            wrapMode: Text.NoWrap
 | 
			
		||||
            text: textMetricsUsername.elidedText
 | 
			
		||||
            TextMetrics {
 | 
			
		||||
                id: textMetricsUsername
 | 
			
		||||
 | 
			
		||||
                text: UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author)
 | 
			
		||||
                elideWidth: 200
 | 
			
		||||
                elide: Qt.ElideMiddle
 | 
			
		||||
            }
 | 
			
		||||
            visible: (seq === MsgSeq.first || seq === MsgSeq.single) && !isOutgoing && !isReply
 | 
			
		||||
 | 
			
		||||
            font.pointSize: JamiTheme.smallFontSize
 | 
			
		||||
@ -141,7 +149,15 @@ Control {
 | 
			
		||||
                    Label {
 | 
			
		||||
                        id: replyTo
 | 
			
		||||
 | 
			
		||||
                        text: isOutgoing ? JamiStrings.inReplyTo : UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) + JamiStrings.repliedTo
 | 
			
		||||
                        wrapMode: Text.NoWrap
 | 
			
		||||
                        text: textMetricsUsername1.elidedText
 | 
			
		||||
                        TextMetrics {
 | 
			
		||||
                            id: textMetricsUsername1
 | 
			
		||||
                            text: isOutgoing ? JamiStrings.inReplyTo : UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) + JamiStrings.repliedTo
 | 
			
		||||
                            elideWidth: 200
 | 
			
		||||
                            elide: Qt.ElideMiddle
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        color: JamiTheme.messageReplyColor
 | 
			
		||||
                        font.pointSize: JamiTheme.textFontSize
 | 
			
		||||
                        font.kerning: true
 | 
			
		||||
@ -166,7 +182,15 @@ Control {
 | 
			
		||||
                    Label {
 | 
			
		||||
                        id: replyToUserName
 | 
			
		||||
 | 
			
		||||
                        text: replyItem.isSelf ? JamiStrings.inReplyToMe : replyToLayout.replyUserName
 | 
			
		||||
                        wrapMode: Text.NoWrap
 | 
			
		||||
                        text: textMetricsUsername2.elidedText
 | 
			
		||||
                        TextMetrics {
 | 
			
		||||
                            id: textMetricsUsername2
 | 
			
		||||
                            text: replyItem.isSelf ? JamiStrings.inReplyToMe : replyToLayout.replyUserName
 | 
			
		||||
                            elideWidth: 200
 | 
			
		||||
                            elide: Qt.ElideMiddle
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        color: JamiTheme.messageReplyColor
 | 
			
		||||
                        font.pointSize: JamiTheme.textFontSize
 | 
			
		||||
                        font.kerning: true
 | 
			
		||||
@ -275,10 +299,7 @@ Control {
 | 
			
		||||
                        anchors.verticalCenter: parent.verticalCenter
 | 
			
		||||
                        anchors.right: isOutgoing ? optionButtonItem.right : undefined
 | 
			
		||||
                        anchors.left: !isOutgoing ? optionButtonItem.left : undefined
 | 
			
		||||
                        visible: CurrentAccount.type !== Profile.Type.SIP
 | 
			
		||||
                                 && root.type !== Interaction.Type.CALL
 | 
			
		||||
                                 && Body !== ""
 | 
			
		||||
                                 && (bubbleArea.bubbleHovered || hovered || reply.hovered || bgHandler.hovered)
 | 
			
		||||
                        visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || bgHandler.hovered)
 | 
			
		||||
                        source: JamiResources.more_vert_24dp_svg
 | 
			
		||||
                        width: optionButtonItem.width / 2
 | 
			
		||||
                        height: optionButtonItem.height
 | 
			
		||||
@ -333,10 +354,7 @@ Control {
 | 
			
		||||
                        anchors.rightMargin: 5
 | 
			
		||||
                        anchors.right: isOutgoing ? more.left : undefined
 | 
			
		||||
                        anchors.left: !isOutgoing ? more.right : undefined
 | 
			
		||||
                        visible: CurrentAccount.type !== Profile.Type.SIP
 | 
			
		||||
                                 && root.type !== Interaction.Type.CALL
 | 
			
		||||
                                 && Body !== ""
 | 
			
		||||
                                 && (bubbleArea.bubbleHovered || hovered || more.hovered || bgHandler.hovered)
 | 
			
		||||
                        visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || more.hovered || bgHandler.hovered)
 | 
			
		||||
 | 
			
		||||
                        onClicked: {
 | 
			
		||||
                            MessagesAdapter.editId = "";
 | 
			
		||||
@ -364,14 +382,14 @@ Control {
 | 
			
		||||
                    property bool bubbleHovered
 | 
			
		||||
                    property string imgSource
 | 
			
		||||
 | 
			
		||||
                    width: (root.type === Interaction.Type.TEXT ? root.textContentWidth + ( IsEmojiOnly || root.bigMsg ? 0 : root.timeWidth + root.editedWidth): innerContent.childrenRect.width)
 | 
			
		||||
                    width: (root.type === Interaction.Type.TEXT ? root.textContentWidth + (IsEmojiOnly || root.bigMsg ? 0 : root.timeWidth + root.editedWidth) : innerContent.childrenRect.width)
 | 
			
		||||
                    height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0) + (root.bigMsg ? 15 : 0)
 | 
			
		||||
 | 
			
		||||
                    HoverHandler {
 | 
			
		||||
                        target: root
 | 
			
		||||
                        enabled: root.type === Interaction.Type.DATA_TRANSFER
 | 
			
		||||
                        onHoveredChanged: {
 | 
			
		||||
                            root.hoveredLink = enabled && hovered ? bubble.imgSource : ""
 | 
			
		||||
                            root.hoveredLink = enabled && hovered ? bubble.imgSource : "";
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -381,12 +399,12 @@ Control {
 | 
			
		||||
                        showTime: IsEmojiOnly && !(root.seq === MsgSeq.last || root.seq === MsgSeq.single) ? false : true
 | 
			
		||||
                        formattedTime: root.formattedTime
 | 
			
		||||
 | 
			
		||||
                        timeColor: IsEmojiOnly || root.timeUnderBubble? (JamiTheme.darkTheme ? "white" : "dark") : (UtilsAdapter.luma(bubble.color) ? "white" : "dark")
 | 
			
		||||
                        timeColor: IsEmojiOnly || root.timeUnderBubble ? (JamiTheme.darkTheme ? "white" : "dark") : (UtilsAdapter.luma(bubble.color) ? "white" : "dark")
 | 
			
		||||
                        timeLabel.opacity: 0.5
 | 
			
		||||
 | 
			
		||||
                        anchors.bottom: parent.bottom
 | 
			
		||||
                        anchors.right: IsEmojiOnly ? (isOutgoing ? parent.right : undefined) : parent.right
 | 
			
		||||
                        anchors.left: ((IsEmojiOnly|| root.timeUnderBubble) && !isOutgoing) ? parent.left : undefined
 | 
			
		||||
                        anchors.left: ((IsEmojiOnly || root.timeUnderBubble) && !isOutgoing) ? parent.left : undefined
 | 
			
		||||
                        anchors.leftMargin: (IsEmojiOnly && !isOutgoing && emojiReactions.visible) ? bubble.timePosition : 0
 | 
			
		||||
                        anchors.rightMargin: IsEmojiOnly ? ((isOutgoing && emojiReactions.visible) ? bubble.timePosition : 0) : (root.timeUnderBubble ? 0 : 10)
 | 
			
		||||
                        timeLabel.Layout.bottomMargin: {
 | 
			
		||||
@ -396,6 +414,8 @@ Control {
 | 
			
		||||
                                return -20;
 | 
			
		||||
                            if (root.bigMsg || bubble.isDeleted)
 | 
			
		||||
                                return 5;
 | 
			
		||||
                            if (root.type === Interaction.Type.CALL)
 | 
			
		||||
                                return 8;
 | 
			
		||||
                            return 9;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@ -405,7 +425,7 @@ Control {
 | 
			
		||||
                        anchors.left: root.bigMsg ? bubble.left : timestampItem.left
 | 
			
		||||
                        anchors.bottom: parent.bottom
 | 
			
		||||
                        anchors.bottomMargin: root.bigMsg || bubble.isDeleted ? 6 : 10
 | 
			
		||||
                        anchors.leftMargin: root.bigMsg ? 10 : - timestampItem.width - 16
 | 
			
		||||
                        anchors.leftMargin: root.bigMsg ? 10 : -timestampItem.width - 16
 | 
			
		||||
                        visible: bubble.isEdited
 | 
			
		||||
                        z: 1
 | 
			
		||||
                        ResponsiveImage {
 | 
			
		||||
@ -462,7 +482,7 @@ Control {
 | 
			
		||||
                    borderColor: root.getBaseColor()
 | 
			
		||||
                    maxWidth: 2 / 3 * maxMsgWidth - JamiTheme.emojiMargins
 | 
			
		||||
 | 
			
		||||
                    state: root.isOutgoing ? "anchorsRight" : (IsEmojiOnly ? "anchorsLeft" :(emojiReactions.width > bubble.width - JamiTheme.emojiMargins ? "anchorsLeft" : "anchorsRight"))
 | 
			
		||||
                    state: root.isOutgoing ? "anchorsRight" : (IsEmojiOnly ? "anchorsLeft" : (emojiReactions.width > bubble.width - JamiTheme.emojiMargins ? "anchorsLeft" : "anchorsRight"))
 | 
			
		||||
 | 
			
		||||
                    TapHandler {
 | 
			
		||||
                        onTapped: {
 | 
			
		||||
@ -570,7 +590,7 @@ Control {
 | 
			
		||||
                    radius: width / 2
 | 
			
		||||
                    width: 12
 | 
			
		||||
                    height: 12
 | 
			
		||||
                    border.color: JamiTheme.tintedBlue
 | 
			
		||||
                    border.color: JamiTheme.sending
 | 
			
		||||
                    border.width: 1
 | 
			
		||||
                    color: JamiTheme.transparentColor
 | 
			
		||||
                    visible: isOutgoing && Status === Interaction.Status.SENDING
 | 
			
		||||
@ -578,18 +598,27 @@ Control {
 | 
			
		||||
                    anchors.bottom: parent.bottom
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                ResponsiveImage {
 | 
			
		||||
                    id: sent
 | 
			
		||||
 | 
			
		||||
                    containerHeight: 12
 | 
			
		||||
                    containerWidth: 12
 | 
			
		||||
 | 
			
		||||
                    width: 12
 | 
			
		||||
                    height: 12
 | 
			
		||||
 | 
			
		||||
                    visible: IsLastSent === true && root.readers.length === 0
 | 
			
		||||
                    anchors.bottom: parent.bottom
 | 
			
		||||
 | 
			
		||||
                    source: JamiResources.receive_svg
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                ReadStatus {
 | 
			
		||||
                    id: readsOne
 | 
			
		||||
 | 
			
		||||
                    visible: root.readers.length === 1 && CurrentAccount.sendReadReceipt
 | 
			
		||||
 | 
			
		||||
                    width: {
 | 
			
		||||
                        if (root.readers.length === 0)
 | 
			
		||||
                            return 0;
 | 
			
		||||
                        var nbAvatars = root.readers.length;
 | 
			
		||||
                        var margin = JamiTheme.avatarReadReceiptSize / 3;
 | 
			
		||||
                        return nbAvatars * JamiTheme.avatarReadReceiptSize - (nbAvatars - 1) * margin;
 | 
			
		||||
                    }
 | 
			
		||||
                    width: JamiTheme.avatarReadReceiptSize
 | 
			
		||||
                    height: JamiTheme.avatarReadReceiptSize
 | 
			
		||||
 | 
			
		||||
                    anchors.bottom: parent.bottom
 | 
			
		||||
@ -613,14 +642,23 @@ Control {
 | 
			
		||||
 | 
			
		||||
            ReadStatus {
 | 
			
		||||
                id: readsMultiple
 | 
			
		||||
                visible: root.readers.length > 1 && CurrentAccount.sendReadReceipt
 | 
			
		||||
                visible: {
 | 
			
		||||
                    if (!readers)
 | 
			
		||||
                        return false;
 | 
			
		||||
                    return readers.length > 1 && CurrentAccount.sendReadReceipt;
 | 
			
		||||
                }
 | 
			
		||||
                width: {
 | 
			
		||||
                    if (root.readers.length === 0)
 | 
			
		||||
                    if (readers.length === 0)
 | 
			
		||||
                        return 0;
 | 
			
		||||
                    var nbAvatars = root.readers.length;
 | 
			
		||||
                    var nbAvatars = readers.length;
 | 
			
		||||
                    var margin = JamiTheme.avatarReadReceiptSize / 3;
 | 
			
		||||
                    return nbAvatars * JamiTheme.avatarReadReceiptSize - (nbAvatars - 1) * margin;
 | 
			
		||||
                }
 | 
			
		||||
                height: {
 | 
			
		||||
                    if (readers.length === 0)
 | 
			
		||||
                        return 0;
 | 
			
		||||
                    return JamiTheme.avatarReadReceiptSize;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                anchors.right: parent.right
 | 
			
		||||
                anchors.top: parent.top
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,10 @@ Item {
 | 
			
		||||
    property real invAspectRatio: (videoOutput.sourceRect.height / videoOutput.sourceRect.width) || 0.5625 // 16:9 default
 | 
			
		||||
    property bool crop: false
 | 
			
		||||
    property bool flip: false
 | 
			
		||||
    property real blurRadius: 0
 | 
			
		||||
 | 
			
		||||
    // We need to know if the frames are being rendered to the screen or not.
 | 
			
		||||
    readonly property bool isRendering: videoProvider.activeRenderers[rendererId] === true
 | 
			
		||||
 | 
			
		||||
    // This rect describes the actual rendered content rectangle
 | 
			
		||||
    // as the VideoOutput component may use PreserveAspectFit
 | 
			
		||||
@ -55,7 +59,7 @@ Item {
 | 
			
		||||
 | 
			
		||||
        antialiasing: true
 | 
			
		||||
        anchors.fill: parent
 | 
			
		||||
        opacity: videoProvider.activeRenderers[rendererId] === true
 | 
			
		||||
        opacity: isRendering
 | 
			
		||||
        visible: opacity
 | 
			
		||||
 | 
			
		||||
        fillMode: crop ? VideoOutput.PreserveAspectCrop : VideoOutput.PreserveAspectFit
 | 
			
		||||
@ -70,7 +74,7 @@ Item {
 | 
			
		||||
        layer.effect: FastBlur {
 | 
			
		||||
            source: videoOutput
 | 
			
		||||
            anchors.fill: root
 | 
			
		||||
            radius: (1. - opacity) * 100
 | 
			
		||||
            radius: blurRadius ? blurRadius : (1. - opacity) * 100
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        transform: Scale {
 | 
			
		||||
 | 
			
		||||
@ -81,7 +81,7 @@ ContactAdapter::getContactSelectableModel(int type)
 | 
			
		||||
    }
 | 
			
		||||
    case SmartListModel::Type::CONFERENCE:
 | 
			
		||||
        selectableProxyModel_->setPredicate([](const QModelIndex& index, const QRegularExpression&) {
 | 
			
		||||
            return index.data(Role::Presence).toBool();
 | 
			
		||||
            return index.data(Role::Presence).toInt();
 | 
			
		||||
        });
 | 
			
		||||
        break;
 | 
			
		||||
    case SmartListModel::Type::TRANSFER:
 | 
			
		||||
@ -259,7 +259,7 @@ ContactAdapter::connectSignals()
 | 
			
		||||
            &ContactAdapter::bannedStatusChanged,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connect(lrcInstance_->getCurrentContactModel(),
 | 
			
		||||
            &ContactModel::modelUpdated,
 | 
			
		||||
            &ContactModel::contactAdded,
 | 
			
		||||
            this,
 | 
			
		||||
            &ContactAdapter::onModelUpdated,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
 | 
			
		||||
@ -112,7 +112,8 @@ ConversationListProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& s
 | 
			
		||||
    using namespace ConversationList;
 | 
			
		||||
    if (index.data(Role::Uris).toStringList().isEmpty()) {
 | 
			
		||||
        // TODO: Find out why, and fix in libjami/libjamiclient.
 | 
			
		||||
        qCritical() << "Filtering 0 member conversation. Fix me";
 | 
			
		||||
        qCritical() << "Filtering 0 member conversation. Fix me"
 | 
			
		||||
                    << index.data(Role::UID).toString();
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,10 @@
 | 
			
		||||
 | 
			
		||||
#include "conversationlistmodelbase.h"
 | 
			
		||||
 | 
			
		||||
#include "global.h"
 | 
			
		||||
 | 
			
		||||
#include <api/contact.h>
 | 
			
		||||
 | 
			
		||||
ConversationListModelBase::ConversationListModelBase(LRCInstance* instance, QObject* parent)
 | 
			
		||||
    : AbstractListModelBase(parent)
 | 
			
		||||
{
 | 
			
		||||
@ -169,18 +173,19 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
    case Role::Presence: {
 | 
			
		||||
        // The conversation can show a green dot if at least one peer is present
 | 
			
		||||
        // A conversation presence is the max of the members presence
 | 
			
		||||
        auto maxPresence = 0;
 | 
			
		||||
        Q_FOREACH (const auto& peerUri, model_->peersForConversation(item.uid))
 | 
			
		||||
            try {
 | 
			
		||||
                auto& accInfo = lrcInstance_->getAccountInfo(accountId_);
 | 
			
		||||
                if (peerUri == accInfo.profileInfo.uri)
 | 
			
		||||
                    return true; // Self account
 | 
			
		||||
                    return 2; // Self account
 | 
			
		||||
                auto contact = accInfo.contactModel->getContact(peerUri);
 | 
			
		||||
                if (contact.isPresent)
 | 
			
		||||
                    return true;
 | 
			
		||||
                if (contact.presence > maxPresence)
 | 
			
		||||
                    maxPresence = contact.presence;
 | 
			
		||||
            } catch (const std::exception&) {
 | 
			
		||||
            }
 | 
			
		||||
        return false;
 | 
			
		||||
        return maxPresence;
 | 
			
		||||
    };
 | 
			
		||||
    default:
 | 
			
		||||
        break;
 | 
			
		||||
@ -215,9 +220,9 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
 | 
			
		||||
        try {
 | 
			
		||||
            contact = contactModel->getContact(peerUri);
 | 
			
		||||
        } catch (const std::exception&) {
 | 
			
		||||
            qWarning() << Q_FUNC_INFO << "Can't find contact" << peerUri << " for account "
 | 
			
		||||
                       << lrcInstance_->accountModel().bestNameForAccount(accInfo.id)
 | 
			
		||||
                       << " - Conv: " << item.uid;
 | 
			
		||||
            C_WARN << "Can't find contact" << peerUri << "for account"
 | 
			
		||||
                   << lrcInstance_->accountModel().bestNameForAccount(accInfo.id)
 | 
			
		||||
                   << "- Conv:" << item.uid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch (role) {
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,9 @@
 | 
			
		||||
#include "systemtray.h"
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_LINUX
 | 
			
		||||
#include "namedirectory.h"
 | 
			
		||||
#include <namedirectory.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include <api/contact.h>
 | 
			
		||||
 | 
			
		||||
#include <QApplication>
 | 
			
		||||
#include <QJsonObject>
 | 
			
		||||
@ -602,9 +603,11 @@ ConversationsAdapter::openDialogConversationWith(const QString& peerUri)
 | 
			
		||||
void
 | 
			
		||||
ConversationsAdapter::onCurrentAccountRemoved()
 | 
			
		||||
{
 | 
			
		||||
    // Unbind proxy model source models.
 | 
			
		||||
    convModel_->bindSourceModel(nullptr);
 | 
			
		||||
    searchModel_->bindSourceModel(nullptr);
 | 
			
		||||
    // Unbind proxy model source models if there is no current account
 | 
			
		||||
    if (lrcInstance_->get_currentAccountId().isEmpty()) {
 | 
			
		||||
        convModel_->bindSourceModel(nullptr);
 | 
			
		||||
        searchModel_->bindSourceModel(nullptr);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
 | 
			
		||||
@ -144,7 +144,9 @@ CurrentAccount::updateData()
 | 
			
		||||
        set_deviceId(accConfig.deviceId);
 | 
			
		||||
        set_peerDiscovery(accConfig.peerDiscovery, true);
 | 
			
		||||
        set_sendReadReceipt(accConfig.sendReadReceipt, true);
 | 
			
		||||
        set_sendComposing(accConfig.sendComposing, true);
 | 
			
		||||
        set_isRendezVous(accConfig.isRendezVous, true);
 | 
			
		||||
        set_dhtPort(accConfig.dhtPort, true);
 | 
			
		||||
        set_autoAnswer(accConfig.autoAnswer, true);
 | 
			
		||||
        set_proxyEnabled(accConfig.proxyEnabled, true);
 | 
			
		||||
        set_upnpEnabled(accConfig.upnpEnabled, true);
 | 
			
		||||
 | 
			
		||||
@ -116,8 +116,10 @@ class CurrentAccount final : public QObject
 | 
			
		||||
    QML_RO_PROPERTY(lrc::api::profile::Type, type)
 | 
			
		||||
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, keepAliveEnabled)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(int, dhtPort)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, peerDiscovery)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, sendReadReceipt)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, sendComposing)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, isRendezVous)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, autoAnswer)
 | 
			
		||||
    QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, proxyEnabled)
 | 
			
		||||
 | 
			
		||||
@ -223,43 +223,14 @@ CurrentCall::updateCallInfo()
 | 
			
		||||
    set_isGrid(callInfo.layout == call::Layout::GRID);
 | 
			
		||||
    set_isAudioOnly(callInfo.isAudioOnly);
 | 
			
		||||
 | 
			
		||||
    bool isAudioMuted {};
 | 
			
		||||
    bool isVideoMuted {};
 | 
			
		||||
    bool isSharing {};
 | 
			
		||||
    QString sharingSource {};
 | 
			
		||||
    bool isCapturing {};
 | 
			
		||||
    QString previewId {};
 | 
			
		||||
    using namespace libjami::Media;
 | 
			
		||||
    if (callInfo.status != lrc::api::call::Status::ENDED) {
 | 
			
		||||
        for (const auto& media : callInfo.mediaList) {
 | 
			
		||||
            if (media[MediaAttributeKey::MEDIA_TYPE] == Details::MEDIA_TYPE_VIDEO) {
 | 
			
		||||
                if (media[MediaAttributeKey::SOURCE].startsWith(VideoProtocolPrefix::DISPLAY)
 | 
			
		||||
                    || media[MediaAttributeKey::SOURCE].startsWith(VideoProtocolPrefix::FILE)) {
 | 
			
		||||
                    isSharing = true;
 | 
			
		||||
                    sharingSource = media[MediaAttributeKey::SOURCE];
 | 
			
		||||
                }
 | 
			
		||||
                if (media[MediaAttributeKey::ENABLED] == TRUE_STR
 | 
			
		||||
                    && media[MediaAttributeKey::MUTED] == FALSE_STR && previewId.isEmpty()) {
 | 
			
		||||
                    previewId = media[libjami::Media::MediaAttributeKey::SOURCE];
 | 
			
		||||
                }
 | 
			
		||||
                if (media[libjami::Media::MediaAttributeKey::SOURCE].startsWith(
 | 
			
		||||
                        libjami::Media::VideoProtocolPrefix::CAMERA)) {
 | 
			
		||||
                    isVideoMuted |= media[MediaAttributeKey::MUTED] == TRUE_STR;
 | 
			
		||||
                    isCapturing = media[MediaAttributeKey::MUTED] == FALSE_STR;
 | 
			
		||||
                }
 | 
			
		||||
            } else if (media[MediaAttributeKey::MEDIA_TYPE] == Details::MEDIA_TYPE_AUDIO) {
 | 
			
		||||
                if (media[MediaAttributeKey::LABEL] == "audio_0") {
 | 
			
		||||
                    isAudioMuted |= media[libjami::Media::MediaAttributeKey::MUTED] == TRUE_STR;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    set_previewId(previewId);
 | 
			
		||||
    set_isAudioMuted(isAudioMuted);
 | 
			
		||||
    set_isVideoMuted(isVideoMuted);
 | 
			
		||||
    set_isSharing(isSharing);
 | 
			
		||||
    set_sharingSource(sharingSource);
 | 
			
		||||
    set_isCapturing(isCapturing);
 | 
			
		||||
    auto callInfoEx = callInfo.getCallInfoEx();
 | 
			
		||||
    set_previewId(callInfoEx["preview_id"].toString());
 | 
			
		||||
    set_isAudioMuted(callInfoEx["is_audio_muted"].toBool());
 | 
			
		||||
    set_isVideoMuted(callInfoEx["is_video_muted"].toBool());
 | 
			
		||||
    set_isSharing(callInfoEx["is_sharing"].toBool());
 | 
			
		||||
    set_sharingSource(isSharing_ ? callInfoEx["preview_id"].toString() : QString());
 | 
			
		||||
    set_isCapturing(callInfoEx["is_capturing"].toBool());
 | 
			
		||||
 | 
			
		||||
    set_isHandRaised(callModel->isHandRaised(id_));
 | 
			
		||||
    set_isModerator(callModel->isModerator(id_));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,10 @@
 | 
			
		||||
 | 
			
		||||
#include "currentconversation.h"
 | 
			
		||||
 | 
			
		||||
#include "global.h"
 | 
			
		||||
 | 
			
		||||
#include <api/conversationmodel.h>
 | 
			
		||||
#include <api/contact.h>
 | 
			
		||||
 | 
			
		||||
CurrentConversation::CurrentConversation(LRCInstance* lrcInstance, QObject* parent)
 | 
			
		||||
    : QObject(parent)
 | 
			
		||||
@ -263,51 +266,39 @@ void
 | 
			
		||||
CurrentConversation::connectModel()
 | 
			
		||||
{
 | 
			
		||||
    membersModel_->setMembers({}, {}, {});
 | 
			
		||||
    auto convModel = lrcInstance_->getCurrentConversationModel();
 | 
			
		||||
    if (!convModel)
 | 
			
		||||
 | 
			
		||||
    auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
 | 
			
		||||
    auto currentCallModel = lrcInstance_->getCurrentCallModel();
 | 
			
		||||
    if (!currentConversationModel || !currentCallModel) {
 | 
			
		||||
        C_DBG << "CurrentConversation: can't connect to unavailable models";
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto connectObjectSignal = [this](auto obj, auto signal, auto slot) {
 | 
			
		||||
        connect(obj, signal, this, slot, Qt::UniqueConnection);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    connectObjectSignal(convModel,
 | 
			
		||||
    connectObjectSignal(currentConversationModel,
 | 
			
		||||
                        &ConversationModel::conversationUpdated,
 | 
			
		||||
                        &CurrentConversation::onConversationUpdated);
 | 
			
		||||
    connectObjectSignal(convModel,
 | 
			
		||||
    connectObjectSignal(currentConversationModel,
 | 
			
		||||
                        &ConversationModel::profileUpdated,
 | 
			
		||||
                        &CurrentConversation::updateProfile);
 | 
			
		||||
 | 
			
		||||
    connect(lrcInstance_->getCurrentConversationModel(),
 | 
			
		||||
            &ConversationModel::profileUpdated,
 | 
			
		||||
            this,
 | 
			
		||||
            &CurrentConversation::updateProfile,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connect(lrcInstance_->getCurrentConversationModel(),
 | 
			
		||||
            &ConversationModel::onConversationErrorsUpdated,
 | 
			
		||||
            this,
 | 
			
		||||
            &CurrentConversation::updateErrors,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connect(lrcInstance_->getCurrentConversationModel(),
 | 
			
		||||
            &ConversationModel::activeCallsChanged,
 | 
			
		||||
            this,
 | 
			
		||||
            &CurrentConversation::updateActiveCalls,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connect(lrcInstance_->getCurrentConversationModel(),
 | 
			
		||||
            &ConversationModel::conversationPreferencesUpdated,
 | 
			
		||||
            this,
 | 
			
		||||
            &CurrentConversation::updateConversationPreferences,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connect(lrcInstance_->getCurrentConversationModel(),
 | 
			
		||||
            &ConversationModel::needsHost,
 | 
			
		||||
            this,
 | 
			
		||||
            &CurrentConversation::onNeedsHost,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connect(lrcInstance_->getCurrentCallModel(),
 | 
			
		||||
            &CallModel::callStatusChanged,
 | 
			
		||||
            this,
 | 
			
		||||
            &CurrentConversation::onCallStatusChanged,
 | 
			
		||||
            Qt::UniqueConnection);
 | 
			
		||||
    connectObjectSignal(currentConversationModel,
 | 
			
		||||
                        &ConversationModel::conversationErrorsUpdated,
 | 
			
		||||
                        &CurrentConversation::updateErrors);
 | 
			
		||||
    connectObjectSignal(currentConversationModel,
 | 
			
		||||
                        &ConversationModel::activeCallsChanged,
 | 
			
		||||
                        &CurrentConversation::updateActiveCalls);
 | 
			
		||||
    connectObjectSignal(currentConversationModel,
 | 
			
		||||
                        &ConversationModel::conversationPreferencesUpdated,
 | 
			
		||||
                        &CurrentConversation::updateConversationPreferences);
 | 
			
		||||
    connectObjectSignal(currentConversationModel,
 | 
			
		||||
                        &ConversationModel::needsHost,
 | 
			
		||||
                        &CurrentConversation::onNeedsHost);
 | 
			
		||||
    connectObjectSignal(currentCallModel,
 | 
			
		||||
                        &CallModel::callStatusChanged,
 | 
			
		||||
                        &CurrentConversation::onCallStatusChanged);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/app/global.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/app/global.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 Savoir-faire Linux Inc.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <QtCore/QLoggingCategory>
 | 
			
		||||
 | 
			
		||||
Q_DECLARE_LOGGING_CATEGORY(clientLog)
 | 
			
		||||
#define C_DBG   qCDebug(clientLog)
 | 
			
		||||
#define C_INFO  qCInfo(clientLog)
 | 
			
		||||
#define C_WARN  qCWarning(clientLog)
 | 
			
		||||
#define C_ERR   qCCritical(clientLog)
 | 
			
		||||
#define C_FATAL qCFatal(clientLog)
 | 
			
		||||
@ -19,6 +19,8 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#include "lrcinstance.h"
 | 
			
		||||
 | 
			
		||||
#include "global.h"
 | 
			
		||||
#include "connectivitymonitor.h"
 | 
			
		||||
 | 
			
		||||
#include <QBuffer>
 | 
			
		||||
@ -28,13 +30,11 @@
 | 
			
		||||
#include <QRegularExpression>
 | 
			
		||||
#include <QtConcurrent/QtConcurrent>
 | 
			
		||||
 | 
			
		||||
LRCInstance::LRCInstance(migrateCallback willMigrateCb,
 | 
			
		||||
                         migrateCallback didMigrateCb,
 | 
			
		||||
                         const QString& updateUrl,
 | 
			
		||||
LRCInstance::LRCInstance(const QString& updateUrl,
 | 
			
		||||
                         ConnectivityMonitor* connectivityMonitor,
 | 
			
		||||
                         bool debugMode,
 | 
			
		||||
                         bool muteDaemon)
 | 
			
		||||
    : lrc_(std::make_unique<Lrc>(willMigrateCb, didMigrateCb, !debugMode || muteDaemon))
 | 
			
		||||
    : lrc_(std::make_unique<Lrc>(!debugMode || muteDaemon))
 | 
			
		||||
    , updateManager_(std::make_unique<AppVersionManager>(updateUrl, connectivityMonitor, this))
 | 
			
		||||
    , connectivityMonitor_(*connectivityMonitor)
 | 
			
		||||
    , threadPool_(new QThreadPool(this))
 | 
			
		||||
@ -282,7 +282,7 @@ LRCInstance::getCurrentContactModel()
 | 
			
		||||
int
 | 
			
		||||
LRCInstance::getCurrentAccountIndex()
 | 
			
		||||
{
 | 
			
		||||
    for (int i = 0; i < accountModel().getAccountList().size(); i++) {
 | 
			
		||||
    for (int i = 0; i < accountModel().getAccountCount(); i++) {
 | 
			
		||||
        if (accountModel().getAccountList()[i] == get_currentAccountId()) {
 | 
			
		||||
            return i;
 | 
			
		||||
        }
 | 
			
		||||
@ -410,6 +410,10 @@ LRCInstance::indexOfActiveCall(const QString& confId, const QString& uri, const
 | 
			
		||||
void
 | 
			
		||||
LRCInstance::deselectConversation()
 | 
			
		||||
{
 | 
			
		||||
    // Only do this if we have an account selected
 | 
			
		||||
    if (get_currentAccountId().isEmpty()) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    set_selectedConvUid();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -482,5 +486,15 @@ LRCInstance::onAccountRemoved(const QString& accountId)
 | 
			
		||||
{
 | 
			
		||||
    if (accountId != currentAccountId_)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    // If there are any accounts left, select the first one, otherwise clear the current account
 | 
			
		||||
    // and request presentation of the wizard view.
 | 
			
		||||
    auto accountList = accountModel().getAccountList();
 | 
			
		||||
    if (accountList.size()) {
 | 
			
		||||
        set_currentAccountId(accountList.at(0));
 | 
			
		||||
    } else {
 | 
			
		||||
        set_currentAccountId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Q_EMIT currentAccountRemoved();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -28,16 +28,15 @@
 | 
			
		||||
#include "qtutils.h"
 | 
			
		||||
#include "utils.h"
 | 
			
		||||
 | 
			
		||||
#include "api/lrc.h"
 | 
			
		||||
#include "api/account.h"
 | 
			
		||||
#include "api/avmodel.h"
 | 
			
		||||
#include "api/behaviorcontroller.h"
 | 
			
		||||
#include "api/contact.h"
 | 
			
		||||
#include "api/contactmodel.h"
 | 
			
		||||
#include "api/conversation.h"
 | 
			
		||||
#include "api/conversationmodel.h"
 | 
			
		||||
#include "api/accountmodel.h"
 | 
			
		||||
#include "api/callmodel.h"
 | 
			
		||||
#include <api/lrc.h>
 | 
			
		||||
#include <api/account.h>
 | 
			
		||||
#include <api/avmodel.h>
 | 
			
		||||
#include <api/behaviorcontroller.h>
 | 
			
		||||
#include <api/contactmodel.h>
 | 
			
		||||
#include <api/conversation.h>
 | 
			
		||||
#include <api/conversationmodel.h>
 | 
			
		||||
#include <api/accountmodel.h>
 | 
			
		||||
#include <api/callmodel.h>
 | 
			
		||||
 | 
			
		||||
#include <QObject>
 | 
			
		||||
#include <QThreadPool>
 | 
			
		||||
@ -48,7 +47,6 @@ class ConnectivityMonitor;
 | 
			
		||||
 | 
			
		||||
using namespace lrc::api;
 | 
			
		||||
 | 
			
		||||
using migrateCallback = std::function<void()>;
 | 
			
		||||
using getConvPredicate = std::function<bool(const conversation::Info& conv)>;
 | 
			
		||||
 | 
			
		||||
class LRCInstance : public QObject
 | 
			
		||||
@ -61,9 +59,7 @@ class LRCInstance : public QObject
 | 
			
		||||
    QML_PROPERTY(bool, currentAccountAvatarSet)
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    explicit LRCInstance(migrateCallback willMigrateCb,
 | 
			
		||||
                         migrateCallback didMigrateCb,
 | 
			
		||||
                         const QString& updateUrl,
 | 
			
		||||
    explicit LRCInstance(const QString& updateUrl,
 | 
			
		||||
                         ConnectivityMonitor* connectivityMonitor,
 | 
			
		||||
                         bool debugMode,
 | 
			
		||||
                         bool muteDaemon);
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@
 | 
			
		||||
 | 
			
		||||
#include <clocale>
 | 
			
		||||
 | 
			
		||||
#ifndef ENABLE_TESTS
 | 
			
		||||
#ifndef BUILD_TESTING
 | 
			
		||||
int
 | 
			
		||||
main(int argc, char* argv[])
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@
 | 
			
		||||
 | 
			
		||||
#include "mainapplication.h"
 | 
			
		||||
 | 
			
		||||
#include "global.h"
 | 
			
		||||
#include "qmlregister.h"
 | 
			
		||||
#include "appsettingsmanager.h"
 | 
			
		||||
#include "connectivitymonitor.h"
 | 
			
		||||
@ -40,7 +41,6 @@
 | 
			
		||||
#include <QTranslator>
 | 
			
		||||
#include <QLibraryInfo>
 | 
			
		||||
#include <QQuickWindow>
 | 
			
		||||
#include <QLoggingCategory>
 | 
			
		||||
 | 
			
		||||
#include <thread>
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@
 | 
			
		||||
#include "dbuserrorhandler.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(app_, "app_")
 | 
			
		||||
Q_LOGGING_CATEGORY(clientLog, "client")
 | 
			
		||||
 | 
			
		||||
static const QtMessageHandler QT_DEFAULT_MESSAGE_HANDLER = qInstallMessageHandler(0);
 | 
			
		||||
 | 
			
		||||
@ -63,23 +63,30 @@ messageHandler(QtMsgType type, const QMessageLogContext& context, const QString&
 | 
			
		||||
    const static std::string fmt[5] = {"DBG", "WRN", "CRT", "FTL", "INF"};
 | 
			
		||||
    const QByteArray localMsg = msg.toUtf8();
 | 
			
		||||
    const auto ts = QString::number(QDateTime::currentMSecsSinceEpoch());
 | 
			
		||||
    const auto tid = QString::number(reinterpret_cast<quintptr>(QThread::currentThreadId()), 16);
 | 
			
		||||
 | 
			
		||||
    QString fileLineInfo = "";
 | 
			
		||||
    const auto isQml = QString(context.category) == QLatin1String("qml");
 | 
			
		||||
#ifdef QT_DEBUG
 | 
			
		||||
    // In debug mode, always include file and line info.
 | 
			
		||||
    fileLineInfo = QString("[%1:%2]").arg(context.file ? context.file : "unknown",
 | 
			
		||||
                                          context.line ? QString::number(context.line) : "0");
 | 
			
		||||
    // In debug mode, always include file URI (including line info).
 | 
			
		||||
    // Only do this when the level Info/Debug, as it is already included in the constructed
 | 
			
		||||
    // message for the other levels.
 | 
			
		||||
    if (type == QtDebugMsg || type == QtInfoMsg || !isQml) {
 | 
			
		||||
        auto fileName = isQml ? context.file : QUrl::fromLocalFile(context.file).toString();
 | 
			
		||||
        fileLineInfo = QString(" %1:%2").arg(!fileName.isEmpty() ? fileName : "unknown",
 | 
			
		||||
                                             context.line ? QString::number(context.line) : "0");
 | 
			
		||||
    }
 | 
			
		||||
#else
 | 
			
		||||
    // In release mode, include file and line info only for QML category which will always
 | 
			
		||||
    // be available and provide a link to the source code in QtCreator.
 | 
			
		||||
    if (QString(context.category) == QLatin1String("qml")) {
 | 
			
		||||
    if (isQml) {
 | 
			
		||||
        fileLineInfo = QString("[%1:%2]").arg(context.file ? context.file : "unknown",
 | 
			
		||||
                                              context.line ? QString::number(context.line) : "0");
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    const auto fmtMsg = QString("[%1][%2]%3: %4")
 | 
			
		||||
                            .arg(ts, fmt[type].c_str(), fileLineInfo, localMsg.constData());
 | 
			
		||||
    const auto fmtMsg = QString("[%1][%2][%3]:%4 %5")
 | 
			
		||||
                            .arg(ts, fmt[type].c_str(), tid, fileLineInfo, localMsg.constData());
 | 
			
		||||
 | 
			
		||||
    (*QT_DEFAULT_MESSAGE_HANDLER)(type, context, fmtMsg);
 | 
			
		||||
}
 | 
			
		||||
@ -142,7 +149,7 @@ MainApplication::MainApplication(int& argc, char** argv)
 | 
			
		||||
{
 | 
			
		||||
    const char* qtVersion = qVersion();
 | 
			
		||||
    if (strncmp(qtVersion, QT_VERSION_STR, strnlen(qtVersion, sizeof qtVersion)) != 0) {
 | 
			
		||||
        qCFatal(app_) << "Qt build version mismatch!" << QT_VERSION_STR;
 | 
			
		||||
        C_FATAL << "Qt build version mismatch!" << QT_VERSION_STR;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    parseArguments();
 | 
			
		||||
@ -152,6 +159,7 @@ MainApplication::MainApplication(int& argc, char** argv)
 | 
			
		||||
    // without using `qt.*=false`. It may be useful for debugging Qt/QtQuick issues.
 | 
			
		||||
    QLoggingCategory::setFilterRules("\n"
 | 
			
		||||
                                     "*.debug=true\n"
 | 
			
		||||
                                     "libclient.debug=false\n"
 | 
			
		||||
                                     "qt.*=false\n"
 | 
			
		||||
                                     "qml.debug=false\n"
 | 
			
		||||
                                     "\n");
 | 
			
		||||
@ -166,7 +174,7 @@ MainApplication::MainApplication(int& argc, char** argv)
 | 
			
		||||
    // the logging features.
 | 
			
		||||
    qInstallMessageHandler(messageHandler);
 | 
			
		||||
 | 
			
		||||
    qCInfo(app_) << "Using Qt runtime version:" << qtVersion;
 | 
			
		||||
    C_INFO << "Using Qt runtime version:" << qtVersion;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MainApplication::~MainApplication()
 | 
			
		||||
@ -277,10 +285,10 @@ MainApplication::handleUriAction(const QString& arg)
 | 
			
		||||
    QString uri {};
 | 
			
		||||
    if (arg.isEmpty() && !runOptions_[Option::StartUri].isNull()) {
 | 
			
		||||
        uri = runOptions_[Option::StartUri].toString();
 | 
			
		||||
        qCDebug(app_) << "URI action invoked by run option" << uri;
 | 
			
		||||
        C_DBG << "URI action invoked by run option" << uri;
 | 
			
		||||
    } else if (!arg.isEmpty()) {
 | 
			
		||||
        uri = arg;
 | 
			
		||||
        qCDebug(app_) << "URI action invoked by secondary instance" << uri;
 | 
			
		||||
        C_DBG << "URI action invoked by secondary instance" << uri;
 | 
			
		||||
        Q_EMIT searchAndSelect(uri.replace("jami:", ""));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -291,30 +299,7 @@ MainApplication::initLrc(const QString& downloadUrl,
 | 
			
		||||
                         bool debugMode,
 | 
			
		||||
                         bool muteDaemon)
 | 
			
		||||
{
 | 
			
		||||
    /*
 | 
			
		||||
     * Init mainwindow and finish splash when mainwindow shows up.
 | 
			
		||||
     */
 | 
			
		||||
    std::atomic_bool isMigrating(false);
 | 
			
		||||
    lrcInstance_.reset(new LRCInstance(
 | 
			
		||||
        [this, &isMigrating] {
 | 
			
		||||
            /*
 | 
			
		||||
             * TODO: splash screen for account migration.
 | 
			
		||||
             */
 | 
			
		||||
            isMigrating = true;
 | 
			
		||||
            while (isMigrating) {
 | 
			
		||||
                this->processEvents();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [&isMigrating] {
 | 
			
		||||
            while (!isMigrating) {
 | 
			
		||||
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
 | 
			
		||||
            }
 | 
			
		||||
            isMigrating = false;
 | 
			
		||||
        },
 | 
			
		||||
        downloadUrl,
 | 
			
		||||
        cm,
 | 
			
		||||
        debugMode,
 | 
			
		||||
        muteDaemon));
 | 
			
		||||
    lrcInstance_.reset(new LRCInstance(downloadUrl, cm, debugMode, muteDaemon));
 | 
			
		||||
    lrcInstance_->subscribeToDebugReceived();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -328,49 +313,59 @@ MainApplication::parseArguments()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    QCommandLineParser parser;
 | 
			
		||||
    parser.addHelpOption();
 | 
			
		||||
    parser.addVersionOption();
 | 
			
		||||
    parser_.addHelpOption();
 | 
			
		||||
    parser_.addVersionOption();
 | 
			
		||||
 | 
			
		||||
    QCommandLineOption webDebugOption(QStringList() << "remote-debugging-port",
 | 
			
		||||
                                      "Web debugging port.",
 | 
			
		||||
                                      "port");
 | 
			
		||||
    parser.addOption(webDebugOption);
 | 
			
		||||
    parser_.addOption(webDebugOption);
 | 
			
		||||
 | 
			
		||||
    QCommandLineOption minimizedOption({"m", "minimized"}, "Start minimized.");
 | 
			
		||||
    parser.addOption(minimizedOption);
 | 
			
		||||
    parser_.addOption(minimizedOption);
 | 
			
		||||
 | 
			
		||||
    QCommandLineOption debugOption({"d", "debug"}, "Debug out.");
 | 
			
		||||
    parser.addOption(debugOption);
 | 
			
		||||
    parser_.addOption(debugOption);
 | 
			
		||||
 | 
			
		||||
    QCommandLineOption logFileOption({"f", "file"}, "Debug to <file>.", "file");
 | 
			
		||||
    parser.addOption(logFileOption);
 | 
			
		||||
    parser_.addOption(logFileOption);
 | 
			
		||||
 | 
			
		||||
#ifdef Q_OS_WINDOWS
 | 
			
		||||
    QCommandLineOption updateUrlOption({"u", "url"}, "<url> for debugging version queries.", "url");
 | 
			
		||||
    parser.addOption(updateUrlOption);
 | 
			
		||||
    parser_.addOption(updateUrlOption);
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
    QCommandLineOption terminateOption({"t", "term"}, "Terminate all instances.");
 | 
			
		||||
    parser.addOption(terminateOption);
 | 
			
		||||
    parser_.addOption(terminateOption);
 | 
			
		||||
 | 
			
		||||
    QCommandLineOption muteDaemonOption({"q", "quiet"}, "Mute daemon logging. (only if debug)");
 | 
			
		||||
    parser.addOption(muteDaemonOption);
 | 
			
		||||
    parser_.addOption(muteDaemonOption);
 | 
			
		||||
 | 
			
		||||
    parser.process(*this);
 | 
			
		||||
#ifdef QT_DEBUG
 | 
			
		||||
    // In debug mode, add an option to test a specific QML component via its name.
 | 
			
		||||
    // e.g. ./jami --test AccountComboBox
 | 
			
		||||
    parser_.addOption(QCommandLineOption("test", "Test a QML component via its name.", "uri"));
 | 
			
		||||
    // We may need to force the test window dimensions in the case that the component to test
 | 
			
		||||
    // does not specify its own dimensions and is dependent on parent/sibling dimensions.
 | 
			
		||||
    // e.g. ./jami --test AccountComboBox -w 200
 | 
			
		||||
    parser_.addOption(QCommandLineOption("width", "Width for the test window.", "width"));
 | 
			
		||||
    parser_.addOption(QCommandLineOption("height", "Height for the test window.", "height"));
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    runOptions_[Option::StartMinimized] = parser.isSet(minimizedOption);
 | 
			
		||||
    runOptions_[Option::Debug] = parser.isSet(debugOption);
 | 
			
		||||
    if (parser.isSet(logFileOption)) {
 | 
			
		||||
        auto logFileValue = parser.value(logFileOption);
 | 
			
		||||
    parser_.process(*this);
 | 
			
		||||
 | 
			
		||||
    runOptions_[Option::StartMinimized] = parser_.isSet(minimizedOption);
 | 
			
		||||
    runOptions_[Option::Debug] = parser_.isSet(debugOption);
 | 
			
		||||
    if (parser_.isSet(logFileOption)) {
 | 
			
		||||
        auto logFileValue = parser_.value(logFileOption);
 | 
			
		||||
        auto logFile = logFileValue.isEmpty() ? Utils::getDebugFilePath() : logFileValue;
 | 
			
		||||
        qputenv("JAMI_LOG_FILE", logFile.toStdString().c_str());
 | 
			
		||||
    }
 | 
			
		||||
#ifdef Q_OS_WINDOWS
 | 
			
		||||
    runOptions_[Option::UpdateUrl] = parser.value(updateUrlOption);
 | 
			
		||||
    runOptions_[Option::UpdateUrl] = parser_.value(updateUrlOption);
 | 
			
		||||
#endif
 | 
			
		||||
    runOptions_[Option::TerminationRequested] = parser.isSet(terminateOption);
 | 
			
		||||
    runOptions_[Option::MuteDaemon] = parser.isSet(muteDaemonOption);
 | 
			
		||||
    runOptions_[Option::TerminationRequested] = parser_.isSet(terminateOption);
 | 
			
		||||
    runOptions_[Option::MuteDaemon] = parser_.isSet(muteDaemonOption);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
@ -386,6 +381,35 @@ MainApplication::setApplicationFont()
 | 
			
		||||
    setFont(font);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QString
 | 
			
		||||
findResource(const QString& targetBasename, const QString& basePath = ":/")
 | 
			
		||||
{
 | 
			
		||||
    QDir dir(basePath);
 | 
			
		||||
    // List all entries in the directory excluding special entries '.' and '..'
 | 
			
		||||
    QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot,
 | 
			
		||||
                                        QDir::DirsFirst);
 | 
			
		||||
 | 
			
		||||
    Q_FOREACH (const QString& entry, entries) {
 | 
			
		||||
        QString fullPath = basePath + "/" + entry;
 | 
			
		||||
        QFileInfo fileInfo(fullPath);
 | 
			
		||||
 | 
			
		||||
        if (fileInfo.isDir()) {
 | 
			
		||||
            // Recursively search in subdirectories
 | 
			
		||||
            QString found = findResource(targetBasename, fullPath);
 | 
			
		||||
            if (!found.isEmpty()) {
 | 
			
		||||
                return found; // Return the first match found in any subdirectory
 | 
			
		||||
            }
 | 
			
		||||
        } else if (fileInfo.isFile()
 | 
			
		||||
                   && fileInfo.fileName().contains(targetBasename, Qt::CaseInsensitive)) {
 | 
			
		||||
            // Match found, return the full path but remove the leading ":/".
 | 
			
		||||
            return fileInfo.absoluteFilePath().mid(2);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // No match found in this directory or its subdirectories
 | 
			
		||||
    return QString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
MainApplication::initQmlLayer()
 | 
			
		||||
{
 | 
			
		||||
@ -399,10 +423,33 @@ MainApplication::initQmlLayer()
 | 
			
		||||
                         &screenInfo_,
 | 
			
		||||
                         this);
 | 
			
		||||
 | 
			
		||||
    engine_->load(QUrl(QStringLiteral("qrc:/MainApplicationWindow.qml")));
 | 
			
		||||
    QUrl url;
 | 
			
		||||
    if (parser_.isSet("test")) {
 | 
			
		||||
        // List the QML files in the project source tree.
 | 
			
		||||
        const auto targetTestComponent = findResource(parser_.value("test"));
 | 
			
		||||
        if (targetTestComponent.isEmpty()) {
 | 
			
		||||
            C_FATAL << "Failed to find QML component:" << parser_.value("test");
 | 
			
		||||
        }
 | 
			
		||||
        engine_->rootContext()->setContextProperty("testComponentURI", targetTestComponent);
 | 
			
		||||
        // Log the width and height values for the test window.
 | 
			
		||||
        const auto testWidth = parser_.isSet("width") ? parser_.value("width").toInt() : 0;
 | 
			
		||||
        const auto testHeight = parser_.isSet("height") ? parser_.value("height").toInt() : 0;
 | 
			
		||||
        engine_->rootContext()->setContextProperty("testWidth", testWidth);
 | 
			
		||||
        engine_->rootContext()->setContextProperty("testHeight", testHeight);
 | 
			
		||||
        url = u"qrc:/ComponentTestWindow.qml"_qs;
 | 
			
		||||
    } else {
 | 
			
		||||
        url = u"qrc:/MainApplicationWindow.qml"_qs;
 | 
			
		||||
    }
 | 
			
		||||
    QObject::connect(
 | 
			
		||||
        engine_.get(),
 | 
			
		||||
        &QQmlApplicationEngine::objectCreationFailed,
 | 
			
		||||
        this,
 | 
			
		||||
        [url]() { C_FATAL << "Failed to load QML component:" << url; },
 | 
			
		||||
        Qt::QueuedConnection);
 | 
			
		||||
    engine_->load(url);
 | 
			
		||||
 | 
			
		||||
    // Report the render interface used.
 | 
			
		||||
    qCWarning(app_) << "Main window loaded using" << getRenderInterfaceString();
 | 
			
		||||
    C_DBG << "Main window loaded using" << getRenderInterfaceString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@
 | 
			
		||||
#include <QQmlEngine>
 | 
			
		||||
#include <QScreen>
 | 
			
		||||
#include <QWindow>
 | 
			
		||||
#include <QCommandLineParser>
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
@ -122,6 +123,6 @@ private:
 | 
			
		||||
    SystemTray* systemTray_;
 | 
			
		||||
    AppSettingsManager* settingsManager_;
 | 
			
		||||
    PreviewEngine* previewEngine_;
 | 
			
		||||
 | 
			
		||||
    ScreenInfo screenInfo_;
 | 
			
		||||
    QCommandLineParser parser_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ Label {
 | 
			
		||||
 | 
			
		||||
    property alias popup: comboBoxPopup
 | 
			
		||||
 | 
			
		||||
    width: parent ? parent.width : o
 | 
			
		||||
    width: parent ? parent.width : 0
 | 
			
		||||
    height: JamiTheme.accountListItemHeight
 | 
			
		||||
 | 
			
		||||
    property bool inSettings: viewCoordinator.currentViewName === "SettingsView"
 | 
			
		||||
@ -111,6 +111,7 @@ Label {
 | 
			
		||||
 | 
			
		||||
                Avatar {
 | 
			
		||||
                    id: avatar
 | 
			
		||||
                    objectName: "accountComboBoxAvatar"
 | 
			
		||||
 | 
			
		||||
                    Layout.preferredWidth: JamiTheme.accountListAvatarSize
 | 
			
		||||
                    Layout.preferredHeight: JamiTheme.accountListAvatarSize
 | 
			
		||||
 | 
			
		||||
@ -105,6 +105,7 @@ Popup {
 | 
			
		||||
 | 
			
		||||
                        Avatar {
 | 
			
		||||
                            id: avatar
 | 
			
		||||
                            objectName: "accountComboBoxPopupAvatar"
 | 
			
		||||
 | 
			
		||||
                            Layout.preferredWidth: JamiTheme.accountListAvatarSize
 | 
			
		||||
                            Layout.preferredHeight: JamiTheme.accountListAvatarSize
 | 
			
		||||
@ -223,6 +224,7 @@ Popup {
 | 
			
		||||
 | 
			
		||||
        JamiListView {
 | 
			
		||||
            id: listView
 | 
			
		||||
            objectName: "accountList"
 | 
			
		||||
 | 
			
		||||
            Layout.fillHeight: true
 | 
			
		||||
            Layout.preferredWidth: parent.width
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ ItemDelegate {
 | 
			
		||||
            height: 1
 | 
			
		||||
            width: parent.width - 20
 | 
			
		||||
            color: JamiTheme.hoverColor
 | 
			
		||||
 | 
			
		||||
            visible: index !== 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        color: {
 | 
			
		||||
@ -62,6 +62,7 @@ ItemDelegate {
 | 
			
		||||
        spacing: 10
 | 
			
		||||
 | 
			
		||||
        Avatar {
 | 
			
		||||
            objectName: "accountComboBoxDelegateAvatar"
 | 
			
		||||
            Layout.preferredWidth: JamiTheme.accountListAvatarSize
 | 
			
		||||
            Layout.preferredHeight: JamiTheme.accountListAvatarSize
 | 
			
		||||
            Layout.alignment: Qt.AlignVCenter
 | 
			
		||||
 | 
			
		||||
@ -112,6 +112,7 @@ Control {
 | 
			
		||||
        },
 | 
			
		||||
        Action {
 | 
			
		||||
            id: shareMenuAction
 | 
			
		||||
            enabled: !CurrentCall.isSharing
 | 
			
		||||
            text: JamiStrings.selectShareMethod
 | 
			
		||||
            property int popupMode: CallActionBar.ActionPopupMode.ListElement
 | 
			
		||||
            property var listModel: ListModel {
 | 
			
		||||
@ -123,7 +124,7 @@ Control {
 | 
			
		||||
                        "Name": JamiStrings.shareScreen,
 | 
			
		||||
                        "IconSource": JamiResources.laptop_black_24dp_svg
 | 
			
		||||
                    });
 | 
			
		||||
                if (Qt.platform.os.toString() !== "osx" && !UtilsAdapter.isWayland()) {
 | 
			
		||||
                if (Qt.platform.os.toString() !== "osx") {
 | 
			
		||||
                    shareModel.append({
 | 
			
		||||
                            "Name": JamiStrings.shareWindow,
 | 
			
		||||
                            "IconSource": JamiResources.window_black_24dp_svg
 | 
			
		||||
@ -293,7 +294,24 @@ Control {
 | 
			
		||||
        },
 | 
			
		||||
        Action {
 | 
			
		||||
            id: muteVideoAction
 | 
			
		||||
            onTriggered: CallAdapter.muteCameraToggle()
 | 
			
		||||
            onTriggered: {
 | 
			
		||||
                if (CurrentCall.isSharing && UtilsAdapter.isWayland()) {
 | 
			
		||||
                    // Unmuting the camera while a screen share is ongoing causes the daemon
 | 
			
		||||
                    // to stop sharing. However, on Wayland, every share has an associated
 | 
			
		||||
                    // ScreenCastPortal object which is managed by the client and needs to
 | 
			
		||||
                    // be destroyed when the share ends. This is why we explicitly call the
 | 
			
		||||
                    // stopSharing function below.
 | 
			
		||||
                    //
 | 
			
		||||
                    // The muteCamera variable is set whenever a share starts and is normally used
 | 
			
		||||
                    // by the stopSharing function to restore the camera to its previous state
 | 
			
		||||
                    // when a share ends. Here we know that the user wants to unmute the camera,
 | 
			
		||||
                    // so we have to explicitly set muteCamera to false.
 | 
			
		||||
                    AvAdapter.muteCamera = false;
 | 
			
		||||
                    AvAdapter.stopSharing(CurrentCall.sharingSource);
 | 
			
		||||
                } else {
 | 
			
		||||
                    CallAdapter.muteCameraToggle();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            checkable: true
 | 
			
		||||
            icon.source: checked ? JamiResources.videocam_off_24dp_svg : JamiResources.videocam_24dp_svg
 | 
			
		||||
            icon.color: checked ? "red" : "white"
 | 
			
		||||
 | 
			
		||||
@ -273,14 +273,46 @@ ItemDelegate {
 | 
			
		||||
        popup: Popup {
 | 
			
		||||
            id: itemPopup
 | 
			
		||||
 | 
			
		||||
            y: isVertical ? -(implicitHeight - root.height) / 2 - 18 : -implicitHeight - 12
 | 
			
		||||
            y: {
 | 
			
		||||
                // Determine the y position based on the orientation.
 | 
			
		||||
                if (isVertical) {
 | 
			
		||||
                    // For a vertical layout, adjust the y position to center the item vertically
 | 
			
		||||
                    // relative to the root's height, with an additional upward offset of 18 pixels.
 | 
			
		||||
                    y = -(implicitHeight - root.height) / 2 - 18;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // For non-vertical layouts, position the item fully above its normal position
 | 
			
		||||
                    // with an upward offset of 12 pixels from its implicit height.
 | 
			
		||||
                    y = -implicitHeight - 12;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            x: {
 | 
			
		||||
                if (isVertical)
 | 
			
		||||
                    return -implicitWidth - 12;
 | 
			
		||||
                var xValue = -(implicitWidth - root.width) / 2 - 18;
 | 
			
		||||
                var mainPoint = mapToItem(viewCoordinator.rootView, xValue, y);
 | 
			
		||||
                var diff = mainPoint.x + itemListView.implicitWidth - viewCoordinator.rootView.width;
 | 
			
		||||
                return diff > 0 ? xValue - diff - 24 : xValue;
 | 
			
		||||
                // Initialize the x position based on the orientation.
 | 
			
		||||
                if (isVertical) {
 | 
			
		||||
                    // If the layout is vertical, position the item to the left of its implicit width
 | 
			
		||||
                    // with an additional offset of 12 pixels.
 | 
			
		||||
                    x = -implicitWidth - 12;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Note: isn't some of this logic built into the Popup?
 | 
			
		||||
 | 
			
		||||
                    // Calculate an initial x value aiming to center the item horizontally
 | 
			
		||||
                    // relative to the root's width, with an additional offset.
 | 
			
		||||
                    var xValue = -(implicitWidth - root.width) / 2 - 18;
 | 
			
		||||
 | 
			
		||||
                    // Map the adjusted x value to the coordinate space of the callOverlay to
 | 
			
		||||
                    // determine the actual position of the item within the overlay.
 | 
			
		||||
                    var pointMappedContainer = mapToItem(callOverlay, xValue, y);
 | 
			
		||||
 | 
			
		||||
                    // Calculate the difference between the right edge of the itemListView
 | 
			
		||||
                    // (considering its position within callOverlay) and the right edge of the callOverlay.
 | 
			
		||||
                    // This checks if the item extends outside the overlay.
 | 
			
		||||
                    var diff = pointMappedContainer.x + itemListView.implicitWidth - callOverlay.width;
 | 
			
		||||
 | 
			
		||||
                    // If the item extends beyond the overlay, adjust x value to the left to ensure
 | 
			
		||||
                    // it fits within the overlay, with an extra leftward margin of 24 pixels.
 | 
			
		||||
                    x = diff > 0 ? xValue - diff - 24 : xValue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            implicitWidth: contentItem.implicitWidth
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ Item {
 | 
			
		||||
    id: root
 | 
			
		||||
 | 
			
		||||
    property bool participantsSide: UtilsAdapter.getAppValue(Settings.ParticipantsSide)
 | 
			
		||||
    property alias mainOverlayOpacity: mainOverlay.opacity
 | 
			
		||||
 | 
			
		||||
    signal chatButtonClicked
 | 
			
		||||
    signal fullScreenClicked
 | 
			
		||||
@ -113,7 +114,9 @@ Item {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function openShareScreen() {
 | 
			
		||||
        if (Qt.application.screens.length === 1) {
 | 
			
		||||
        if (UtilsAdapter.isWayland()) {
 | 
			
		||||
            AvAdapter.shareEntireScreenWayland();
 | 
			
		||||
        } else if (Qt.application.screens.length === 1) {
 | 
			
		||||
            AvAdapter.shareEntireScreen(0);
 | 
			
		||||
        } else {
 | 
			
		||||
            SelectScreenWindowCreation.presentSelectScreenWindow(appWindow, false);
 | 
			
		||||
@ -121,6 +124,10 @@ Item {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function openShareWindow() {
 | 
			
		||||
        if (UtilsAdapter.isWayland()) {
 | 
			
		||||
            AvAdapter.shareWindowWayland();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        AvAdapter.getListWindows();
 | 
			
		||||
        if (AvAdapter.windowsNames.length >= 1) {
 | 
			
		||||
            SelectScreenWindowCreation.presentSelectScreenWindow(appWindow, true);
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,6 @@ import net.jami.Models 1.1
 | 
			
		||||
import net.jami.Adapters 1.1
 | 
			
		||||
import net.jami.Constants 1.1
 | 
			
		||||
import net.jami.Enums 1.1
 | 
			
		||||
 | 
			
		||||
import "../../commoncomponents"
 | 
			
		||||
import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
 | 
			
		||||
 | 
			
		||||
@ -39,6 +38,10 @@ Rectangle {
 | 
			
		||||
 | 
			
		||||
    property var mapPositions: PositionManager.mapStatus
 | 
			
		||||
 | 
			
		||||
    // The purpose of this alias is to make the message bar
 | 
			
		||||
    // accessible to the EmojiPicker
 | 
			
		||||
    property alias messageBar: chatViewFooter.messageBar
 | 
			
		||||
 | 
			
		||||
    required property bool inCallView
 | 
			
		||||
 | 
			
		||||
    // Hide the extrasPanel when going into a call view, but save the previous
 | 
			
		||||
@ -129,6 +132,11 @@ Rectangle {
 | 
			
		||||
            Connections {
 | 
			
		||||
                target: CurrentConversation
 | 
			
		||||
 | 
			
		||||
                function onIdChanged() {
 | 
			
		||||
                    if (!chatViewHeader.interactionButtonsVisibility)
 | 
			
		||||
                        extrasPanel.closePanel();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function onNeedsHost() {
 | 
			
		||||
                    viewCoordinator.presentDialog(appWindow, "mainview/components/HostPopup.qml");
 | 
			
		||||
                }
 | 
			
		||||
@ -242,14 +250,10 @@ Rectangle {
 | 
			
		||||
                    chatContents.visible = true;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const isExpanding = width > previousWidth;
 | 
			
		||||
 | 
			
		||||
                // Provide a detailed log here, as this function seems problematic.
 | 
			
		||||
                console.debug("ChatViewSplitView.resolvePanes: f: %1 w: %2 pw: %3 epw: %4 pepw: %5 ie: %6"
 | 
			
		||||
                              .arg(force).arg(width).arg(previousWidth)
 | 
			
		||||
                              .arg(extrasPanelWidth).arg(previousExtrasPanelWidth).arg(isExpanding));
 | 
			
		||||
 | 
			
		||||
                console.debug("ChatViewSplitView.resolvePanes: f: %1 w: %2 pw: %3 epw: %4 pepw: %5 ie: %6".arg(force).arg(width).arg(previousWidth).arg(extrasPanelWidth).arg(previousExtrasPanelWidth).arg(isExpanding));
 | 
			
		||||
                const maximizePredicate = (!isExpanding || force) && chatContents.visible;
 | 
			
		||||
                const minimizePredicate = (isExpanding || force) && !chatContents.visible;
 | 
			
		||||
                const mainViewMinWidth = JamiTheme.mainViewPaneMinWidth;
 | 
			
		||||
@ -314,6 +318,7 @@ Rectangle {
 | 
			
		||||
 | 
			
		||||
                ChatViewFooter {
 | 
			
		||||
                    id: chatViewFooter
 | 
			
		||||
                    objectName: "chatViewFooter"
 | 
			
		||||
 | 
			
		||||
                    visible: {
 | 
			
		||||
                        if (CurrentAccount.type === Profile.Type.SIP)
 | 
			
		||||
@ -322,7 +327,7 @@ Rectangle {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        else if (CurrentConversation.needsSyncing)
 | 
			
		||||
                            return false;
 | 
			
		||||
                        else if (CurrentConversation.isSwarm && CurrentConversation.isRequest)
 | 
			
		||||
                        else if (CurrentConversation.isRequest)
 | 
			
		||||
                            return false;
 | 
			
		||||
                        return CurrentConversation.isSwarm || CurrentConversation.isTemporary;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import "../../commoncomponents"
 | 
			
		||||
Rectangle {
 | 
			
		||||
    id: root
 | 
			
		||||
    property alias textInput: messageBar.textAreaObj
 | 
			
		||||
    property alias messageBar: messageBar
 | 
			
		||||
    property string previousConvId
 | 
			
		||||
    property string previousAccountId
 | 
			
		||||
    property bool showTypo: messageBar.showTypo
 | 
			
		||||
 | 
			
		||||
@ -75,8 +75,8 @@ Rectangle {
 | 
			
		||||
 | 
			
		||||
        anchors.fill: parent
 | 
			
		||||
        // QWK: spacing
 | 
			
		||||
        anchors.leftMargin: qwkSystemButtonSpacing.left
 | 
			
		||||
        anchors.rightMargin: 10 + qwkSystemButtonSpacing.right
 | 
			
		||||
        anchors.leftMargin: layoutManager.qwkSystemButtonSpacing.left
 | 
			
		||||
        anchors.rightMargin: 10 + layoutManager.qwkSystemButtonSpacing.right
 | 
			
		||||
        spacing: 16
 | 
			
		||||
 | 
			
		||||
        JamiPushButton { QWKSetParentHitTestVisible {}
 | 
			
		||||
@ -230,8 +230,7 @@ Rectangle {
 | 
			
		||||
 | 
			
		||||
            checkable: true
 | 
			
		||||
            checked: extrasPanel.isOpen(ChatView.SwarmDetailsPanel)
 | 
			
		||||
            visible: interactionButtonsVisibility &&
 | 
			
		||||
                     (swarmDetailsVisibility || LRCInstance.currentAccountType === Profile.Type.SIP) // TODO if SIP not a request
 | 
			
		||||
            visible: (swarmDetailsVisibility || LRCInstance.currentAccountType === Profile.Type.SIP)
 | 
			
		||||
            source: JamiResources.swarm_details_panel_svg
 | 
			
		||||
            toolTipText: JamiStrings.details
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ Item {
 | 
			
		||||
    id: root
 | 
			
		||||
 | 
			
		||||
    property alias imageId: avatar.imageId
 | 
			
		||||
    property alias presenceStatus: avatar.presenceStatus
 | 
			
		||||
    property alias showPresenceIndicator: avatar.showPresenceIndicator
 | 
			
		||||
    property alias animationMode: animation.mode
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -128,11 +128,11 @@ Item {
 | 
			
		||||
                        fillMode: Image.PreserveAspectCrop
 | 
			
		||||
 | 
			
		||||
                        source: {
 | 
			
		||||
                            fileSource = "file://" + Body;
 | 
			
		||||
                            fileSource = UtilsAdapter.urlFromLocalPath(Body);
 | 
			
		||||
                            if (!mediaInfo.isImage && !mediaInfo.isAnimatedImage) {
 | 
			
		||||
                                return "";
 | 
			
		||||
                            }
 | 
			
		||||
                            return "file://" + Body;
 | 
			
		||||
                            return fileSource;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										231
									
								
								src/app/mainview/components/InCallLocalVideo.qml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/app/mainview/components/InCallLocalVideo.qml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,231 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 Savoir-faire Linux Inc.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import Qt5Compat.GraphicalEffects
 | 
			
		||||
 | 
			
		||||
import net.jami.Enums 1.1
 | 
			
		||||
import net.jami.Constants 1.1
 | 
			
		||||
import net.jami.Adapters 1.1
 | 
			
		||||
 | 
			
		||||
import "../../commoncomponents"
 | 
			
		||||
 | 
			
		||||
// This component uses anchors and they are set within this component.
 | 
			
		||||
LocalVideo {
 | 
			
		||||
    id: localPreview
 | 
			
		||||
 | 
			
		||||
    required property var container
 | 
			
		||||
    required property real opacityModifier
 | 
			
		||||
 | 
			
		||||
    readonly property int previewMargin: 15
 | 
			
		||||
    readonly property int previewMarginYTop: previewMargin + 42
 | 
			
		||||
    readonly property int previewMarginYBottom: previewMargin + 84
 | 
			
		||||
 | 
			
		||||
    anchors.bottomMargin: previewMarginYBottom
 | 
			
		||||
    anchors.leftMargin: sideMargin
 | 
			
		||||
    anchors.rightMargin: sideMargin
 | 
			
		||||
    anchors.topMargin: previewMarginYTop
 | 
			
		||||
 | 
			
		||||
    visibilityCondition: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) &&
 | 
			
		||||
                         !CurrentCall.isConference
 | 
			
		||||
 | 
			
		||||
    // Keep the area of the preview a proportion of the screen size plus a
 | 
			
		||||
    // modifier to allow the user to scale it.
 | 
			
		||||
    readonly property real containerArea: container.width * container.height
 | 
			
		||||
    property real scalingFactor: 1
 | 
			
		||||
    width: Math.sqrt(containerArea / 16) * scalingFactor
 | 
			
		||||
    height: width * invAspectRatio
 | 
			
		||||
 | 
			
		||||
    flip: CurrentCall.flipSelf && !CurrentCall.isSharing
 | 
			
		||||
    blurRadius: hidden ? 25 : 0
 | 
			
		||||
 | 
			
		||||
    opacity: hidden ? opacityModifier : 1
 | 
			
		||||
 | 
			
		||||
    // Allow hiding the preview (available when anchored)
 | 
			
		||||
    readonly property bool hovered: hoverHandler.hovered
 | 
			
		||||
    readonly property bool anchored: state !== "unanchored"
 | 
			
		||||
    property bool hidden: false
 | 
			
		||||
    readonly property real hiddenHandleSize: 32
 | 
			
		||||
    // Compute the margin as a function of the preview width in order to
 | 
			
		||||
    // apply a negative margin and expose a constant width handle.
 | 
			
		||||
    // If not hidden, return the previewMargin.
 | 
			
		||||
    property real sideMargin: !hidden ? previewMargin : -(width - hiddenHandleSize)
 | 
			
		||||
    // Animate the hiddenSize with a Behavior.
 | 
			
		||||
    Behavior on sideMargin { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
 | 
			
		||||
    readonly property bool onLeft: state.indexOf("left") !== -1
 | 
			
		||||
 | 
			
		||||
    MouseArea {
 | 
			
		||||
        anchors.fill: parent
 | 
			
		||||
        enabled: !localPreview.hidden
 | 
			
		||||
        onWheel: function(event) {
 | 
			
		||||
            const delta = event.angleDelta.y / 120 * 0.1;
 | 
			
		||||
            if (event.modifiers & Qt.ControlModifier) {
 | 
			
		||||
                parent.opacity = JamiQmlUtils.clamp(parent.opacity + delta, 0.25, 1);
 | 
			
		||||
            } else {
 | 
			
		||||
                localPreview.scalingFactor = JamiQmlUtils.clamp(localPreview.scalingFactor + delta, 0.5, 4);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    PushButton {
 | 
			
		||||
        id: hidePreviewButton
 | 
			
		||||
        objectName: "hidePreviewButton"
 | 
			
		||||
 | 
			
		||||
        width: localPreview.hiddenHandleSize
 | 
			
		||||
        state: localPreview.onLeft ?
 | 
			
		||||
                   (localPreview.hidden ? "right" : "left") :
 | 
			
		||||
                   (localPreview.hidden ? "left" : "right")
 | 
			
		||||
        states: [
 | 
			
		||||
            State {
 | 
			
		||||
                name: "left"
 | 
			
		||||
                AnchorChanges {
 | 
			
		||||
                    target: hidePreviewButton
 | 
			
		||||
                    anchors.left: parent.left
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            State {
 | 
			
		||||
                name: "right"
 | 
			
		||||
                AnchorChanges {
 | 
			
		||||
                    target: hidePreviewButton
 | 
			
		||||
                    anchors.right: parent.right
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
        anchors.top: parent.top
 | 
			
		||||
        anchors.bottom: parent.bottom
 | 
			
		||||
        opacity: (localPreview.anchored && localPreview.hovered) || localPreview.hidden
 | 
			
		||||
        Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
 | 
			
		||||
        visible: opacity > 0
 | 
			
		||||
        background: Rectangle {
 | 
			
		||||
            readonly property color normalColor: JamiTheme.mediumGrey
 | 
			
		||||
            color: JamiTheme.mediumGrey
 | 
			
		||||
            opacity: hidePreviewButton.hovered ? 0.7 : 0.5
 | 
			
		||||
            Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
 | 
			
		||||
        }
 | 
			
		||||
        normalImageSource: hidePreviewButton.state === "left" ?
 | 
			
		||||
                               JamiResources.chevron_left_black_24dp_svg :
 | 
			
		||||
                               JamiResources.chevron_right_black_24dp_svg
 | 
			
		||||
        imageColor: JamiTheme.darkGreyColor
 | 
			
		||||
        onClicked: localPreview.hidden = !localPreview.hidden
 | 
			
		||||
        toolTipText: localPreview.hidden ?
 | 
			
		||||
                         JamiStrings.showLocalVideo :
 | 
			
		||||
                         JamiStrings.hideLocalVideo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state: "anchor_top_right"
 | 
			
		||||
    states: [
 | 
			
		||||
        State {
 | 
			
		||||
            name: "unanchored"
 | 
			
		||||
            AnchorChanges {
 | 
			
		||||
                target: localPreview
 | 
			
		||||
                anchors.top: undefined
 | 
			
		||||
                anchors.right: undefined
 | 
			
		||||
                anchors.bottom: undefined
 | 
			
		||||
                anchors.left: undefined
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        State {
 | 
			
		||||
            name: "anchor_top_left"
 | 
			
		||||
            AnchorChanges {
 | 
			
		||||
                target: localPreview
 | 
			
		||||
                anchors.top: localPreview.container.top
 | 
			
		||||
                anchors.left: localPreview.container.left
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        State {
 | 
			
		||||
            name: "anchor_top_right"
 | 
			
		||||
            AnchorChanges {
 | 
			
		||||
                target: localPreview
 | 
			
		||||
                anchors.top: localPreview.container.top
 | 
			
		||||
                anchors.right: localPreview.container.right
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        State {
 | 
			
		||||
            name: "anchor_bottom_right"
 | 
			
		||||
            AnchorChanges {
 | 
			
		||||
                target: localPreview
 | 
			
		||||
                anchors.bottom: localPreview.container.bottom
 | 
			
		||||
                anchors.right: localPreview.container.right
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        State {
 | 
			
		||||
            name: "anchor_bottom_left"
 | 
			
		||||
            AnchorChanges {
 | 
			
		||||
                target: localPreview
 | 
			
		||||
                anchors.bottom: localPreview.container.bottom
 | 
			
		||||
                anchors.left: localPreview.container.left
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    transitions: Transition {
 | 
			
		||||
        AnchorAnimation {
 | 
			
		||||
            duration: 250
 | 
			
		||||
            easing.type: Easing.OutBack
 | 
			
		||||
            easing.overshoot: 1.5
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    HoverHandler {
 | 
			
		||||
        id: hoverHandler
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    DragHandler {
 | 
			
		||||
        id: dragHandler
 | 
			
		||||
        readonly property var container: localPreview.container
 | 
			
		||||
        target: parent
 | 
			
		||||
        dragThreshold: 4
 | 
			
		||||
        enabled: !localPreview.hidden
 | 
			
		||||
        xAxis.maximum: container.width - parent.width - previewMargin
 | 
			
		||||
        xAxis.minimum: previewMargin
 | 
			
		||||
        yAxis.maximum: container.height - parent.height - previewMarginYBottom
 | 
			
		||||
        yAxis.minimum: previewMarginYTop
 | 
			
		||||
        onActiveChanged: {
 | 
			
		||||
            if (active) {
 | 
			
		||||
                localPreview.state = "unanchored";
 | 
			
		||||
            } else {
 | 
			
		||||
                const center = Qt.point(target.x + target.width / 2,
 | 
			
		||||
                                        target.y + target.height / 2);
 | 
			
		||||
                const containerCenter = Qt.point(container.x + container.width / 2,
 | 
			
		||||
                                                 container.y + container.height / 2);
 | 
			
		||||
                if (center.x >= containerCenter.x) {
 | 
			
		||||
                    if (center.y >= containerCenter.y) {
 | 
			
		||||
                        localPreview.state = "anchor_bottom_right";
 | 
			
		||||
                    } else {
 | 
			
		||||
                        localPreview.state = "anchor_top_right";
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (center.y >= containerCenter.y) {
 | 
			
		||||
                        localPreview.state = "anchor_bottom_left";
 | 
			
		||||
                    } else {
 | 
			
		||||
                        localPreview.state = "anchor_top_left";
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    layer.enabled: true
 | 
			
		||||
    layer.effect: OpacityMask {
 | 
			
		||||
        maskSource: Rectangle {
 | 
			
		||||
            width: localPreview.width
 | 
			
		||||
            height: localPreview.height
 | 
			
		||||
            radius: JamiTheme.primaryRadius
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -36,34 +36,28 @@ Rectangle {
 | 
			
		||||
    color: "black"
 | 
			
		||||
 | 
			
		||||
    LocalVideo {
 | 
			
		||||
        id: previewRenderer
 | 
			
		||||
        id: localPreview
 | 
			
		||||
        anchors.centerIn: parent
 | 
			
		||||
        anchors.fill: parent
 | 
			
		||||
        visible: !CurrentCall.isAudioOnly && CurrentAccount.videoEnabled_Video && VideoDevices.listSize !== 0 && ((CurrentCall.status >= Call.Status.INCOMING_RINGING && CurrentCall.status <= Call.Status.SEARCHING) || CurrentCall.status === Call.Status.CONNECTED)
 | 
			
		||||
        opacity: 0.5
 | 
			
		||||
 | 
			
		||||
        // HACK: this is a workaround to the preview video starting
 | 
			
		||||
        // and stopping a few times. The root cause should be investigated ASAP.
 | 
			
		||||
        Timer {
 | 
			
		||||
            id: controlPreview
 | 
			
		||||
            property bool startVideo
 | 
			
		||||
            interval: 1000
 | 
			
		||||
            onTriggered: {
 | 
			
		||||
                var rendId = visible && startVideo ? VideoDevices.getDefaultDevice() : "";
 | 
			
		||||
                previewRenderer.startWithId(rendId);
 | 
			
		||||
        readonly property bool start: {
 | 
			
		||||
            if (CurrentCall.isAudioOnly || !CurrentAccount.videoEnabled_Video) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        onVisibleChanged: {
 | 
			
		||||
            controlPreview.stop();
 | 
			
		||||
            if (visible) {
 | 
			
		||||
                controlPreview.startVideo = true;
 | 
			
		||||
                controlPreview.interval = 1000;
 | 
			
		||||
            } else {
 | 
			
		||||
                controlPreview.startVideo = false;
 | 
			
		||||
                controlPreview.interval = 0;
 | 
			
		||||
            if (!VideoDevices.listSize) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            controlPreview.start();
 | 
			
		||||
            const isCallStatusEligible =
 | 
			
		||||
                (CurrentCall.status >= Call.Status.INCOMING_RINGING &&
 | 
			
		||||
                 CurrentCall.status <= Call.Status.SEARCHING) ||
 | 
			
		||||
                CurrentCall.status === Call.Status.CONNECTED;
 | 
			
		||||
            if (!isCallStatusEligible) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        onStartChanged: localPreview.startWithId(start ? VideoDevices.getDefaultDevice() : "")
 | 
			
		||||
        opacity: 0.5
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ListModel {
 | 
			
		||||
 | 
			
		||||
@ -20,9 +20,11 @@
 | 
			
		||||
 */
 | 
			
		||||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
 | 
			
		||||
import net.jami.Models 1.1
 | 
			
		||||
import net.jami.Adapters 1.1
 | 
			
		||||
import net.jami.Constants 1.1
 | 
			
		||||
 | 
			
		||||
import "../../commoncomponents"
 | 
			
		||||
 | 
			
		||||
Item {
 | 
			
		||||
@ -61,9 +63,15 @@ Item {
 | 
			
		||||
 | 
			
		||||
    // (un)subscribe to an app-wide mouse move event trap filtered
 | 
			
		||||
    // for the overlay's geometry
 | 
			
		||||
    onVisibleChanged: {
 | 
			
		||||
        visible ? CallOverlayModel.registerFilter(appWindow, this) : CallOverlayModel.unregisterFilter(appWindow, this);
 | 
			
		||||
    function setupFilter() {
 | 
			
		||||
        if (visible) {
 | 
			
		||||
            CallOverlayModel.registerFilter(appWindow, this);
 | 
			
		||||
        } else {
 | 
			
		||||
            CallOverlayModel.unregisterFilter(appWindow, this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    Component.onCompleted: setupFilter()
 | 
			
		||||
    onVisibleChanged: setupFilter()
 | 
			
		||||
 | 
			
		||||
    Connections {
 | 
			
		||||
        target: CallOverlayModel
 | 
			
		||||
@ -131,8 +139,8 @@ Item {
 | 
			
		||||
        anchors.left: parent.left
 | 
			
		||||
        anchors.right: parent.right
 | 
			
		||||
        // QWK: spacing
 | 
			
		||||
        anchors.leftMargin: qwkSystemButtonSpacing.left
 | 
			
		||||
        anchors.rightMargin: qwkSystemButtonSpacing.right
 | 
			
		||||
        anchors.leftMargin: layoutManager.qwkSystemButtonSpacing.left
 | 
			
		||||
        anchors.rightMargin: layoutManager.qwkSystemButtonSpacing.right
 | 
			
		||||
 | 
			
		||||
        RowLayout {
 | 
			
		||||
            anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
@ -193,7 +193,6 @@ RowLayout {
 | 
			
		||||
                    sendMessageButtonClicked();
 | 
			
		||||
                }
 | 
			
		||||
                onTextChanged: {
 | 
			
		||||
                    MessagesAdapter.userIsComposing(text ? true : false);
 | 
			
		||||
                    if (!text) {
 | 
			
		||||
                        messageBarTextArea.heightBinding();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -171,6 +171,7 @@ JamiFlickable {
 | 
			
		||||
        textFormat: TextEdit.PlainText
 | 
			
		||||
        placeholderTextColor: JamiTheme.messageBarPlaceholderTextColor
 | 
			
		||||
        horizontalAlignment: Text.AlignLeft
 | 
			
		||||
        property var cacheText: ""
 | 
			
		||||
 | 
			
		||||
        background: Rectangle {
 | 
			
		||||
            border.width: 0
 | 
			
		||||
@ -183,10 +184,10 @@ JamiFlickable {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onTextChanged: {
 | 
			
		||||
            if (text)
 | 
			
		||||
                MessagesAdapter.userIsComposing(true);
 | 
			
		||||
            else
 | 
			
		||||
                MessagesAdapter.userIsComposing(false);
 | 
			
		||||
            if (text != cacheText) {
 | 
			
		||||
                cacheText = text;
 | 
			
		||||
                MessagesAdapter.userIsComposing(text ? true : false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Intercept paste event to use C++ QMimeData
 | 
			
		||||
 | 
			
		||||
@ -147,7 +147,7 @@ JamiListView {
 | 
			
		||||
        id: overlay
 | 
			
		||||
        anchors.fill: parent
 | 
			
		||||
        color: JamiTheme.chatviewBgColor
 | 
			
		||||
        visible: opacity !== 0
 | 
			
		||||
        visible: opacity > 0
 | 
			
		||||
        SequentialAnimation {
 | 
			
		||||
            id: fadeAnimation
 | 
			
		||||
            NumberAnimation {
 | 
			
		||||
 | 
			
		||||
@ -30,22 +30,12 @@ import "../../commoncomponents"
 | 
			
		||||
Rectangle {
 | 
			
		||||
    id: root
 | 
			
		||||
 | 
			
		||||
    property point clickPos
 | 
			
		||||
    property int previewMargin: 15
 | 
			
		||||
    property int previewMarginYTop: previewMargin + 42
 | 
			
		||||
    property int previewMarginYBottom: previewMargin + 84
 | 
			
		||||
    property int previewToX: 0
 | 
			
		||||
    property int previewToY: 0
 | 
			
		||||
    property alias chatViewContainer: chatViewContainer
 | 
			
		||||
    property string callPreviewId
 | 
			
		||||
 | 
			
		||||
    // A link to the first child will provide access to the chat view.
 | 
			
		||||
    property var chatView: chatViewContainer.children[0]
 | 
			
		||||
 | 
			
		||||
    onCallPreviewIdChanged: {
 | 
			
		||||
        controlPreview.start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    color: "black"
 | 
			
		||||
 | 
			
		||||
    Connections {
 | 
			
		||||
@ -92,42 +82,6 @@ Rectangle {
 | 
			
		||||
        callOverlay.closeContextMenuAndRelatedWindows();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function previewMagneticSnap() {
 | 
			
		||||
        // Calculate the position where the previewRenderer should attach to.
 | 
			
		||||
        var previewRendererCenter = Qt.point(previewRenderer.x + previewRenderer.width / 2, previewRenderer.y + previewRenderer.height / 2);
 | 
			
		||||
        var parentCenter = Qt.point(parent.x + parent.width / 2, parent.y + parent.height / 2);
 | 
			
		||||
        if (previewRendererCenter.x >= parentCenter.x) {
 | 
			
		||||
            if (previewRendererCenter.y >= parentCenter.y) {
 | 
			
		||||
                // Bottom right.
 | 
			
		||||
                previewToX = Qt.binding(function () {
 | 
			
		||||
                        return callPageMainRect.width - previewRenderer.width - previewMargin;
 | 
			
		||||
                    });
 | 
			
		||||
                previewToY = Qt.binding(function () {
 | 
			
		||||
                        return callPageMainRect.height - previewRenderer.height - previewMarginYBottom;
 | 
			
		||||
                    });
 | 
			
		||||
            } else {
 | 
			
		||||
                // Top right.
 | 
			
		||||
                previewToX = Qt.binding(function () {
 | 
			
		||||
                        return callPageMainRect.width - previewRenderer.width - previewMargin;
 | 
			
		||||
                    });
 | 
			
		||||
                previewToY = previewMarginYTop;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (previewRendererCenter.y >= parentCenter.y) {
 | 
			
		||||
                // Bottom left.
 | 
			
		||||
                previewToX = previewMargin;
 | 
			
		||||
                previewToY = Qt.binding(function () {
 | 
			
		||||
                        return callPageMainRect.height - previewRenderer.height - previewMarginYBottom;
 | 
			
		||||
                    });
 | 
			
		||||
            } else {
 | 
			
		||||
                // Top left.
 | 
			
		||||
                previewToX = previewMargin;
 | 
			
		||||
                previewToY = previewMarginYTop;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        previewRenderer.state = "geoChanging";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onWidthChanged: {
 | 
			
		||||
        if (chatViewContainer.visible && root.width < JamiTheme.mainViewPaneMinWidth * 2) {
 | 
			
		||||
            callPageMainRect.visible = false;
 | 
			
		||||
@ -168,8 +122,8 @@ Rectangle {
 | 
			
		||||
 | 
			
		||||
                onTapped: function (eventPoint, button) {
 | 
			
		||||
                    if (button === Qt.RightButton) {
 | 
			
		||||
                        var isOnLocal = eventPoint.position.x >= previewRenderer.x && eventPoint.position.x <= previewRenderer.x + previewRenderer.width;
 | 
			
		||||
                        isOnLocal &= eventPoint.position.y >= previewRenderer.y && eventPoint.position.y <= previewRenderer.y + previewRenderer.height;
 | 
			
		||||
                        var isOnLocal = eventPoint.position.x >= localPreview.x && eventPoint.position.x <= localPreview.x + localPreview.width;
 | 
			
		||||
                        isOnLocal &= eventPoint.position.y >= localPreview.y && eventPoint.position.y <= localPreview.y + localPreview.height;
 | 
			
		||||
                        isOnLocal |= participantsLayer.hoveredOverlaySinkId.indexOf("camera://") === 0;
 | 
			
		||||
                        callOverlay.openCallViewContextMenuInPos(eventPoint.position.x, eventPoint.position.y, participantsLayer.hoveredOverlayUri, participantsLayer.hoveredOverlaySinkId, participantsLayer.hoveredOverVideoMuted, isOnLocal);
 | 
			
		||||
                    }
 | 
			
		||||
@ -207,99 +161,15 @@ Rectangle {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LocalVideo {
 | 
			
		||||
                id: previewRenderer
 | 
			
		||||
                visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference
 | 
			
		||||
            // Note: this component should not be used within a layout, as
 | 
			
		||||
            // it implements anchor management itself.
 | 
			
		||||
            InCallLocalVideo {
 | 
			
		||||
                id: localPreview
 | 
			
		||||
                objectName: "localPreview"
 | 
			
		||||
 | 
			
		||||
                height: width * invAspectRatio
 | 
			
		||||
                width: Math.max(callPageMainRect.width / 5, JamiTheme.minimumPreviewWidth)
 | 
			
		||||
                x: callPageMainRect.width - previewRenderer.width - previewMargin
 | 
			
		||||
                y: previewMarginYTop
 | 
			
		||||
                flip: CurrentCall.flipSelf && !CurrentCall.isSharing
 | 
			
		||||
 | 
			
		||||
                // HACK: this is a workaround to the preview video starting
 | 
			
		||||
                // and stopping a few times. The root cause should be investigated ASAP.
 | 
			
		||||
                Timer {
 | 
			
		||||
                    id: controlPreview
 | 
			
		||||
                    property bool startVideo
 | 
			
		||||
                    interval: 1000
 | 
			
		||||
                    onTriggered: {
 | 
			
		||||
                        var rendId = visible && startVideo ? root.callPreviewId : "";
 | 
			
		||||
                        previewRenderer.startWithId(rendId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                onVisibleChanged: {
 | 
			
		||||
                    controlPreview.stop();
 | 
			
		||||
                    if (visible) {
 | 
			
		||||
                        controlPreview.startVideo = true;
 | 
			
		||||
                        controlPreview.interval = 1000;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        controlPreview.startVideo = false;
 | 
			
		||||
                        controlPreview.interval = 0;
 | 
			
		||||
                    }
 | 
			
		||||
                    controlPreview.start();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                states: [
 | 
			
		||||
                    State {
 | 
			
		||||
                        name: "geoChanging"
 | 
			
		||||
                        PropertyChanges {
 | 
			
		||||
                            target: previewRenderer
 | 
			
		||||
                            x: previewToX
 | 
			
		||||
                            y: previewToY
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
                transitions: Transition {
 | 
			
		||||
                    PropertyAnimation {
 | 
			
		||||
                        properties: "x,y"
 | 
			
		||||
                        easing.type: Easing.OutExpo
 | 
			
		||||
                        duration: 250
 | 
			
		||||
 | 
			
		||||
                        onStopped: {
 | 
			
		||||
                            previewRenderer.state = "";
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                MouseArea {
 | 
			
		||||
                    id: dragMouseArea
 | 
			
		||||
 | 
			
		||||
                    anchors.fill: previewRenderer
 | 
			
		||||
 | 
			
		||||
                    onPressed: function (mouse) {
 | 
			
		||||
                        clickPos = Qt.point(mouse.x, mouse.y);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    onReleased: {
 | 
			
		||||
                        previewRenderer.state = "";
 | 
			
		||||
                        previewMagneticSnap();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    onPositionChanged: function (mouse) {
 | 
			
		||||
                        // Calculate mouse position relative change.
 | 
			
		||||
                        var delta = Qt.point(mouse.x - clickPos.x, mouse.y - clickPos.y);
 | 
			
		||||
                        var deltaW = previewRenderer.x + delta.x + previewRenderer.width;
 | 
			
		||||
                        var deltaH = previewRenderer.y + delta.y + previewRenderer.height;
 | 
			
		||||
 | 
			
		||||
                        // Check if the previewRenderer exceeds the border of callPageMainRect.
 | 
			
		||||
                        if (deltaW < callPageMainRect.width && previewRenderer.x + delta.x > 1)
 | 
			
		||||
                            previewRenderer.x += delta.x;
 | 
			
		||||
                        if (deltaH < callPageMainRect.height && previewRenderer.y + delta.y > 1)
 | 
			
		||||
                            previewRenderer.y += delta.y;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                layer.enabled: true
 | 
			
		||||
                layer.effect: OpacityMask {
 | 
			
		||||
                    maskSource: Rectangle {
 | 
			
		||||
                        width: previewRenderer.width
 | 
			
		||||
                        height: previewRenderer.height
 | 
			
		||||
                        radius: JamiTheme.primaryRadius
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                container: parent
 | 
			
		||||
                rendererId: CurrentCall.previewId
 | 
			
		||||
                opacityModifier: callOverlay.mainOverlayOpacity
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CallOverlay {
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,14 @@ Rectangle {
 | 
			
		||||
            Label {
 | 
			
		||||
                id: username
 | 
			
		||||
 | 
			
		||||
                text: author === CurrentAccount.uri ? CurrentAccount.bestName : UtilsAdapter.getBestNameForUri(CurrentAccount.id, author)
 | 
			
		||||
                wrapMode: Text.NoWrap
 | 
			
		||||
                text: textMetricsUsername.elidedText
 | 
			
		||||
                TextMetrics {
 | 
			
		||||
                    id: textMetricsUsername
 | 
			
		||||
                    text: author === CurrentAccount.uri ? CurrentAccount.bestName : UtilsAdapter.getBestNameForUri(CurrentAccount.id, author)
 | 
			
		||||
                    elideWidth: 200
 | 
			
		||||
                    elide: Qt.ElideMiddle
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                color: UtilsAdapter.luma(root.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
 | 
			
		||||
                font.pointSize: JamiTheme.textFontSize
 | 
			
		||||
 | 
			
		||||
@ -341,7 +341,7 @@ SidePanelBase {
 | 
			
		||||
                Layout.fillWidth: true
 | 
			
		||||
                Layout.leftMargin: 15
 | 
			
		||||
                Layout.rightMargin: 15
 | 
			
		||||
                Layout.topMargin: 10
 | 
			
		||||
                Layout.topMargin: sidePanelTabBar.visible ? 10 : 0
 | 
			
		||||
 | 
			
		||||
                visible: JamiQmlUtils.isDonationBannerVisible
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -21,12 +21,10 @@ import QtQuick
 | 
			
		||||
import QtQuick.Controls
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Qt5Compat.GraphicalEffects
 | 
			
		||||
 | 
			
		||||
import net.jami.Adapters 1.1
 | 
			
		||||
import net.jami.Constants 1.1
 | 
			
		||||
import net.jami.Enums 1.1
 | 
			
		||||
import net.jami.Models 1.1
 | 
			
		||||
 | 
			
		||||
import "../../commoncomponents"
 | 
			
		||||
 | 
			
		||||
ItemDelegate {
 | 
			
		||||
@ -40,9 +38,7 @@ ItemDelegate {
 | 
			
		||||
 | 
			
		||||
    highlighted: ListView.isCurrentItem
 | 
			
		||||
    property bool interactive: true
 | 
			
		||||
    property string lastInteractionDate: LastInteractionTimeStamp === undefined
 | 
			
		||||
                                         ? ""
 | 
			
		||||
                                         : LastInteractionTimeStamp
 | 
			
		||||
    property string lastInteractionDate: LastInteractionTimeStamp === undefined ? "" : LastInteractionTimeStamp
 | 
			
		||||
 | 
			
		||||
    property string lastInteractionFormattedDate: MessagesAdapter.getBestFormattedDate(lastInteractionDate)
 | 
			
		||||
 | 
			
		||||
@ -51,25 +47,25 @@ ItemDelegate {
 | 
			
		||||
 | 
			
		||||
    Connections {
 | 
			
		||||
        target: PositionManager
 | 
			
		||||
        function onPositionShareConvIdsCountChanged () {
 | 
			
		||||
            root.showSharePositionIndicator = PositionManager.isPositionSharedToConv(accountId, UID)
 | 
			
		||||
        function onPositionShareConvIdsCountChanged() {
 | 
			
		||||
            root.showSharePositionIndicator = PositionManager.isPositionSharedToConv(accountId, UID);
 | 
			
		||||
        }
 | 
			
		||||
        function onSharingUrisCountChanged () {
 | 
			
		||||
            root.showSharedPositionIndicator = PositionManager.isConvSharingPosition(accountId, UID)
 | 
			
		||||
        function onSharingUrisCountChanged() {
 | 
			
		||||
            root.showSharedPositionIndicator = PositionManager.isConvSharingPosition(accountId, UID);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Connections {
 | 
			
		||||
        target: MessagesAdapter
 | 
			
		||||
        function onTimestampUpdated() {
 | 
			
		||||
            lastInteractionFormattedDate = MessagesAdapter.getBestFormattedDate(lastInteractionDate)
 | 
			
		||||
            lastInteractionFormattedDate = MessagesAdapter.getBestFormattedDate(lastInteractionDate);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Component.onCompleted: {
 | 
			
		||||
        // Store to avoid undefined at the end
 | 
			
		||||
        root.accountId = Qt.binding(() => CurrentAccount.id)
 | 
			
		||||
        root.convId = UID
 | 
			
		||||
        root.accountId = Qt.binding(() => CurrentAccount.id);
 | 
			
		||||
        root.convId = UID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RowLayout {
 | 
			
		||||
@ -80,8 +76,10 @@ ItemDelegate {
 | 
			
		||||
 | 
			
		||||
        ConversationAvatar {
 | 
			
		||||
            id: avatar
 | 
			
		||||
            objectName: "smartlistItemDelegateAvatar"
 | 
			
		||||
 | 
			
		||||
            imageId: UID
 | 
			
		||||
            presenceStatus: Presence
 | 
			
		||||
            showPresenceIndicator: Presence !== undefined ? Presence : false
 | 
			
		||||
 | 
			
		||||
            Layout.preferredWidth: JamiTheme.smartListAvatarSize
 | 
			
		||||
@ -111,7 +109,6 @@ ItemDelegate {
 | 
			
		||||
                    source: JamiResources.check_black_24dp_svg
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ColumnLayout {
 | 
			
		||||
@ -133,10 +130,7 @@ ItemDelegate {
 | 
			
		||||
                color: JamiTheme.textColor
 | 
			
		||||
            }
 | 
			
		||||
            RowLayout {
 | 
			
		||||
                visible: ContactType !== Profile.Type.TEMPORARY
 | 
			
		||||
                         && !IsBanned
 | 
			
		||||
                         && lastInteractionFormattedDate !== undefined
 | 
			
		||||
                         && interactive
 | 
			
		||||
                visible: ContactType !== Profile.Type.TEMPORARY && !IsBanned && lastInteractionFormattedDate !== undefined && interactive
 | 
			
		||||
                Layout.fillWidth: true
 | 
			
		||||
                Layout.minimumHeight: 20
 | 
			
		||||
                Layout.alignment: Qt.AlignTop
 | 
			
		||||
@ -157,9 +151,7 @@ ItemDelegate {
 | 
			
		||||
                    Layout.fillWidth: true
 | 
			
		||||
                    Layout.alignment: Qt.AlignVCenter
 | 
			
		||||
                    horizontalAlignment: Text.AlignLeft
 | 
			
		||||
                    text: Draft ?
 | 
			
		||||
                              Draft :
 | 
			
		||||
                              (LastInteraction === undefined ? "" : LastInteraction)
 | 
			
		||||
                    text: Draft ? Draft : (LastInteraction === undefined ? "" : LastInteraction)
 | 
			
		||||
                    textFormat: TextEdit.PlainText
 | 
			
		||||
                    font.pointSize: JamiTheme.smallFontSize
 | 
			
		||||
                    font.weight: UnreadMessagesCount ? Font.Normal : Font.Light
 | 
			
		||||
@ -222,7 +214,7 @@ ItemDelegate {
 | 
			
		||||
            Text {
 | 
			
		||||
                id: callStatusText
 | 
			
		||||
 | 
			
		||||
                visible : text
 | 
			
		||||
                visible: text
 | 
			
		||||
                Layout.minimumHeight: 20
 | 
			
		||||
                Layout.alignment: Qt.AlignRight
 | 
			
		||||
                text: InCall ? UtilsAdapter.getCallStatusStr(CallState) : ""
 | 
			
		||||
@ -247,51 +239,47 @@ ItemDelegate {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Accessible.role: Accessible.Button
 | 
			
		||||
        Accessible.name: Title === undefined? "" : Title
 | 
			
		||||
        Accessible.description: LastInteraction === undefined? "" : LastInteraction
 | 
			
		||||
        Accessible.name: Title === undefined ? "" : Title
 | 
			
		||||
        Accessible.description: LastInteraction === undefined ? "" : LastInteraction
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    background: Rectangle {
 | 
			
		||||
        color: {
 | 
			
		||||
            if (root.pressed || root.highlighted)
 | 
			
		||||
                return JamiTheme.smartListSelectedColor
 | 
			
		||||
                return JamiTheme.smartListSelectedColor;
 | 
			
		||||
            else if (root.hovered)
 | 
			
		||||
                return JamiTheme.smartListHoveredColor
 | 
			
		||||
                return JamiTheme.smartListHoveredColor;
 | 
			
		||||
            else
 | 
			
		||||
                return "transparent"
 | 
			
		||||
                return "transparent";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onClicked: {
 | 
			
		||||
        if (!interactive) {
 | 
			
		||||
            highlighted = !highlighted
 | 
			
		||||
            highlighted = !highlighted;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        ListView.view.model.select(index)
 | 
			
		||||
        ListView.view.model.select(index);
 | 
			
		||||
    }
 | 
			
		||||
    onDoubleClicked: {
 | 
			
		||||
        if (!interactive)
 | 
			
		||||
            return;
 | 
			
		||||
        ListView.view.model.select(index)
 | 
			
		||||
        ListView.view.model.select(index);
 | 
			
		||||
        if (CurrentConversation.isSwarm && !CurrentConversation.isCoreDialog && !UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
 | 
			
		||||
            return; // For now disable calls for swarm with multiple participants
 | 
			
		||||
        if (LRCInstance.currentAccountType === Profile.Type.SIP || !CurrentAccount.videoEnabled_Video)
 | 
			
		||||
            CallAdapter.placeAudioOnlyCall()
 | 
			
		||||
            CallAdapter.placeAudioOnlyCall();
 | 
			
		||||
        else {
 | 
			
		||||
            if (!CurrentConversation.readOnly) {
 | 
			
		||||
                CallAdapter.placeCall()
 | 
			
		||||
                CallAdapter.placeCall();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    onPressAndHold: {
 | 
			
		||||
        if (!interactive)
 | 
			
		||||
            return;
 | 
			
		||||
        ListView.view.openContextMenuAt(pressX, pressY, root)
 | 
			
		||||
        ListView.view.openContextMenuAt(pressX, pressY, root);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MouseArea {
 | 
			
		||||
@ -299,7 +287,7 @@ ItemDelegate {
 | 
			
		||||
        enabled: interactive
 | 
			
		||||
        acceptedButtons: Qt.RightButton
 | 
			
		||||
        onClicked: function (mouse) {
 | 
			
		||||
            root.ListView.view.openContextMenuAt(mouse.x, mouse.y, root)
 | 
			
		||||
            root.ListView.view.openContextMenuAt(mouse.x, mouse.y, root);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -624,7 +624,8 @@ Rectangle {
 | 
			
		||||
                            opacity: (MemberRole === Member.Role.INVITED || MemberRole === Member.Role.BANNED) ? 0.5 : 1
 | 
			
		||||
 | 
			
		||||
                            imageId: CurrentAccount.uri === MemberUri ? CurrentAccount.id : MemberUri
 | 
			
		||||
                            showPresenceIndicator: UtilsAdapter.getContactPresence(CurrentAccount.id, MemberUri)
 | 
			
		||||
                            presenceStatus: UtilsAdapter.getContactPresence(CurrentAccount.id, MemberUri)
 | 
			
		||||
                            showPresenceIndicator: presenceStatus > 0
 | 
			
		||||
                            mode: CurrentAccount.uri === MemberUri ? Avatar.Mode.Account : Avatar.Mode.Contact
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@
 | 
			
		||||
#include "previewengine.h"
 | 
			
		||||
 | 
			
		||||
#include <api/datatransfermodel.h>
 | 
			
		||||
#include <api/contact.h>
 | 
			
		||||
 | 
			
		||||
#include <QApplication>
 | 
			
		||||
#include <QBuffer>
 | 
			
		||||
@ -390,10 +391,8 @@ MessagesAdapter::dataForInteraction(const QString& interactionId, int role) cons
 | 
			
		||||
void
 | 
			
		||||
MessagesAdapter::userIsComposing(bool isComposing)
 | 
			
		||||
{
 | 
			
		||||
    if (!settingsManager_->getValue(Settings::Key::EnableTypingIndicator).toBool()
 | 
			
		||||
        || lrcInstance_->get_selectedConvUid().isEmpty()) {
 | 
			
		||||
    if (lrcInstance_->get_selectedConvUid().isEmpty())
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    lrcInstance_->getCurrentConversationModel()->setIsComposing(lrcInstance_->get_selectedConvUid(),
 | 
			
		||||
                                                                isComposing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -36,11 +36,18 @@
 | 
			
		||||
class FilteredMsgListModel final : public QSortFilterProxyModel
 | 
			
		||||
{
 | 
			
		||||
    Q_OBJECT
 | 
			
		||||
    Q_PROPERTY(int count READ count NOTIFY countChanged)
 | 
			
		||||
public:
 | 
			
		||||
    explicit FilteredMsgListModel(QObject* parent = nullptr)
 | 
			
		||||
        : QSortFilterProxyModel(parent)
 | 
			
		||||
    {
 | 
			
		||||
        sort(0, Qt::AscendingOrder);
 | 
			
		||||
 | 
			
		||||
        connect(this, &QAbstractItemModel::rowsInserted, this, &FilteredMsgListModel::countChanged);
 | 
			
		||||
        connect(this, &QAbstractItemModel::rowsRemoved, this, &FilteredMsgListModel::countChanged);
 | 
			
		||||
        connect(this, &QAbstractItemModel::modelReset, this, &FilteredMsgListModel::countChanged);
 | 
			
		||||
        connect(this, &QAbstractItemModel::layoutChanged, this, &FilteredMsgListModel::countChanged);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
 | 
			
		||||
    {
 | 
			
		||||
@ -60,6 +67,22 @@ public:
 | 
			
		||||
        auto index = mapFromSource(sourceModel()->index(sourceRow, 0));
 | 
			
		||||
        return index.row();
 | 
			
		||||
    };
 | 
			
		||||
    Q_INVOKABLE QVariantMap get(int row) const
 | 
			
		||||
    {
 | 
			
		||||
        QVariantMap map;
 | 
			
		||||
        QModelIndex modelIndex = index(row, 0);
 | 
			
		||||
        QHash<int, QByteArray> roles = roleNames();
 | 
			
		||||
        for (QHash<int, QByteArray>::const_iterator it = roles.begin(); it != roles.end(); ++it)
 | 
			
		||||
            map.insert(it.value(), data(modelIndex, it.key()));
 | 
			
		||||
        return map;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int count() const
 | 
			
		||||
    {
 | 
			
		||||
        return rowCount();
 | 
			
		||||
    }
 | 
			
		||||
Q_SIGNALS:
 | 
			
		||||
    void countChanged();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MessagesAdapter final : public QmlAdapterBase
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,8 @@
 | 
			
		||||
 | 
			
		||||
#include "lrcinstance.h"
 | 
			
		||||
 | 
			
		||||
#include <api/contact.h>
 | 
			
		||||
 | 
			
		||||
ModeratorListModel::ModeratorListModel(LRCInstance* instance, QObject* parent)
 | 
			
		||||
    : AbstractListModelBase(parent)
 | 
			
		||||
{
 | 
			
		||||
@ -133,4 +135,4 @@ ModeratorListModel::connectAccount()
 | 
			
		||||
{
 | 
			
		||||
    if (!lrcInstance_->get_currentAccountId().isEmpty())
 | 
			
		||||
        reset();
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,12 @@ Item {
 | 
			
		||||
        return accountCreationInputParaObject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For list models (1 column).
 | 
			
		||||
    function getModelData(model, index, role) {
 | 
			
		||||
        const modelIndex = model.index(index, 0);
 | 
			
		||||
        return model.data(modelIndex, role);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MessageBar buttons in mainview points
 | 
			
		||||
    property var mainViewRectObj
 | 
			
		||||
    property var messageBarButtonsRowObj
 | 
			
		||||
 | 
			
		||||
@ -162,6 +162,7 @@ Item {
 | 
			
		||||
    property string videoRTPMaxPort: qsTr("Video RTP maximum port")
 | 
			
		||||
 | 
			
		||||
    // AdvancedOpenDHTSettings
 | 
			
		||||
    property string dhtPortUsed: qsTr("Current DHT port used")
 | 
			
		||||
    property string enablePeerDiscovery: qsTr("Enable local peer discovery")
 | 
			
		||||
    property string tooltipPeerDiscovery: qsTr("Connect to other DHT nodes advertising on your local network.")
 | 
			
		||||
    property string openDHTConfig: qsTr("OpenDHT configuration")
 | 
			
		||||
@ -785,6 +786,8 @@ Item {
 | 
			
		||||
    property string becomeHostOneCall: qsTr("Host only this call")
 | 
			
		||||
    property string hostThisCall: qsTr("Host this call")
 | 
			
		||||
    property string becomeDefaultHost: qsTr("Make me the default host for future calls")
 | 
			
		||||
    property string showLocalVideo: qsTr("Show local video")
 | 
			
		||||
    property string hideLocalVideo: qsTr("Hide local video")
 | 
			
		||||
 | 
			
		||||
    // Invitation View
 | 
			
		||||
    property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.")
 | 
			
		||||
 | 
			
		||||
@ -389,7 +389,7 @@ Item {
 | 
			
		||||
    property int mosaicButtonPreferredWidth: 70
 | 
			
		||||
    property int mosaicButtonMaxWidth: 100
 | 
			
		||||
    property real avatarPresenceRatio: 0.26
 | 
			
		||||
    property int avatarReadReceiptSize: 18
 | 
			
		||||
    property int avatarReadReceiptSize: 15
 | 
			
		||||
 | 
			
		||||
    property int menuItemsPreferredWidth: 220
 | 
			
		||||
    property int menuItemsPreferredHeight: 36
 | 
			
		||||
@ -588,6 +588,7 @@ Item {
 | 
			
		||||
 | 
			
		||||
    property real cornerIconSize: 40
 | 
			
		||||
 | 
			
		||||
    property color sending: darkTheme ? "#8c8c8c" : "#7f7f7f"
 | 
			
		||||
    property color wizardIconColor: darkTheme ? "#8c8c8c" : "#7f7f7f"
 | 
			
		||||
 | 
			
		||||
    // InfoBox
 | 
			
		||||
 | 
			
		||||
@ -21,9 +21,71 @@
 | 
			
		||||
#include <QImage>
 | 
			
		||||
#include <QObject>
 | 
			
		||||
#include <QQuickImageProvider>
 | 
			
		||||
#include <QThreadPool>
 | 
			
		||||
 | 
			
		||||
class LRCInstance;
 | 
			
		||||
 | 
			
		||||
class AsyncImageResponseRunnable : public QObject, public QRunnable
 | 
			
		||||
{
 | 
			
		||||
    Q_OBJECT
 | 
			
		||||
public:
 | 
			
		||||
    AsyncImageResponseRunnable(const QString& id,
 | 
			
		||||
                               const QSize& requestedSize,
 | 
			
		||||
                               LRCInstance* lrcInstance)
 | 
			
		||||
        : id_(id)
 | 
			
		||||
        , requestedSize_(requestedSize)
 | 
			
		||||
        , lrcInstance_(lrcInstance)
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
    Q_SIGNAL void done(QImage image);
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    QString id_;
 | 
			
		||||
    QSize requestedSize_;
 | 
			
		||||
    LRCInstance* lrcInstance_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename T_Runnable>
 | 
			
		||||
class AsyncImageResponse : public QQuickImageResponse
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
    AsyncImageResponse(const QString& id,
 | 
			
		||||
                       const QSize& requestedSize,
 | 
			
		||||
                       QThreadPool* pool,
 | 
			
		||||
                       LRCInstance* instance)
 | 
			
		||||
    {
 | 
			
		||||
        auto runnable = new T_Runnable(id, requestedSize, instance);
 | 
			
		||||
        connect(runnable, &T_Runnable::done, this, &AsyncImageResponse::handleDone);
 | 
			
		||||
        pool->start(runnable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void handleDone(QImage image)
 | 
			
		||||
    {
 | 
			
		||||
        image_ = image;
 | 
			
		||||
        Q_EMIT finished();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    QQuickTextureFactory* textureFactory() const override
 | 
			
		||||
    {
 | 
			
		||||
        return QQuickTextureFactory::textureFactoryForImage(image_);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    QImage image_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class AsyncImageProviderBase : public QQuickAsyncImageProvider
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
    AsyncImageProviderBase(LRCInstance* instance = nullptr)
 | 
			
		||||
        : QQuickAsyncImageProvider()
 | 
			
		||||
        , lrcInstance_(instance)
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    LRCInstance* lrcInstance_ {nullptr};
 | 
			
		||||
    QThreadPool pool_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class QuickImageProviderBase : public QQuickImageProvider
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
@ -35,6 +97,5 @@ public:
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    // LRCInstance pointer
 | 
			
		||||
    LRCInstance* lrcInstance_ {nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										520
									
								
								src/app/screencastportal.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										520
									
								
								src/app/screencastportal.cpp
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,520 @@
 | 
			
		||||
/*!
 | 
			
		||||
 * Copyright (C) 2024 Savoir-faire Linux Inc.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#include "screencastportal.h"
 | 
			
		||||
 | 
			
		||||
#include <QDebug>
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
 | 
			
		||||
#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/%s"
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * PipeWire supported cursor modes
 | 
			
		||||
 */
 | 
			
		||||
enum PortalCursorMode {
 | 
			
		||||
    PORTAL_CURSOR_MODE_HIDDEN = 1 << 0,
 | 
			
		||||
    PORTAL_CURSOR_MODE_EMBEDDED = 1 << 1,
 | 
			
		||||
    PORTAL_CURSOR_MODE_METADATA = 1 << 2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Helper function to allow getPipewireFd to stop and return an error
 | 
			
		||||
 * code if a DBus operation/callback fails.
 | 
			
		||||
 */
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::abort(int error, const char* message)
 | 
			
		||||
{
 | 
			
		||||
    portal_error = error;
 | 
			
		||||
    qWarning() << "Aborting:" << message;
 | 
			
		||||
 | 
			
		||||
    if (glib_main_loop && g_main_loop_is_running(glib_main_loop)) {
 | 
			
		||||
        g_main_loop_quit(glib_main_loop);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Callback to free a DbusCallData object's memory and unsubscribe from the
 | 
			
		||||
 * associated dbus signal.
 | 
			
		||||
 */
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::dbusCallDataFree(DbusCallData* ptr_dbus_call_data)
 | 
			
		||||
{
 | 
			
		||||
    if (!ptr_dbus_call_data)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    if (ptr_dbus_call_data->signal_id)
 | 
			
		||||
        g_dbus_connection_signal_unsubscribe(ptr_dbus_call_data->portal->connection,
 | 
			
		||||
                                             ptr_dbus_call_data->signal_id);
 | 
			
		||||
 | 
			
		||||
    g_clear_pointer(&ptr_dbus_call_data->request_path, g_free);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DbusCallData*
 | 
			
		||||
ScreenCastPortal::subscribeToSignal(const char* path, GDBusSignalCallback callback)
 | 
			
		||||
{
 | 
			
		||||
    DbusCallData* ptr_dbus_call_data = new DbusCallData;
 | 
			
		||||
 | 
			
		||||
    ptr_dbus_call_data->portal = this;
 | 
			
		||||
    ptr_dbus_call_data->request_path = g_strdup(path);
 | 
			
		||||
    ptr_dbus_call_data->signal_id
 | 
			
		||||
        = g_dbus_connection_signal_subscribe(connection,
 | 
			
		||||
                                             "org.freedesktop.portal.Desktop" /*sender*/,
 | 
			
		||||
                                             "org.freedesktop.portal.Request" /*interface_name*/,
 | 
			
		||||
                                             "Response" /*member: dbus signal name*/,
 | 
			
		||||
                                             ptr_dbus_call_data->request_path /*object_path*/,
 | 
			
		||||
                                             NULL,
 | 
			
		||||
                                             G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
 | 
			
		||||
                                             callback,
 | 
			
		||||
                                             ptr_dbus_call_data,
 | 
			
		||||
                                             NULL);
 | 
			
		||||
    return ptr_dbus_call_data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::openPipewireRemote()
 | 
			
		||||
{
 | 
			
		||||
    GUnixFDList* fd_list = NULL;
 | 
			
		||||
    GVariant* result = NULL;
 | 
			
		||||
    GError* error = NULL;
 | 
			
		||||
    int fd_index;
 | 
			
		||||
    GVariantBuilder builder;
 | 
			
		||||
 | 
			
		||||
    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
 | 
			
		||||
 | 
			
		||||
    result = g_dbus_proxy_call_with_unix_fd_list_sync(proxy,
 | 
			
		||||
                                                      "OpenPipeWireRemote",
 | 
			
		||||
                                                      g_variant_new("(oa{sv})",
 | 
			
		||||
                                                                    session_handle,
 | 
			
		||||
                                                                    &builder),
 | 
			
		||||
                                                      G_DBUS_CALL_FLAGS_NONE,
 | 
			
		||||
                                                      -1,
 | 
			
		||||
                                                      NULL,
 | 
			
		||||
                                                      &fd_list,
 | 
			
		||||
                                                      NULL,
 | 
			
		||||
                                                      &error);
 | 
			
		||||
    if (error)
 | 
			
		||||
        goto fail;
 | 
			
		||||
 | 
			
		||||
    g_variant_get(result, "(h)", &fd_index);
 | 
			
		||||
    g_variant_unref(result);
 | 
			
		||||
 | 
			
		||||
    pipewireFd = g_unix_fd_list_get(fd_list, fd_index, &error);
 | 
			
		||||
    g_object_unref(fd_list);
 | 
			
		||||
    if (error)
 | 
			
		||||
        goto fail;
 | 
			
		||||
 | 
			
		||||
    g_main_loop_quit(glib_main_loop);
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
fail:
 | 
			
		||||
    qWarning() << "Error retrieving PipeWire fd:" << error->message;
 | 
			
		||||
    g_error_free(error);
 | 
			
		||||
    abort(EIO, "Failed to open PipeWire remote");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::onStartResponseReceivedCallback(GDBusConnection* connection,
 | 
			
		||||
                                                  const char* sender_name,
 | 
			
		||||
                                                  const char* object_path,
 | 
			
		||||
                                                  const char* interface_name,
 | 
			
		||||
                                                  const char* signal_name,
 | 
			
		||||
                                                  GVariant* parameters,
 | 
			
		||||
                                                  gpointer user_data)
 | 
			
		||||
{
 | 
			
		||||
    GVariant* stream_properties = NULL;
 | 
			
		||||
    GVariant* streams = NULL;
 | 
			
		||||
    GVariant* result = NULL;
 | 
			
		||||
    GVariantIter iter;
 | 
			
		||||
    uint32_t response;
 | 
			
		||||
 | 
			
		||||
    DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
 | 
			
		||||
    ScreenCastPortal* portal = ptr_dbus_call_data->portal;
 | 
			
		||||
 | 
			
		||||
    g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
 | 
			
		||||
 | 
			
		||||
    g_variant_get(parameters, "(u@a{sv})", &response, &result);
 | 
			
		||||
 | 
			
		||||
    if (response) {
 | 
			
		||||
        g_variant_unref(result);
 | 
			
		||||
        portal->abort(EACCES, "Failed to start screencast, denied or cancelled by user");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    streams = g_variant_lookup_value(result, "streams", G_VARIANT_TYPE_ARRAY);
 | 
			
		||||
 | 
			
		||||
    g_variant_iter_init(&iter, streams);
 | 
			
		||||
 | 
			
		||||
    g_variant_iter_loop(&iter, "(u@a{sv})", &portal->pipewireNode, &stream_properties);
 | 
			
		||||
 | 
			
		||||
    qInfo() << "Monitor selected, setting up screencast\n";
 | 
			
		||||
 | 
			
		||||
    g_variant_unref(result);
 | 
			
		||||
    g_variant_unref(streams);
 | 
			
		||||
    g_variant_unref(stream_properties);
 | 
			
		||||
 | 
			
		||||
    portal->openPipewireRemote();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int
 | 
			
		||||
ScreenCastPortal::callDBusMethod(const gchar* method_name, GVariant* parameters)
 | 
			
		||||
{
 | 
			
		||||
    GVariant* result;
 | 
			
		||||
    GError* error = NULL;
 | 
			
		||||
 | 
			
		||||
    result = g_dbus_proxy_call_sync(proxy,
 | 
			
		||||
                                    method_name,
 | 
			
		||||
                                    parameters,
 | 
			
		||||
                                    G_DBUS_CALL_FLAGS_NONE,
 | 
			
		||||
                                    -1,
 | 
			
		||||
                                    NULL,
 | 
			
		||||
                                    &error);
 | 
			
		||||
    if (error) {
 | 
			
		||||
        qWarning() << "Call to DBus method" << method_name << "failed:" << error->message;
 | 
			
		||||
        g_error_free(error);
 | 
			
		||||
        return EIO;
 | 
			
		||||
    }
 | 
			
		||||
    g_variant_unref(result);
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::start()
 | 
			
		||||
{
 | 
			
		||||
    int ret;
 | 
			
		||||
    const char* request_token;
 | 
			
		||||
    g_autofree char* request_path;
 | 
			
		||||
    GVariantBuilder builder;
 | 
			
		||||
    GVariant* parameters;
 | 
			
		||||
    struct DbusCallData* ptr_dbus_call_data;
 | 
			
		||||
 | 
			
		||||
    request_token = "pipewiregrabStart";
 | 
			
		||||
    request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
 | 
			
		||||
 | 
			
		||||
    qInfo() << "Asking for monitor...";
 | 
			
		||||
 | 
			
		||||
    ptr_dbus_call_data = subscribeToSignal(request_path, onStartResponseReceivedCallback);
 | 
			
		||||
    if (!ptr_dbus_call_data) {
 | 
			
		||||
        abort(ENOMEM, "Failed to allocate DBus call data");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
 | 
			
		||||
    g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
 | 
			
		||||
    parameters = g_variant_new("(osa{sv})", session_handle, "", &builder);
 | 
			
		||||
 | 
			
		||||
    ret = callDBusMethod("Start", parameters);
 | 
			
		||||
    if (ret != 0)
 | 
			
		||||
        abort(ret, "Failed to start screen cast session");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::onSelectSourcesResponseReceivedCallback(GDBusConnection* connection,
 | 
			
		||||
                                                          const char* sender_name,
 | 
			
		||||
                                                          const char* object_path,
 | 
			
		||||
                                                          const char* interface_name,
 | 
			
		||||
                                                          const char* signal_name,
 | 
			
		||||
                                                          GVariant* parameters,
 | 
			
		||||
                                                          gpointer user_data)
 | 
			
		||||
{
 | 
			
		||||
    GVariant* ret = NULL;
 | 
			
		||||
    uint32_t response;
 | 
			
		||||
    struct DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
 | 
			
		||||
    ScreenCastPortal* portal = ptr_dbus_call_data->portal;
 | 
			
		||||
 | 
			
		||||
    g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
 | 
			
		||||
 | 
			
		||||
    g_variant_get(parameters, "(u@a{sv})", &response, &ret);
 | 
			
		||||
    g_variant_unref(ret);
 | 
			
		||||
    if (response) {
 | 
			
		||||
        portal->abort(EACCES, "Failed to select screencast sources, denied or cancelled by user");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    portal->start();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::selectSources()
 | 
			
		||||
{
 | 
			
		||||
    int ret;
 | 
			
		||||
    const char* request_token;
 | 
			
		||||
    g_autofree char* request_path;
 | 
			
		||||
    GVariantBuilder builder;
 | 
			
		||||
    GVariant* parameters;
 | 
			
		||||
    struct DbusCallData* ptr_dbus_call_data;
 | 
			
		||||
 | 
			
		||||
    request_token = "pipewiregrabSelectSources";
 | 
			
		||||
    request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
 | 
			
		||||
 | 
			
		||||
    ptr_dbus_call_data = subscribeToSignal(request_path, onSelectSourcesResponseReceivedCallback);
 | 
			
		||||
    if (!ptr_dbus_call_data) {
 | 
			
		||||
        abort(ENOMEM, "Failed to allocate DBus call data");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
 | 
			
		||||
    g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(capture_type));
 | 
			
		||||
    g_variant_builder_add(&builder, "{sv}", "multiple", g_variant_new_boolean(FALSE));
 | 
			
		||||
    g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
 | 
			
		||||
 | 
			
		||||
    if ((available_cursor_modes & PORTAL_CURSOR_MODE_EMBEDDED) && draw_mouse)
 | 
			
		||||
        g_variant_builder_add(&builder,
 | 
			
		||||
                              "{sv}",
 | 
			
		||||
                              "cursor_mode",
 | 
			
		||||
                              g_variant_new_uint32(PORTAL_CURSOR_MODE_EMBEDDED));
 | 
			
		||||
    else
 | 
			
		||||
        g_variant_builder_add(&builder,
 | 
			
		||||
                              "{sv}",
 | 
			
		||||
                              "cursor_mode",
 | 
			
		||||
                              g_variant_new_uint32(PORTAL_CURSOR_MODE_HIDDEN));
 | 
			
		||||
    parameters = g_variant_new("(oa{sv})", session_handle, &builder);
 | 
			
		||||
 | 
			
		||||
    ret = callDBusMethod("SelectSources", parameters);
 | 
			
		||||
    if (ret != 0)
 | 
			
		||||
        abort(ret, "Failed to select sources for screen cast session");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::onCreateSessionResponseReceivedCallback(GDBusConnection* connection,
 | 
			
		||||
                                                          const char* sender_name,
 | 
			
		||||
                                                          const char* object_path,
 | 
			
		||||
                                                          const char* interface_name,
 | 
			
		||||
                                                          const char* signal_name,
 | 
			
		||||
                                                          GVariant* parameters,
 | 
			
		||||
                                                          gpointer user_data)
 | 
			
		||||
{
 | 
			
		||||
    uint32_t response;
 | 
			
		||||
    GVariant* result = NULL;
 | 
			
		||||
    DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
 | 
			
		||||
    ScreenCastPortal* portal = ptr_dbus_call_data->portal;
 | 
			
		||||
 | 
			
		||||
    g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
 | 
			
		||||
 | 
			
		||||
    g_variant_get(parameters, "(u@a{sv})", &response, &result);
 | 
			
		||||
 | 
			
		||||
    if (response != 0) {
 | 
			
		||||
        g_variant_unref(result);
 | 
			
		||||
        portal->abort(EACCES, "Failed to create screencast session, denied or cancelled by user");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    qDebug() << "Screencast session created";
 | 
			
		||||
 | 
			
		||||
    g_variant_lookup(result, "session_handle", "s", &portal->session_handle);
 | 
			
		||||
    g_variant_unref(result);
 | 
			
		||||
 | 
			
		||||
    portal->selectSources();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::createSession()
 | 
			
		||||
{
 | 
			
		||||
    int ret;
 | 
			
		||||
    GVariantBuilder builder;
 | 
			
		||||
    GVariant* parameters;
 | 
			
		||||
    const char* request_token;
 | 
			
		||||
    g_autofree char* request_path;
 | 
			
		||||
    DbusCallData* ptr_dbus_call_data;
 | 
			
		||||
 | 
			
		||||
    request_token = "pipewiregrabCreateSession";
 | 
			
		||||
    request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
 | 
			
		||||
 | 
			
		||||
    ptr_dbus_call_data = subscribeToSignal(request_path, onCreateSessionResponseReceivedCallback);
 | 
			
		||||
    if (!ptr_dbus_call_data) {
 | 
			
		||||
        abort(ENOMEM, "Failed to allocate DBus call data");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
 | 
			
		||||
    g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
 | 
			
		||||
    g_variant_builder_add(&builder,
 | 
			
		||||
                          "{sv}",
 | 
			
		||||
                          "session_handle_token",
 | 
			
		||||
                          g_variant_new_string("pipewiregrab"));
 | 
			
		||||
    parameters = g_variant_new("(a{sv})", &builder);
 | 
			
		||||
 | 
			
		||||
    ret = callDBusMethod("CreateSession", parameters);
 | 
			
		||||
    if (ret != 0)
 | 
			
		||||
        abort(ret, "Failed to create screen cast session");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Helper function: get available cursor modes and update the
 | 
			
		||||
 *                  PipewireGrabContext accordingly
 | 
			
		||||
 */
 | 
			
		||||
void
 | 
			
		||||
ScreenCastPortal::updateAvailableCursorModes()
 | 
			
		||||
{
 | 
			
		||||
    GVariant* cached_cursor_modes = NULL;
 | 
			
		||||
 | 
			
		||||
    cached_cursor_modes = g_dbus_proxy_get_cached_property(proxy, "AvailableCursorModes");
 | 
			
		||||
    available_cursor_modes = cached_cursor_modes ? g_variant_get_uint32(cached_cursor_modes) : 0;
 | 
			
		||||
 | 
			
		||||
    // Only use embedded or hidden mode for now
 | 
			
		||||
    available_cursor_modes &= PORTAL_CURSOR_MODE_EMBEDDED | PORTAL_CURSOR_MODE_HIDDEN;
 | 
			
		||||
 | 
			
		||||
    g_variant_unref(cached_cursor_modes);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int
 | 
			
		||||
ScreenCastPortal::createDBusProxy()
 | 
			
		||||
{
 | 
			
		||||
    GError* error = NULL;
 | 
			
		||||
 | 
			
		||||
    proxy = g_dbus_proxy_new_sync(connection,
 | 
			
		||||
                                  G_DBUS_PROXY_FLAGS_NONE,
 | 
			
		||||
                                  NULL,
 | 
			
		||||
                                  "org.freedesktop.portal.Desktop",
 | 
			
		||||
                                  "/org/freedesktop/portal/desktop",
 | 
			
		||||
                                  "org.freedesktop.portal.ScreenCast",
 | 
			
		||||
                                  NULL,
 | 
			
		||||
                                  &error);
 | 
			
		||||
    if (error) {
 | 
			
		||||
        qWarning() << "Error creating proxy:" << error->message;
 | 
			
		||||
        g_error_free(error);
 | 
			
		||||
        return EPERM;
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Create DBus connection and related objects
 | 
			
		||||
 */
 | 
			
		||||
int
 | 
			
		||||
ScreenCastPortal::createDBusConnection()
 | 
			
		||||
{
 | 
			
		||||
    char* aux;
 | 
			
		||||
    GError* error = NULL;
 | 
			
		||||
 | 
			
		||||
    connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
 | 
			
		||||
    if (error) {
 | 
			
		||||
        qWarning() << "Error getting session bus:" << error->message;
 | 
			
		||||
        g_error_free(error);
 | 
			
		||||
        return EPERM;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sender_name = g_strdup(g_dbus_connection_get_unique_name(connection) + 1);
 | 
			
		||||
    while ((aux = g_strstr_len(sender_name, -1, ".")) != NULL)
 | 
			
		||||
        *aux = '_';
 | 
			
		||||
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Use XDG Desktop Portal's ScreenCast interface to open a file descriptor that
 | 
			
		||||
 * can be used by PipeWire to access the screen cast streams.
 | 
			
		||||
 * (https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html)
 | 
			
		||||
 */
 | 
			
		||||
int
 | 
			
		||||
ScreenCastPortal::getPipewireFd()
 | 
			
		||||
{
 | 
			
		||||
    int ret = 0;
 | 
			
		||||
    GMainContext* glib_main_context;
 | 
			
		||||
 | 
			
		||||
    // Create a new GLib context and set it as the default for the current thread.
 | 
			
		||||
    // This ensures that the callbacks from DBus operations started in this thread are
 | 
			
		||||
    // handled by the GLib main loop defined below, even if pipewiregrab_init was
 | 
			
		||||
    // called by a program which also uses GLib and already had its own main loop running.
 | 
			
		||||
    glib_main_context = g_main_context_new();
 | 
			
		||||
    g_main_context_push_thread_default(glib_main_context);
 | 
			
		||||
    glib_main_loop = g_main_loop_new(glib_main_context, FALSE);
 | 
			
		||||
    if (!glib_main_loop) {
 | 
			
		||||
        qWarning() << "g_main_loop_new failed!";
 | 
			
		||||
        ret = ENOMEM;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ret = createDBusConnection();
 | 
			
		||||
    if (ret != 0)
 | 
			
		||||
        goto exit_glib_loop;
 | 
			
		||||
 | 
			
		||||
    ret = createDBusProxy();
 | 
			
		||||
    if (ret != 0)
 | 
			
		||||
        goto exit_glib_loop;
 | 
			
		||||
 | 
			
		||||
    updateAvailableCursorModes();
 | 
			
		||||
    createSession();
 | 
			
		||||
    if (portal_error) {
 | 
			
		||||
        ret = portal_error;
 | 
			
		||||
        goto exit_glib_loop;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    g_main_loop_run(glib_main_loop);
 | 
			
		||||
    // The main loop will run until it's stopped by openPipewireRemote (if
 | 
			
		||||
    // all DBus method calls were successfully), abort (in case of error) or
 | 
			
		||||
    // on_cancelled_callback (if a DBus request is cancelled).
 | 
			
		||||
    // In the latter two cases, pw_ctx->portal_error gets set to a nonzero value.
 | 
			
		||||
    if (portal_error)
 | 
			
		||||
        ret = portal_error;
 | 
			
		||||
 | 
			
		||||
exit_glib_loop:
 | 
			
		||||
    g_main_loop_unref(glib_main_loop);
 | 
			
		||||
    glib_main_loop = NULL;
 | 
			
		||||
    g_main_context_pop_thread_default(glib_main_context);
 | 
			
		||||
    g_main_context_unref(glib_main_context);
 | 
			
		||||
 | 
			
		||||
    return ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ScreenCastPortal::ScreenCastPortal(PortalCaptureType captureType)
 | 
			
		||||
    : draw_mouse(true)
 | 
			
		||||
    , pipewireFd(0)
 | 
			
		||||
{
 | 
			
		||||
    switch (captureType) {
 | 
			
		||||
    case PortalCaptureType::SCREEN:
 | 
			
		||||
        capture_type = 1;
 | 
			
		||||
        break;
 | 
			
		||||
    case PortalCaptureType::WINDOW:
 | 
			
		||||
        capture_type = 2;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ScreenCastPortal::~ScreenCastPortal()
 | 
			
		||||
{
 | 
			
		||||
    if (session_handle) {
 | 
			
		||||
        g_dbus_connection_call(connection,
 | 
			
		||||
                               "org.freedesktop.portal.Desktop",
 | 
			
		||||
                               session_handle,
 | 
			
		||||
                               "org.freedesktop.portal.Session",
 | 
			
		||||
                               "Close",
 | 
			
		||||
                               NULL,
 | 
			
		||||
                               NULL,
 | 
			
		||||
                               G_DBUS_CALL_FLAGS_NONE,
 | 
			
		||||
                               -1,
 | 
			
		||||
                               NULL,
 | 
			
		||||
                               NULL,
 | 
			
		||||
                               NULL);
 | 
			
		||||
 | 
			
		||||
        g_clear_pointer(&session_handle, g_free);
 | 
			
		||||
    }
 | 
			
		||||
    g_clear_object(&connection);
 | 
			
		||||
    g_clear_object(&proxy);
 | 
			
		||||
    g_clear_pointer(&sender_name, g_free);
 | 
			
		||||
 | 
			
		||||
#ifndef ENABLE_LIBWRAP
 | 
			
		||||
    // If the daemon is running as a separate process, then it can't directly use the
 | 
			
		||||
    // PipeWire file descriptor opened by the client, so it will have to duplicate it.
 | 
			
		||||
    // The duplicated file descriptor will be closed by the daemon, but the original
 | 
			
		||||
    // file descriptor needs to be closed by the client.
 | 
			
		||||
    if (close(pipewireFd) != 0) {
 | 
			
		||||
        int err = errno;
 | 
			
		||||
        qWarning() << "Error while attempting to close PipeWire file descriptor: errno =" << err;
 | 
			
		||||
    } else {
 | 
			
		||||
        qInfo() << "Successfully closed PipeWire file descriptor";
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								src/app/screencastportal.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/app/screencastportal.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
			
		||||
/*!
 | 
			
		||||
 * Copyright (C) 2024 Savoir-faire Linux Inc.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation; either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <QString>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <gio/gio.h>
 | 
			
		||||
#include <gio/gunixfdlist.h>
 | 
			
		||||
 | 
			
		||||
enum class PortalCaptureType {
 | 
			
		||||
    SCREEN = 1,
 | 
			
		||||
    WINDOW = 2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct DbusCallData;
 | 
			
		||||
 | 
			
		||||
class ScreenCastPortal
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
    ScreenCastPortal(PortalCaptureType captureType);
 | 
			
		||||
    ~ScreenCastPortal();
 | 
			
		||||
    int getPipewireFd();
 | 
			
		||||
    int pipewireFd;
 | 
			
		||||
    uint32_t pipewireNode = 0;
 | 
			
		||||
    QString videoInputId;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void createSession();
 | 
			
		||||
    void selectSources();
 | 
			
		||||
    void start();
 | 
			
		||||
    void openPipewireRemote();
 | 
			
		||||
    void abort(int error, const char* message);
 | 
			
		||||
 | 
			
		||||
    static void onCreateSessionResponseReceivedCallback(GDBusConnection* connection,
 | 
			
		||||
                                                        const char* sender_name,
 | 
			
		||||
                                                        const char* object_path,
 | 
			
		||||
                                                        const char* interface_name,
 | 
			
		||||
                                                        const char* signal_name,
 | 
			
		||||
                                                        GVariant* parameters,
 | 
			
		||||
                                                        gpointer user_data);
 | 
			
		||||
    static void onSelectSourcesResponseReceivedCallback(GDBusConnection* connection,
 | 
			
		||||
                                                        const char* sender_name,
 | 
			
		||||
                                                        const char* object_path,
 | 
			
		||||
                                                        const char* interface_name,
 | 
			
		||||
                                                        const char* signal_name,
 | 
			
		||||
                                                        GVariant* parameters,
 | 
			
		||||
                                                        gpointer user_data);
 | 
			
		||||
    static void onStartResponseReceivedCallback(GDBusConnection* connection,
 | 
			
		||||
                                                const char* sender_name,
 | 
			
		||||
                                                const char* object_path,
 | 
			
		||||
                                                const char* interface_name,
 | 
			
		||||
                                                const char* signal_name,
 | 
			
		||||
                                                GVariant* parameters,
 | 
			
		||||
                                                gpointer user_data);
 | 
			
		||||
 | 
			
		||||
    int callDBusMethod(const gchar* method_name, GVariant* parameters);
 | 
			
		||||
    int createDBusProxy();
 | 
			
		||||
    int createDBusConnection();
 | 
			
		||||
    void updateAvailableCursorModes();
 | 
			
		||||
    DbusCallData* subscribeToSignal(const char* path, GDBusSignalCallback callback);
 | 
			
		||||
    static void dbusCallDataFree(DbusCallData* ptr_dbus_call_data);
 | 
			
		||||
 | 
			
		||||
    GDBusConnection* connection = nullptr;
 | 
			
		||||
    GDBusProxy* proxy = nullptr;
 | 
			
		||||
 | 
			
		||||
    char* sender_name = nullptr;
 | 
			
		||||
    char* session_handle = nullptr;
 | 
			
		||||
 | 
			
		||||
    uint32_t available_cursor_modes = 0;
 | 
			
		||||
 | 
			
		||||
    GMainLoop* glib_main_loop = nullptr;
 | 
			
		||||
    struct pw_thread_loop* thread_loop = nullptr;
 | 
			
		||||
    struct pw_context* context = nullptr;
 | 
			
		||||
 | 
			
		||||
    guint32 capture_type;
 | 
			
		||||
 | 
			
		||||
    bool draw_mouse;
 | 
			
		||||
 | 
			
		||||
    int portal_error = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct DbusCallData
 | 
			
		||||
{
 | 
			
		||||
    ScreenCastPortal* portal;
 | 
			
		||||
    char* request_path;
 | 
			
		||||
    guint signal_id;
 | 
			
		||||
};
 | 
			
		||||
@ -136,6 +136,7 @@ SelectableListProxyModel::updateSelection(bool rowsRemoved)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Q_EMIT validSelectionChanged();
 | 
			
		||||
    Q_EMIT countChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ class SelectableListProxyModel : public QSortFilterProxyModel
 | 
			
		||||
{
 | 
			
		||||
    Q_OBJECT
 | 
			
		||||
    QML_PROPERTY(int, currentFilteredRow)
 | 
			
		||||
    Q_PROPERTY(int count READ count NOTIFY countChanged)
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    explicit SelectableListProxyModel(QAbstractListModel* model, QObject* parent = nullptr);
 | 
			
		||||
@ -42,11 +43,27 @@ public:
 | 
			
		||||
    Q_INVOKABLE QVariant dataForRow(int row, int role) const;
 | 
			
		||||
    void selectSourceRow(int row);
 | 
			
		||||
 | 
			
		||||
    Q_INVOKABLE QVariantMap get(int row) const
 | 
			
		||||
    {
 | 
			
		||||
        QVariantMap map;
 | 
			
		||||
        QModelIndex modelIndex = index(row, 0);
 | 
			
		||||
        QHash<int, QByteArray> roles = roleNames();
 | 
			
		||||
        for (QHash<int, QByteArray>::const_iterator it = roles.begin(); it != roles.end(); ++it)
 | 
			
		||||
            map.insert(it.value(), data(modelIndex, it.key()));
 | 
			
		||||
        return map;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int count() const
 | 
			
		||||
    {
 | 
			
		||||
        return rowCount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
public Q_SLOTS:
 | 
			
		||||
    void updateSelection(bool rowsRemoved = false);
 | 
			
		||||
 | 
			
		||||
Q_SIGNALS:
 | 
			
		||||
    void validSelectionChanged();
 | 
			
		||||
    void countChanged();
 | 
			
		||||
 | 
			
		||||
private Q_SLOTS:
 | 
			
		||||
    void onModelUpdated();
 | 
			
		||||
 | 
			
		||||
@ -75,11 +75,6 @@ ListSelectionView {
 | 
			
		||||
        // Trigger an update to messages if needed.
 | 
			
		||||
        // Currently needed when changing the show link preview setting.
 | 
			
		||||
        CurrentConversation.reloadInteractions();
 | 
			
		||||
        if (UtilsAdapter.getAccountListSize() === 0) {
 | 
			
		||||
            viewCoordinator.present("WizardView");
 | 
			
		||||
        } else {
 | 
			
		||||
            AccountAdapter.changeAccount(0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    property int selectedMenu: index
 | 
			
		||||
 | 
			
		||||
@ -53,4 +53,17 @@ ColumnLayout {
 | 
			
		||||
 | 
			
		||||
        onSwitchToggled: CurrentAccount.sendReadReceipt = checked
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ToggleSwitch {
 | 
			
		||||
        id: enableTypingIndicatorCheckbox
 | 
			
		||||
 | 
			
		||||
        Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
        checked: CurrentAccount.sendComposing
 | 
			
		||||
        labelText: JamiStrings.enableTypingIndicator
 | 
			
		||||
        descText: JamiStrings.enableTypingIndicatorDescription
 | 
			
		||||
        tooltipText: JamiStrings.enableTypingIndicator
 | 
			
		||||
 | 
			
		||||
        onSwitchToggled: CurrentAccount.sendComposing = checked
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,20 @@ ColumnLayout {
 | 
			
		||||
    ColumnLayout {
 | 
			
		||||
        Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
         SettingSpinBox {
 | 
			
		||||
            id: dhtPortUsed
 | 
			
		||||
 | 
			
		||||
            visible: !root.isSIP
 | 
			
		||||
 | 
			
		||||
            title: JamiStrings.dhtPortUsed
 | 
			
		||||
            itemWidth: root.itemWidth
 | 
			
		||||
            bottomValue: 0
 | 
			
		||||
            topValue: 65535
 | 
			
		||||
 | 
			
		||||
            valueField: CurrentAccount.dhtPort
 | 
			
		||||
            onNewValue: CurrentAccount.dhtPort = valueField
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ToggleSwitch {
 | 
			
		||||
            id: checkAutoConnectOnLocalNetwork
 | 
			
		||||
            visible: !root.isSIP
 | 
			
		||||
 | 
			
		||||
@ -65,19 +65,6 @@ SettingsPageBase {
 | 
			
		||||
                font.kerning: true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ToggleSwitch {
 | 
			
		||||
                id: enableTypingIndicatorCheckbox
 | 
			
		||||
 | 
			
		||||
                Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
                checked: UtilsAdapter.getAppValue(Settings.EnableTypingIndicator)
 | 
			
		||||
                labelText: JamiStrings.enableTypingIndicator
 | 
			
		||||
                descText: JamiStrings.enableTypingIndicatorDescription
 | 
			
		||||
                tooltipText: JamiStrings.enableTypingIndicator
 | 
			
		||||
 | 
			
		||||
                onSwitchToggled: UtilsAdapter.setAppValue(Settings.Key.EnableTypingIndicator, checked)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ToggleSwitch {
 | 
			
		||||
                id: displayImagesCheckbox
 | 
			
		||||
                visible: WITH_WEBENGINE
 | 
			
		||||
@ -258,10 +245,8 @@ SettingsPageBase {
 | 
			
		||||
                acceptTransferBelowSpinBox.valueField = UtilsAdapter.getDefault(Settings.Key.AcceptTransferBelow);
 | 
			
		||||
                UtilsAdapter.setToDefault(Settings.Key.AutoAcceptFiles);
 | 
			
		||||
                UtilsAdapter.setToDefault(Settings.Key.AcceptTransferBelow);
 | 
			
		||||
                UtilsAdapter.setToDefault(Settings.Key.EnableTypingIndicator);
 | 
			
		||||
                UtilsAdapter.setToDefault(Settings.Key.ChatViewEnterIsNewLine);
 | 
			
		||||
                UtilsAdapter.setToDefault(Settings.Key.DisplayHyperlinkPreviews);
 | 
			
		||||
                enableTypingIndicatorCheckbox.checked = UtilsAdapter.getAppValue(Settings.EnableTypingIndicator);
 | 
			
		||||
                displayImagesCheckbox.checked = UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews);
 | 
			
		||||
                if (UtilsAdapter.getAppValue(Settings.Key.ChatViewEnterIsNewLine))
 | 
			
		||||
                    enterButton.checked = true;
 | 
			
		||||
 | 
			
		||||
@ -428,7 +428,7 @@ SettingsPageBase {
 | 
			
		||||
                id: deleteDescription
 | 
			
		||||
 | 
			
		||||
                Layout.alignment: Qt.AlignLeft
 | 
			
		||||
                Layout.preferredWidth: parent.width
 | 
			
		||||
                Layout.fillWidth: true
 | 
			
		||||
 | 
			
		||||
                text: JamiStrings.deleteAccountDescription
 | 
			
		||||
                color: JamiTheme.textColor
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user