Compare commits

..

76 Commits

Author SHA1 Message Date
479da3ca54 misc: bump daemon
Change-Id: I695d57f8feb393a23a3e36b89e03d1c2bb64b86c
2025-04-30 13:37:04 -04:00
02140a11c4 i18n: automatic bump
Change-Id: I13067c46aa61b9fb46ee7c58c23616afac0ef02f
2025-04-30 13:03:09 -04:00
b5dbe23c17 windowsharing: set the preview to 1 FPS
Set the preview of the windows shared at one FPS and allows to set
custom FPS count to a window.

GitLab: #1242
Change-Id: Ia189500267de18d0809d8d4db781c7015623fa8c
2025-04-29 14:27:59 -04:00
ff5f94b34a settings: update devices list when devices changed
When a new device is connected/disconnected update audio devices list in
the media settings.

This commit also bumps jami-daemon to support DevicesChanged events on
Windows.

Gitlab: #689
Gitlab: #1878
Change-Id: Ibb607939897853794fdbd09eab72f378257d458c
2025-04-24 13:14:28 -04:00
def2e19abe packaging: stop using dpkg-sig
The dpkg-sig tool appears to no longer be maintained and, as far as I
can tell, was never actually used by Debian or Ubuntu to verify the
integrity of packages. Both distributions no longer include the dpkg-sig
package in their newest releases [1, 2].

[1] https://tracker.debian.org/pkg/dpkg-sig
[2] https://manpages.ubuntu.com/manpages/jammy/en/man1/dpkg-sig.1.html

Change-Id: I356ff7eafb340a49622cc37899dad7ef84dbd100
2025-04-24 11:35:52 -04:00
69430c4af3 packaging: fix Qt build on Debian testing
Change-Id: I798cdcf297ac3c0b448b4f61e192edd177e7fcbf
2025-04-24 10:13:21 -04:00
911fdbc9e5 frameless: fix participant overlay actions
Fixed click actions on the overlay for group calls.

Gitlab: #1990
Change-Id: I2331c6f8b34f0b2c2a89ecb52bb65c3563d50f17
2025-04-23 14:54:05 -04:00
80b3336f1b misc: bump daemon
Change-Id: I86ab9faec20b702091136063e5e86d70f89c81f7
2025-04-22 13:43:48 -04:00
f8bafd4154 packaging: remove Fedora 37 and 38 (EOL)
Change-Id: I8d25303236781cad8f97b2f6f35ee159d814cdb6
2025-04-22 13:39:48 -04:00
b8b06ccfa1 packaging: remove openSUSE Leap 15.4 (EOL)
Change-Id: I7d4dac8a9a8463e402b4eaff501bbab4fb368661
2025-04-21 10:30:45 -04:00
6cb5a8206a packaging: add openSUSE Leap 15.6
Change-Id: Iacd096705f8e88bc886715d04a378e66c4042ede
2025-04-21 10:30:37 -04:00
ae1a2462e2 packaging: add Ubuntu 25.04
Change-Id: I19a0d3e5de48a4e85822d5fcd00f83af0da16e1a
2025-04-18 11:16:41 -04:00
31581db7f5 misc: bump daemon
Brings in some build fixes. Also prevents applying the qmsetup patch
on Windows, and calls build-windows.py --init with the qt parameter
which is required if when using Qt installed in a non-standard location.

Change-Id: I40facbdc93289792130a15b0f4151f78137bc1ef
2025-04-16 17:16:47 -04:00
c323dcfe13 bump daemon
Change-Id: I30104807a42b70d9e3d4dbbc9baff3ffea9ec923
2025-04-16 13:31:00 -04:00
8a31aca346 JamiStrings.qml: cleanup
other → {source, new}

Change-Id: I60f4b81a8fc2a47395f73768caed383e0a597b64
2025-04-13 03:29:00 -04:00
7eeabbe1c6 i18n: automatic bump
Change-Id: I50397f9858be1db3447afce3ceac6eb8086f8fbc
2025-04-11 16:25:03 -04:00
8c0ecaf3c5 bump daemon
Change-Id: I6d010bcec7e2ae2507349b5982f7c20a80af0bea
2025-04-11 15:48:15 -04:00
c91bff35b6 QWK: fix build issue on Debian/Ubuntu
This commit adds a patch for QWindowKit to ensure that it always
installs its dependency QMSetup in a directory where CMake will be able
to find it. This makes the previous QWindowKit patch unnecessary and
fixes a build issue when packaging Jami on Debian and Ubuntu that
started occurring when QWindowKit was updated in commit 6b70ffc.

GitLab: #1976
Change-Id: I5f23fcbb612aa6d0eb0e77e9a5a006b24a5af082
2025-04-03 09:29:45 -04:00
cb05b4afd0 i18n: automatic bump
Change-Id: Ic2efc297abc2410f951f2bdd185d5327e35829df
2025-03-31 16:42:43 -04:00
4419f7bfbc PopUp: Title margins
Added right margins to PopUp titles.

GitLab: #1968
Change-Id: I5a3ce3eb1abc6e7737acea8cb2ef6271df515f16
2025-03-31 13:34:39 -04:00
deaa15a36e misc: remove deprecated operator
The ""_qs operator (used to create QString literals) was deprecated in
version 6.8 of Qt:
https://doc.qt.io/qt-6/qstring-obsolete.html#operator-22-22_qs

GitLab: #1944
Change-Id: I4fc69f3aae6b1e2096735c3714e899ee3c8203bc
2025-03-30 21:55:29 -04:00
6b70ffcf3e QWK: fix build issue with version 6.8 of Qt
This commit updates QWindowKit in order to fix the following build issue
on Qt 6.8: https://github.com/stdware/qwindowkit/pull/154

GitLab: #1944
Change-Id: Iab82514b51577148ff5dc1ba382f8bb517c94f8a
2025-03-30 21:54:32 -04:00
a407fa2c47 fullscreen: reimplement fullscreen for reliability
Reimplement the fullscreen behavior to only work during calls
and to make it more consistent.

Change-Id: I8fbad30dfcf577f5af7d809073e20105374deb35
2025-03-28 09:32:43 -04:00
3143d60760 ConversationModel: use index in slotCallEnded
Change-Id: I2d2952007ce1c437bac9c96d35c2931816f185ac
2025-03-26 10:31:50 -04:00
73eacd5125 src/app/linkdevicemodel.cpp: cleanup
New device identifier is not recognized.\nPlease follow above instruction. → Unrecognized new device identifier. Please follow the instructions above.

Change-Id: I75a1f89d738629eff031bdd3258435d7b72a4fce
2025-03-25 18:12:38 -04:00
fc70ddc6dc src/app/currentconversation.cpp: cleanup
Change-Id: I9514660fc4a5b39232fb4b293b5f7957338185f4
2025-03-25 17:05:41 -04:00
e2e5a0c8cc RTL: RecordBox buttons
Adapted radius for RTL.

GitLab: #1967
Change-Id: Ic14b089f12d4a50227036b800353654a54584cc1
2025-03-25 11:28:14 -04:00
dd9ed8d57d i18n: automatic bump
Change-Id: If32bee03cbac617649bbded29bf732f907aed5d3
2025-03-24 16:42:45 -04:00
7eea1484c5 misc: bump daemon
Change-Id: I789d0ed1a76ebe3364e6fddae69393164d80b72d
2025-03-21 15:27:24 -04:00
6fac40340b messageparser: fix link recognition
The attempted fix in commit 65d3bef was based on the incorrect
assumption that md4c systematically fails to detect links that contain
at least one +, - or _. This commit removes the postprocessing step
added in 65d3bef and instead patches md4c to directly fix its link
recognition algorithm.

GitLab: #1903
GitLab: #1964
Change-Id: Ic1d6302e3250390af66611a16715a6397578abb5
2025-03-21 15:10:15 -04:00
04a1544d56 calls: fix call-action button sub-menu positioning
The submenus are no longer created dynamically, so bindings are
required for positioning.

Gitlab: #1965
Change-Id: Idc2fe77f00ff9b2124f0bad4cc1a8160cd01bf04
2025-03-21 15:07:45 -04:00
91fd8a0295 screen-share: fix the screen/window selection widget
The bug is caused by the computation of `elementWidth` when the list
model length is zero and `showWindows` is true. In this case, division
by zero causes the `elementWidth` to have the value `Infinity`. We
presume that at this point, the component's rendering may take a
while.

This commit refactors the layout to use a GridLayout and a more stable
approach to displaying the delegate components, while removing a
potentially harmful Qt bug [1] workaround.

[1]: https://bugreports.qt.io/browse/QTBUG-110323

Gitlab: #1242
Change-Id: Ib3df1568fdbcdd799fcdcf0b39cb87e3de50dc42
2025-03-21 15:07:45 -04:00
bad5698e71 RTL: JamiIdentifier layout fix
Adapted radius for RTL.

GitLab: #1953
Change-Id: I732cd33b403fea1f115527db19faffe131b259eb
2025-03-21 14:30:40 -04:00
7f0a94dd48 RTL: AboutPopUp fix
Fixed the alignment problem with RTL languages on the About Jami PopUp.

GitLab: #1952
Change-Id: I8262cb658ac3099a113e04574df3e93bd7993f0b
2025-03-21 13:58:56 -04:00
81112ff1f8 RTL: Popup layout fix
Reorganized layout margins for BaseModelDialog and adapted affected components.

GitLab: #1946
Change-Id: I36d0a99fb81fd645cdcdd33a3d10ccea6d4c0424
2025-03-21 13:23:16 -04:00
b63eb384b6 i18n: automatic bump
Change-Id: Ib0106a894896c24d466fd00c1025f1a7072823ff
2025-03-21 13:05:22 -04:00
ebcc60c570 RTL: display Azerbaijani as a RTL language
Azerbaijani is now recognised as an RTL language.

GitLab: #1957
Change-Id: Id1a4f947bf5d0371f558ad5fa507e375c4a2be32
2025-03-21 12:03:28 -04:00
c12a753979 misc: update background image
Homogenizes the image sizes and effect parameters.

Change-Id: Icd8b3968aec7311de5700664f03358c289622a40
2025-03-19 16:11:35 -04:00
386b578e47 UX: fix behavior with lists scrolling offscreen
Fixes behavior introduced in Qt where flickables (list) can scroll
infinitely off screen. Temporary fix to have more polished feel to
the app. Functionality to have a soft overscroll is supposedly fixed
in Qt 6.7 onward.

Change-Id: I031c590e1a351a0fe0d0ea1ea9ed17c24c5c572b
2025-03-19 14:49:25 -04:00
76417edfa4 build: update macOS dependencies
- Enable building natpmp
- Disable building freetype

Change-Id: Iba6c311dff4fb175efbe16cc1f82e51355f5d25e
2025-03-17 12:31:05 -05:00
960fdc0f05 misc: bump daemon
Change-Id: Ic85d1b225778c4e038091e8c1aa04f5706d59543
2025-03-14 17:12:19 -04:00
2567d81359 misc: update release name to Εἰρήνη
Change-Id: Id3d532358c3f5f8b4b17ce85efd0492e51e6877d
2025-03-14 16:49:08 -04:00
e700d8160a downloadprogressbar: fix the implementation
Implement a timer to refresh the download progress bar periodically.

GitLab: #1809
Change-Id: I0c9e142ce5d30c443c4fc6998de3387ec751b1d7
2025-03-14 16:40:26 -04:00
37065cb7d6 theme: reload cached image on theme change
Change-Id: Idf683c3c68c22889fb3e605112167930f2729cd8
2025-03-14 16:23:01 -04:00
03efee4c14 theme: update welcome page background
Change-Id: Ie534479b1af33b4a2d089425c26157a31dc8a5d8
2025-03-14 16:23:01 -04:00
04e43b07f4 RTL: fix GeneralMenuItem text position
Added anchors to improve design for RTL languages

Gitlab: #1949
Change-Id: I2f867a0cda6a9e70ddbb2960a8f466f5ee322421
2025-03-14 15:32:12 -04:00
a00a191371 RTLlanguages: Fix crash when opening settings
Delete the preferedWidth of a row in the accountComboBox to avoid an
infinite polish() loop when accessing the settings with an RTL language.

GitLab: #1945

Change-Id: I37a4a1c61a296743835d677ab242c0fe214e8d16
2025-03-14 14:07:05 -04:00
65d3befad8 messages display: fix the display of links
The links are now postprocessed to compensate for the malfunctions of
md4c. It now displays properly even with a + or - in it. It's also
checked by the unittests.

GitLab: #1903
Change-Id: I0d7f520a7a3fe06fb89107fe6b08f83325218cd4
2025-03-14 11:21:19 -04:00
84ac5dba02 main: fix desktop file name
Commit 49d83fd937 changed the name of
Jami's .desktop file, but didn't make the corresponding change to the
argument passed to setDesktopFileName in the main() function. This
causes a bug where GNOME (and possibly other desktop environments)
doesn't display the correct icon in the taskbar when Jami is running.

GitLab: #1948
Change-Id: I695057979b180777011a9995d799d38f9a4a0487
2025-03-13 16:23:41 -04:00
a149a575a7 cmake: add missing argument to add_subdirectory
We need the EXCLUDE_FROM_ALL argument when adding git submodules via
add_subdirectory in the CMakeLists file. Without it, the "install" make
target for Jami will also install the submodule's files.

GitLab: #1947
Change-Id: I477c4733951ac5a9fa2b6a52a1463c4a585ab8b7
2025-03-13 16:20:29 -04:00
407561732f packaging: add missing submodule
The ZXing-C++ library was recently added to the project as a git
submodule, but some of the packaging scripts were not updated
accordingly.

GitLab: #1947
Change-Id: Ic5324bd807e30bec44c0e07076b1f38903e12895
2025-03-12 15:38:07 -04:00
82c2a9d9c6 link device: correct tooltip for the close button
Change-Id: Ia0057b83b0484c656c33835dd4233e1c5b8fb9ee
2025-03-11 13:17:18 -04:00
1414e1804f link device: fix manual token entry
This patch fixes the export process when a token
is entered manually.

Gitlab: #1695
Change-Id: Ib87a933baa42319bcd17acf10402aafdccf23d1a
2025-03-11 11:09:36 -04:00
7146f20b18 UI/UX: fix link device issues
- Make token copyable
- Fix UI in dark mode
- Remove text animation for export side

Gitlab: #1695
Change-Id: Idb701867ba4998bb75715e598ddac66a5e1ac8ca
2025-03-11 11:09:36 -04:00
5ee4990534 tests: fix arguments for createNewAccount
Change-Id: Ied19a578f1dbe80d468fcf4c5220f2c340cadcab
2025-03-11 10:41:22 -04:00
0d1bdfdfdd link device: fix ui for dark mode
Change-Id: I2d978db411036dab5af45cfc44a7e404d97a177f
2025-03-10 14:52:15 -04:00
f3dd3b4643 chatview: settings: details: fix alignment issue
Fix the alignment of the avatars and text in the details panel

GitLab: #1796
Change-Id: I982ba3982b8128c6e43478146fa80dbfaa8f38cc
2025-03-07 20:53:18 -05:00
d3c76eac8d account: implement export-from-device using new API
- Implements new APIs
- Implements export-from-device mechanism
(link device in account settings)

Gitlab: #1695
Change-Id: I3d3486380e695ea44c199dbe0a0448f724b4d2db
2025-03-07 15:54:21 -05:00
33da15daba account: implement import-from-device using new API
- Implements new APIs
- Implements import-from-device mechanism (creation wizard)
- Minor refactoring of accountmodel and accountadapter

Gitlab: #1695
Change-Id: Ib3c6301b82b19a25320dd703f2f7e941f8048a8e
2025-03-07 15:54:15 -05:00
82c876c0fa message_search: remake component
GitLab: #1827
Change-Id: I877d8a6d15e56c6a8a40ffaa5768ba2812c86d39
2025-03-07 11:08:09 -05:00
83765dcebf view-coordinator: use the view name to destroy dialogs
In the case of a unique view, we need to use the view name used in
the viewmanager to destroy the dialog. This is now provided by the
viewName parameter of the callback used when creating the dialog.

Gitlab: #1938
Change-Id: I3645fc1c7fda44eea9fde8d9d5886647820685ea
2025-03-06 17:14:49 -05:00
b76570b892 conversation model: avoid using database for jami accounts
GitLab: #1794
Change-Id: I48e5e7c42854440f3ee389a7256b8b99a0520eb2
2025-03-06 17:14:37 -05:00
ffb9bb8748 misc: bump daemon
Change-Id: I60872d3444d91c33ce48e477f708fc1f26cc6f13
2025-03-06 10:33:53 -05:00
416137d6dd JamiStrings.qml: merge "clearConversation" with "deleteConversation"
https://review.jami.net/c/jami-client-ios/+/29862

Change-Id: I4a657f73a9240b335cfe04e88f0465abf9a73081
2025-03-05 18:00:31 -05:00
ffcfaffc90 Conversation: add "You left the conversation." string
Follow up to https://review.jami.net/c/jami-client-qt/+/30295

GitLab: #1932

Change-Id: Ic6e78bd3384c9b825a7fd3bc82409d3e6df2b721
2025-03-05 00:06:41 -05:00
a950a3f9e7 appversionmanager: improve version logging
Gitlab: #1934
Change-Id: Ic73b39bb3b6e9c14cbe4b4e0773d744da68f5c72
2025-03-04 15:02:32 -05:00
3c279b292d SimpleMessageDialog.qml: use var for buttonRoles
In practice, the QML engine fails to set arrays of non-variant types
when creating QML components dynamically.

Gitlab: #1934
Change-Id: I5e9c0e73dd35c088bdaf070e04cf73c009ea9099
2025-03-04 15:01:57 -05:00
c19af7f97f networkmanager: prevent generic network error when canceling download
To avoid multiple popups when canceling download, we avoid emitting the
`CANCELLED` signal directly, and rely on the QNetworkReply error handler
to emit the `CANCELLED` signal after translating the error triggered by
aborting the download.

Gitlab: #1934
Change-Id: I87bc1404405a9140b52c2c43d2aeb3501e06aec7
2025-03-04 15:01:09 -05:00
0087f1b8a8 UpdateDownloadDialog.qml: set autoClose to false
The `autoClose` property was set to the default value of `true`.
This causes an issue where if a user refocuses the Jami window, the
update dialog closes and cancels the update.

Gitlab: #1934
Change-Id: Ia7a90875702a0998de71133ed2215fb0583e161e
2025-03-04 14:06:10 -05:00
19f7f43912 crashreportclient: add build variant metadata
This will make it easier to reproduce the build that crashed.

Note: we should be able to use the build ID (build timestamp), but the
git tag is not the same as the build ID currently. This is a problem and
should be fixed in the future.

Gitlab: #1454
Change-Id: I8c6e25a685421398eef3052a9f48681ac369926c
2025-03-04 12:53:32 -05:00
c818eeedce MainApplicationWindow.qml: remove unused import of QtQuick.Dialogs
The module is not used anywhere in the file and breaks the client
on Windows.

GitLab: #1905
Change-Id: I7e4f6e3516a3fd88c7e150b3eeb70cab823d388b
2025-03-04 12:50:03 -05:00
0f08dbcf59 misc: bump daemon
Change-Id: I8743f9c6b0fcbd7cf187d78018420f2b94e5eb01
2025-03-04 11:41:11 -05:00
7d3331d235 i18n: automatic bump
Change-Id: Icf3c5ad4bf2757b2f16368d62f4a0bd837c4f602
2025-03-03 16:42:42 -05:00
aa375a7f89 FileDialog: make a single instance filedialog
Modify the presentDialog method to account for single instance windows and implement a "hack" around the modality issues of Qt.

GitLab: #1905
Change-Id: I166bfc028939240955f20ec9b5777d6f282ccf78
2025-03-03 15:35:36 -05:00
9b51f26e80 i18n: automatic bump
Change-Id: Ia7d8a7341370c77df4ddfb30cea75d8fe3c72e69
2025-03-03 13:58:25 -05:00
6b0adb7005 notifications: no longer blank notifications when leaving or joining a group
Fixed the issue of blank notification when leaving a group by updating the
OnNewUnreadNotification function.

GitLab: #1921

Change-Id: I87f4c3828a72c9b504a9a68707d6b257ce00154c
2025-03-03 09:19:16 -05:00
224 changed files with 110676 additions and 98272 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ doc/Doxyfile
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files

6
.gitmodules vendored
View File

@ -21,9 +21,13 @@
ignore = dirty
[submodule "3rdparty/md4c"]
path = 3rdparty/md4c
url = https://github.com/mity/md4c.git
url = https://github.com/fsimonfc/md4c.git
ignore = dirty
[submodule "3rdparty/tidy-html5"]
path = 3rdparty/tidy-html5
url = https://github.com/htacg/tidy-html5.git
ignore = dirty
[submodule "3rdparty/zxing-cpp"]
path = 3rdparty/zxing-cpp
url = https://github.com/nu-book/zxing-cpp.git
ignore = dirty

2
3rdparty/md4c vendored

1
3rdparty/zxing-cpp vendored Submodule

Submodule 3rdparty/zxing-cpp added at a920817b6f

View File

@ -76,6 +76,7 @@ list(APPEND QWINDOWKIT_OPTIONS
QWINDOWKIT_BUILD_WIDGETS OFF
QWINDOWKIT_INSTALL OFF
QWINDOWKIT_BUILD_STATIC ON
QWINDOWKIT_BUILD_QUICK ON
)
if(WIN32)
@ -93,19 +94,19 @@ if(WIN32)
list(APPEND QWINDOWKIT_OPTIONS QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS OFF)
endif()
# qmsetup uses the wrong package dir on some distributions
# (including Fedora and openSUSE Leap at least)
check_distro_needs_qmsetup_patch(DISTRO_NEEDS_QMSETUP_PATCH)
if(DISTRO_NEEDS_QMSETUP_PATCH)
list(APPEND QWINDOWKIT_PATCHES ${EXTRA_PATCHES_DIR}/0001-fix-fedora-fc-build.patch)
set(qmsetup_cmake_path ${CMAKE_BINARY_DIR}/_install/lib64/cmake/qmsetup)
# If qwindowkit can't find qmsetup via cmake's find_package function, it will install it and
# then call find_package again. Unfortunately, even the second call to find_package sometimes
# fails due to qmsetup having been installed in the wrong directory. The following patch
# ensures that qmsetup is always installed in the directory where find_package looks for it.
if(NOT WIN32)
list(APPEND QWINDOWKIT_PATCHES ${EXTRA_PATCHES_DIR}/0001-fix-qm_install_package-function.patch)
endif()
# qwindowkit (frameless window)
add_fetch_content(
TARGET qwindowkit
URL https://github.com/stdware/qwindowkit.git
BRANCH 79b1f3110754f9c21af2d7dacbd07b1a9dbaf6ef
BRANCH 758b00cb6c2d924be3a1ea137ec366dc33a5132d
PATCHES ${QWINDOWKIT_PATCHES}
OPTIONS ${QWINDOWKIT_OPTIONS}
)
@ -364,6 +365,8 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/connectioninfolistmodel.cpp
${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/linkdevicemodel.cpp
${APP_SRC_DIR}/qrcodescannermodel.cpp
)
set(COMMON_HEADERS
@ -436,6 +439,8 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/pttlistener.h
${APP_SRC_DIR}/crashreportclient.h
${APP_SRC_DIR}/crashreporter.h
${APP_SRC_DIR}/linkdevicemodel.h
${APP_SRC_DIR}/qrcodescannermodel.h
)
# For libavutil/avframe.
@ -678,6 +683,15 @@ list(APPEND CLIENT_LINK_DIRS ${tidy_BINARY_DIR}/Release)
list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include)
list(APPEND CLIENT_LIBS tidy-static)
# ZXing-cpp configuration
set(BUILD_EXAMPLES OFF CACHE BOOL "")
set(BUILD_BLACKBOX_TESTS OFF CACHE BOOL "")
add_subdirectory(3rdparty/zxing-cpp EXCLUDE_FROM_ALL)
# Add ZXing-cpp to includes and libraries
list(APPEND CLIENT_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zxing-cpp/core/src)
list(APPEND CLIENT_LIBS ZXing)
# common executable sources
qt_add_executable(
${PROJECT_NAME}

View File

@ -374,7 +374,7 @@ def run_install(args):
# Prepare the build-windows.py script call
build_windows = 'extras/scripts/build-windows.py'
# Initialize build environment
execute_script([f'python {build_windows} --init'])
execute_script([f'python {build_windows} --init --qt={args.qt}'])
# Construct build command with options
build_cmd = [

2
daemon

Submodule daemon updated: 5e2d9e027b...68fc552fca

View File

@ -17,32 +17,6 @@
include(FetchContent)
include(CMakeParseArguments)
# Helper function to check if we're on a distribution that requires us
# to apply a patch in order for qmsetup to use the right package directory
function(check_distro_needs_qmsetup_patch DISTRO_NEEDS_QMSETUP_PATCH)
set(${DISTRO_NEEDS_QMSETUP_PATCH} FALSE PARENT_SCOPE)
# Check for the existence of /etc/os-release
if(EXISTS "/etc/os-release")
# Read the content of the file
file(READ "/etc/os-release" OS_RELEASE_CONTENT)
# Check if the distribution is Fedora or Red Hat-based
string(REGEX MATCH "ID=fedora|ID_LIKE=\"rhel fedora\"|ID_LIKE=\"rhel centos fedora\"" RED_HAT_BASED "${OS_RELEASE_CONTENT}")
# Check if the distribution is openSUSE Leap
string(REGEX MATCH "ID=\"opensuse-leap\"" OPENSUSE_LEAP "${OS_RELEASE_CONTENT}")
if(RED_HAT_BASED)
set(${DISTRO_NEEDS_QMSETUP_PATCH} TRUE PARENT_SCOPE)
message(STATUS "Running on a Red Hat-based distribution (Fedora, RHEL, CentOS, etc.)")
elseif(OPENSUSE_LEAP)
set(${DISTRO_NEEDS_QMSETUP_PATCH} TRUE PARENT_SCOPE)
message(STATUS "Running on openSUSE Leap")
else()
message(STATUS "Distribution is not openSUSE Leap or Red Hat-based")
endif()
else()
message(STATUS "Cannot determine the distribution type: /etc/os-release not found")
endif()
endfunction()
# Helper function to add external content with patches and options.
# Parameters:
# TARGET: Name of the target to create

View File

@ -33,7 +33,8 @@
def SUBMODULES = ['daemon',
'3rdparty/SortFilterProxyModel',
'3rdparty/md4c',
'3rdparty/tidy-html5']
'3rdparty/tidy-html5',
'3rdparty/zxing-cpp']
def TARGETS = [:]
def REMOTE_HOST = env.SSH_HOST_DL_RING_CX
def REMOTE_BASE_DIR = '/srv/repository/ring'

View File

@ -127,7 +127,8 @@ $(RELEASE_TARBALL_FILENAME): tarballs.manifest
. \
./3rdparty/SortFilterProxyModel \
./3rdparty/md4c \
./3rdparty/tidy-html5; do \
./3rdparty/tidy-html5 \
./3rdparty/zxing-cpp; do \
(cd "$$m" && git archive --prefix "$$m/" HEAD \
| tar xf - -C $(TMPDIR)/$(RELEASE_DIRNAME)); \
done
@ -166,14 +167,13 @@ DISTRIBUTIONS := \
ubuntu_22.04 \
ubuntu_24.04 \
ubuntu_24.10 \
fedora_37 \
fedora_38 \
ubuntu_25.04 \
fedora_39 \
fedora_40 \
fedora_41 \
alma_9 \
opensuse-leap_15.4 \
opensuse-leap_15.5 \
opensuse-leap_15.6 \
snap
IS_SHELL_INTERACTIVE := $(shell [ -t 0 ] && echo yes)

View File

@ -1,106 +0,0 @@
FROM fedora:37
RUN dnf clean all
RUN dnf update -y
RUN dnf install -y dnf-command\(builddep\) rpmdevtools && \
dnf install -y mock
RUN dnf groupinstall -y "X Software Development"
RUN dnf install -y \
git \
rpm-build \
tar \
make \
autoconf \
automake \
nasm \
speexdsp-devel \
pulseaudio-libs-devel \
libcanberra-devel \
libcurl-devel \
libtool \
mesa-libgbm-devel \
mesa-dri-drivers \
dbus-devel \
expat-devel \
pcre-devel \
yaml-cpp-devel \
libXext-devel \
libXfixes-devel \
yasm \
python2.7 \
python3-html5lib \
speex-devel \
gsm-devel \
chrpath \
check \
astyle \
uuid-c++-devel \
gettext-devel \
gcc-c++ \
which \
alsa-lib-devel \
systemd-devel \
libuuid-devel \
uuid-devel \
gnutls-devel \
nettle-devel \
opus-devel \
patch \
jsoncpp-devel \
libnatpmp-devel \
webkitgtk4-devel \
cryptopp-devel \
libva-devel \
libvdpau-devel \
msgpack-devel \
NetworkManager-libnm-devel \
openssl-devel \
clutter-devel \
clutter-gtk-devel \
libappindicator-gtk3-devel \
libnotify-devel \
libupnp-devel \
qrencode-devel \
libargon2-devel \
libsndfile-devel \
libdrm \
gperf \
bison \
clang \
clang-devel \
llvm-devel \
nodejs \
flex \
gstreamer1 gstreamer1-devel \
gstreamer1-plugins-base-devel \
gstreamer1-plugins-good \
gstreamer1-plugins-bad-free-devel \
nss-devel \
libxcb* \
libxkb* \
libX11-devel \
vulkan-devel \
libXrender-devel \
xcb-util-* \
xz \
xkeyboard-config \
libnotify \
wget \
libstdc++-static \
sqlite-devel \
perl-generators \
perl-English \
libxshmfence-devel \
ninja-build \
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
CMD ["/opt/build-package-rpm.sh"]

View File

@ -1,106 +0,0 @@
FROM fedora:38
RUN dnf clean all
RUN dnf update -y
RUN dnf install -y dnf-command\(builddep\) rpmdevtools && \
dnf install -y mock
RUN dnf groupinstall -y "X Software Development"
RUN dnf install -y \
git \
rpm-build \
tar \
make \
autoconf \
automake \
nasm \
speexdsp-devel \
pulseaudio-libs-devel \
libcanberra-devel \
libcurl-devel \
libtool \
mesa-libgbm-devel \
mesa-dri-drivers \
dbus-devel \
expat-devel \
pcre-devel \
yaml-cpp-devel \
libXext-devel \
libXfixes-devel \
yasm \
python2.7 \
speex-devel \
gsm-devel \
chrpath \
check \
astyle \
uuid-c++-devel \
gettext-devel \
gcc-c++ \
which \
alsa-lib-devel \
systemd-devel \
libuuid-devel \
uuid-devel \
gnutls-devel \
nettle-devel \
opus-devel \
patch \
jsoncpp-devel \
libnatpmp-devel \
webkitgtk4-devel \
cryptopp-devel \
libva-devel \
libvdpau-devel \
msgpack-devel \
NetworkManager-libnm-devel \
openssl-devel \
clutter-devel \
clutter-gtk-devel \
libappindicator-gtk3-devel \
libnotify-devel \
libupnp-devel \
qrencode-devel \
libargon2-devel \
libsndfile-devel \
libdrm \
gperf \
bison \
clang \
clang-devel \
llvm-devel \
nodejs \
flex \
gstreamer1 gstreamer1-devel \
gstreamer1-plugins-base-devel \
gstreamer1-plugins-good \
gstreamer1-plugins-bad-free-devel \
nss-devel \
libxcb* \
libxkb* \
libX11-devel \
vulkan-devel \
libXrender-devel \
xcb-util-* \
xz \
xkeyboard-config \
libnotify \
wget \
libstdc++-static \
sqlite-devel \
perl-generators \
perl-English \
libxshmfence-devel \
ninja-build \
clang \
cmake \
fmt-devel \
python3-html5lib \
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

@ -1,10 +1,9 @@
FROM opensuse/leap:15.4
FROM opensuse/leap:15.6
RUN zypper refresh
RUN zypper --gpg-auto-import-keys refresh
RUN zypper --non-interactive install -y \
dnf \
dnf-command\(builddep\) \
rpmdevtools \
Mesa-dri-devel Mesa-dri \
git \
@ -29,11 +28,11 @@ RUN zypper --non-interactive install -y \
speex-devel \
libgsm-devel \
chrpath \
check \
check-devel \
astyle \
gettext-devel \
gettext-tools \
which \
alsa-lib-devel \
alsa-devel \
systemd-devel \
libuuid-devel \
uuid-devel \
@ -44,9 +43,10 @@ RUN zypper --non-interactive install -y \
libcryptopp-devel \
libva-devel \
libvdpau-devel \
msgpack-devel \
msgpack-c-devel \
msgpack-cxx-devel \
clutter-devel \
openssl-devel \
libopenssl-devel \
clutter-gtk-devel \
libnma-devel \
libcryptopp-devel \
@ -55,20 +55,20 @@ RUN zypper --non-interactive install -y \
libgsm-devel \
gtk3-devel \
libappindicator-devel \
sqlite-devel \
ffmpeg-4-libavutil-devel \
sqlite3-devel \
gtk3-devel\
qrencode-devel \
python310 \
python3-python-dateutil \
python3-html5lib \
libsndfile-devel \
libdrm \
libdrm-devel \
gperf \
bison \
flex \
ffmpeg ffmpeg-devel \
nodejs18 \
ffmpeg \
ffmpeg-devel \
nodejs20 \
mozilla-nss-devel \
python-xml \
python3-six \
@ -85,7 +85,7 @@ RUN zypper --non-interactive install -y \
xorg-x11-devel \
xz \
xkeyboard-config \
libnotify \
libnotify-devel \
argon2-devel \
libxshmfence-devel \
xproto-devel \
@ -102,7 +102,7 @@ RUN zypper --non-interactive install -y \
wget \
pipewire-devel
# openSUSE Leap 15.4 comes with Python 3.6 by default,
# openSUSE Leap 15.6 comes with Python 3.6 by default,
# but we need at least 3.7 to compile Qt 6.6.1
RUN rm /usr/bin/python3 && ln -s /usr/bin/python3.10 /usr/bin/python3
@ -113,10 +113,4 @@ 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

@ -0,0 +1,29 @@
FROM ubuntu:25.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get clean
RUN apt-get update && \
apt-get install -y -o Acquire::Retries=10 \
devscripts \
equivs \
python-is-python3 \
wget
ADD extras/packaging/gnu-linux/scripts/prebuild-package-debian.sh /opt/prebuild-package-debian.sh
COPY extras/packaging/gnu-linux/rules/debian-qt/control /tmp/builddeps/debian/control
RUN /opt/prebuild-package-debian.sh qt-deps
COPY extras/packaging/gnu-linux/rules/debian/control /tmp/builddeps/debian/control
RUN /opt/prebuild-package-debian.sh jami-deps
# Remove the libre2-dev package in order to force Qt to build using the bundled
# version of the RE2 library. This is necessary because the system version of the
# library on Ubuntu 25.04 (libre2-11) is not compatible with the one used in
# Qt 6.6.1 due to an API change:
# https://codereview.qt-project.org/c/qt/qtwebengine/+/516094
RUN apt-get remove -y libre2-dev libre2-11
ADD extras/packaging/gnu-linux/scripts/build-package-debian.sh /opt/build-package-debian.sh
CMD ["/opt/build-package-debian.sh"]

View File

@ -103,6 +103,8 @@ if [ -f /etc/os-release ]; then
ENDTAG="ubuntu_24.04"
elif [ "${UBUNTU_CODENAME}" = "oracular" ] || [ "${ID}_${VERSION_ID}" = "ubuntu_24.10" ]; then
ENDTAG="ubuntu_24.10"
elif [ "${UBUNTU_CODENAME}" = "plucky" ] || [ "${ID}_${VERSION_ID}" = "ubuntu_25.04" ]; then
ENDTAG="ubuntu_25.04"
elif [ "${ID}" = "debian" ] && \
[ "$(command -v lsb_release)" ] && \
[ "$(lsb_release -rs)" = "testing" ]; then

View File

@ -47,7 +47,11 @@ if [ ! -f "${qt_deb_path}" ] || [ "${FORCE_REBUILD_QT}" = "true" ]; then
# HACK: For now on ubuntu 24.04 there is no python3.10 package
# So create a PyEnv environment to install the required packages
if cat /etc/os-release | grep -Eq "24.04"; then
# NOTE: We use this on Ubuntu 25.04 and Debian 13 ("trixie") too
# because otherwise we get a ModuleNotFoundError when building
# Qt 6.6.1 (specifically the chromium submodule in QtWebEngine)
# due to the version of python used (3.13) being too recent.
if cat /etc/os-release | grep -Eq "24.04|25.04|trixie"; then
apt-get install git gcc make python3-pip libssl-dev curl libreadline-dev -y
curl https://pyenv.run | bash
export PYENV_ROOT="$HOME/.pyenv"

View File

@ -101,12 +101,6 @@ if [ ! -f "${RPM_PATH}" ]; then
# Cache the built Qt RPM package.
if [[ "${DISTRIBUTION:0:4}" == "rhel" ]]; then
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.el8.x86_64.rpm "${RPM_PATH}"
elif [[ "${DISTRIBUTION}" == "fedora_36" ]]; then
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc36.x86_64.rpm "${RPM_PATH}"
elif [[ "${DISTRIBUTION}" == "fedora_37" ]]; then
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc37.x86_64.rpm "${RPM_PATH}"
elif [[ "${DISTRIBUTION}" == "fedora_38" ]]; then
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc38.x86_64.rpm "${RPM_PATH}"
elif [[ "${DISTRIBUTION}" == "fedora_39" ]]; then
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc39.x86_64.rpm "${RPM_PATH}"
elif [[ "${DISTRIBUTION}" == "fedora_40" ]]; then

View File

@ -80,9 +80,6 @@ EOF
find ./extras/packaging/gnu-linux/packages -type f -name '*.ddeb' -print0 | xargs -0 -I{} mv {} {}.deb
for package in ./extras/packaging/gnu-linux/packages/${DISTRIBUTION}*/*.deb; do
echo "## signing: ${package} ##"
dpkg-sig -k ${KEYID} --sign builder ${package}
echo "## including ${package} ##"
package_name=$(dpkg -I ${package} | grep -m 1 Package: | awk '{print $2}')
package_arch=$(dpkg -I ${package} | grep -m 1 Architecture: | awk '{print $2}')

View File

@ -1,25 +0,0 @@
From 161d28abb6784115ad71fcb6977e112e9d5756d4 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Tue, 23 Jan 2024 15:38:34 -0500
Subject: [PATCH] fix-fedora-fc-build
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0fb89c8..3a6ad6d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,7 +65,7 @@ if(NOT TARGET qmsetup::library)
)
# Find package again
- find_package(qmsetup REQUIRED PATHS ${_package_path})
+ find_package(qmsetup REQUIRED PATHS ${_package_path} ${qmsetup_cmake_path})
# Update import path
set(qmsetup_DIR ${_package_path} CACHE PATH "" FORCE)
--
2.34.1

View File

@ -0,0 +1,32 @@
From 56830725e641705e0113a068ee58df7029202439 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois-Simon=20Fauteux-Chapleau?=
<francois-simon.fauteux-chapleau@savoirfairelinux.com>
Date: Wed, 2 Apr 2025 20:54:02 -0400
Subject: [PATCH] fix qm_install_package function
---
cmake/modules/private/InstallPackage.cmake | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/qmsetup/cmake/modules/private/InstallPackage.cmake b/qmsetup/cmake/modules/private/InstallPackage.cmake
index 70174bc..f067de5 100644
--- a/qmsetup/cmake/modules/private/InstallPackage.cmake
+++ b/qmsetup/cmake/modules/private/InstallPackage.cmake
@@ -101,6 +101,7 @@ function(qm_install_package _name)
execute_process(
COMMAND ${CMAKE_COMMAND} -S ${_src_dir} -B ${_build_dir}
${_extra_args} ${_build_type}
+ "-DCMAKE_INSTALL_LIBDIR=${CMAKE_INSTALL_LIBDIR}"
"-DCMAKE_INSTALL_PREFIX=${_install_dir}" ${FUNC_CONFIGURE_ARGS}
OUTPUT_FILE ${_log_file}
ERROR_FILE ${_log_file}
@@ -150,4 +151,4 @@ function(qm_install_package _name)
if(FUNC_RESULT_PATH)
set(${FUNC_RESULT_PATH} ${_install_cmake_dir} PARENT_SCOPE)
endif()
-endfunction()
\ No newline at end of file
+endfunction()
--
2.34.1

View File

@ -211,6 +211,7 @@ def init_submodules():
"3rdparty/SortFilterProxyModel",
"3rdparty/md4c",
"3rdparty/tidy-html5",
"3rdparty/zxing-cpp",
]
if execute_cmd(["git", "submodule", "update", "--init" ] + submodules,
False):

View File

@ -52,7 +52,7 @@ for ARCH in "${ARCHS[@]}"; do
# force to build every contrib
for dir in "$DAEMON"/contrib/src/*/; do
PKG=$(basename -- "$dir")
if [ "$PKG" != "sdbus-cpp" ] && [ "$PKG" != "natpmp" ] &&
if [ "$PKG" != "sdbus-cpp" ] && [ "$PKG" != "freetype" ] &&
[ "$PKG" != "portaudio" ] && [ "$PKG" != "pthreads" ] &&
[ "$PKG" != "lttng-ust" ] && [ "$PKG" != "openssl" ] &&
[ "$PKG" != "media-sdk" ] && [ "$PKG" != "jack" ] &&

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View File

@ -17,6 +17,7 @@
import QtQuick
import QtQuick.Controls
import QtWebEngine
import net.jami.Adapters 1.1
import net.jami.Enums 1.1
@ -37,9 +38,12 @@ QtObject {
readonly property bool isHidden: visibility === Window.Hidden ||
visibility === Window.Minimized
// Used to store if a OngoingCallPage component is fullscreened.
// Used to store if a CallStackView component is fullscreened.
property bool isCallFullscreen: false
// Used to store if a WebEngineView component is fullscreened.
property bool isWebFullscreen: false
// QWK: Provide spacing for widgets that may be occluded by the system buttons.
property QtObject qwkSystemButtonSpacing: QtObject {
id: qwkSystemButtonSpacing
@ -150,9 +154,8 @@ QtObject {
// Adds an item to the fullscreen item stack. Automatically puts
// the main window in fullscreen mode if needed. Callbacks should be used
// to perform component-specific tasks upon successful transitions.
function pushFullScreenItem(item, prevParent, pushedCb, removedCb) {
if (item === null || item === undefined
|| priv.fullScreenItems.length >= 3) {
function pushFullScreenItem(item, removedCb=undefined) {
if (!item || priv.fullScreenItems.length >= 3) {
return
}
@ -162,15 +165,13 @@ QtObject {
// Add the item to our list and reparent it to appContainer.
priv.fullScreenItems.push({
"item": item,
"prevParent": prevParent,
"prevParent": item.parent,
"prevAnchorsFill": item.anchors.fill,
"removedCb": removedCb
})
item.parent = appContainer
item.anchors.fill = item.parent
if (pushedCb) {
pushedCb()
}
item.anchors.fill = appContainer
// Reevaluate isCallFullscreen.
priv.fullScreenItemsChanged()
@ -178,34 +179,37 @@ QtObject {
// Remove an item if specified, or by default, the top item. Automatically
// resets the main window to windowed mode if no items remain in the stack.
function popFullScreenItem(obj=null) {
function popFullScreenItem(obj = undefined) {
// Remove the item and reparent it to its original parent.
if (obj === null) {
obj = priv.fullScreenItems.pop()
if (obj === undefined) {
obj = priv.fullScreenItems.pop();
} else {
const index = priv.fullScreenItems.indexOf(obj);
if (index > -1) {
priv.fullScreenItems.splice(index, 1);
}
}
if (obj !== undefined) {
if (obj && typeof obj === 'object') {
if (obj.item !== appWindow) {
obj.item.anchors.fill = obj.prevAnchorsFill
obj.item.parent = obj.prevParent
if (obj.removedCb) {
obj.removedCb()
// Clear anchors first, then set parent, then reset anchors.
obj.item.anchors.fill = undefined;
obj.item.parent = obj.prevParent;
obj.item.anchors.fill = obj.prevAnchorsFill;
// Call removed callback if it's a function.
if (typeof obj.removedCb === 'function') {
obj.removedCb();
}
}
// Reevaluate isCallFullscreen.
priv.fullScreenItemsChanged()
priv.fullScreenItemsChanged();
}
// Only leave fullscreen mode if our window isn't in fullscreen
// mode already.
// Only leave fullscreen mode if our window isn't in fullscreen mode already.
if (priv.fullScreenItems.length === 0 && priv.windowedVisibility !== Window.Hidden) {
// Simply recall the last visibility state.
visibility = priv.windowedVisibility
visibility = priv.windowedVisibility;
}
}
@ -247,7 +251,10 @@ QtObject {
// When fullScreenItems is changed, we can recompute isCallFullscreen.
onFullScreenItemsChanged: {
isCallFullscreen = fullScreenItems
.filter(o => o.item instanceof OngoingCallPage)
.filter(o => o.item.objectName === "callViewLoader")
.length
isWebFullscreen = fullScreenItems
.filter(o => o.item instanceof WebEngineView)
.length
}
@ -258,7 +265,7 @@ QtObject {
function onHasCallChanged() {
if (!CallAdapter.hasCall && isCallFullscreen) {
priv.fullScreenItems.forEach(o => {
if (o.item instanceof OngoingCallPage) {
if (o.item.objectName === "callViewLoader") {
popFullScreenItem(o)
return
}

View File

@ -19,18 +19,15 @@ import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
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"
import QWindowKit
ApplicationWindow {
@ -63,7 +60,7 @@ ApplicationWindow {
sourceComponent: GenericErrorsRow {
id: genericError
text: CurrentAccount.enabled ? JamiStrings.noNetworkConnectivity : JamiStrings.disabledAccount
height: visible? JamiTheme.qwkTitleBarHeight : 0
height: visible ? JamiTheme.qwkTitleBarHeight : 0
}
}
@ -87,9 +84,11 @@ ApplicationWindow {
appContainer: fullscreenContainer
}
// Used to manage dynamic view loading and unloading.
property ViewManager viewManager: ViewManager {}
property ViewManager viewManager: ViewManager {
}
// Used to manage the view stack and the current view.
property ViewCoordinator viewCoordinator: 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
@ -199,7 +198,6 @@ ApplicationWindow {
if (useFrameless) {
windowAgent.setup(appWindow);
}
mainViewLoader.active = true;
// Dbus error handler for Linux.
@ -216,10 +214,14 @@ ApplicationWindow {
"confirmLabel": JamiStrings.send,
"rejectLabel": JamiStrings.dontSend,
"textHAlign": Text.AlignLeft,
"textMaxWidth": 400,
"textMaxWidth": 400
});
dlg.accepted.connect(function () {
crashReporter.uploadLastReport();
});
dlg.rejected.connect(function () {
crashReporter.clearReports();
});
dlg.accepted.connect(function () { crashReporter.uploadLastReport(); });
dlg.rejected.connect(function () { crashReporter.clearReports(); });
}
}
@ -293,7 +295,7 @@ ApplicationWindow {
target: MainApplication
function onAboutToQuit() {
cleanupMainView()
cleanupMainView();
}
function onCloseRequested() {
@ -331,7 +333,7 @@ ApplicationWindow {
});
}
function presentUpdateConfirmInstallDialog(switchToBeta=false) {
function presentUpdateConfirmInstallDialog(switchToBeta = false) {
return viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", {
"title": JamiStrings.updateDialogTitle,
"infoText": switchToBeta ? JamiStrings.confirmBeta : JamiStrings.updateFound,
@ -382,7 +384,7 @@ ApplicationWindow {
presentUpdateInfoDialog(JamiStrings.updateNotFound);
} else {
// Show a dialog describing that an update were found, and offering to install it.
presentUpdateConfirmInstallDialog()
presentUpdateConfirmInstallDialog();
}
}
@ -393,4 +395,20 @@ ApplicationWindow {
}
onClosing: appWindow.close()
// Capture the inputs to the main window while the File Dialog is open
// This is used to mitigate modality issues on Ubuntu 22.04 systems that use wayland.
Loader {
active: JamiQmlUtils.openFileDialogCount > 0
sourceComponent: Popup {
modal: true
visible: true
closePolicy: Popup.NoAutoClose
width: appWindow.width
height: appWindow.height
background: Rectangle {
color: "#80808080" // Semi-transparent grey
}
}
}
}

View File

@ -49,13 +49,13 @@ QtObject {
// right side when not in RTL and should represent the main or content-type view.
readonly property var visibleViews: {
if (!currentView)
return []
return [];
if (isDualPane) {
if (isInSinglePaneMode)
return [currentView.rightPaneItem]
return [currentView.leftPaneItem, currentView.rightPaneItem]
return [currentView.rightPaneItem];
return [currentView.leftPaneItem, currentView.rightPaneItem];
}
return [currentView]
return [currentView];
}
// Aggregate this info and expose it as a single string for convenience.
// JSON indented by 2 spaces.
@ -64,12 +64,12 @@ QtObject {
currentViewName: currentViewName,
isDualPane: isDualPane,
isInSinglePaneMode: isInSinglePaneMode,
visibleViews: visibleViews.map(function(view) {
return view && view.objectName || null;
}),
visibleViewWidths: visibleViews.map(function(view) {
return view && view.width || null;
}),
visibleViews: visibleViews.map(function (view) {
return view && view.objectName || null;
}),
visibleViewWidths: visibleViews.map(function (view) {
return view && view.width || null;
})
};
return JSON.stringify(info, null, 2);
}
@ -96,11 +96,12 @@ QtObject {
}
// Create, present, and return a dialog object.
function presentDialog(parent, path, props = {}) {
function presentDialog(parent, path, props = {}, singleInstance = false) {
// Open the dialog once the object is created
return viewManager.createUniqueView(path, parent, function (obj) {
let createFunc = singleInstance ? viewManager.createView : viewManager.createUniqueView;
return createFunc(path, parent, function (obj, viewName) {
const doneCb = function () {
viewManager.destroyView(path);
viewManager.destroyView(viewName);
};
if (obj.closed !== undefined) {
obj.closed.connect(doneCb);

View File

@ -59,7 +59,7 @@ QtObject {
if (views.hasOwnProperty(viewName)) {
// an instance of the view already exists
if (cb !== null) {
cb(views[viewName])
cb(views[viewName], viewName)
}
return views[viewName]
}
@ -76,7 +76,7 @@ QtObject {
viewName.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, "")
viewPaths[friendlyName] = viewName
if (cb !== null) {
cb(obj)
cb(obj , viewName)
}
return views[viewName]
}
@ -103,6 +103,7 @@ QtObject {
function destroyView(path) {
// The view may already have been destroyed.
if (!views.hasOwnProperty(path)) {
console.warn("View not found:", path, "Available views:", Object.keys(views))
return false
}
views[path].destroy()

View File

@ -22,8 +22,11 @@
#include "systemtray.h"
#include "lrcinstance.h"
#include "accountlistmodel.h"
#include "wizardviewstepmodel.h"
#include "global.h"
#include "api/account.h"
#include <QtConcurrent/QtConcurrent>
#include <QThreadPool>
AccountAdapter::AccountAdapter(AppSettingsManager* settingsManager,
SystemTray* systemTray,
@ -111,7 +114,10 @@ AccountAdapter::createJamiAccount(const QVariantMap& settings)
&lrcInstance_->accountModel(),
&lrc::api::AccountModel::accountAdded,
[this, registeredName, settings](const QString& accountId) {
lrcInstance_->accountModel().setAvatar(accountId, settings["avatar"].toString(), true,1);
lrcInstance_->accountModel().setAvatar(accountId,
settings["avatar"].toString(),
true,
1);
Utils::oneShotConnect(&lrcInstance_->accountModel(),
&lrc::api::AccountModel::accountDetailsChanged,
[this](const QString& accountId) {
@ -159,8 +165,9 @@ AccountAdapter::createJamiAccount(const QVariantMap& settings)
connectFailure();
auto futureResult = QtConcurrent::run([this, settings] {
QThreadPool::globalInstance()->start([this, settings] {
lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::JAMI,
{},
settings["alias"].toString(),
settings["archivePath"].toString(),
settings["password"].toString(),
@ -206,14 +213,14 @@ AccountAdapter::createSIPAccount(const QVariantMap& settings)
connectFailure();
auto futureResult = QtConcurrent::run([this, settings] {
QThreadPool::globalInstance()->start([this, settings] {
lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::SIP,
{},
settings["alias"].toString(),
settings["archivePath"].toString(),
"",
"",
settings["username"].toString(),
{});
settings["username"].toString());
});
}
@ -250,7 +257,7 @@ AccountAdapter::createJAMSAccount(const QVariantMap& settings)
connectFailure();
auto futureResult = QtConcurrent::run([this, settings] {
QThreadPool::globalInstance()->start([this, settings] {
lrcInstance_->accountModel().connectToAccountManager(settings["username"].toString(),
settings["password"].toString(),
settings["manager"].toString());
@ -293,7 +300,7 @@ AccountAdapter::setCurrAccDisplayName(const QString& text)
void
AccountAdapter::setCurrentAccountAvatarFile(const QString& source)
{
auto futureResult = QtConcurrent::run([this, source]() {
QThreadPool::globalInstance()->start([this, source]() {
QPixmap image;
if (!image.load(source)) {
qWarning() << "Not a valid image file";
@ -308,7 +315,7 @@ AccountAdapter::setCurrentAccountAvatarFile(const QString& source)
void
AccountAdapter::setCurrentAccountAvatarBase64(const QString& data)
{
auto futureResult = QtConcurrent::run([this, data]() {
QThreadPool::globalInstance()->start([this, data]() {
auto accountId = lrcInstance_->get_currentAccountId();
lrcInstance_->accountModel().setAvatar(accountId, data, true, 1);
});
@ -339,9 +346,73 @@ AccountAdapter::exportToFile(const QString& accountId,
void
AccountAdapter::setArchivePasswordAsync(const QString& accountID, const QString& password)
{
auto futureResult = QtConcurrent::run([this, accountID, password] {
QThreadPool::globalInstance()->start([this, accountID, password] {
auto config = lrcInstance_->accountModel().getAccountConfig(accountID);
config.archivePassword = password;
lrcInstance_->accountModel().setAccountConfig(accountID, config);
});
}
void
AccountAdapter::startImportAccount()
{
auto wizardModel = qApp->property("WizardViewStepModel").value<WizardViewStepModel*>();
wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT);
wizardModel->set_deviceLinkDetails({});
// This will create an account with the ARCHIVE_URL configured to start the import process.
importAccountId_ = lrcInstance_->accountModel().createDeviceImportAccount();
}
void
AccountAdapter::provideAccountAuthentication(const QString& password)
{
if (importAccountId_.isEmpty()) {
qWarning() << "No import account to provide password to";
return;
}
auto wizardModel = qApp->property("WizardViewStepModel").value<WizardViewStepModel*>();
wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::IN_PROGRESS);
Utils::oneShotConnect(
&lrcInstance_->accountModel(),
&lrc::api::AccountModel::accountAdded,
[this](const QString& accountId) {
Q_EMIT lrcInstance_->accountListChanged();
Q_EMIT accountAdded(accountId,
lrcInstance_->accountModel().getAccountList().indexOf(accountId));
},
this,
&AccountAdapter::accountCreationFailed);
connectFailure();
QThreadPool::globalInstance()->start([this, password] {
lrcInstance_->accountModel().provideAccountAuthentication(importAccountId_, password);
});
}
QString
AccountAdapter::getImportErrorMessage(QVariantMap details)
{
QString errorString = details.value("error").toString();
if (!errorString.isEmpty() && errorString != "none") {
auto error = lrc::api::account::mapLinkDeviceError(errorString.toStdString());
return lrc::api::account::getLinkDeviceString(error);
}
return "";
}
void
AccountAdapter::cancelImportAccount()
{
auto wizardModel = qApp->property("WizardViewStepModel").value<WizardViewStepModel*>();
wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT);
wizardModel->set_deviceLinkDetails({});
// Remove the account if it was created
lrcInstance_->accountModel().removeAccount(importAccountId_);
importAccountId_.clear();
}

View File

@ -81,6 +81,13 @@ public:
const bool& state);
Q_INVOKABLE QStringList getDefaultModerators(const QString& accountId);
// New import account / link device functions
// import: (note: Listen for: DeviceAuthStateChanged)
Q_INVOKABLE void startImportAccount();
Q_INVOKABLE void provideAccountAuthentication(const QString& password = {});
Q_INVOKABLE QString getImportErrorMessage(QVariantMap details);
Q_INVOKABLE void cancelImportAccount();
Q_SIGNALS:
// Trigger other components to reconnect account related signals.
void accountStatusChanged(QString accountId);
@ -98,6 +105,9 @@ private:
QMetaObject::Connection registeredNameSavedConnection_;
// The account ID of the last used import account.
QString importAccountId_;
AppSettingsManager* settingsManager_;
SystemTray* systemTray_;
};

View File

@ -77,10 +77,10 @@ struct AppVersionManager::Impl : public QObject
auto latestVersion = latestVersionString.toULongLong();
const QString channelStr = isBeta ? "beta" : "stable";
const auto newVersionFound = latestVersion > currentVersion;
qInfo().noquote() << "--------- Version info ------------"
<< QString("\n - Current: %1 (%2)").arg(currentVersion).arg(channelStr);
qInfo().noquote() << "--------- Version info ------------";
qInfo().noquote() << QString("\tCurrent: \t%1 (%2)").arg(currentVersion).arg(channelStr);
if (newVersionFound) {
qDebug() << " - Latest: " << latestVersion;
qInfo().noquote() << QString("\tLatest: \t%1").arg(latestVersion);
Q_EMIT parent_.updateCheckReplyReceived(true, true);
} else if (!quiet) {
Q_EMIT parent_.updateCheckReplyReceived(true, false);

View File

@ -345,9 +345,11 @@ AvAdapter::shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned heig
}
void
AvAdapter::shareWindow(const QString& windowProcessId, const QString& windowId)
AvAdapter::shareWindow(const QString& windowProcessId, const QString& windowId, const int fps)
{
auto resource = lrcInstance_->getCurrentCallModel()->getDisplay(windowProcessId, windowId);
auto resource = lrcInstance_->getCurrentCallModel()->getDisplay(windowProcessId,
windowId,
fps);
auto callId = lrcInstance_->getCurrentCallId();
muteCamera_ = !isCapturing();
@ -356,7 +358,10 @@ AvAdapter::shareWindow(const QString& windowProcessId, const QString& windowId)
}
QString
AvAdapter::getSharingResource(int screenId, const QString& windowProcessId, const QString& windowId)
AvAdapter::getSharingResource(int screenId,
const QString& windowProcessId,
const QString& windowId,
const int fps)
{
if (screenId == -1) {
const auto arrangementRect = getAllScreensBoundingRect();
@ -387,7 +392,7 @@ AvAdapter::getSharingResource(int screenId, const QString& windowProcessId, cons
rect.height()
* screen->devicePixelRatio());
} else if (!windowId.isEmpty()) {
return lrcInstance_->getCurrentCallModel()->getDisplay(windowProcessId, windowId);
return lrcInstance_->getCurrentCallModel()->getDisplay(windowProcessId, windowId, fps);
}
return "";

View File

@ -96,7 +96,9 @@ protected:
Q_INVOKABLE void shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned height);
// Select window to display (all platforms except Wayland).
Q_INVOKABLE void shareWindow(const QString& windowProcessId, const QString& windowId);
Q_INVOKABLE void shareWindow(const QString& windowProcessId,
const QString& windowId,
const int fps = -1);
#ifdef Q_OS_LINUX
// Share a window on Wayland.
@ -110,7 +112,8 @@ protected:
// Returns the screensharing resource
Q_INVOKABLE QString getSharingResource(int screenId = -2,
const QString& windowProcessId = "",
const QString& key = "");
const QString& key = "",
const int fps = -1);
Q_INVOKABLE void getListWindows();

View File

@ -22,6 +22,7 @@
#include "lrcinstance.h"
#include <QImage>
#include <QRegularExpression>
class AsyncAvatarImageResponseRunnable : public AsyncImageResponseRunnable
{
@ -69,6 +70,16 @@ public:
image = Utils::accountPhoto(lrcInstance_, imageId, requestedSize_);
} else if (type == "contact") {
image = Utils::contactPhoto(lrcInstance_, imageId, requestedSize_);
} else if (type == "temporaryAccount") {
// Check if imageId is a SHA-1 hash (jamiId or registered name)
static const QRegularExpression sha1Pattern("^[0-9a-fA-F]{40}$");
if (sha1Pattern.match(imageId).hasMatch()) {
// If we only have a jamiId use default avatar
image = Utils::fallbackAvatar("jami:" + imageId, QString(), requestedSize_);
} else {
// For registered usernames, use fallbackAvatar avatar with the name
image = Utils::fallbackAvatar(QString(), imageId, requestedSize_);
}
} else {
qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url";
return;

View File

@ -28,7 +28,8 @@ Item {
enum Mode {
Account,
Contact,
Conversation
Conversation,
TemporaryAccount
}
property int mode: Avatar.Mode.Account
property alias sourceSize: image.sourceSize
@ -45,6 +46,8 @@ Item {
return 'contact';
case Avatar.Mode.Conversation:
return 'conversation';
case Avatar.Mode.TemporaryAccount:
return 'temporaryAccount';
}
}

View File

@ -56,11 +56,11 @@ Popup {
id: container
property color color: JamiTheme.secondaryBackgroundColor
leftPadding: popupMargins
bottomPadding: action1.visible || action2.visible ? 10 :popupMargins
bottomPadding: action1.visible || action2.visible ? 10 : popupMargins
background: Rectangle {
id: bgRect
radius: 5
color: container.color
layer.enabled: true
@ -99,6 +99,7 @@ Popup {
Label {
id: titleText
Layout.leftMargin: popupMargins
Layout.rightMargin: popupMargins
Layout.bottomMargin: 20
Layout.topMargin: closeButtonVisible ? 0 : 30
@ -115,9 +116,9 @@ Popup {
id: flickable
Layout.fillHeight: true
Layout.preferredHeight: Math.min(contentHeight, root.height)
Layout.preferredWidth: contentItem.childrenRect.width
Layout.leftMargin: popupMargins
Layout.rightMargin: popupMargins
Layout.alignment: Qt.AlignCenter
@ -126,11 +127,13 @@ Popup {
contentItem.children: Loader {
id: containerSubContentLoader
}
ScrollBar.horizontal.visible: false
}
DialogButtonBox {
id: buttonBox
Layout.alignment: Qt.AlignRight
spacing: 1.5
@ -179,7 +182,7 @@ Popup {
color: JamiTheme.transparentColor
// Color animation for overlay when pop up is shown.
ColorAnimation on color {
ColorAnimation on color {
to: JamiTheme.popupOverlayColor
duration: 500
}

View File

@ -126,6 +126,20 @@ Loader {
property bool canOpen: root.transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED || isOutgoing
property real maxMsgWidth: root.width - senderMargin - 2 * hPadding - avatarBlockWidth - buttonsLoader.width - 24 - 6 - 24
// Timer to update the translation bar
Loader {
id: timerLoader
active: root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING
sourceComponent: Timer {
interval: 1000 // Update every second
running: true
repeat: true
onTriggered: {
transferStats = MessagesAdapter.getTransferStats(transferId, root.transferStatus);
}
}
}
isOutgoing: Author === CurrentAccount.uri
showTime: root.showTime
seq: root.seq
@ -136,7 +150,7 @@ Loader {
timestamp: root.timestamp
formattedTime: root.formattedTime
formattedDay: root.formattedTime
extraHeight: progressBar.visible ? 18 : 0
extraHeight: progressBar.visible ? 25 : 0
innerContent.children: [
RowLayout {
@ -212,9 +226,10 @@ Loader {
imageColor: JamiTheme.chatviewButtonColor
onClicked: {
if (root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING) {
return MessagesAdapter.cancelFile(transferId);
MessagesAdapter.cancelFile(transferId);
} else {
return MessagesAdapter.acceptFile(transferId);
buttonsLoader.iconSource = JamiResources.connecting_black_24dp_svg;
MessagesAdapter.acceptFile(transferId);
}
}
}

View File

@ -27,6 +27,14 @@ FileDialog {
signal fileAccepted(string file)
signal filesAccepted(var files)
Component.onCompleted: {
JamiQmlUtils.openFileDialogCount++;
}
Component.onDestruction: {
JamiQmlUtils.openFileDialogCount--;
}
onAccepted: {
switch (fileMode) {
case FileDialog.OpenFile:

View File

@ -40,6 +40,9 @@ Flickable {
orientation: Qt.Horizontal
}
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
Keys.onLeftPressed: horizontalScrollBar.decrease()
Keys.onRightPressed: horizontalScrollBar.increase()
Keys.onUpPressed: verticalScrollBar.decrease()

View File

@ -32,7 +32,7 @@ Item {
property bool validated: false
property bool outsideClic: false
property bool justChanged: false
property bool clic : false
property bool clic: false
height: getHeight()
function getHeight() {
@ -45,7 +45,6 @@ Item {
if (usernameTextEdit.editMode) {
usernameTextEdit.editMode = false;
}
}
}
@ -57,10 +56,16 @@ Item {
RoundedBorderRectangle {
id: leftRect
fillColor: JamiTheme.jamiIdBackgroundColor
Layout.preferredWidth: usernameTextEdit.visible ? childrenRect.width + JamiTheme.pushButtonMargins : childrenRect.width
Layout.preferredHeight: childrenRect.height
radius: {
radius: isRTL ? {
"tl": 0,
"tr": 5,
"br": 5,
"bl": 0
} : {
"tl": 5,
"tr": 0,
"br": 0,
@ -105,7 +110,7 @@ Item {
dynamicText = '';
}
}
Label{
Label {
id: usernameLabel
visible: !usernameTextEdit.editMode
@ -118,7 +123,7 @@ Item {
Layout.fillHeight: true
elide: Text.ElideRight
color: JamiTheme.tintedBlue
font.pixelSize : text.length > 16 ? JamiTheme.jamiIdSmallFontSize : JamiTheme.bigFontSize
font.pixelSize: text.length > 16 ? JamiTheme.jamiIdSmallFontSize : JamiTheme.bigFontSize
property string registeredName: CurrentAccount.registeredName
property string infohash: CurrentAccount.uri
text: (btnId.clicked && registeredName) ? registeredName : infohash
@ -132,7 +137,12 @@ Item {
Layout.preferredWidth: childrenRect.width + 2 * JamiTheme.pushButtonMargins
Layout.preferredHeight: leftRect.height
radius: {
radius: isRTL ? {
"tl": 5,
"tr": 0,
"br": 0,
"bl": 5
} : {
"tl": 0,
"tr": 5,
"br": 5,
@ -228,10 +238,14 @@ Item {
toolTipText: JamiStrings.identifierURI
onClicked: {
if (clicked) {
usernameTextEdit.staticText = Qt.binding(function() {return CurrentAccount.uri} );
usernameTextEdit.staticText = Qt.binding(function () {
return CurrentAccount.uri;
});
btnId.toolTipText = JamiStrings.identifierRegisterName;
} else {
usernameTextEdit.staticText = Qt.binding(function() {return CurrentAccount.registeredName} );
usernameTextEdit.staticText = Qt.binding(function () {
return CurrentAccount.registeredName;
});
btnId.toolTipText = JamiStrings.identifierURI;
}
clicked = !clicked;

View File

@ -34,6 +34,9 @@ ListView {
attachedFlickableMoving: root.moving
}
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
Keys.onUpPressed: verticalScrollBar.decrease()
Keys.onDownPressed: verticalScrollBar.increase()
}

View File

@ -35,7 +35,7 @@ BaseModalDialog {
property var buttonStyles: []
property string infoText: ""
property var innerContentData: []
property int buttonRoles: []
property var buttonRoles: []
function openWithParameters(title, info = "") {
root.title = title;

View File

@ -94,6 +94,8 @@ MenuItem {
Text {
id: contextMenuItemText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
height: parent.height
text: itemName
color: dangerous ? JamiTheme.redColor : isActif ? JamiTheme.textColor : JamiTheme.chatViewFooterImgColor

View File

@ -171,10 +171,20 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
if (interaction.authorUri == accountInfo.profileInfo.uri)
return;
auto from = accountInfo.contactModel->bestNameForContact(interaction.authorUri);
auto body_ = interaction.body;
QString displayedString;
if (interaction.type == interaction::Type::DATA_TRANSFER) {
body_ = interaction.commit.value("displayName");
// Add special handling for member events
if (interaction.type == interaction::Type::CONTACT) {
auto action = interaction.commit.value("action");
if (action == "join") {
displayedString = tr("%1 has joined the conversation.").arg(from);
} else if (action == "remove") {
displayedString = tr("%1 has left the conversation.").arg(from);
}
} else if (interaction.type == interaction::Type::DATA_TRANSFER) {
displayedString = from + ": " + interaction.commit.value("displayName");
} else {
displayedString = from + ": " + interaction.body;
}
auto preferences = accountInfo.conversationModel->getConversationPreferences(convUid);
@ -190,7 +200,7 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
auto notifId = QString("%1;%2;%3").arg(accountId, convUid, interactionId);
systemTray_->showNotification(notifId,
tr("%1 received a new message").arg(to),
from + ": " + body_,
displayedString,
SystemTray::NotificationType::CHAT,
Utils::QImageToByteArray(contactPhoto));

View File

@ -109,6 +109,9 @@ protected:
{"client_sha", APP_VERSION_STRING},
{"jamicore_sha", CORE_VERSION_STRING},
{"build_id", QString(VERSION_STRING)},
#if defined(Q_OS_WIN) && defined(BETA)
{"build_variant", "beta"},
#endif
};
};

View File

@ -313,15 +313,15 @@ CurrentConversation::updateErrors(const QString& convId)
auto& convInfo = optConv->get();
for (const auto& [code, error] : convInfo.errors) {
if (code == 1) {
newErrors.append(tr("An error occurred while fetching this repository"));
newErrors.append(tr("An error occurred while fetching this repository."));
} else if (code == 2) {
newErrors.append(tr("Unrecognized conversation mode"));
newErrors.append(tr("Unrecognized conversation mode."));
} else if (code == 3) {
newErrors.append(tr("An invalid message was detected"));
newErrors.append(tr("An invalid message was detected."));
} else if (code == 4) {
newErrors.append(tr("Insufficient permission to update conversation information"));
newErrors.append(tr("Insufficient permission to update conversation information."));
} else if (code == 5) {
newErrors.append(tr("An error occurred while committing a new message"));
newErrors.append(tr("An error occurred while committing a new message."));
} else {
continue;
}

140
src/app/linkdevicemodel.cpp Normal file
View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 2025-2025 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 "linkdevicemodel.h"
#include "lrcinstance.h"
#include "api/accountmodel.h"
#include "api/account.h"
using namespace lrc::api::account;
LinkDeviceModel::LinkDeviceModel(LRCInstance* lrcInstance, QObject* parent)
: QObject(parent)
, lrcInstance_(lrcInstance)
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::INIT));
connect(&lrcInstance_->accountModel(),
&lrc::api::AccountModel::addDeviceStateChanged,
this,
[this](const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details) {
if (operationId != operationId_)
return;
auto deviceState = static_cast<DeviceAuthState>(state);
switch (deviceState) {
case DeviceAuthState::CONNECTING:
handleConnectingSignal();
break;
case DeviceAuthState::AUTHENTICATING:
handleAuthenticatingSignal(Utils::mapStringStringToVariantMap(details));
break;
case DeviceAuthState::IN_PROGRESS:
handleInProgressSignal();
break;
case DeviceAuthState::DONE:
handleDoneSignal(Utils::mapStringStringToVariantMap(details));
break;
default:
break;
}
});
}
void
LinkDeviceModel::addDevice(const QString& token)
{
set_tokenErrorMessage("");
auto errorMessage = QObject::tr(
"Unrecognized new device identifier. Please follow the instructions above.");
if (!token.startsWith("jami-auth://") || (token.length() != 59)) {
set_tokenErrorMessage(errorMessage);
return;
}
int32_t result = lrcInstance_->accountModel().addDevice(lrcInstance_->getCurrentAccountInfo().id,
token);
if (result > 0) {
operationId_ = result;
} else {
set_tokenErrorMessage(errorMessage);
}
}
void
LinkDeviceModel::handleConnectingSignal()
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::CONNECTING));
}
void
LinkDeviceModel::handleAuthenticatingSignal(const QVariantMap& details)
{
QString peerAddress = details.value("peer_address").toString();
set_ipAddress(peerAddress);
set_deviceAuthState(static_cast<int>(DeviceAuthState::AUTHENTICATING));
}
void
LinkDeviceModel::handleInProgressSignal()
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::IN_PROGRESS));
}
void
LinkDeviceModel::handleDoneSignal(const QVariantMap& details)
{
QString errorString = details.value("error").toString();
if (!errorString.isEmpty() && errorString != "none") {
auto error = mapLinkDeviceError(errorString.toStdString());
set_linkDeviceError(getLinkDeviceString(error));
set_deviceAuthState(static_cast<int>(DeviceAuthState::DONE));
} else {
set_deviceAuthState(static_cast<int>(DeviceAuthState::DONE));
}
}
void
LinkDeviceModel::confirmAddDevice()
{
handleInProgressSignal();
lrcInstance_->accountModel().confirmAddDevice(lrcInstance_->getCurrentAccountInfo().id,
operationId_);
}
void
LinkDeviceModel::cancelAddDevice()
{
handleInProgressSignal();
lrcInstance_->accountModel().cancelAddDevice(lrcInstance_->getCurrentAccountInfo().id,
operationId_);
}
void
LinkDeviceModel::reset()
{
set_deviceAuthState(static_cast<int>(DeviceAuthState::INIT));
set_linkDeviceError("");
set_ipAddress("");
set_tokenErrorMessage("");
}

57
src/app/linkdevicemodel.h Normal file
View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2025-2025 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 "api/account.h"
#include "qmladapterbase.h"
#include "qtutils.h"
#include <QObject>
#include <QVariant>
#include <QMap>
class LRCInstance;
class LinkDeviceModel : public QObject
{
Q_OBJECT
QML_PROPERTY(QString, tokenErrorMessage);
QML_PROPERTY(QString, linkDeviceError);
QML_PROPERTY(int, deviceAuthState);
QML_PROPERTY(QString, ipAddress);
public:
explicit LinkDeviceModel(LRCInstance* lrcInstance, QObject* parent = nullptr);
Q_INVOKABLE void addDevice(const QString& token);
Q_INVOKABLE void confirmAddDevice();
Q_INVOKABLE void cancelAddDevice();
Q_INVOKABLE void reset();
private:
bool checkNewStateValidity(lrc::api::account::DeviceAuthState newState) const;
void handleConnectingSignal();
void handleAuthenticatingSignal(const QVariantMap& details);
void handleInProgressSignal();
void handleDoneSignal(const QVariantMap& details);
LRCInstance* lrcInstance_ = nullptr;
uint32_t operationId_;
};

View File

@ -80,7 +80,7 @@ main(int argc, char* argv[])
MainApplication app(argc, argv);
app.setDesktopFileName(QStringLiteral("jami"));
app.setDesktopFileName(QStringLiteral("net.jami.Jami"));
#if defined(Q_OS_MACOS)
if (macutils::isMetalSupported()) {
QQuickWindow::setGraphicsApi(QSGRendererInterface::MetalRhi);

View File

@ -431,7 +431,7 @@ MainApplication::initQmlLayer()
// Register the crash reporter as a context property in the QML engine.
engine_->rootContext()->setContextProperty("crashReporter", crashReporter_);
QUrl url = u"qrc:/MainApplicationWindow.qml"_qs;
QUrl url = QStringLiteral("qrc:/MainApplicationWindow.qml");
#ifdef QT_DEBUG
if (parser_.isSet("test")) {
// List the QML files in the project source tree.
@ -445,7 +445,7 @@ MainApplication::initQmlLayer()
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;
url = QStringLiteral("qrc:/ComponentTestWindow.qml");
}
#endif
QObject::connect(

View File

@ -136,17 +136,12 @@ Rectangle {
}
Shortcut {
sequence: "F11"
sequence: "Esc"
context: Qt.ApplicationShortcut
onActivated: layoutManager.toggleWindowFullScreen()
}
Keys.onPressed: function (keyEvent) {
if (keyEvent.key === Qt.Key_Escape) {
onActivated: {
MessagesAdapter.replyToId = "";
MessagesAdapter.editId = "";
layoutManager.popFullScreenItem();
keyEvent.accepted = true;
}
}

View File

@ -32,219 +32,223 @@ BaseModalDialog {
button1.text: JamiStrings.contribute
button2.text: JamiStrings.feedback
button1.onClicked: { Qt.openUrlExternally("https://jami.net/contribute/")}
button2.onClicked: { Qt.openUrlExternally("mailto:jami@gnu.org")}
button1.onClicked: {
Qt.openUrlExternally("https://jami.net/contribute/");
}
button2.onClicked: {
Qt.openUrlExternally("mailto:jami@gnu.org");
}
popupContent: JamiFlickable {
id: aboutPopUpScrollView
id: aboutPopUpScrollView
width: aboutPopUpContentRectColumnLayout.implicitWidth
height: Math.min(root.implicitHeight, aboutPopUpContentRectColumnLayout.implicitHeight)
width: aboutPopUpContentRectColumnLayout.implicitWidth
height: Math.min(root.implicitHeight, aboutPopUpContentRectColumnLayout.implicitHeight)
contentHeight: aboutPopUpContentRectColumnLayout.implicitHeight
contentHeight: aboutPopUpContentRectColumnLayout.implicitHeight
ColumnLayout {
id: aboutPopUpContentRectColumnLayout
anchors.centerIn: parent
ColumnLayout {
id: aboutPopUpContentRectColumnLayout
anchors.centerIn: parent
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
spacing: 10
ResponsiveImage {
id: aboutPopUPJamiLogoImage
RowLayout{
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
spacing: 10
Layout.margins: 10
Layout.preferredWidth: 150
Layout.preferredHeight: 50
ResponsiveImage {
id: aboutPopUPJamiLogoImage
Layout.alignment: Qt.AlignCenter
Layout.margins: 10
Layout.preferredWidth: 150
Layout.preferredHeight: 50
source: JamiTheme.darkTheme ? JamiResources.logo_jami_standard_coul_white_svg : JamiResources.logo_jami_standard_coul_svg
}
Control {
Layout.fillHeight: true
Layout.fillWidth: true
background: Rectangle {
color: JamiTheme.backgroundRectangleColor
radius: 5
}
padding: 10
contentItem: ColumnLayout {
spacing: 4
TextEdit {
id: jamiSlogansText
Layout.alignment: Qt.AlignLeft
wrapMode: Text.WordWrap
font.pixelSize: JamiTheme.menuFontSize
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: textMetricsjamiSlogansText.text
selectByMouse: true
readOnly: true
color: JamiTheme.textColor
TextMetrics {
id: textMetricsjamiSlogansText
font: jamiSlogansText.font
text: JamiStrings.slogan
}
}
TextEdit {
Layout.alignment: Qt.AlignLeft
font.pixelSize: JamiTheme.textFontSize
padding: 0
readonly property bool isBeta: AppVersionManager.isCurrentVersionBeta()
text: {
// HACK: Only display the version string if it has been constructed properly.
// This is a workaround for an issue that occurs due to the way Linux
// packaging is done, where the git repository is not available in the
// build source at configure time, which is when the version files are
// generated, so we prevent a "." from being displayed if the version
// string is not available.
var contentStr = JamiStrings.buildID + ": " + UtilsAdapter.getBuildIDStr();
const versionStr = UtilsAdapter.getVersionStr()
if (versionStr.length > 1) {
contentStr += "\n" + JamiStrings.version + ": " + (isBeta ? "(Beta) " : "") + versionStr
}
return contentStr
}
selectByMouse: true
readOnly: true
color: JamiTheme.faddedFontColor
}
}
}
source: JamiTheme.darkTheme ? JamiResources.logo_jami_standard_coul_white_svg : JamiResources.logo_jami_standard_coul_svg
}
TextEdit {
id: jamiDeclarationHyperText
Layout.alignment: Qt.AlignLeft
Control {
Layout.fillHeight: true
Layout.fillWidth: true
// Strangely, hoveredLink works badly when width grows too large
Layout.maximumWidth: JamiTheme.preferredDialogWidth - 2*JamiTheme.preferredMarginSize
Layout.topMargin: 15
color: JamiTheme.textColor
font.pixelSize: JamiTheme.menuFontSize
verticalAlignment: Text.AlignVCenter
text: textMetricsjamiDeclarationHyperText.text
textFormat: TextEdit.RichText
wrapMode: TextEdit.WordWrap
selectByMouse: true
readOnly: true
onLinkActivated: Qt.openUrlExternally(link)
TextMetrics {
id: textMetricsjamiDeclarationHyperText
font: jamiDeclarationHyperText.font
text: JamiStrings.declaration
background: Rectangle {
color: JamiTheme.backgroundRectangleColor
radius: 5
}
MouseArea {
anchors.fill: parent
padding: 10
contentItem: ColumnLayout {
spacing: 4
TextEdit {
id: jamiSlogansText
Layout.alignment: Qt.AlignLeft
// We don't want to eat clicks on the Text.
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
wrapMode: Text.WordWrap
font.pixelSize: JamiTheme.menuFontSize
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: textMetricsjamiSlogansText.text
selectByMouse: true
readOnly: true
color: JamiTheme.textColor
TextMetrics {
id: textMetricsjamiSlogansText
font: jamiSlogansText.font
text: JamiStrings.slogan
}
}
TextEdit {
Layout.alignment: Qt.AlignLeft
font.pixelSize: JamiTheme.textFontSize
padding: 0
readonly property bool isBeta: AppVersionManager.isCurrentVersionBeta()
text: {
// HACK: Only display the version string if it has been constructed properly.
// This is a workaround for an issue that occurs due to the way Linux
// packaging is done, where the git repository is not available in the
// build source at configure time, which is when the version files are
// generated, so we prevent a "." from being displayed if the version
// string is not available.
var contentStr = JamiStrings.buildID + ": " + UtilsAdapter.getBuildIDStr();
const versionStr = UtilsAdapter.getVersionStr();
if (versionStr.length > 1) {
contentStr += "\n" + JamiStrings.version + ": " + (isBeta ? "(Beta) " : "") + versionStr;
}
return contentStr;
}
selectByMouse: true
readOnly: true
color: JamiTheme.faddedFontColor
}
}
}
}
TextEdit {
id: jamiNoneWarrantyHyperText
TextEdit {
id: jamiDeclarationHyperText
Layout.alignment: Qt.AlignLeft
Layout.maximumWidth: JamiTheme.preferredDialogWidth - 2*JamiTheme.preferredMarginSize
Layout.topMargin: 15
wrapMode: Text.WordWrap
font.pixelSize: JamiTheme.menuFontSize
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
verticalAlignment: Text.AlignTop
color: JamiTheme.textColor
// Strangely, hoveredLink works badly when width grows too large
Layout.maximumWidth: JamiTheme.preferredDialogWidth - 2 * JamiTheme.preferredMarginSize
Layout.topMargin: 15
text: textMetricsjamiNoneWarrantyHyperText.text
textFormat: TextEdit.RichText
selectByMouse: true
readOnly: true
onLinkActivated: Qt.openUrlExternally(link)
color: JamiTheme.textColor
TextMetrics {
id: textMetricsjamiNoneWarrantyHyperText
font: jamiDeclarationHyperText.font
text: JamiStrings.noWarranty
}
font.pixelSize: JamiTheme.menuFontSize
verticalAlignment: Text.AlignVCenter
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
text: textMetricsjamiDeclarationHyperText.text
textFormat: TextEdit.RichText
wrapMode: TextEdit.WordWrap
selectByMouse: true
readOnly: true
onLinkActivated: Qt.openUrlExternally(link)
TextMetrics {
id: textMetricsjamiDeclarationHyperText
font: jamiDeclarationHyperText.font
text: JamiStrings.declaration
}
TextEdit {
id: jamiYears
MouseArea {
anchors.fill: parent
Layout.alignment: Qt.AlignLeft
Layout.maximumWidth: JamiTheme.preferredDialogWidth - 2*JamiTheme.preferredMarginSize
Layout.topMargin: 15
// We don't want to eat clicks on the Text.
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
wrapMode: Text.WordWrap
font.pixelSize: JamiTheme.menuFontSize
verticalAlignment: Text.AlignTop
TextEdit {
id: jamiNoneWarrantyHyperText
color: JamiTheme.textColor
Layout.alignment: Qt.AlignLeft
Layout.maximumWidth: JamiTheme.preferredDialogWidth - 2 * JamiTheme.preferredMarginSize
Layout.topMargin: 15
wrapMode: Text.WordWrap
font.pixelSize: JamiTheme.menuFontSize
text: textMetricsYears.text
textFormat: TextEdit.RichText
selectByMouse: true
readOnly: true
verticalAlignment: Text.AlignTop
color: JamiTheme.textColor
onLinkActivated: Qt.openUrlExternally(link)
text: textMetricsjamiNoneWarrantyHyperText.text
textFormat: TextEdit.RichText
selectByMouse: true
readOnly: true
onLinkActivated: Qt.openUrlExternally(link)
TextMetrics {
id: textMetricsYears
font: jamiDeclarationHyperText.font
text: JamiStrings.declarationYear + " " + '<a href="https://savoirfairelinux.com/" style="color: ' + JamiTheme.buttonTintedBlue + '">Savoir-faire Linux Inc.</a><br>'
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
TextMetrics {
id: textMetricsjamiNoneWarrantyHyperText
font: jamiDeclarationHyperText.font
text: JamiStrings.noWarranty
}
Rectangle {
width: projectCreditsScrollView.width + 20
height: projectCreditsScrollView.height + 20
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
color: JamiTheme.backgroundRectangleColor
radius: 5
TextEdit {
id: jamiYears
ProjectCreditsScrollView {
id: projectCreditsScrollView
Layout.alignment: Qt.AlignLeft
Layout.maximumWidth: JamiTheme.preferredDialogWidth - 2 * JamiTheme.preferredMarginSize
Layout.topMargin: 15
anchors.centerIn: parent
width: JamiTheme.preferredDialogWidth - 2*JamiTheme.preferredMarginSize
height: 140
anchors.margins: 10
}
wrapMode: Text.WordWrap
font.pixelSize: JamiTheme.menuFontSize
verticalAlignment: Text.AlignTop
color: JamiTheme.textColor
text: textMetricsYears.text
textFormat: TextEdit.RichText
selectByMouse: true
readOnly: true
onLinkActivated: Qt.openUrlExternally(link)
TextMetrics {
id: textMetricsYears
font: jamiDeclarationHyperText.font
text: JamiStrings.declarationYear + " " + '<a href="https://savoirfairelinux.com/" style="color: ' + JamiTheme.buttonTintedBlue + '">Savoir-faire Linux Inc.</a><br>'
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
Rectangle {
width: JamiTheme.preferredDialogWidth - 2 * JamiTheme.preferredMarginSize
height: 160
color: JamiTheme.backgroundRectangleColor
radius: 5
ProjectCreditsScrollView {
id: projectCreditsScrollView
anchors.fill: parent
anchors.topMargin: JamiTheme.preferredMarginSize
anchors.bottomMargin: JamiTheme.preferredMarginSize
anchors.leftMargin: JamiTheme.preferredMarginSize / 2
}
}
}
}
}

View File

@ -45,7 +45,7 @@ Label {
anchors.fill: parent
color: JamiTheme.backgroundColor
Behavior on color {
Behavior on color {
ColorAnimation {
duration: JamiTheme.shortFadeDuration
}
@ -88,7 +88,6 @@ Label {
spacing: 10
Avatar {
id: avatar
objectName: "accountComboBoxAvatar"
@ -147,7 +146,6 @@ Label {
spacing: 10
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: parent.height
JamiPushButton {

View File

@ -35,6 +35,14 @@ Item {
property int imageFillMode: 0
property alias image: image
// On darkTheme changed, reload the image
Connections {
target: JamiTheme
function onDarkThemeChanged() {
updateImageSource(downloadUrl, localPath, defaultImage);
}
}
AnimatedImage {
id: image
objectName: "image"

View File

@ -171,8 +171,10 @@ ItemDelegate {
Connections {
target: menuAction !== undefined ? menuAction : null
function onTriggered() {
if (menuAction.popupMode !== CallActionBar.ActionPopupMode.ListElement)
itemListView.currentIndex = menuAction.listModel.getCurrentIndex();
if (menuAction.popupMode !== CallActionBar.ActionPopupMode.ListElement) {
var index = menuAction.listModel.currentIndex;
itemListView.currentIndex = index !== undefined ? index : 0;
}
}
}
@ -277,11 +279,11 @@ ItemDelegate {
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;
return -(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;
return -implicitHeight - 12;
}
}
@ -290,7 +292,7 @@ ItemDelegate {
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;
return -implicitWidth - 12;
} else {
// Note: isn't some of this logic built into the Popup?
@ -309,7 +311,7 @@ ItemDelegate {
// 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;
return diff > 0 ? xValue - diff - 24 : xValue;
}
}

View File

@ -40,6 +40,13 @@ Item {
onActivatedAmbiguously: CallAdapter.hangUpThisCall()
}
Shortcut {
sequence: "F11"
context: Qt.ApplicationShortcut
enabled: CurrentConversation.hasCall && !layoutManager.isWebFullscreen
onActivated: toggleFullScreen();
}
Keys.onPressed: {
if (LRCInstance.currentAccountType !== Profile.Type.SIP)
return;
@ -72,14 +79,15 @@ Item {
function toggleFullScreen() {
if (!layoutManager.isCallFullscreen) {
layoutManager.pushFullScreenItem(callStackMainView.item, callStackMainView, null, null);
layoutManager.pushFullScreenItem(callStackMainView);
} else {
layoutManager.removeFullScreenItem(callStackMainView.item);
layoutManager.removeFullScreenItem(callStackMainView);
}
}
Loader {
id: callStackMainView
objectName: "callViewLoader"
anchors.fill: parent

View File

@ -44,7 +44,7 @@ Rectangle {
function updateMessageDraft() {
// Store the current files that have not been sent, if any. Do the same for the message draft.
var filePathDraft = [];
while(messageBar.fileContainer.filesToSendCount > 0) {
while (messageBar.fileContainer.filesToSendCount > 0) {
var currentIndex = messageBar.fileContainer.filesToSendListModel.index(0, 0);
var filePath = messageBar.fileContainer.filesToSendListModel.data(currentIndex, FilesToSend.FilePath);
filePathDraft.push(filePath);
@ -66,7 +66,6 @@ Rectangle {
messageBar.fileContainer.filesToSendListModel.addToPending(restoredContent["files"][i]);
}
}
}
Connections {
@ -203,7 +202,7 @@ Rectangle {
var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/JamiFileDialog.qml", {
"fileMode": JamiFileDialog.OpenFiles,
"nameFilters": [JamiStrings.allFiles]
});
}, true); // is a single instance
dlg.filesAccepted.connect(function (files) {
setFilePathsToSend(files);
});

View File

@ -65,10 +65,10 @@ ContextMenuAutoLoader {
}
},
GeneralMenuItem {
id: clearConversation
id: deleteConversation
canTrigger: mode === Conversation.Mode.NON_SWARM && !hasCall && !root.isBanned
itemName: JamiStrings.clearConversation
itemName: JamiStrings.deleteConversation
iconSource: JamiResources.ic_clear_24dp_svg
onClicked: MessagesAdapter.clearConversationHistory(responsibleAccountId, responsibleConvUid)
},

View File

@ -59,10 +59,6 @@ Window {
shortcut: "Ctrl+F"
description: qsTr("Search bar")
}
ListElement {
shortcut: "F11"
description: qsTr("Full screen")
}
ListElement {
shortcut: "Ctrl++"
description: qsTr("Increase font size")
@ -131,6 +127,10 @@ Window {
shortcut: "Ctrl+Shift+D"
description: qsTr("Decline call")
}
ListElement {
shortcut: "F11"
description: qsTr("Full screen")
}
ListElement {
shortcut: "M"
description: qsTr("Mute microphone")

View File

@ -82,8 +82,9 @@ JamiFlickable {
ScrollBar.vertical.visible: text
ScrollBar.horizontal.visible: text
boundsMovement: Flickable.StopAtBounds
boundsBehavior: Flickable.DragOverBounds
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
interactive: true
function resetEditableText() {

View File

@ -30,6 +30,10 @@ ListView {
id: root
spacing: 10
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
model: SortFilterProxyModel {
id: proxyModel
@ -49,6 +53,12 @@ ListView {
MessagesAdapter.startSearch(prompt, false);
}
onVisibleChanged: {
if (visible) {
MessagesAdapter.startSearch(prompt, true);
}
}
Connections {
target: researchTabBar
function onFilterTabChange() {
@ -56,6 +66,27 @@ ListView {
}
}
// This function will take a filtered message and further format it to fit
// into the research panel in a coherent way. Find the first occurence of the search term in the message
// highlight it and wrap it with numChars characters on either side.
function formatMessage(searchTerm, message, numChars) {
var index = message.toLowerCase().indexOf(searchTerm.toLowerCase());
if (index === -1)
return message;
var prefix = message.substring(Math.max(0, index - numChars), index);
var suffix = message.substring(index + searchTerm.length, Math.min(index + searchTerm.length + numChars, message.length));
var before = (Math.max(0, index - numChars) === 0);
var after = (Math.min(index + searchTerm.length + numChars, message.length) === message.length);
var highlightedTerm = '<span style="background-color: #48ffff00">' + message.substring(index, index + searchTerm.length) + "</span>";
var result = "";
if (!before)
result += "... ";
result += prefix + highlightedTerm + suffix;
if (!after)
result += " ...";
return result;
}
delegate: Item {
width: root.width
height: msgLayout.height
@ -75,7 +106,6 @@ ListView {
id: timestampItem
showDay: true
showTime: true
formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
formattedDay: MessagesAdapter.getFormattedDay(Timestamp)
}
@ -94,55 +124,56 @@ ListView {
showPresenceIndicator: false
mode: contentRow.isMe ? Avatar.Mode.Account : Avatar.Mode.Contact
Layout.leftMargin: 10
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Text {
text: contentRow.isMe ? CurrentAccount.bestName : UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) + " :"
Layout.preferredWidth: myText.width
Layout.rightMargin: 10
Layout.leftMargin: 10
font.pixelSize: 0
color: JamiTheme.chatviewSecondaryInformationColor
font.bold: true
}
Text {
TextArea {
id: myText
text: Body
text: formatMessage(prompt, Body, 100)
readOnly: true
background: Rectangle {
radius: 5
color: JamiTheme.messageInBgColor
}
color: JamiTheme.textColor
Layout.preferredWidth: msgLayout.width - avatar.width - 30 - 10
elide: Text.ElideRight
Layout.fillWidth: true
wrapMode: Text.Wrap
Layout.rightMargin: 10
Layout.leftMargin: 10
textFormat: TextEdit.MarkdownText
font.pixelSize: IsEmojiOnly ? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize
Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignLeft
}
}
}
}
Button {
id: buttonJumpTo
visible: msgHover.hovered || hovered
anchors.top: msgLayout.top
anchors.right: msgLayout.right
anchors.rightMargin: 20
anchors.topMargin: timestampItem.height - 20
anchors.topMargin: timestampItem.height - 21
width: buttonJumpText.width + 10
height: buttonJumpText.height + 10
background.visible: false
onClicked: {
CurrentConversation.scrollToMsg(Id);
}
Text {
id: buttonJumpText
text: JamiStrings.jumpTo
color: buttonJumpTo.hovered ? JamiTheme.blueLinkColor : JamiTheme.chatviewSecondaryInformationColor
font.underline: buttonJumpTo.hovered

View File

@ -72,6 +72,9 @@ Item {
}
}
QWKSetParentHitTestVisible {
}
TextMetrics {
id: nameTextMetrics
text: bestName

View File

@ -145,9 +145,9 @@ Popup {
height: 300
Rectangle {
id: previewWidget
radius: 5
id: previewWidget
anchors.centerIn: parent
height: root.isAudio ? 100 : 300
width: 300
@ -210,7 +210,7 @@ Popup {
}
}
ColumnLayout{
ColumnLayout {
id: mainLayout
anchors.fill: parent
@ -258,7 +258,6 @@ Popup {
Layout.alignment: Qt.AlignCenter
preferredSize: btnSize
imageColor: JamiTheme.whiteColor
source: JamiResources.record_round_black_24dp_svg
@ -295,7 +294,6 @@ Popup {
preferredSize: btnSize
source: JamiResources.record_black_24dp_svg
imageContainerHeight: 20
imageContainerWidth: 20
imageColor: JamiTheme.whiteColor
@ -314,7 +312,7 @@ Popup {
onClicked: {
root.photo = videoProvider.captureVideoFrame(VideoDevices.getDefaultDevice());
updateState(RecordBox.States.REC_SUCCESS);
updateState(RecordBox.States.REC_SUCCESS);
}
}
@ -373,7 +371,12 @@ Popup {
background: RoundedBorderRectangle {
opacity: btnRestart.hovered ? 1 : 0.7
fillColor: btnRestart.hovered ? JamiTheme.recordBoxHoverColor : JamiTheme.recordBoxButtonColor
radius: {
radius: isRTL ? {
"tl": 0,
"tr": 5,
"br": 5,
"bl": 0
} : {
"tl": 5,
"tr": 0,
"br": 0,
@ -381,7 +384,6 @@ Popup {
}
}
onClicked: {
if (!root.isPhoto)
stopRecording();
@ -415,7 +417,6 @@ Popup {
}
}
JamiPushButton {
id: btnSend
@ -436,7 +437,12 @@ Popup {
background: RoundedBorderRectangle {
opacity: btnSend.hovered ? 1 : 0.7
fillColor: JamiTheme.chatViewFooterSendButtonColor //btnSend.hovered ? JamiTheme.recordBoxHoverColor : JamiTheme.recordBoxButtonColor
radius: {
radius: isRTL ? {
"tl": 5,
"tr": 0,
"br": 0,
"bl": 5
} : {
"tl": 0,
"tr": 5,
"br": 5,
@ -464,9 +470,8 @@ Popup {
repeat: true
onTriggered: updateTimer()
}
}
}
}
}
}
}
}

View File

@ -44,34 +44,39 @@ Window {
property var listModel: []
property real componentMinWidth: 350
property real marginSize: JamiTheme.preferredMarginSize
property real elementWidth: {
var layoutWidth = selectScreenWindowLayout.width;
var minSize = componentMinWidth + 2 * marginSize;
var numberElementPerRow = Math.floor(layoutWidth / minSize);
if (numberElementPerRow == 1 && layoutWidth > componentMinWidth * 1.5) {
numberElementPerRow = 2;
}
if (showWindows)
numberElementPerRow = Math.min(listModel.length, numberElementPerRow);
else
numberElementPerRow = Math.min(listModel.length + 1, numberElementPerRow);
var spacingLength = marginSize * (numberElementPerRow + 2);
return (layoutWidth - spacingLength) / numberElementPerRow;
}
// Function to safely populate screen/window list
function calculateRepeaterModel() {
listModel = [];
var newModel = [];
var idx;
if (!showWindows) {
for (idx in Qt.application.screens) {
listModel.push(JamiStrings.screen.arg(idx));
newModel.push({
title: JamiStrings.screen.arg(idx),
index: parseInt(idx),
isAllScreens: false
});
}
} else {
AvAdapter.getListWindows();
for (idx in AvAdapter.windowsNames) {
listModel.push(AvAdapter.windowsNames[idx]);
newModel.push({
title: AvAdapter.windowsNames[idx],
index: parseInt(idx),
isAllScreens: false
});
}
}
// Add "All Screens" option for non-Windows platforms when showing screens
if (!showWindows && Qt.application.screens.length > 1 && Qt.platform.os.toString() !== "windows") {
newModel.unshift({
title: JamiStrings.allScreens,
index: -1,
isAllScreens: true
});
}
listModel = newModel;
}
onVisibleChanged: {
@ -80,23 +85,21 @@ Window {
if (!active) {
selectedScreenNumber = undefined;
}
screenSharePreviewRepeater.model = {};
calculateRepeaterModel();
screenSharePreviewRepeater.model = root.listModel;
}
Rectangle {
id: selectScreenWindowRect
anchors.fill: parent
color: JamiTheme.backgroundColor
ColumnLayout {
id: selectScreenWindowLayout
anchors.fill: parent
spacing: marginSize
Text {
id: titleText
font.pointSize: JamiTheme.menuFontSize
font.bold: true
text: showWindows ? JamiStrings.windows : JamiStrings.screens
@ -107,54 +110,47 @@ Window {
ScrollView {
id: screenSelectionScrollView
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: selectScreenWindowLayout.width
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
ScrollBar.vertical.policy: ScrollBar.AsNeeded
Flow {
id: screenSelectionScrollViewFlow
GridView {
id: screenGrid
anchors.fill: parent
anchors.margins: marginSize
// https://bugreports.qt.io/browse/QTBUG-110323
width: screenSelectionScrollView.width
height: screenSelectionScrollView.height
topPadding: marginSize
rightPadding: marginSize
leftPadding: marginSize
spacing: marginSize
Loader {
// Show all screens
active: !showWindows && Qt.application.screens.length > 1 && Qt.platform.os.toString() !== "windows"
sourceComponent: ScreenSharePreview {
id: screenSelectionRectAll
elementIndex: -1
rectTitle: JamiStrings.allScreens
rId: AvAdapter.getSharingResource(-1)
}
cellWidth: {
var cellsPerRow = Math.floor(width / (componentMinWidth + marginSize));
cellsPerRow = Math.max(1, cellsPerRow);
var calculatedWidth = Math.floor(width / cellsPerRow);
return Math.max(componentMinWidth, calculatedWidth);
}
cellHeight: cellWidth * 3 / 4 + marginSize * 2
Repeater {
id: screenSharePreviewRepeater
model: listModel
model: listModel.length
delegate: Item {
width: screenGrid.cellWidth - marginSize
height: screenGrid.cellHeight - marginSize
delegate: ScreenSharePreview {
visible: JamiStrings.selectScreen !== modelData.title && JamiStrings.selectWindow !== modelData.title
ScreenSharePreview {
id: screenItem
anchors.centerIn: parent
width: parent.width
height: parent.height - marginSize
visible: JamiStrings.selectScreen !== listModel[index] && JamiStrings.selectWindow !== listModel[index]
elementIndex: index
rectTitle: listModel[index] ? listModel[index] : ""
elementIndex: modelData.index
rectTitle: modelData.title
rId: {
if (showWindows)
return rId = AvAdapter.getSharingResource(-2, AvAdapter.windowsIds[index], AvAdapter.windowsNames[index]);
return rId = AvAdapter.getSharingResource(index);
if (modelData.isAllScreens)
return AvAdapter.getSharingResource(-1);
else if (showWindows)
return AvAdapter.getSharingResource(-2, AvAdapter.windowsIds[modelData.index], AvAdapter.windowsNames[modelData.index], 1);
return AvAdapter.getSharingResource(modelData.index);
}
}
}
@ -163,19 +159,18 @@ Window {
RowLayout {
Layout.margins: marginSize
Layout.preferredWidth: selectScreenWindowLayout.width
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
spacing: marginSize
MaterialButton {
id: selectButton
Layout.maximumWidth: 200
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: marginSize
enabled: selectedScreenNumber != undefined
enabled: selectedScreenNumber !== undefined
opacity: enabled ? 1.0 : 0.5
color: JamiTheme.buttonTintedBlack
@ -193,7 +188,7 @@ Window {
if (!showWindows)
AvAdapter.shareEntireScreen(selectedScreenNumber);
else {
AvAdapter.shareWindow(AvAdapter.windowsIds[selectedScreenNumber], AvAdapter.windowsNames[selectedScreenNumber - Qt.application.screens.length]);
AvAdapter.shareWindow(AvAdapter.windowsIds[selectedScreenNumber], AvAdapter.windowsNames[selectedScreenNumber]);
}
}
root.close();

View File

@ -618,7 +618,7 @@ Rectangle {
width: JamiTheme.smartListAvatarSize
height: JamiTheme.smartListAvatarSize
Layout.leftMargin: JamiTheme.preferredMarginSize
Layout.topMargin: JamiTheme.preferredMarginSize / 2
Layout.alignment: Qt.AlignVCenter
z: -index
opacity: (MemberRole === Member.Role.INVITED || MemberRole === Member.Role.BANNED) ? 0.5 : 1
@ -632,7 +632,7 @@ Rectangle {
id: nameTextEdit
Layout.preferredHeight: JamiTheme.preferredFieldHeight
Layout.topMargin: JamiTheme.preferredMarginSize / 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
eText: UtilsAdapter.getContactBestName(CurrentAccount.id, MemberUri)
@ -654,8 +654,7 @@ Rectangle {
id: roleLabel
Layout.preferredHeight: JamiTheme.preferredFieldHeight
Layout.topMargin: JamiTheme.preferredMarginSize / 2
Layout.alignment: Qt.AlignVCenter
eText: {
if (MemberRole === Member.Role.ADMIN)
return JamiStrings.administrator;

View File

@ -17,6 +17,8 @@
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

View File

@ -83,7 +83,8 @@ Item {
function onDonationCampaignSettingsChanged() {
// Changing any of the donation campaign settings will trigger a recompute
// of the banner visibility.
updateIsDonationBannerVisible(); }
updateIsDonationBannerVisible();
}
}
function updateIsDonationBannerVisible() {
@ -99,4 +100,7 @@ Item {
const now = new Date();
return isVisible && now < endDate && now >= startDate;
}
// Track if a fileDialog is opened. Is int to account for eventual future features including multiple FileDialog
property int openFileDialogCount: 0
}

View File

@ -46,7 +46,7 @@ Item {
property string buildID: qsTr("Build ID")
property string version: qsTr("Version")
property string declarationYear: "Copyright © 20152025"
property string slogan: "Astarte"
property string slogan: "Εἰρήνη"
property string declaration: qsTr('Jami, a GNU package, is software for universal and distributed peer-to-peer communication that respects the freedom and privacy of its users. Visit <a href="https://jami.net" style="color: ' + JamiTheme.buttonTintedBlue + '">jami.net</a>' + ' to learn more.')
property string noWarranty: qsTr('This program comes with absolutely no warranty. See the <a href="https://www.gnu.org/licenses/gpl-3.0.html" style="color: ' + JamiTheme.buttonTintedBlue + '">GNU General Public License</a>, version 3 or later for details.')
property string contribute: qsTr('Contribute')
@ -70,6 +70,21 @@ Item {
property string transferThisCall: qsTr("Transfer this call")
property string transferTo: qsTr("Transfer to")
// Device import/linking
property string scanToImportAccount: qsTr("To continue the import account operation, scan the following QR code on the source device.")
property string waitingForToken: qsTr("Please wait…")
property string scanQRCode: qsTr("Scan QR code")
property string connectingToDevice: qsTr("Action required. Please confirm account on the source device.")
property string confirmAccountImport: qsTr("Authenticating device")
property string transferringAccount: qsTr("Transferring account…")
property string cantScanQRCode: qsTr("If you are unable to scan the QR code, enter the following token on the source device.")
property string optionConfirm: qsTr("Confirm")
property string optionTryAgain: qsTr("Try again")
property string importFailed: qsTr("An error occurred while importing the account.")
property string importFromAnotherAccount: qsTr("Import from another account")
property string connectToAccount: qsTr("Connect to account")
property string authenticationError: qsTr("An authentication error occurred while linking the device. Please check credentials and try again.")
// AccountMigrationDialog
property string authenticationRequired: qsTr("Authentication required")
property string migrationReason: qsTr("Your session has expired or been revoked on this device. Please enter your password.")
@ -264,7 +279,7 @@ Item {
// ConversationContextMenu
property string startAudioCall: qsTr("Start audio call")
property string startVideoCall: qsTr("Start video call")
property string clearConversation: qsTr("Clear conversation")
property string deleteConversation: qsTr("Delete conversation")
property string confirmAction: qsTr("Confirm action")
property string removeConversation: qsTr("Leave conversation")
property string confirmLeaveConversation: qsTr("Do you want to leave this conversation?")
@ -520,10 +535,10 @@ Item {
property string enableAutoUpdates: qsTr("Enable/Disable automatic updates")
property string updatesTitle: qsTr("Updates")
property string updateDialogTitle: qsTr("Update")
property string updateFound: qsTr("A new version of Jami is available.\nDo you want to update Jami now?\nTo continue, click Update.")
property string updateNotFound: qsTr("No new version of Jami was found")
property string updateCheckError: qsTr("An error occurred while checking for a new version.")
property string updateNetworkError: qsTr("A network error occurred.")
property string updateFound: qsTr("A new version of the Jami application is available. Do you want to update now? To continue, click Update.")
property string updateNotFound: qsTr("The application is up to date.")
property string updateCheckError: qsTr("An error occurred while checking for updates.")
property string updateNetworkError: qsTr("A network error occurred while checking for updates.")
property string updateSSLError: qsTr("An SSL error occurred.")
property string updateDownloadCanceled: qsTr("Installer download was canceled by user.")
property string updateDownloading: "Downloading"
@ -579,29 +594,23 @@ Item {
// ImportFromDevicePage
property string importButton: qsTr("Import")
property string pin: qsTr("Enter the PIN code")
property string importFromDeviceDescription: qsTr("A PIN code is required to use an existing Jami account on this device.")
property string importStep1: qsTr("Step 1")
property string importStep2: qsTr("Step 2")
property string importStep3: qsTr("Step 3")
property string importStep4: qsTr("Step 4")
property string importStep1Desc: qsTr("Open the manage account tab in the settings of the previous device.")
property string importStep2Desc: qsTr("Select the account to link.")
property string importStep3Desc: qsTr("Select “Link new device.”")
property string importStep4Desc: qsTr("The PIN code will expire in 10 minutes.")
property string importPasswordDesc: qsTr("Fill if the account is password-encrypted.")
// LinkDevicesDialog
property string pinTimerInfos: qsTr("The PIN code and the account password should be entered in the device within 10 minutes.")
property string close: qsTr("Close")
property string enterAccountPassword: qsTr("Enter account password")
property string enterPasswordPinCode: qsTr("This account is password encrypted, enter the password to generate a PIN code.")
property string addDevice: qsTr("Add Device")
property string pinExpired: qsTr("PIN code has expired.")
property string onAnotherDevice: qsTr("On another device")
property string onAnotherDeviceInstruction: qsTr("Install and launch Jami, select “Import from another device” and scan the QR code.")
property string linkNewDevice: qsTr("Link new device")
property string linkingInstructions: qsTr("In Jami, scan the QR code or manually enter the PIN code.")
property string pinValidity: qsTr("The PIN code will expire in: ")
property string linkDeviceConnecting: qsTr("Connecting to your new device…")
property string linkDeviceInProgress: qsTr("The export account operation to the new device is in progress.\nPlease confirm the import on the new device.")
property string linkDeviceScanQR: qsTr("On the new device, initiate a new account.\nSelect Add account -> Connect from another device.\nWhen ready, scan the QR code.")
property string linkDeviceEnterManually: qsTr("Alternatively you could enter the authentication code manually.")
property string linkDeviceEnterCodePlaceholder: qsTr("Enter authentication code")
property string linkDeviceAllSet: qsTr("The account was imported successfully.")
property string linkDeviceFoundAddress: qsTr("New device found at address below. Is that you?\nClicking on confirm will continue transfering account.")
property string linkDeviceNewDeviceIP: qsTr("New device IP address: %1")
property string linkDeviceCloseWarningTitle: qsTr("Do you want to exit?")
property string linkDeviceCloseWarningMessage: qsTr("Exiting will cancel the import account operation.")
// PasswordDialog
property string enterPassword: qsTr("Enter password")
@ -741,7 +750,7 @@ Item {
property string removeDefaultModerator: qsTr("Remove default moderator")
// Daemon reconnection
property string reconnectDaemon: qsTr("Reconnection of the Jami daemon (jamid) is in progress. Please wait…")
property string reconnectDaemon: qsTr("Jami daemon (jamid) reconnection is in progress. Please wait…")
property string reconnectionFailed: qsTr("An error occurred while reconnecting to the Jami daemon (jamid).\nThe application will now exit.")
// Message view

View File

@ -573,7 +573,7 @@ Item {
property real welcomeGridWidth: 3 * JamiTheme.tipBoxWidth + 2 * JamiTheme.welcomePageSpacing
property real welcomeThirdGridWidth: (welcomeGridWidth - JamiTheme.welcomePageSpacing) / 3
property real welcomeShortGridWidth: 2 * JamiTheme.tipBoxWidth + JamiTheme.welcomePageSpacing
readonly property string welcomeBg: darkTheme ? JamiResources.background_don_dark_jpg : JamiResources.background_don_white_jpg
readonly property string welcomeBg: darkTheme ? JamiResources.welcome_bg_dark_jpg : JamiResources.welcome_bg_light_jpg
property color welcomeBlockColor: darkTheme ? "#4D000000" : "#4DFFFFFF"
// WizardView Advanced Account Settings

View File

@ -36,6 +36,8 @@ translateErrorCode(QNetworkReply::NetworkError error)
static auto inRange = [](int value, int min, int max) -> bool {
return (value >= min && value <= max);
};
if (error == QNetworkReply::OperationCanceledError)
return NetworkManager::CANCELED;
if (inRange(error, 1, 199))
return NetworkManager::NETWORK_ERROR;
if (inRange(error, 201, 201))
@ -187,8 +189,8 @@ NetworkManager::downloadFile(const QUrl& url,
[this, uuid, reply](QNetworkReply::NetworkError error) {
reply->disconnect();
resetDownload(uuid);
qWarning() << Q_FUNC_INFO
<< QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error);
qDebug() << Q_FUNC_INFO
<< QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error);
Q_EMIT errorOccurred(translateErrorCode(error));
});
@ -215,7 +217,9 @@ void
NetworkManager::cancelDownload(int replyId)
{
if (downloadReplies_.value(replyId) != NULL) {
Q_EMIT errorOccurred(GetError::CANCELED);
// Aborting the download will trigger the emission of a QNetworkReply error
// (`QNetworkReply::OperationCanceledError`), and be caught, translated to our internal
// error `GetError::CANCELED`, and re-emitted.
downloadReplies_[replyId]->abort();
resetDownload(replyId);
}

View File

@ -62,6 +62,8 @@
#include "pluginlistpreferencemodel.h"
#include "preferenceitemlistmodel.h"
#include "wizardviewstepmodel.h"
#include "linkdevicemodel.h"
#include "qrcodescannermodel.h"
#include "api/peerdiscoverymodel.h"
#include "api/codecmodel.h"
@ -179,6 +181,18 @@ registerTypes(QQmlEngine* engine,
QQmlEngine::setObjectOwnership(pluginStoreListModel, QQmlEngine::CppOwnership);
REG_QML_SINGLETON<PluginStoreListModel>(REG_MODEL, "PluginStoreListModel", CREATE(pluginStoreListModel));
// WizardViewStepModel
auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, settingsManager, app);
qApp->setProperty("WizardViewStepModel", QVariant::fromValue(wizardViewStepModel));
QQmlEngine::setObjectOwnership(wizardViewStepModel, QQmlEngine::CppOwnership);
REG_QML_SINGLETON<WizardViewStepModel>(REG_MODEL, "WizardViewStepModel", CREATE(wizardViewStepModel));
// LinkDeviceModel
auto linkdevicemodel = new LinkDeviceModel(lrcInstance);
qApp->setProperty("LinkDeviceModel", QVariant::fromValue(linkdevicemodel));
QQmlEngine::setObjectOwnership(linkdevicemodel, QQmlEngine::CppOwnership);
REG_QML_SINGLETON<LinkDeviceModel>(REG_MODEL, "LinkDeviceModel", CREATE(linkdevicemodel));
// Register app-level objects that are used by QML created objects.
// These MUST be set prior to loading the initial QML file, in order to
// be available to the QML adapter class factory creation methods.
@ -189,6 +203,7 @@ registerTypes(QQmlEngine* engine,
qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
// qml adapter registration
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, QRCodeScannerModel);
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, AvatarRegistry);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, AccountAdapter);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CallAdapter);
@ -205,7 +220,6 @@ registerTypes(QQmlEngine* engine,
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, TipsModel);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
QML_REGISTERSINGLETON_TYPE(NS_MODELS, WizardViewStepModel);
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader);
// TODO: remove these
@ -263,12 +277,12 @@ registerTypes(QQmlEngine* engine,
// Enums
QML_REGISTERUNCREATABLE(NS_ENUMS, Settings)
QML_REGISTERUNCREATABLE(NS_ENUMS, NetworkManager)
QML_REGISTERUNCREATABLE(NS_ENUMS, WizardViewStepModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, ModeratorListModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoInputDeviceModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatResolutionModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatFpsModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceAuthStateEnum)
engine->addImageProvider(QLatin1String("qrImage"), new QrImageProvider(lrcInstance));
engine->addImageProvider(QLatin1String("avatarimage"), new AvatarImageProvider(lrcInstance));

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2025-2025 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 "qrcodescannermodel.h"
#include <Barcode.h>
#include <MultiFormatReader.h>
#include <ReadBarcode.h>
#include <QDebug>
QRCodeScannerModel::QRCodeScannerModel(QObject* parent)
: QObject(parent)
{}
QString
QRCodeScannerModel::scanImage(const QImage& image)
{
if (image.isNull())
return QString();
// Convert QImage to grayscale and get raw data
QImage grayImage = image.convertToFormat(QImage::Format_Grayscale8);
int width = grayImage.width();
int height = grayImage.height();
try {
// Create ZXing image
ZXing::ImageView imageView(grayImage.bits(), width, height, ZXing::ImageFormat::Lum);
// Configure reader
ZXing::ReaderOptions options;
options.setTryHarder(true);
options.setTryRotate(true);
options.setFormats(ZXing::BarcodeFormat::QRCode);
// Try to detect QR code
auto result = ZXing::ReadBarcode(imageView, options);
if (result.isValid()) {
QString text = QString::fromStdString(result.text());
return text;
}
} catch (const std::exception& e) {
qWarning() << "QR code scanning error:" << e.what();
}
return QString();
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2025-2025 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 <QObject>
#include <QString>
#include <QImage>
#include <QQmlEngine> // QML registration
class QRCodeScannerModel : public QObject
{
Q_OBJECT
public:
static QRCodeScannerModel* create(QQmlEngine*, QJSEngine*)
{
return new QRCodeScannerModel();
}
explicit QRCodeScannerModel(QObject* parent = nullptr);
Q_INVOKABLE QString scanImage(const QImage& image);
};

View File

@ -18,7 +18,6 @@
#pragma once
#include "quickimageproviderbase.h"
#include "accountlistmodel.h"
#include <QPair>
#include <QString>

View File

@ -281,6 +281,9 @@ SidePanelBase {
clip: true
contentHeight: contentItem.childrenRect.height
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
model: getHeaders()
delegate: ColumnLayout {
id: col
@ -329,6 +332,9 @@ SidePanelBase {
clip: true
visible: isChildSelected
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
model: modelData.children
delegate: ColumnLayout {
id: childCol

View File

@ -25,7 +25,7 @@ import "../../commoncomponents"
SettingsPageBase {
id: root
property int itemWidth: 188
property int itemWidth: 250
title: JamiStrings.audio
flickableContent: ColumnLayout {
@ -45,16 +45,34 @@ SettingsPageBase {
target: UtilsAdapter
function onChangeLanguage() {
inputAudioModel.reset();
outputAudioModel.reset();
ringtoneAudioModel.reset();
rootLayout.resetDeviceModels();
rootLayout.resetDeviceIndices();
}
}
function resetDeviceModels() {
inputAudioModel.reset();
outputAudioModel.reset();
ringtoneAudioModel.reset();
}
function resetDeviceIndices() {
inputComboBoxSetting.modelIndex = inputComboBoxSetting.comboModel.getCurrentIndex();
outputComboBoxSetting.modelIndex = outputComboBoxSetting.comboModel.getCurrentIndex();
ringtoneComboBoxSetting.modelIndex = ringtoneComboBoxSetting.comboModel.getCurrentIndex();
}
Connections {
target: AvAdapter
function onAudioDeviceListChanged(inputs, outputs) {
rootLayout.resetDeviceModels();
rootLayout.resetDeviceIndices();
}
}
function populateAudioSettings() {
inputComboBoxSetting.modelIndex = inputComboBoxSetting.comboModel.getCurrentIndex();
outputComboBoxSetting.modelIndex = outputComboBoxSetting.comboModel.getCurrentIndex();
ringtoneComboBoxSetting.modelIndex = ringtoneComboBoxSetting.comboModel.getCurrentIndex();
rootLayout.resetDeviceIndices();
if (audioManagerComboBoxSetting.comboModel.rowCount() > 0) {
audioManagerComboBoxSetting.modelIndex = audioManagerComboBoxSetting.comboModel.getCurrentSettingIndex();
}

View File

@ -37,6 +37,9 @@ ListView {
spacing: 5
cacheBuffer: 10
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
property int rota: 0
header: Rectangle {
@ -141,6 +144,9 @@ ListView {
model: Count
// HACK: remove after migration to Qt 6.7+
boundsBehavior: Flickable.StopAtBounds
delegate: RowLayout {
id: rowLayoutDelegate
height: 40

View File

@ -20,6 +20,8 @@ import QtQuick.Layouts
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 Qt.labs.platform
import "../../commoncomponents"
import "../../mainview/components"
@ -32,388 +34,363 @@ BaseModalDialog {
property bool darkTheme: UtilsAdapter.useApplicationTheme()
popupContent: StackLayout {
id: stackedWidget
autoClose: false
closeButtonVisible: false
function setGeneratingPage() {
if (passwordEdit.length === 0 && CurrentAccount.hasArchivePassword) {
setExportPage(NameDirectory.ExportOnRingStatus.WRONG_PASSWORD, "");
return;
}
stackedWidget.currentIndex = exportingSpinnerPage.pageIndex;
spinnerMovie.playing = true;
timerForExport.restart();
// Function to check if dialog can be closed directly
function canCloseDirectly() {
return LinkDeviceModel.deviceAuthState === DeviceAuthStateEnum.INIT ||
LinkDeviceModel.deviceAuthState === DeviceAuthStateEnum.DONE
}
// Close button. Use custom close button to show a confirmation dialog.
JamiPushButton {
anchors {
top: parent.top
right: parent.right
topMargin: 5
rightMargin: 5
}
function setExportPage(status, pin) {
if (status === NameDirectory.ExportOnRingStatus.SUCCESS) {
infoLabel.success = true;
pinRectangle.visible = true
exportedPIN.text = pin;
Layout.preferredHeight: 20
Layout.preferredWidth: 20
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
normalColor: "transparent"
source: JamiResources.round_close_24dp_svg
onClicked: {
if (canCloseDirectly()) {
root.close();
} else {
infoLabel.success = false;
infoLabel.visible = true;
switch (status) {
case NameDirectory.ExportOnRingStatus.WRONG_PASSWORD:
infoLabel.text = JamiStrings.incorrectPassword;
break;
case NameDirectory.ExportOnRingStatus.NETWORK_ERROR:
infoLabel.text = JamiStrings.linkDeviceNetWorkError;
break;
case NameDirectory.ExportOnRingStatus.INVALID:
infoLabel.text = JamiStrings.somethingWentWrong;
break;
}
}
stackedWidget.currentIndex = exportingInfoPage.pageIndex;
stackedWidget.height = exportingLayout.implicitHeight;
}
Timer {
id: timerForExport
repeat: false
interval: 200
onTriggered: {
AccountAdapter.model.exportOnRing(LRCInstance.currentAccountId, passwordEdit.dynamicText);
confirmCloseDialog.open();
}
}
}
Connections {
target: NameDirectory
MessageDialog {
id: confirmCloseDialog
function onExportOnRingEnded(status, pin) {
stackedWidget.setExportPage(status, pin);
}
text: JamiStrings.linkDeviceCloseWarningTitle
informativeText: JamiStrings.linkDeviceCloseWarningMessage
buttons: MessageDialog.Ok | MessageDialog.Cancel
onOkClicked: function(button) {
root.close();
}
}
onVisibleChanged: {
if (visible) {
if (CurrentAccount.hasArchivePassword) {
stackedWidget.currentIndex = enterPasswordPage.pageIndex;
} else {
setGeneratingPage();
}
}
}
popupContent: Item {
id: content
width: 400
height: 450
// Index = 0
Item {
id: enterPasswordPage
// Scrollable container for StackLayout
ScrollView {
id: scrollView
readonly property int pageIndex: 0
anchors.fill: parent
Component.onCompleted: passwordEdit.forceActiveFocus()
anchors.leftMargin: 20
anchors.rightMargin: 20
anchors.bottomMargin: 20
clip: true
onHeightChanged: {
stackedWidget.height = passwordLayout.implicitHeight
}
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentHeight: stackLayout.implicitHeight
ColumnLayout {
id: passwordLayout
spacing: JamiTheme.preferredMarginSize
anchors.centerIn: parent
StackLayout {
id: stackLayout
width: Math.min(scrollView.width, scrollView.availableWidth)
Label {
Layout.alignment: Qt.AlignCenter
Layout.maximumWidth: root.width - 4 * JamiTheme.preferredMarginSize
wrapMode: Text.Wrap
currentIndex: scanAndEnterCodeView.index
text: JamiStrings.enterPasswordPinCode
color: JamiTheme.textColor
font.pointSize: JamiTheme.textFontSize
font.kerning: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Connections {
target: LinkDeviceModel
RowLayout {
Layout.topMargin: 10
Layout.leftMargin: JamiTheme.cornerIconSize
Layout.rightMargin: JamiTheme.cornerIconSize
spacing: JamiTheme.preferredMarginSize
Layout.bottomMargin: JamiTheme.preferredMarginSize
PasswordTextEdit {
id: passwordEdit
firstEntry: true
placeholderText: JamiStrings.password
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
KeyNavigation.up: btnConfirm
KeyNavigation.down: KeyNavigation.up
onDynamicTextChanged: {
btnConfirm.enabled = dynamicText.length > 0;
btnConfirm.hoverEnabled = dynamicText.length > 0;
}
onAccepted: btnConfirm.clicked()
}
JamiPushButton {
id: btnConfirm
Layout.alignment: Qt.AlignCenter
height: 36
width: 36
hoverEnabled: false
enabled: false
imageColor: JamiTheme.secondaryBackgroundColor
hoveredColor: JamiTheme.buttonTintedBlueHovered
source: JamiResources.check_black_24dp_svg
normalColor: JamiTheme.tintedBlue
onClicked: stackedWidget.setGeneratingPage()
}
}
}
}
// Index = 1
Item {
id: exportingSpinnerPage
readonly property int pageIndex: 1
onHeightChanged: {
stackedWidget.height = spinnerLayout.implicitHeight
}
onWidthChanged: stackedWidget.width = exportingLayout.implicitWidth
ColumnLayout {
id: spinnerLayout
spacing: JamiTheme.preferredMarginSize
anchors.centerIn: parent
Label {
Layout.alignment: Qt.AlignCenter
text: JamiStrings.linkDevice
color: JamiTheme.textColor
font.pointSize: JamiTheme.headerFontSize
font.kerning: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
AnimatedImage {
id: spinnerMovie
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 30
Layout.preferredHeight: 30
source: JamiResources.jami_rolling_spinner_gif
playing: visible
fillMode: Image.PreserveAspectFit
mipmap: true
}
}
}
// Index = 2
Item {
id: exportingInfoPage
readonly property int pageIndex: 2
width: childrenRect.width
height: childrenRect.height
onHeightChanged: {
stackedWidget.height = exportingLayout.implicitHeight
}
onWidthChanged: stackedWidget.width = exportingLayout.implicitWidth
ColumnLayout {
id: exportingLayout
spacing: JamiTheme.preferredMarginSize
Label {
id: instructionLabel
Layout.maximumWidth: Math.min(root.maximumPopupWidth, root.width) - 2 * root.popupMargins
Layout.alignment: Qt.AlignLeft
color: JamiTheme.textColor
wrapMode: Text.Wrap
text: JamiStrings.linkingInstructions
font.pointSize: JamiTheme.textFontSize
font.kerning: true
verticalAlignment: Text.AlignVCenter
}
RowLayout {
spacing: 10
Layout.maximumWidth: Math.min(root.maximumPopupWidth, root.width) - 2 * root.popupMargins
Rectangle {
Layout.alignment: Qt.AlignCenter
radius: 5
color: JamiTheme.backgroundRectangleColor
width: 100
height: 100
Rectangle {
width: qrImage.width + 4
height: qrImage.height + 4
anchors.centerIn: parent
radius: 5
color: JamiTheme.whiteColor
Image {
id: qrImage
anchors.centerIn: parent
mipmap: false
smooth: false
source: "image://qrImage/raw_" + exportedPIN.text
sourceSize.width: 80
sourceSize.height: 80
function onDeviceAuthStateChanged() {
switch (LinkDeviceModel.deviceAuthState) {
case DeviceAuthStateEnum.INIT:
stackLayout.currentIndex = scanAndEnterCodeView.index
break
case DeviceAuthStateEnum.CONNECTING:
stackLayout.currentIndex = deviceLinkLoadingView.index
deviceLinkLoadingView.loadingText = JamiStrings.linkDeviceConnecting
break
case DeviceAuthStateEnum.AUTHENTICATING:
stackLayout.currentIndex = deviceConfirmationView.index
break
case DeviceAuthStateEnum.IN_PROGRESS:
stackLayout.currentIndex = deviceLinkLoadingView.index
deviceLinkLoadingView.loadingText = JamiStrings.linkDeviceInProgress
break
case DeviceAuthStateEnum.DONE:
if (LinkDeviceModel.linkDeviceError.length > 0) {
stackLayout.currentIndex = deviceLinkErrorView.index
} else {
stackLayout.currentIndex = deviceLinkSuccessView.index
}
}
}
Rectangle {
id: pinRectangle
radius: 5
color: JamiTheme.backgroundRectangleColor
Layout.fillWidth: true
height: 100
Layout.minimumWidth: exportedPIN.width + 20
Layout.alignment: Qt.AlignCenter
MaterialLineEdit {
id: exportedPIN
padding: 10
anchors.centerIn: parent
text: JamiStrings.pin
wrapMode: Text.NoWrap
backgroundColor: JamiTheme.backgroundRectangleColor
color: darkTheme ? JamiTheme.editLineColor : JamiTheme.darkTintedBlue
selectByMouse: true
readOnly: true
font.pointSize: JamiTheme.tinyCreditsTextSize
font.kerning: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
break
default:
break
}
}
}
Rectangle {
radius: 5
color: JamiTheme.infoRectangleColor
// Common base component for stack layout items
component StackViewBase: Item {
id: baseItem
required property string title
default property alias content: contentLayout.data
Layout.fillWidth: true
Layout.preferredHeight: infoLabels.height + 38
Layout.alignment: Qt.AlignHCenter
implicitHeight: contentLayout.implicitHeight
ColumnLayout {
id: contentLayout
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
Layout.preferredWidth: scrollView.width
spacing: 20
}
}
StackViewBase {
id: deviceLinkErrorView
property int index: 0
title: "Error"
Text {
Layout.alignment: Qt.AlignHCenter
text: LinkDeviceModel.linkDeviceError
Layout.preferredWidth: scrollView.width
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
MaterialButton {
Layout.alignment: Qt.AlignHCenter
text: JamiStrings.close
toolTipText: JamiStrings.optionTryAgain
primary: true
onClicked: {
root.close();
}
}
}
StackViewBase {
id: deviceLinkSuccessView
property int index: 1
title: "Success"
Text {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceAllSet
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
MaterialButton {
Layout.alignment: Qt.AlignHCenter
text: JamiStrings.close
toolTipText: JamiStrings.close
primary: true
onClicked: {
root.close();
}
}
}
StackViewBase {
id: deviceLinkLoadingView
property int index: 2
title: "Loading"
property string loadingText: ""
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 50
Layout.preferredHeight: 50
running: true
}
Text {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: deviceLinkLoadingView.loadingText
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
StackViewBase {
id: deviceConfirmationView
property int index: 3
title: "Confirmation"
Text {
id: explanationConnect
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceFoundAddress
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
Text {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceNewDeviceIP.arg(LinkDeviceModel.ipAddress)
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
font.weight: Font.Bold
}
RowLayout {
id: infoLayout
Layout.alignment: Qt.AlignHCenter
spacing: 16
anchors.centerIn: parent
anchors.fill: parent
anchors.margins: 14
spacing: 10
ResponsiveImage{
Layout.fillWidth: true
source: JamiResources.outline_info_24dp_svg
fillMode: Image.PreserveAspectFit
color: darkTheme ? JamiTheme.editLineColor : JamiTheme.darkTintedBlue
Layout.fillHeight: true
MaterialButton {
id: confirm
primary: true
Layout.alignment: Qt.AlignCenter
text: JamiStrings.optionConfirm
toolTipText: JamiStrings.optionConfirm
onClicked: {
LinkDeviceModel.confirmAddDevice()
}
}
ColumnLayout{
id: infoLabels
Layout.fillHeight: true
Layout.fillWidth: true
Label {
id: otherDeviceLabel
Layout.alignment: Qt.AlignLeft
color: JamiTheme.textColor
text: JamiStrings.onAnotherDevice
font.pointSize: JamiTheme.smallFontSize
font.kerning: true
font.bold: true
}
Label {
id: otherInstructionLabel
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
wrapMode: Text.Wrap
color: JamiTheme.textColor
text: JamiStrings.onAnotherDeviceInstruction
font.pointSize: JamiTheme.smallFontSize
font.kerning: true
MaterialButton {
id: cancel
Layout.alignment: Qt.AlignCenter
secondary: true
toolTipText: JamiStrings.cancel
textLeftPadding: JamiTheme.buttontextWizzardPadding / 2
textRightPadding: JamiTheme.buttontextWizzardPadding / 2
text: JamiStrings.cancel
onClicked: {
LinkDeviceModel.cancelAddDevice()
}
}
}
}
// Displays error messages
Label {
id: infoLabel
StackViewBase {
id: scanAndEnterCodeView
property int index: 4
title: "Scan"
visible: false
Component.onDestruction: {
if (qrScanner) {
qrScanner.stopScanner()
}
}
property bool success: false
property int borderWidth: success ? 1 : 0
property int borderRadius: success ? 15 : 0
property string backgroundColor: success ? "whitesmoke" : "transparent"
property string borderColor: success ? "lightgray" : "transparent"
Text {
id: explanationScan
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceScanQR
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
Layout.maximumWidth: JamiTheme.preferredDialogWidth
Layout.margins: JamiTheme.preferredMarginSize
QRCodeScanner {
id: qrScanner
Layout.alignment: Qt.AlignHCenter
width: 250
height: width * aspectRatio
visible: VideoDevices.listSize !== 0
Layout.alignment: Qt.AlignCenter
onQrCodeDetected: function(code) {
console.log("QR code detected:", code)
LinkDeviceModel.addDevice(code)
}
}
color: success ? JamiTheme.successLabelColor : JamiTheme.redColor
padding: success ? 8 : 0
ColumnLayout {
id: manualEntry
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
spacing: 10
wrapMode: Text.Wrap
font.pointSize: success ? JamiTheme.textFontSize : JamiTheme.textFontSize + 3
font.kerning: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Text {
id: explanation
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
text: JamiStrings.linkDeviceEnterManually
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
background: Rectangle {
id: infoLabelBackground
ModalTextEdit {
id: codeInput
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: scrollView.width
Layout.preferredHeight: JamiTheme.preferredFieldHeight
placeholderText: JamiStrings.linkDeviceEnterCodePlaceholder
}
border.width: infoLabel.borderWidth
border.color: infoLabel.borderColor
radius: infoLabel.borderRadius
color: JamiTheme.secondaryBackgroundColor
Text {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width - 40
visible: LinkDeviceModel.tokenErrorMessage.length > 0
text: LinkDeviceModel.tokenErrorMessage
font.pointSize: JamiTheme.tinyFontSize
color: JamiTheme.redColor
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
MaterialButton {
id: connect
Layout.alignment: Qt.AlignHCenter
primary: true
text: JamiStrings.connect
toolTipText: JamiStrings.connect
enabled: codeInput.dynamicText.length > 0
onClicked: {
LinkDeviceModel.addDevice(codeInput.dynamicText)
}
}
}
}
}
}
//Reset everything when dialog is closed
onClosed: {
LinkDeviceModel.reset()
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright (C) 2025-2025 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 net.jami.Constants 1.1
import net.jami.Adapters 1.1
import net.jami.Helpers 1.1
import "../../commoncomponents"
Item {
id: root
property bool isScanning: false
property real aspectRatio: 0.5625
onVisibleChanged: {
if (visible) {
startScanner()
} else {
stopScanner()
}
}
Component.onDestruction: {
stopScanner()
}
Rectangle {
id: cameraContainer
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
height: parent.height
color: JamiTheme.primaryForegroundColor
clip: true
LocalVideo {
id: previewWidget
anchors.fill: parent
flip: true
// Camera not available
underlayItems: Text {
id: noCameraText
anchors.centerIn: parent
font.pointSize: 18
font.capitalization: Font.AllUppercase
color: "white"
text: JamiStrings.noCamera
visible: false // Start hidden
// Delay "No Camera" message to avoid flashing it when camera is starting up.
// If camera starts successfully within 5 seconds, user won't see this message.
// If there's a camera issue, message will be shown after the delay.
Timer {
id: visibilityTimer
interval: 5000
running: true
repeat: false
onTriggered: {
noCameraText.visible = true
destroy() // Remove the timer after it's done
}
}
}
}
// Scanning line animation
Rectangle {
id: scanLine
width: parent.width
height: 2
color: JamiTheme.whiteColor
opacity: 0.8
visible: root.isScanning && previewWidget.isRendering
SequentialAnimation on y {
running: root.isScanning
loops: Animation.Infinite
NumberAnimation {
from: 0
to: cameraContainer.height
duration: 2500
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: cameraContainer.height
to: 0
duration: 2500
easing.type: Easing.InOutQuad
}
}
}
}
Timer {
id: scanTimer
interval: 500
repeat: true
running: root.isScanning && previewWidget.isRendering
onTriggered: {
var result = QRCodeScannerModel.scanImage(videoProvider.captureRawVideoFrame(VideoDevices.getDefaultDevice()));
if (result !== "") {
root.isScanning = false
root.qrCodeDetected(result)
}
}
}
signal qrCodeDetected(string code)
function startScanner() {
previewWidget.startWithId(VideoDevices.getDefaultDevice())
root.isScanning = true
}
function stopScanner() {
previewWidget.startWithId("")
root.isScanning = true
}
}

View File

@ -34,6 +34,7 @@ SimpleMessageDialog {
property alias progressBarValue: progressBar.value
closeButtonVisible: false
autoClose: false
button1.text: JamiStrings.optionCancel
button1Role: DialogButtonBox.RejectRole

View File

@ -165,8 +165,7 @@ Utils::CreateStartupLink(const std::wstring& wstrAppName)
#endif
if (desktopPath.isEmpty() || !(QFile::exists(desktopPath))) {
qDebug() << "Error while attempting to locate .desktop file at"
<< desktopPath;
qDebug() << "Error while attempting to locate .desktop file at" << desktopPath;
return false;
}
@ -193,8 +192,7 @@ Utils::CreateStartupLink(const std::wstring& wstrAppName)
if (QDir().mkdir(autoStartDir)) {
qDebug() << "Created autostart directory:" << autoStartDir;
} else {
qWarning() << "Error while creating autostart directory:"
<< autoStartDir;
qWarning() << "Error while creating autostart directory:" << autoStartDir;
return false;
}
}
@ -283,7 +281,8 @@ Utils::CheckStartupLink(const std::wstring& wstrAppName)
#else
Q_UNUSED(wstrAppName)
return (
!QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/net.jami.Jami.desktop").isEmpty());
!QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/net.jami.Jami.desktop")
.isEmpty());
#endif
}
@ -616,14 +615,16 @@ Utils::getProjectCredits()
return {};
}
QTextStream in(&projectCreditsFile);
return in.readAll().arg(
QObject::tr("We would like to thank our contributors, whose efforts over many years have made this software what it is."),
QObject::tr("Developers"),
QObject::tr("Media"),
QObject::tr("Community Management"),
QObject::tr("Special thanks to"),
QObject::tr("This is a list of people who have made a significant investment of time, with useful results, into Jami. Any such contributors who want to be added to the list should contact us.")
);
return in.readAll().arg(QObject::tr("We would like to thank our contributors, whose efforts "
"over many years have made this software what it is."),
QObject::tr("Developers"),
QObject::tr("Media"),
QObject::tr("Community Management"),
QObject::tr("Special thanks to"),
QObject::tr(
"This is a list of people who have made a significant investment "
"of time, with useful results, into Jami. Any such contributors "
"who want to be added to the list should contact us."));
}
inline QString
@ -951,3 +952,13 @@ Utils::getTempSwarmAvatarPath()
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QDir::separator()
+ "tmpSwarmImage";
}
QVariantMap
Utils::mapStringStringToVariantMap(const MapStringString& map)
{
QVariantMap variantMap;
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
variantMap.insert(it.key(), it.value());
}
return variantMap;
}

View File

@ -120,4 +120,7 @@ QString generateUid();
QString humanFileSize(qint64 fileSize);
QString getDebugFilePath();
// Convert a MapStringString to a QVariantMap
QVariantMap mapStringStringToVariantMap(const MapStringString& map);
} // namespace Utils

View File

@ -811,18 +811,19 @@ UtilsAdapter::isRTL()
pref = pref == "SYSTEM" ? QLocale::system().name() : pref;
static const QStringList rtlLanguages {
// as defined by ISO 639-1
"ar", // Arabic
"he", // Hebrew
"fa", // Persian (Farsi)
"ur", // Urdu
"ps", // Pashto
"ku", // Kurdish
"sd", // Sindhi
"dv", // Dhivehi (Maldivian)
"yi", // Yiddish
"am", // Amharic
"ti", // Tigrinya
"kk" // Kazakh (in Arabic script)
"ar", // Arabic
"he", // Hebrew
"fa", // Persian (Farsi)
"az_IR", // Azerbaijani
"ur", // Urdu
"ps", // Pashto
"ku", // Kurdish
"sd", // Sindhi
"dv", // Dhivehi (Maldivian)
"yi", // Yiddish
"am", // Amharic
"ti", // Tigrinya
"kk" // Kazakh (in Arabic script)
};
return rtlLanguages.contains(pref);
}

View File

@ -53,7 +53,7 @@ WebEngineView {
}
onFullScreenRequested: function (request) {
if (request.toggleOn) {
layoutManager.pushFullScreenItem(this, localMediaCompLoader, null, function () {
layoutManager.pushFullScreenItem(this, function () {
wev.fullScreenCancelled();
});
} else if (!request.toggleOn) {

View File

@ -38,7 +38,7 @@ Rectangle {
Component.onCompleted: loadHtml(root.html, 'file:///')
onFullScreenRequested: function (request) {
if (request.toggleOn) {
layoutManager.pushFullScreenItem(this, root, null, function () {
layoutManager.pushFullScreenItem(this, function () {
wev.fullScreenCancelled();
});
} else if (!request.toggleOn) {

View File

@ -56,9 +56,11 @@ BaseView {
case WizardViewStepModel.AccountCreationOption.CreateJamiAccount:
case WizardViewStepModel.AccountCreationOption.CreateRendezVous:
case WizardViewStepModel.AccountCreationOption.ImportFromBackup:
case WizardViewStepModel.AccountCreationOption.ImportFromDevice:
AccountAdapter.createJamiAccount(WizardViewStepModel.accountCreationInfo);
break;
case WizardViewStepModel.AccountCreationOption.ImportFromDevice:
AccountAdapter.startImportAccount();
break;
case WizardViewStepModel.AccountCreationOption.ConnectToAccountManager:
AccountAdapter.createJAMSAccount(WizardViewStepModel.accountCreationInfo);
break;

View File

@ -35,7 +35,7 @@ BaseModalDialog {
InfoBox {
id: info
width: root.width - 2 * root.popupMargins
width: parent.width
icoSource: JamiResources.laptop_black_24dp_svg
title: JamiStrings.local
description: JamiStrings.localAccount
@ -43,7 +43,7 @@ BaseModalDialog {
}
InfoBox {
width: root.width - 2 * root.popupMargins
width: parent.width
icoSource: JamiResources.assignment_ind_black_24dp_svg
title: JamiStrings.username
description: JamiStrings.usernameRecommened
@ -51,7 +51,7 @@ BaseModalDialog {
}
InfoBox {
width: root.width - 2 * root.popupMargins
width: parent.width
icoSource: JamiResources.lock_svg
title: JamiStrings.encrypt
description: JamiStrings.passwordOptional
@ -59,7 +59,7 @@ BaseModalDialog {
}
InfoBox {
width: root.width - 2 * root.popupMargins
width: parent.width
icoSource: JamiResources.brush_black_24dp_svg
title: JamiStrings.customize
description: JamiStrings.customizeOptional
@ -67,4 +67,3 @@ BaseModalDialog {
}
}
}

View File

@ -17,9 +17,13 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import net.jami.Adapters 1.1
import net.jami.Models 1.1
import net.jami.Constants 1.1
import net.jami.Enums 1.1
import Qt.labs.platform
import "../../commoncomponents"
import "../../mainview/components"
Rectangle {
id: root
@ -27,30 +31,97 @@ Rectangle {
property string errorText: ""
property int preferredHeight: importFromDevicePageColumnLayout.implicitHeight + 2 * JamiTheme.preferredMarginSize
signal showThisPage
// The token is used to generate the QR code and is also provided to the user as a backup if the QR
// code cannot be scanned. It is a URI using the scheme "jami-auth".
readonly property string tokenUri: WizardViewStepModel.deviceLinkDetails["token"] || ""
function initializeOnShowUp() {
clearAllTextFields();
property string jamiId: ""
function isPasswordWrong() {
return WizardViewStepModel.deviceLinkDetails["auth_error"] !== undefined &&
WizardViewStepModel.deviceLinkDetails["auth_error"] !== "" &&
WizardViewStepModel.deviceLinkDetails["auth_error"] !== "none"
}
function requiresPassword() {
return WizardViewStepModel.deviceLinkDetails["auth_scheme"] === "password"
}
function requiresConfirmationBeforeClosing() {
const state = WizardViewStepModel.deviceAuthState
return state !== DeviceAuthStateEnum.INIT &&
state !== DeviceAuthStateEnum.DONE
}
function isLoadingState() {
const state = WizardViewStepModel.deviceAuthState
return state === DeviceAuthStateEnum.INIT ||
state === DeviceAuthStateEnum.CONNECTING ||
state === DeviceAuthStateEnum.IN_PROGRESS
}
signal showThisPage
function clearAllTextFields() {
connectBtn.spinnerTriggered = false;
errorText = "";
}
function errorOccurred(errorMessage) {
errorText = errorMessage;
connectBtn.spinnerTriggered = false;
}
MessageDialog {
id: confirmCloseDialog
text: JamiStrings.linkDeviceCloseWarningTitle
informativeText: JamiStrings.linkDeviceCloseWarningMessage
buttons: MessageDialog.Ok | MessageDialog.Cancel
onOkClicked: function(button) {
AccountAdapter.cancelImportAccount();
WizardViewStepModel.previousStep();
}
}
Connections {
target: WizardViewStepModel
function onMainStepChanged() {
if (WizardViewStepModel.mainStep === WizardViewStepModel.MainSteps.AccountCreation && WizardViewStepModel.accountCreationOption === WizardViewStepModel.AccountCreationOption.ImportFromDevice) {
if (WizardViewStepModel.mainStep === WizardViewStepModel.MainSteps.DeviceAuthorization) {
clearAllTextFields();
root.showThisPage();
}
}
function onDeviceAuthStateChanged() {
switch (WizardViewStepModel.deviceAuthState) {
case DeviceAuthStateEnum.TOKEN_AVAILABLE:
// Token is available and displayed as QR code
clearAllTextFields();
break;
case DeviceAuthStateEnum.CONNECTING:
// P2P connection being established
clearAllTextFields();
break;
case DeviceAuthStateEnum.AUTHENTICATING:
jamiId = WizardViewStepModel.deviceLinkDetails["peer_id"] || "";
if (jamiId.length > 0) {
NameDirectory.lookupAddress(CurrentAccount.id, jamiId)
}
break;
case DeviceAuthStateEnum.IN_PROGRESS:
// Account archive is being transferred
clearAllTextFields();
break;
case DeviceAuthStateEnum.DONE:
// Final state - check for specific errors
const error = AccountAdapter.getImportErrorMessage(WizardViewStepModel.deviceLinkDetails);
if (error.length > 0) {
errorOccurred(error)
}
break;
}
}
}
color: JamiTheme.secondaryBackgroundColor
@ -65,184 +136,337 @@ Rectangle {
width: Math.max(508, root.width - 100)
Text {
text: JamiStrings.importAccountFromAnotherDevice
text: JamiStrings.importFromAnotherAccount
Layout.alignment: Qt.AlignCenter
Layout.topMargin: JamiTheme.preferredMarginSize
Layout.preferredWidth: Math.min(360, root.width - JamiTheme.preferredMarginSize * 2)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: JamiTheme.textColor
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewTitleFontPixelSize
wrapMode: Text.WordWrap
}
Text {
text: JamiStrings.importFromDeviceDescription
Layout.preferredWidth: Math.min(360, root.width - JamiTheme.preferredMarginSize * 2)
Layout.topMargin: JamiTheme.wizardViewDescriptionMarginSize
Layout.alignment: Qt.AlignCenter
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
font.weight: Font.Medium
color: JamiTheme.textColor
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
lineHeight: JamiTheme.wizardViewTextLineHeight
}
Flow {
spacing: 30
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize
Layout.preferredWidth: Math.min(step1.width * 2 + spacing, root.width - JamiTheme.preferredMarginSize * 2)
InfoBox {
id: step1
icoSource: JamiResources.settings_24dp_svg
title: JamiStrings.importStep1
description: JamiStrings.importStep1Desc
icoColor: JamiTheme.buttonTintedBlue
}
InfoBox {
id: step2
icoSource: JamiResources.person_24dp_svg
title: JamiStrings.importStep2
description: JamiStrings.importStep2Desc
icoColor: JamiTheme.buttonTintedBlue
}
InfoBox {
id: step3
icoSource: JamiResources.finger_select_svg
title: JamiStrings.importStep3
description: JamiStrings.importStep3Desc
icoColor: JamiTheme.buttonTintedBlue
}
InfoBox {
id: step4
icoSource: JamiResources.time_clock_svg
title: JamiStrings.importStep4
description: JamiStrings.importStep4Desc
icoColor: JamiTheme.buttonTintedBlue
}
}
ModalTextEdit {
id: pinFromDevice
objectName: "pinFromDevice"
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: Math.min(410, root.width - JamiTheme.preferredMarginSize * 2)
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize
focus: visible
placeholderText: JamiStrings.pin
staticText: ""
KeyNavigation.up: backButton
KeyNavigation.down: passwordFromDevice
KeyNavigation.tab: KeyNavigation.down
onAccepted: passwordFromDevice.forceActiveFocus()
}
Text {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize
color: JamiTheme.textColor
wrapMode: Text.WordWrap
text: JamiStrings.importPasswordDesc
Layout.maximumWidth: parent.width
horizontalAlignment: Text.AlignHCenter
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
font.weight: Font.Medium
lineHeight: JamiTheme.wizardViewTextLineHeight
text: {
switch (WizardViewStepModel.deviceAuthState) {
case DeviceAuthStateEnum.INIT:
return JamiStrings.waitingForToken;
case DeviceAuthStateEnum.TOKEN_AVAILABLE:
return JamiStrings.scanToImportAccount;
case DeviceAuthStateEnum.CONNECTING:
return JamiStrings.connectingToDevice;
case DeviceAuthStateEnum.AUTHENTICATING:
return JamiStrings.confirmAccountImport;
case DeviceAuthStateEnum.IN_PROGRESS:
return JamiStrings.transferringAccount;
case DeviceAuthStateEnum.DONE:
return errorText.length > 0 ? JamiStrings.importFailed : "";
default:
return "";
}
}
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: JamiTheme.textColor
}
PasswordTextEdit {
id: passwordFromDevice
// Confirmation form
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: Math.min(parent.width - 40, 400)
visible: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.AUTHENTICATING
spacing: JamiTheme.wizardViewPageLayoutSpacing
objectName: "passwordFromDevice"
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: Math.min(410, root.width - JamiTheme.preferredMarginSize * 2)
Layout.topMargin: JamiTheme.wizardViewMarginSize
placeholderText: JamiStrings.enterPassword
KeyNavigation.up: pinFromDevice
KeyNavigation.down: {
if (connectBtn.enabled)
return connectBtn;
else if (connectBtn.spinnerTriggered)
return passwordFromDevice;
return backButton;
}
KeyNavigation.tab: KeyNavigation.down
onAccepted: pinFromDevice.forceActiveFocus()
}
SpinnerButton {
id: connectBtn
TextMetrics {
id: textSize
font.weight: Font.Bold
font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
text: connectBtn.normalText
Text {
Layout.fillWidth: true
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
text: JamiStrings.connectToAccount
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
horizontalAlignment: Text.AlignHCenter
color: JamiTheme.textColor
font.bold: true
}
objectName: "importFromDevicePageConnectBtn"
// Account Widget (avatar + username + ID)
Rectangle {
id: accountContainer
Layout.alignment: Qt.AlignHCenter
implicitWidth: accountLayout.implicitWidth + 40
implicitHeight: accountLayout.implicitHeight + 40
radius: 8
color: JamiTheme.primaryBackgroundColor
border.width: 1
border.color: JamiTheme.textColorHovered
Layout.alignment: Qt.AlignCenter
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize
Layout.bottomMargin: errorLabel.visible ? 0 : JamiTheme.wizardViewPageBackButtonMargins
RowLayout {
id: accountLayout
anchors {
centerIn: parent
}
spacing: 20
preferredWidth: textSize.width + 2 * JamiTheme.buttontextWizzardPadding + 1
primary: true
Avatar {
id: accountAvatar
showPresenceIndicator: false
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 48
Layout.preferredHeight: 48
mode: Avatar.Mode.TemporaryAccount
imageId: name.text || jamiId
}
spinnerTriggeredtext: JamiStrings.generatingAccount
normalText: JamiStrings.importButton
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
spacing: 4
enabled: pinFromDevice.dynamicText.length !== 0 && !spinnerTriggered
Text {
id: name
color: JamiTheme.textColor
visible: text !== undefined && text !== ""
KeyNavigation.tab: backButton
KeyNavigation.up: passwordFromDevice
KeyNavigation.down: backButton
Connections {
id: registeredNameFoundConnection
target: NameDirectory
enabled: jamiId.length > 0
onClicked: {
spinnerTriggered = true;
WizardViewStepModel.accountCreationInfo = JamiQmlUtils.setUpAccountCreationInputPara({
"archivePin": pinFromDevice.dynamicText,
"password": passwordFromDevice.dynamicText
});
WizardViewStepModel.nextStep();
function onRegisteredNameFound(status, address, registeredName, requestedName) {
if (address === jamiId && status === NameDirectory.LookupStatus.SUCCESS) {
name.text = registeredName;
}
}
}
}
Text {
id: userId
text: jamiId
color: JamiTheme.textColor
}
}
}
}
// Password
PasswordTextEdit {
id: passwordField
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.topMargin: 10
Layout.bottomMargin: 10
visible: requiresPassword()
placeholderText: JamiStrings.enterPassword
echoMode: TextInput.Password
onAccepted: confirmButton.clicked()
}
Text {
id: passwordErrorField
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width - 40
visible: isPasswordWrong()
text: JamiStrings.authenticationError
font.pointSize: JamiTheme.tinyFontSize
color: JamiTheme.redColor
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 16
Layout.margins: 10
MaterialButton {
id: confirmButton
text: JamiStrings.optionConfirm
primary: true
enabled: !passwordField.visible || passwordField.dynamicText.length > 0
onClicked: {
AccountAdapter.provideAccountAuthentication(passwordField.visible ? passwordField.dynamicText : "");
}
}
}
}
Label {
id: errorLabel
// Show busy indicator when waiting for token
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
visible: isLoadingState()
Layout.preferredWidth: 50
Layout.preferredHeight: 50
running: visible
}
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: JamiTheme.wizardViewPageBackButtonMargins
// QR Code container with frame
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: qrLoader.Layout.preferredWidth + 40
Layout.preferredHeight: qrLoader.Layout.preferredHeight + 40
visible: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.TOKEN_AVAILABLE
color: JamiTheme.whiteColor
radius: 8
border.width: 1
border.color: JamiTheme.whiteColor
visible: errorText.length !== 0
Loader {
id: qrLoader
anchors.centerIn: parent
active: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.TOKEN_AVAILABLE
Layout.preferredWidth: Math.min(parent.parent.width - 60, 250)
Layout.preferredHeight: Layout.preferredWidth
text: errorText
sourceComponent: Image {
width: qrLoader.Layout.preferredWidth
height: qrLoader.Layout.preferredHeight
anchors.centerIn: parent
smooth: false
fillMode: Image.PreserveAspectFit
source: "image://qrImage/raw_" + tokenUri
}
}
}
font.pixelSize: JamiTheme.textEditError
color: JamiTheme.redColor
// Token URI backup text
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
visible: tokenUri !== ""
spacing: 8
Text {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.parent.width - 40
horizontalAlignment: Text.AlignHCenter
text: JamiStrings.cantScanQRCode
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
lineHeight: JamiTheme.wizardViewTextLineHeight
color: JamiTheme.textColor
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
// Container for TextArea and copy button
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 0
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: contentRow.implicitWidth + 40
Layout.preferredHeight: contentRow.implicitHeight + 20
color: JamiTheme.jamiIdBackgroundColor
radius: 5
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: 5
TextEdit {
id: tokenUriTextArea
text: tokenUri
color: JamiTheme.textColor
font.pointSize: JamiTheme.wizardViewDescriptionFontPixelSize
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
selectByMouse: true
readOnly: true
cursorVisible: false
}
// Copy button
PushButton {
id: copyButton
Layout.alignment: Qt.AlignVCenter
preferredSize: 30
radius: 5
normalColor: JamiTheme.transparentColor
imageContainerWidth: JamiTheme.pushButtonSize
imageContainerHeight: JamiTheme.pushButtonSize
border.color: JamiTheme.transparentColor
imageColor: JamiTheme.tintedBlue
source: JamiResources.content_copy_24dp_svg
toolTipText: JamiStrings.copy
onClicked: {
UtilsAdapter.setClipboardText(tokenUri);
}
}
}
}
MouseArea {
parent: tokenUriTextArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
propagateComposedEvents: true
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true
contextMenu.open()
}
}
}
Menu {
id: contextMenu
MenuItem {
text: JamiStrings.copy
enabled: tokenUriTextArea.selectedText.length > 0
onTriggered: {
UtilsAdapter.setClipboardText(tokenUri);
}
}
}
}
}
// Error view
ColumnLayout {
id: errorColumn
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width - 40
visible: errorText !== ""
spacing: 16
Text {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
text: errorText
color: JamiTheme.textColor
font.pointSize: JamiTheme.mediumFontSize
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
MaterialButton {
Layout.alignment: Qt.AlignHCenter
text: JamiStrings.optionTryAgain
toolTipText: JamiStrings.optionTryAgain
primary: true
onClicked: {
AccountAdapter.cancelImportAccount();
WizardViewStepModel.previousStep();
}
}
}
}
BackButton {
// Back button
JamiPushButton {
id: backButton
QWKSetParentHitTestVisible {
}
objectName: "importFromDevicePageBackButton"
@ -250,12 +474,18 @@ Rectangle {
anchors.top: parent.top
anchors.margins: JamiTheme.wizardViewPageBackButtonMargins
visible: !connectBtn.spinnerTriggered
preferredSize: 36
imageContainerWidth: 20
source: JamiResources.ic_arrow_back_24dp_svg
KeyNavigation.tab: pinFromDevice
KeyNavigation.up: connectBtn.enabled ? connectBtn : passwordFromDevice
KeyNavigation.down: pinFromDevice
visible: WizardViewStepModel.deviceAuthState !== DeviceAuthStateEnum.IN_PROGRESS
onClicked: WizardViewStepModel.previousStep()
onClicked: {
if (requiresConfirmationBeforeClosing()) {
confirmCloseDialog.open();
} else {
WizardViewStepModel.previousStep();
}
}
}
}

View File

@ -19,6 +19,7 @@
#include "appsettingsmanager.h"
#include "lrcinstance.h"
#include "global.h"
#include "api/accountmodel.h"
@ -46,17 +47,31 @@ WizardViewStepModel::WizardViewStepModel(LRCInstance* lrcInstance,
Q_EMIT accountIsReady(accountId);
});
// Connect to account model signals to track import progress
connect(&lrcInstance_->accountModel(),
&AccountModel::deviceAuthStateChanged,
this,
[this](const QString& accountID, int state, const MapStringString& details) {
set_deviceLinkDetails(Utils::mapStringStringToVariantMap(details));
set_deviceAuthState(static_cast<lrc::api::account::DeviceAuthState>(state));
});
}
void
WizardViewStepModel::startAccountCreationFlow(AccountCreationOption accountCreationOption)
{
using namespace lrc::api::account;
set_accountCreationOption(accountCreationOption);
if (accountCreationOption == AccountCreationOption::CreateJamiAccount
|| accountCreationOption == AccountCreationOption::CreateRendezVous)
if (accountCreationOption == AccountCreationOption::ImportFromDevice) {
set_mainStep(MainSteps::DeviceAuthorization);
Q_EMIT createAccountRequested(accountCreationOption);
} else if (accountCreationOption == AccountCreationOption::CreateJamiAccount
|| accountCreationOption == AccountCreationOption::CreateRendezVous) {
set_mainStep(MainSteps::NameRegistration);
else
} else {
set_mainStep(MainSteps::AccountCreation);
}
}
void
@ -80,6 +95,10 @@ WizardViewStepModel::previousStep()
reset();
break;
}
case MainSteps::DeviceAuthorization: {
reset();
break;
}
}
}
@ -88,4 +107,6 @@ WizardViewStepModel::reset()
{
set_accountCreationOption(AccountCreationOption::None);
set_mainStep(MainSteps::Initial);
set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT);
set_deviceLinkDetails({});
}

View File

@ -18,6 +18,7 @@
#pragma once
#include "qtutils.h"
#include "api/account.h" // Include for DeviceAuthState
#include <QObject>
#include <QVariant>
@ -29,6 +30,21 @@ class AccountAdapter;
class LRCInstance;
class AppSettingsManager;
class DeviceAuthStateEnum : public QObject
{
Q_OBJECT
public:
enum State {
INIT = static_cast<int>(lrc::api::account::DeviceAuthState::INIT),
TOKEN_AVAILABLE = static_cast<int>(lrc::api::account::DeviceAuthState::TOKEN_AVAILABLE),
CONNECTING = static_cast<int>(lrc::api::account::DeviceAuthState::CONNECTING),
AUTHENTICATING = static_cast<int>(lrc::api::account::DeviceAuthState::AUTHENTICATING),
IN_PROGRESS = static_cast<int>(lrc::api::account::DeviceAuthState::IN_PROGRESS),
DONE = static_cast<int>(lrc::api::account::DeviceAuthState::DONE)
};
Q_ENUM(State)
};
class WizardViewStepModel : public QObject
{
Q_OBJECT
@ -37,9 +53,10 @@ class WizardViewStepModel : public QObject
public:
enum class MainSteps {
Initial, // Initial welcome step.
AccountCreation, // General account creation step.
NameRegistration, // Name registration step : CreateJamiAccount, CreateRendezVous
Initial, // Initial welcome step.
AccountCreation, // General account creation step.
NameRegistration, // Name registration step : CreateJamiAccount, CreateRendezVous
DeviceAuthorization // Add new step for device authorization.
};
Q_ENUM(MainSteps)
@ -57,6 +74,8 @@ public:
QML_PROPERTY(MainSteps, mainStep)
QML_PROPERTY(AccountCreationOption, accountCreationOption)
QML_PROPERTY(QVariantMap, accountCreationInfo)
QML_PROPERTY(lrc::api::account::DeviceAuthState, deviceAuthState)
QML_PROPERTY(QVariantMap, deviceLinkDetails)
public:
static WizardViewStepModel* create(QQmlEngine*, QJSEngine*)

View File

@ -117,12 +117,26 @@ public Q_SLOTS:
void slotAccountStatusChanged(const QString& accountID, const api::account::Status status);
/**
* Emit exportOnRingEnded.
* Emit deviceAuthStateChanged.
* @param accountId
* @param status
* @param pin
* @param state
* @param details map
*/
void slotExportOnRingEnded(const QString& accountID, int status, const QString& pin);
void slotDeviceAuthStateChanged(const QString& accountID,
int state,
const MapStringString& details);
/**
* Emit addDeviceStateChanged.
* @param accountId
* @param operationId
* @param state
* @param details
*/
void slotAddDeviceStateChanged(const QString& accountID,
uint32_t operationId,
int state,
const MapStringString& details);
/**
* @param accountId
@ -282,11 +296,12 @@ AccountModel::setAlias(const QString& accountId, const QString& alias, bool save
accountInfo.profileInfo.alias = alias;
if (save)
ConfigurationManager::instance().updateProfile(accountId,
alias,
"",
"",
5);// flag out of range to avoid updating avatar
ConfigurationManager::instance()
.updateProfile(accountId,
alias,
"",
"",
5); // flag out of range to avoid updating avatar
Q_EMIT profileUpdated(accountId);
}
@ -323,9 +338,30 @@ AccountModel::exportToFile(const QString& accountId,
}
bool
AccountModel::exportOnRing(const QString& accountId, const QString& password) const
AccountModel::provideAccountAuthentication(const QString& accountId,
const QString& credentialsFromUser) const
{
return ConfigurationManager::instance().exportOnRing(accountId, password);
return ConfigurationManager::instance().provideAccountAuthentication(accountId,
credentialsFromUser,
"password");
}
int32_t
AccountModel::addDevice(const QString& accountId, const QString& token) const
{
return ConfigurationManager::instance().addDevice(accountId, token);
}
bool
AccountModel::confirmAddDevice(const QString& accountId, uint32_t operationId) const
{
return ConfigurationManager::instance().confirmAddDevice(accountId, operationId);
}
bool
AccountModel::cancelAddDevice(const QString& accountId, uint32_t operationId) const
{
return ConfigurationManager::instance().cancelAddDevice(accountId, operationId);
}
void
@ -403,9 +439,13 @@ AccountModelPimpl::AccountModelPimpl(AccountModel& linked,
this,
&AccountModelPimpl::slotVolatileAccountDetailsChanged);
connect(&callbacksHandler,
&CallbacksHandler::exportOnRingEnded,
this,
&AccountModelPimpl::slotExportOnRingEnded);
&CallbacksHandler::deviceAuthStateChanged,
&linked,
&AccountModel::deviceAuthStateChanged);
connect(&callbacksHandler,
&CallbacksHandler::addDeviceStateChanged,
&linked,
&AccountModel::addDeviceStateChanged);
connect(&callbacksHandler,
&CallbacksHandler::nameRegistrationEnded,
this,
@ -594,23 +634,22 @@ AccountModelPimpl::slotVolatileAccountDetailsChanged(const QString& accountId,
}
void
AccountModelPimpl::slotExportOnRingEnded(const QString& accountID, int status, const QString& pin)
AccountModelPimpl::slotDeviceAuthStateChanged(const QString& accountId,
int state,
const MapStringString& details)
{
account::ExportOnRingStatus convertedStatus = account::ExportOnRingStatus::INVALID;
switch (status) {
case 0:
convertedStatus = account::ExportOnRingStatus::SUCCESS;
break;
case 1:
convertedStatus = account::ExportOnRingStatus::WRONG_PASSWORD;
break;
case 2:
convertedStatus = account::ExportOnRingStatus::NETWORK_ERROR;
break;
default:
break;
}
Q_EMIT linked.exportOnRingEnded(accountID, convertedStatus, pin);
// implement business logic here
// can be bypassed with a signal to signal
Q_EMIT linked.deviceAuthStateChanged(accountId, state, details);
}
void
AccountModelPimpl::slotAddDeviceStateChanged(const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details)
{
Q_EMIT linked.addDeviceStateChanged(accountId, operationId, state, details);
}
void
@ -673,7 +712,11 @@ AccountModelPimpl::slotRegisteredNameFound(const QString& accountId,
default:
break;
}
Q_EMIT linked.registeredNameFound(accountId, requestedName, convertedStatus, address, registeredName);
Q_EMIT linked.registeredNameFound(accountId,
requestedName,
convertedStatus,
address,
registeredName);
}
void
@ -1041,32 +1084,47 @@ account::ConfProperties_t::toDetails() const
QString
AccountModel::createNewAccount(profile::Type type,
const MapStringString& config,
const QString& displayName,
const QString& archivePath,
const QString& password,
const QString& pin,
const QString& uri,
const MapStringString& config)
const QString& uri)
{
// Get the template for the account type to prefill the details
MapStringString details = type == profile::Type::SIP
? ConfigurationManager::instance().getAccountTemplate("SIP")
: ConfigurationManager::instance().getAccountTemplate("RING");
using namespace libjami::Account;
details[ConfProperties::TYPE] = type == profile::Type::SIP ? "SIP" : "RING";
details[ConfProperties::DISPLAYNAME] = displayName;
details[ConfProperties::ALIAS] = displayName;
details[ConfProperties::UPNP_ENABLED] = "true";
details[ConfProperties::ARCHIVE_PASSWORD] = password;
details[ConfProperties::ARCHIVE_PIN] = pin;
details[ConfProperties::ARCHIVE_PATH] = archivePath;
if (type == profile::Type::SIP)
details[ConfProperties::USERNAME] = uri;
// Add the supplied config to the details
if (!config.isEmpty()) {
for (MapStringString::const_iterator it = config.begin(); it != config.end(); it++) {
details[it.key()] = it.value();
}
}
using namespace libjami::Account;
// Add the rest of the details if we are not creating an ephemeral account for linking
// in which case the ARCHIVE_URL was set to "jami-auth" or the MANAGER_URI was set to
// the account manager URI in the case of a remote account manager connection
if (details[ConfProperties::ARCHIVE_URL].isEmpty()
&& details[ConfProperties::MANAGER_URI].isEmpty()) {
details[ConfProperties::TYPE] = type == profile::Type::SIP ? "SIP" : "RING";
details[ConfProperties::DISPLAYNAME] = displayName;
details[ConfProperties::ALIAS] = displayName;
details[ConfProperties::UPNP_ENABLED] = "true";
details[ConfProperties::ARCHIVE_PASSWORD] = password;
details[ConfProperties::ARCHIVE_PIN] = pin;
details[ConfProperties::ARCHIVE_PATH] = archivePath;
// Override the username with the provided URI if it's a SIP account
if (type == profile::Type::SIP) {
details[ConfProperties::USERNAME] = uri;
}
}
// Actually add the account and return the account ID
QString accountId = ConfigurationManager::instance().addAccount(details);
return accountId;
}
@ -1077,20 +1135,24 @@ AccountModel::connectToAccountManager(const QString& username,
const QString& serverUri,
const MapStringString& config)
{
MapStringString details = ConfigurationManager::instance().getAccountTemplate("RING");
MapStringString details = config;
using namespace libjami::Account;
details[ConfProperties::TYPE] = "RING";
details[ConfProperties::MANAGER_URI] = serverUri;
details[ConfProperties::MANAGER_USERNAME] = username;
details[ConfProperties::ARCHIVE_PASSWORD] = password;
if (!config.isEmpty()) {
for (MapStringString::const_iterator it = config.begin(); it != config.end(); it++) {
details[it.key()] = it.value();
}
}
return createNewAccount(profile::Type::JAMI, details);
}
QString accountId = ConfigurationManager::instance().addAccount(details);
return accountId;
QString
AccountModel::createDeviceImportAccount()
{
// auto details = ConfigurationManager::instance().getAccountTemplate("RING");
MapStringString details;
using namespace libjami::Account;
details[ConfProperties::TYPE] = "RING";
details[ConfProperties::ARCHIVE_URL] = "jami-auth";
return createNewAccount(profile::Type::JAMI, details);
}
void

View File

@ -188,9 +188,65 @@ struct ConfProperties_t
MapStringString toDetails() const;
};
// Possible account export status
enum class ExportOnRingStatus { SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2, INVALID };
Q_ENUM_NS(ExportOnRingStatus)
// The following statuses are used to track the status of
// device-linking and account-import
enum class DeviceAuthState {
INIT = 0,
TOKEN_AVAILABLE = 1,
CONNECTING = 2,
AUTHENTICATING = 3,
IN_PROGRESS = 4,
DONE = 5
};
Q_ENUM_NS(DeviceAuthState)
enum class DeviceLinkError {
WRONG_PASSWORD, // auth_error, invalid_credentials
NETWORK, // network
TIMEOUT, // timeout
STATE, // state
CANCELED, // canceled
UNKNOWN // fallback
};
Q_ENUM_NS(DeviceLinkError)
inline DeviceLinkError
mapLinkDeviceError(const std::string& error)
{
if (error == "auth_error" || error == "invalid_credentials")
return DeviceLinkError::WRONG_PASSWORD;
if (error == "network")
return DeviceLinkError::NETWORK;
if (error == "timeout")
return DeviceLinkError::TIMEOUT;
if (error == "state")
return DeviceLinkError::STATE;
if (error == "canceled")
return DeviceLinkError::CANCELED;
return DeviceLinkError::UNKNOWN;
}
inline QString
getLinkDeviceString(DeviceLinkError error)
{
switch (error) {
case DeviceLinkError::WRONG_PASSWORD:
return QObject::tr(
"An authentication error occurred.\nPlease check credentials and try again.");
case DeviceLinkError::NETWORK:
return QObject::tr("A network error occurred.\nPlease verify your connection.");
case DeviceLinkError::TIMEOUT:
return QObject::tr("The operation has timed out.\nPlease try again.");
case DeviceLinkError::STATE:
return QObject::tr("An error occurred while exporting the account.\nPlease try again.");
case DeviceLinkError::CANCELED:
return QObject::tr("Operation was canceled.");
case DeviceLinkError::UNKNOWN:
default:
return QObject::tr("An unexpected error occurred.\nPlease try again.");
}
}
enum class RegisterNameStatus {
SUCCESS = 0,

View File

@ -112,13 +112,37 @@ public:
Q_INVOKABLE bool exportToFile(const QString& accountId,
const QString& path,
const QString& password = {}) const;
/**
* Call exportOnRing from the daemon
* Provide authentication for an account
* @param accountId
* @param password
* @param credentialsFromUser
* @return if the authentication is successful
*/
Q_INVOKABLE bool provideAccountAuthentication(const QString& accountId,
const QString& credentialsFromUser) const;
/**
* @param accountId
* @param uri
* @return if the export is initialized
*/
Q_INVOKABLE bool exportOnRing(const QString& accountId, const QString& password) const;
Q_INVOKABLE int32_t addDevice(const QString& accountId, const QString& token) const;
/**
* Confirm the addition of a device
* @param accountId
* @param operationId
*/
Q_INVOKABLE bool confirmAddDevice(const QString& accountId, uint32_t operationId) const;
/**
* Cancel the addition of a device
* @param accountId
* @param operationId
*/
Q_INVOKABLE bool cancelAddDevice(const QString& accountId, uint32_t operationId) const;
/**
* Call removeAccount from the daemon
* @param accountId to remove
@ -141,7 +165,7 @@ public:
* @param avatar
* @throws out_of_range exception if account is not found
*/
void setAvatar(const QString& accountId, const QString& avatar, bool save = true, int flag =0);
void setAvatar(const QString& accountId, const QString& avatar, bool save = true, int flag = 0);
/**
* Change the alias of an account
* @param accountId
@ -159,18 +183,7 @@ public:
Q_INVOKABLE bool registerName(const QString& accountId,
const QString& password,
const QString& username);
/**
* Connect to JAMS to retrieve the account
* @param username
* @param password
* @param serverUri
* @param config
* @return the account id
*/
static QString connectToAccountManager(const QString& username,
const QString& password,
const QString& serverUri,
const MapStringString& config = MapStringString());
/**
* Create a new Ring or SIP account
* @param type determine if the new account will be a Ring account or a SIP one
@ -184,12 +197,32 @@ public:
* @return the created account
*/
static QString createNewAccount(profile::Type type,
const MapStringString& config = MapStringString(),
const QString& displayName = "",
const QString& archivePath = "",
const QString& password = "",
const QString& pin = "",
const QString& uri = "",
const MapStringString& config = MapStringString());
const QString& uri = "");
/**
* Connect to JAMS to retrieve the account
* @param username
* @param password
* @param serverUri
* @param config
* @return the account id
*/
static QString connectToAccountManager(const QString& username,
const QString& password,
const QString& serverUri,
const MapStringString& config = MapStringString());
/**
* Create a simple ephemeral account from a device import
* @return the account id of the created account
*/
static QString createDeviceImportAccount();
/**
* Set an account to the first position
*/
@ -296,14 +329,24 @@ Q_SIGNALS:
void profileUpdated(const QString& accountID);
/**
* Connect this signal to know when an account is exported on the DHT
* Device authentication state has changed
* @param accountID
* @param status
* @param pin
* @param state
* @param details map
*/
void exportOnRingEnded(const QString& accountID,
account::ExportOnRingStatus status,
const QString& pin);
void deviceAuthStateChanged(const QString& accountID, int state, const MapStringString& details);
/**
* Add device state has changed
* @param accountID
* @param operationId
* @param state
* @param details map
*/
void addDeviceStateChanged(const QString& accountID,
uint32_t operationId,
int state,
const MapStringString& details);
/**
* Name registration has ended

View File

@ -406,7 +406,9 @@ public:
* @param windowProcessId
* @param windowId
*/
QString getDisplay(const QString& windowProcessId, const QString& windowId);
QString getDisplay(const QString& windowProcessId,
const QString& windowId,
const int fps = -1);
void emplaceConversationConference(const QString& callId);
@ -481,7 +483,9 @@ Q_SIGNALS:
* @param conversationId
* @param confId
*/
void callAddedToConference(const QString& callId, const QString& conversationId, const QString& confId) const;
void callAddedToConference(const QString& callId,
const QString& conversationId,
const QString& confId) const;
/**
* Emitted when a voice mail notice arrives

View File

@ -288,6 +288,9 @@ getContactInteractionString(const QString& authorUri, const ContactAction& actio
}
return QObject::tr("%1 has joined the conversation.").arg(authorUri);
case ContactAction::LEAVE:
if (authorUri.isEmpty()) {
return QObject::tr("You left the conversation.");
}
return QObject::tr("%1 has left the conversation.").arg(authorUri);
case ContactAction::BANNED:
return QObject::tr("%1 was blocked from the conversation.").arg(authorUri);

View File

@ -242,9 +242,15 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
Qt::QueuedConnection);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::exportOnRingEnded,
&ConfigurationManagerInterface::deviceAuthStateChanged,
this,
&CallbacksHandler::slotExportOnRingEnded,
&CallbacksHandler::slotDeviceAuthStateChanged,
Qt::QueuedConnection);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::addDeviceStateChanged,
this,
&CallbacksHandler::slotAddDeviceStateChanged,
Qt::QueuedConnection);
connect(&ConfigurationManager::instance(),
@ -546,7 +552,9 @@ CallbacksHandler::slotIncomingMessage(const QString& accountId,
}
void
CallbacksHandler::slotConferenceCreated(const QString& accountId, const QString& convId, const QString& callId)
CallbacksHandler::slotConferenceCreated(const QString& accountId,
const QString& convId,
const QString& callId)
{
Q_EMIT conferenceCreated(accountId, convId, callId);
}
@ -678,9 +686,20 @@ CallbacksHandler::slotDeviceRevokationEnded(const QString& accountId,
}
void
CallbacksHandler::slotExportOnRingEnded(const QString& accountId, int status, const QString& pin)
CallbacksHandler::slotDeviceAuthStateChanged(const QString& accountId,
int state,
const MapStringString& details)
{
Q_EMIT exportOnRingEnded(accountId, status, pin);
Q_EMIT deviceAuthStateChanged(accountId, state, details);
}
void
CallbacksHandler::slotAddDeviceStateChanged(const QString& accountId,
uint32_t operationId,
int state,
const MapStringString& details)
{
Q_EMIT addDeviceStateChanged(accountId, operationId, state, details);
}
void

Some files were not shown because too many files have changed in this diff Show More