Compare commits

..

73 Commits

Author SHA1 Message Date
73aeb02ebd misc: bump daemon submodule
Change-Id: I800aad6362be0124a33904b834ff8e04b560d6a8
2024-04-08 15:11:47 -04:00
9d91317089 snap: add libpipewire to build dependencies
Change-Id: Ie2d24de1aabe59c9506786cfb5fa18fcf4e8cad2
2024-04-07 12:32:32 -04:00
474bc5f6a4 incalllocalvideo: allow resizing/changing opacity of local video
This feature was accidentally removed in a recent refactoring of the
local video preview:
https://review.jami.net/c/jami-client-qt/+/27740

Change-Id: I8b621d4f692124311f0807d1bc0be0e96717a499
2024-04-04 11:39:44 -04:00
f5b64e955b i18n: automatic bump
Change-Id: I0589f432edc46aba5effaaca85f1a53df00760dd
2024-04-01 16:42:26 -04:00
b88627d125 misc: bump daemon
Change-Id: Ifcbfe71e3f9d3ab8966ddfa3af8fb70a4d3b0c7d
2024-03-25 08:57:23 -04:00
200978a044 screensharing: add Wayland support
Change-Id: Ida5516630c6f95b16aa45f31ee8111a924273b3f
2024-03-22 10:47:45 -04:00
a673ff9890 chatviewheader: show details panel when syncing
Change-Id: Ifd0caafc6ec6cf10b0a834875e9dcf6daf527868
2024-03-21 10:01:46 -04:00
7803dd0991 i18n: automatic bump
Change-Id: Ic551429e2416c8ae81640a80788d0fa6412c8653
2024-03-18 16:42:27 -04:00
a8a736bc8c misc: bump daemon submodule
Change-Id: Ifb9bca812499d5b324223dc6fabe1a109ded662d
2024-03-12 17:58:13 -04:00
ff7acf9932 localvideo: refactor preview component device control
Change-Id: Ibcd88c5a3c73a0e67f94d70bc420845aa7b8c822
2024-03-12 16:33:21 -04:00
afde816b23 i18n: automatic bump
Change-Id: I50b14a21c3c442f7dd4b805a018d9af11c2c8305
2024-03-11 16:42:29 -04:00
0745c3b798 misc: bump daemon
Change-Id: I5148eabbe57f708dce6d92673d0a86329999a063
2024-03-06 12:07:47 -05:00
1376ee1f4b MessageBar: avoid multiple composing status changed
Change-Id: I6bd2a7d961228584a74a731beb73b291f6c9a6bf
2024-03-05 15:22:42 -05:00
2b03107bd5 ReadStatus: fix visibility for multiple readers
Change-Id: Iff3ff0a9ff7a8d7b175375582e6bd42937a80b1c
2024-03-05 08:40:14 -05:00
cd1ab0ed12 i18n: automatic bump
Change-Id: I08361b0ed6402bde159bea25f250e47086f28115
2024-03-05 08:10:54 -05:00
a13c6ae0e7 ongoingcallpage: resize + change opacity of local preview
This is an experimental feature and is unadvertised for now.

Change-Id: I40aa84f54c135c2060552231d4ed7c2de4976ca3
2024-03-05 08:10:30 -05:00
1ef9a85148 QWK: enforce a min Qt version of 6.6.2 on Windows
This will prevent a graphical issue present when disabling QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS to avoid the Windows 10 top border issue for the frameless window option. This allows us to remove the temp workaround that likely introduces side effects.

Gitlab: #1581
Change-Id: I36801064d31e2380312d73f88233e8ed9b757403
2024-03-04 15:40:18 -05:00
072eafbaf4 presence: show connected with self
GitLab: #1589
Change-Id: Iaa753a5ed9a690a63bba75442a4e04d1c9d9218c
2024-03-04 13:10:41 -05:00
201f3182ca videoview: prevent stale rendered frame from showing on start
Gitlab: #1576
Change-Id: I85d0b18bd15f531b1d029de1f0a88dea2d34a4c4
2024-02-29 16:10:53 -05:00
23130a5752 misc: bump daemon (contains fixes for Windows build)
Change-Id: If2d7f3879f15dd8e9d54544793e68958a5c16cd8
2024-02-29 16:10:47 -05:00
f28d47bc51 misc: bump daemon
Change-Id: Id32a2a306fd80a2d009f96117f06c8ea860df835
2024-02-29 11:19:14 -05:00
ee7818eefb AccountListModel: avoid calling getAccountList when unneeded
Gitlab: #1459
Change-Id: I962f38935acd8e97895587076a448125213fc4bc
2024-02-29 09:24:35 -05:00
f25e66aa6a utilsadapter: avoid wrong geometry when getting systray visibility
Change-Id: I05d9770b7198e784356c10164c935b0844cd53b0
2024-02-29 09:13:38 -05:00
79b19aec01 conversation: use account config for send typing indicator
git.jami.net/savoirfairelinux/jami-daemon/-/issues/951

Change-Id: I0d952d6916ad5c4a8c51cbb80998f46665af9141
2024-02-29 09:13:06 -05:00
4c92cb9936 chatview: add check for last sent message
Change-Id: I233d5df05432371adf6b2bdf8e8de25bd7e65058
2024-02-29 09:12:58 -05:00
1c81553245 conversationmodel: do not add wrong call to wrong conversation
GitLab: #1578
Change-Id: Ibe980844acf1b44afb0ea6aa0e105ffa99e5c36f
2024-02-29 09:12:32 -05:00
5c2fec53da conversationmodel: avoid to emit needsHost multiple time
Only do it for current account, this avoid to emit needsHost() for
all accounts with the same conversation

GitLab: #1579
Change-Id: I147b2f72fd4c1000949500309eae1970cdbd033f
2024-02-29 09:12:03 -05:00
f706abe5a6 CallMessageDelegate: do not show button to join current call
GitLab: #963
Change-Id: If26b9413e5a94e1b9da0842b84eaf8019b08140f
2024-02-29 08:55:41 -05:00
610c27f751 contactmodel: refactor profile loading
Loading the profile elements from disk is now deferred to consumption. Implements a basic caching mechanism for the displayname and avatar elements.

Gitlab: #1459
Change-Id: Ic5aeec6649c198f617b9269409ded647c7536b8f
2024-02-28 14:53:28 -05:00
6d20d3b515 misc: bump daemon
Change-Id: Ie720ef7deac208a869d32a0a49e6d653d40e7fb0
2024-02-27 15:30:43 -05:00
a0b583aa8d misc: update some strings
Change-Id: I635c527b8a5b83b91f70008ce6471af8f72f6aa9
2024-02-27 15:19:26 -05:00
3855a5e951 chatview: fix footer visibility
Change-Id: I4bb6268547da6549a143da1d64b56f70cdd7dcc7
2024-02-22 15:39:15 -05:00
6689bce782 Revert "callactionbar: add forward call for jami accounts"
This reverts commit ef716d657d.

Reason for revert: Missing a lot of design

Change-Id: I9f289d107dab25251e3de98a64c90446f9bf7c12
2024-02-22 14:58:29 -05:00
860ddf22b6 chatview: hide extra panels if interactions buttons are absent
Change-Id: I8e56fc3e0a456bc214baf321e4c9e60b77004d2c
GitLab: #1476
2024-02-21 15:56:34 -05:00
ef716d657d callactionbar: add forward call for jami accounts
Change-Id: I3559ef5398c73cd7c76196fe3b61cf005bc2408d
2024-02-21 15:56:00 -05:00
b0fe0251d1 Reply: ellide too long display names
GitLab: #1550
Change-Id: I0234d9c6993438fe4580961a85d86632def1c354
2024-02-21 15:55:09 -05:00
1ec2d5f27b conversationmodel: fix insert last interaction for conv request
GitLab: #1571
Change-Id: I848c6f8e1867f552a55105a4d528f51a59676ce4
2024-02-21 15:54:42 -05:00
23316993e5 accountcombobox: remove first line in list
GitLab: #1559
Change-Id: I8bc70c95354546b5c31a376daf07f60a96b1ede0
2024-02-21 15:53:57 -05:00
d42fe78676 presence: fix presence status for swarm with multiple members
Change-Id: Ic2e86b932c4805016689ffc41e4cede26b715954
2024-02-21 15:53:04 -05:00
78724c2a7b call: clarify call messages if call fails
Change-Id: I0dca1ef919cb6f60e53c57c3a3ccf81c2333c231
2024-02-21 15:53:04 -05:00
e14fbe9437 CallMessageDelegate: add icon and follow font-size
GitLab: #1463
Change-Id: I8c61d1c526ddf69ae910627d0804608fd17b5c45
2024-02-21 15:53:04 -05:00
82c63d5a89 tests: add dummy mock data for conversationListView and MessageListView
Change-Id: I64e145754843513a36e7b52dca9be90f2ab7688d
2024-02-21 15:52:56 -05:00
a72af9cba5 tests: add example for mocking datas
Change-Id: I060a991726bc8c1cd57f267d97833dcd04519bab
2024-02-21 15:52:50 -05:00
d7c642a2fe revoke: fix revocation with pwd
GitLab: #1573
Change-Id: I55dacf92ceeeba077b52488835e8d48b8ccd39a2
2024-02-21 15:24:55 -05:00
08f3339693 accountconfig: avoid duplication between dhtPort and DHT.port
Also show it in advanced settings

Change-Id: I8de880657530c4a957846ca334332f7ccf79ef8c
2024-02-21 09:03:36 -05:00
402515365d presence: follow daemon changes for presence indicator
Change-Id: Ie82a15da023ab97f133c8beadb8ddeb81b67666f
2024-02-20 12:47:11 -05:00
df102068bc i18n: automatic bump
Change-Id: I39656f5fe57b37316138680345d9c52a7675a968
2024-02-20 08:08:17 -05:00
d40e884a1f misc: remove wrong logs
Change-Id: I4010660c6bef3af553019deb9bcabc65a4b484fc
2024-02-19 16:55:27 -05:00
5371dac882 misc: use BUILD_TESTING like the daemon and update instructions
Change-Id: I5e92e47ada4c4225c68065179245d96723397575
2024-02-19 16:32:04 -05:00
0f62829588 misc: remove legacy mocking inclusion
Change-Id: Ie187f459b136c36fbb65c21fdaa4f05958244474
2024-02-19 16:31:55 -05:00
39da97396c contactmodel: fix add on second request
GitLab: #1572
Change-Id: If588d22b80ea2f77b21f2ddd081ba32fdffefc7c
2024-02-19 13:50:59 -05:00
406edda453 conversationmodel: fix status update for messages other than text
Change-Id: I0f1cb45a6cca9c8e95366c81a9d7813c740e2987
2024-02-19 13:26:08 -05:00
bbbeda6a26 EmojiPicker: fix reference error
GitLab: #1545
Change-Id: Ie0cdc650a8f1468ed14cb43ccfd223167ddf8b7e
2024-02-19 11:13:03 -05:00
6b3efff7cc misc: use logical or instead of bitwise
No impact on logic. Just clarifies intention and allows short-circuiting.

Change-Id: If18f9d28cf4f4ead58a4f94b0ba0e4514ac9eea1
2024-02-16 08:14:42 -05:00
3531b8b354 ManageAccountPage: fix polish loop
Change-Id: I31c05a8163cc03f8d181b5509e966df348814515
GitLab: #1541
2024-02-15 17:08:33 -05:00
487446cbc3 sidepanel: fix excess margin above donation box
The top margin is unnecessary when the invitations tab is not present.

Change-Id: Ic0aafdd72d3d99f3764eeac72b2efe0c11a604ed
2024-02-15 16:58:48 -05:00
d5349490f5 conversation: follow daemon changes for sending status
cf jami-docs/developer/delivery-status
+ Basically this patch uses the new SwarmMessage.status to show
current interaction status.
+ setRead only updates the status if the interaction is newer (else,
because signal are not guaranteed to be ordered, this can cause the
lastDisplayed_ to be incorrect).
+ Some old code is removed and unused signal
+ MessageReceived updates status if needed

GitLab: #1487
Change-Id: I4d4d5dce8dc12ab638e89e3f8431810b29a72087
2024-02-15 16:15:51 -05:00
7650f45d6f misc: libclient: remove legacy account database migration mechanism
Removes all migration mechanisms and support for database versions that haven't been used for several years. Also cleans up some includes.

Change-Id: Iaf071a455f77dd4daa57f16f9924703961aa64e0
2024-02-15 15:16:00 -05:00
a98f6ca4e3 SettingSpinBox: fix binding loop
Change-Id: I0426d7cea16aedff3dc5ed0f493422ffb091d45e
GitLab: #1543
2024-02-15 14:27:46 -05:00
0b96cf5f1f SBSMessageBase: add text metrics on author
Change-Id: Idf3aee2c667c86ea9a224d68624f733a2250e83a
GitLab: #1551
2024-02-15 14:27:46 -05:00
07e0b10478 tests: avoid initializing the ViewCoordinator root view
This doesn't make much sense with our current test structure, and will add a StackView component on top of the UUT.

Change-Id: Ice3425bfea0b5229c87caf3fa22b181ce6aa520d
2024-02-15 13:33:41 -05:00
b38e216721 ongoingcallpage: local-preview: add a hide-preview feature
Gitlab: #1555
Change-Id: Ifa196b91fed4d13d1cd0acf535cc3e1802c22a29
2024-02-15 10:46:49 -05:00
91f32f2421 ongoingcallpage: refactor local preview corner snapping
Uses a more declarative approach to anchor the local preview.

Change-Id: I2544428a0c2585a8629639566c808dfc2808fd14
2024-02-15 10:46:49 -05:00
06c3ffa6ce testing: add a configuration tool to the dev-testing window
This will allow the addition of custom parameters to a second anchored window.

"Conversation ID" and "Force local preview" are implemented.

Change-Id: I2366b57e6bb36efb568b06e40ef124a440a39397
2024-02-15 10:46:49 -05:00
ae53d92c2e testing: add a way to test individual QML components
This is a WIP and is intended to be adapted continuously to support more and more UI elements and reduce the time spent debugging components.

Some components will require additional configuration (e.g. the conversation ID must be set), which may require additional changes.

Change-Id: Iaa5d49693f874202439e746a274da4911adf7d15
2024-02-15 10:46:49 -05:00
97e477416a misc: bump daemon
Change-Id: Id39ed7d7135b0757f233a00039b59276257e0c6a
2024-02-14 11:10:48 -05:00
3d3b4612df chat-view: fix loading data transfer items
- Avoids manually building local file URIs which was causing long load times for conversations on Windows.

- Fixes an issue where missing images were caused by a interaction updates erasing the message bodies.

Change-Id: I4c65f73cf9f46da5a9ae899940cb205cb34ffae2
2024-02-13 17:06:26 -05:00
7060afe467 build.py: add an argument to add client cmake flags
Change-Id: I6b0eae47d4fd52935cc4cef02d79115b80f3d809
2024-02-13 11:27:06 -05:00
f56026439a i18n: automatic bump
Change-Id: Id74cec7e9ff47a6bc53864ef53788bb697b9f9a8
2024-02-12 16:42:45 -05:00
0a24bec5ec 3rdparty: md4c: bump version + turn off building md2html executable
Change-Id: Ib7c978e2b5ea8e16115c8818afc387988c13d72a
2024-02-12 15:09:39 -05:00
38b7880d5f misc: improve logging for the client and libclient projects
- Declares global logging categories for libclient and the app
- Introduces some macros for categorized logging
- Removes the noisy namedirectory logs by default
- Logs file/line number URIs in debug mode

Change-Id: I9dadadc6e93ef91cc70d206b7225aeb7a06f8773
2024-02-12 11:14:17 -05:00
71a88b75ab misc: bump daemon submodule
Change-Id: I9971b5bf1236387f42663b5ef0193f4ef5b75bb3
2024-02-09 15:00:40 -05:00
37e1780762 snap: update cmake
Change-Id: I89fe8ef4bcf7c2f1f437517d1ea7978435157604
2024-02-09 11:41:13 -05:00
273 changed files with 85579 additions and 84188 deletions

2
3rdparty/md4c vendored

View File

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

View File

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

View File

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

Submodule daemon updated: 302a4322b1...59a8e41ca8

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View 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

View 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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@
#include <clocale>
#ifndef ENABLE_TESTS
#ifndef BUILD_TESTING
int
main(int argc, char* argv[])
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -193,7 +193,6 @@ RowLayout {
sendMessageButtonClicked();
}
onTextChanged: {
MessagesAdapter.userIsComposing(text ? true : false);
if (!text) {
messageBarTextArea.heightBinding();
}

View File

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

View File

@ -147,7 +147,7 @@ JamiListView {
id: overlay
anchors.fill: parent
color: JamiTheme.chatviewBgColor
visible: opacity !== 0
visible: opacity > 0
SequentialAnimation {
id: fadeAnimation
NumberAnimation {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -136,6 +136,7 @@ SelectableListProxyModel::updateSelection(bool rowsRemoved)
}
Q_EMIT validSelectionChanged();
Q_EMIT countChanged();
}
void

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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