Compare commits

..

1 Commits

Author SHA1 Message Date
Adasauce 47d884c996
Better image overlay handling when downloading 5 years ago
  1. 1
      .ci/install.sh
  2. 24
      .ci/script.sh
  3. 4
      .gitignore
  4. 39
      .travis.yml
  5. 117
      CHANGELOG.md
  6. 93
      CMakeLists.txt
  7. 60
      README.md
  8. 42
      appveyor.yml
  9. 2
      cmake/Hunter/config.cmake
  10. 77
      cmake/HunterGate.cmake
  11. 2
      cmake/Translations.cmake
  12. 17
      io.github.NhekoReborn.Nheko.json
  13. BIN
      resources/icons/ui/end-call.png
  14. BIN
      resources/icons/ui/microphone-mute.png
  15. BIN
      resources/icons/ui/microphone-unmute.png
  16. BIN
      resources/icons/ui/place-call.png
  17. BIN
      resources/icons/ui/search.png
  18. BIN
      resources/icons/ui/search@2x.png
  19. BIN
      resources/icons/ui/unlock.png
  20. BIN
      resources/icons/ui/unlock@2x.png
  21. 1
      resources/langs/.gitignore
  22. 2243
      resources/langs/nheko_cs.ts
  23. 1265
      resources/langs/nheko_de.ts
  24. 1369
      resources/langs/nheko_el.ts
  25. 1121
      resources/langs/nheko_en.ts
  26. 2252
      resources/langs/nheko_eo.ts
  27. 2257
      resources/langs/nheko_et.ts
  28. 1339
      resources/langs/nheko_fi.ts
  29. 1511
      resources/langs/nheko_fr.ts
  30. 2249
      resources/langs/nheko_it.ts
  31. 1065
      resources/langs/nheko_ja.ts
  32. 1369
      resources/langs/nheko_nl.ts
  33. 1381
      resources/langs/nheko_pl.ts
  34. 2241
      resources/langs/nheko_pt_PT.ts
  35. 2243
      resources/langs/nheko_ro.ts
  36. 1225
      resources/langs/nheko_ru.ts
  37. 2241
      resources/langs/nheko_si.ts
  38. 1381
      resources/langs/nheko_zh_CN.ts
  39. 5
      resources/media/README.txt
  40. BIN
      resources/media/callend.ogg
  41. BIN
      resources/media/ring.ogg
  42. BIN
      resources/media/ringback.ogg
  43. 115
      resources/nheko.appdata.xml
  44. 113
      resources/qml/ActiveCallBar.qml
  45. 113
      resources/qml/Avatar.qml
  46. 58
      resources/qml/EncryptionIndicator.qml
  47. 51
      resources/qml/ImageButton.qml
  48. 55
      resources/qml/MatrixText.qml
  49. 95
      resources/qml/Reactions.qml
  50. 108
      resources/qml/ScrollHelper.qml
  51. 84
      resources/qml/StatusIndicator.qml
  52. 252
      resources/qml/TimelineRow.qml
  53. 894
      resources/qml/TimelineView.qml
  54. 175
      resources/qml/UserProfile.qml
  55. 124
      resources/qml/delegates/FileMessage.qml
  56. 85
      resources/qml/delegates/ImageMessage.qml
  57. 450
      resources/qml/delegates/MessageDelegate.qml
  58. 6
      resources/qml/delegates/NoticeMessage.qml
  59. 17
      resources/qml/delegates/Pill.qml
  60. 6
      resources/qml/delegates/Placeholder.qml
  61. 371
      resources/qml/delegates/PlayableMediaMessage.qml
  62. 118
      resources/qml/delegates/Reply.qml
  63. 11
      resources/qml/delegates/TextMessage.qml
  64. 46
      resources/qml/device-verification/AwaitingVerificationConfirmation.qml
  65. 144
      resources/qml/device-verification/DeviceVerification.qml
  66. 69
      resources/qml/device-verification/DigitVerification.qml
  67. 33
      resources/qml/device-verification/EmojiElement.qml
  68. 414
      resources/qml/device-verification/EmojiVerification.qml
  69. 56
      resources/qml/device-verification/Failed.qml
  70. 46
      resources/qml/device-verification/NewVerificationRequest.qml
  71. 38
      resources/qml/device-verification/Success.qml
  72. 56
      resources/qml/device-verification/Waiting.qml
  73. 66
      resources/qml/device-verification/sas-emoji.json
  74. 16
      resources/qml/emoji/EmojiButton.qml
  75. 332
      resources/qml/emoji/EmojiPicker.qml
  76. 2
      resources/qtquickcontrols2.conf
  77. 29
      resources/res.qrc
  78. 21
      resources/styles/nheko-dark.qss
  79. 15
      resources/styles/nheko.qss
  80. 18
      resources/styles/system.qss
  81. 17
      scripts/emoji_codegen.py
  82. 38
      scripts/generate_icns.sh
  83. 19
      src/AvatarProvider.cpp
  84. 1852
      src/Cache.cpp
  85. 46
      src/Cache.h
  86. 50
      src/CacheCryptoStructs.h
  87. 12
      src/CacheStructs.h
  88. 165
      src/Cache_p.h
  89. 457
      src/CallManager.cpp
  90. 76
      src/CallManager.h
  91. 659
      src/ChatPage.cpp
  92. 53
      src/ChatPage.h
  93. 12
      src/CommunitiesList.cpp
  94. 1
      src/CommunitiesList.h
  95. 2
      src/CommunitiesListItem.cpp
  96. 1
      src/CommunitiesListItem.h
  97. 20
      src/CompletionModel.h
  98. 6
      src/Config.h
  99. 794
      src/DeviceVerificationFlow.cpp
  100. 235
      src/DeviceVerificationFlow.h
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,7 +3,6 @@
set -ex set -ex
if [ "$FLATPAK" ]; then if [ "$FLATPAK" ]; then
sudo apt-get -y install flatpak flatpak-builder elfutils
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --noninteractive install --user flathub org.kde.Platform//5.14 flatpak --noninteractive install --user flathub org.kde.Platform//5.14
flatpak --noninteractive install --user flathub org.kde.Sdk//5.14 flatpak --noninteractive install --user flathub org.kde.Sdk//5.14

@ -6,27 +6,8 @@ if [ "$FLATPAK" ]; then
mkdir -p build-flatpak mkdir -p build-flatpak
cd build-flatpak cd build-flatpak
jobsarg="" flatpak-builder --ccache --repo=repo --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json
if [ "$ARCH" = "arm64" ]; then flatpak build-bundle repo nheko-${VERSION}-${ARCH}.flatpak io.github.NhekoReborn.Nheko 0.7.0-dev
jobsarg="--jobs=2"
fi
flatpak-builder --ccache --repo=repo --subject="Build of Nheko ${VERSION} $jobsarg `date`" app ../io.github.NhekoReborn.Nheko.json &
# to prevent flatpak builder from timing out on arm, run it in the background and print something every minute for up to 30 minutes.
minutes=0
limit=40
while kill -0 $! >/dev/null 2>&1; do
if [ $minutes == $limit ]; then
break;
fi
minutes=$((minutes+1))
sleep 60
done
flatpak build-bundle repo nheko-${VERSION}-${ARCH}.flatpak io.github.NhekoReborn.Nheko master
mkdir ../artifacts mkdir ../artifacts
mv nheko-*.flatpak ../artifacts mv nheko-*.flatpak ../artifacts
@ -71,6 +52,7 @@ cmake -GNinja -H. -Bbuild \
-DHUNTER_ROOT=".hunter" \ -DHUNTER_ROOT=".hunter" \
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \ -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \
-DUSE_BUNDLED_OPENSSL=OFF \
-DCI_BUILD=ON -DCI_BUILD=ON
fi fi
cmake --build build cmake --build build

4
.gitignore vendored

@ -6,7 +6,6 @@ cscope*
/.ccls-cache /.ccls-cache
/.exrc /.exrc
.gdb_history .gdb_history
.hunter
# GTAGS # GTAGS
GTAGS GTAGS
@ -71,8 +70,7 @@ install_manifest.txt
.LSOverride .LSOverride
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*

@ -63,21 +63,21 @@ matrix:
env: env:
- CXX=g++-8 - CXX=g++-8
- CC=gcc-8 - CC=gcc-8
- QT_PKG=510 - QT_PKG=59
addons: addons:
apt: apt:
sources: sources:
- ubuntu-toolchain-r-test - ubuntu-toolchain-r-test
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial' - sourceline: 'ppa:beineri/opt-qt597-xenial'
packages: packages:
- g++-8 - g++-8
- ninja-build - ninja-build
- qt510base - qt59base
- qt510tools - qt59tools
- qt510svg - qt59svg
- qt510multimedia - qt59multimedia
- qt510quickcontrols2 - qt59quickcontrols2
- qt510graphicaleffects - qt59graphicaleffects
- liblmdb-dev - liblmdb-dev
- libgl1-mesa-dev # needed for missing gl.h - libgl1-mesa-dev # needed for missing gl.h
- os: linux - os: linux
@ -85,23 +85,23 @@ matrix:
env: env:
- CXX=clang++-6.0 - CXX=clang++-6.0
- CC=clang-6.0 - CC=clang-6.0
- QT_PKG=510 - QT_PKG=59
addons: addons:
apt: apt:
sources: sources:
- ubuntu-toolchain-r-test - ubuntu-toolchain-r-test
- llvm-toolchain-xenial-6.0 - llvm-toolchain-xenial-6.0
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial' - sourceline: 'ppa:beineri/opt-qt597-xenial'
packages: packages:
- clang++-6.0 - clang++-6.0
- g++-7 - g++-7
- ninja-build - ninja-build
- qt510base - qt59base
- qt510tools - qt59tools
- qt510svg - qt59svg
- qt510multimedia - qt59multimedia
- qt510quickcontrols2 - qt59quickcontrols2
- qt510graphicaleffects - qt59graphicaleffects
- liblmdb-dev - liblmdb-dev
- libgl1-mesa-dev # needed for missing gl.h - libgl1-mesa-dev # needed for missing gl.h
- os: linux - os: linux
@ -113,6 +113,10 @@ matrix:
apt: apt:
sources: sources:
- sourceline: 'ppa:alexlarsson/flatpak' - sourceline: 'ppa:alexlarsson/flatpak'
packages:
- flatpak
- flatpak-builder
- elfutils
- os: linux - os: linux
arch: arm64 arch: arm64
env: env:
@ -124,6 +128,9 @@ matrix:
sources: sources:
- sourceline: 'ppa:alexlarsson/flatpak' - sourceline: 'ppa:alexlarsson/flatpak'
packages: packages:
- flatpak
- flatpak-builder
- elfutils
- librsvg2-bin - librsvg2-bin
before_install: before_install:

@ -1,126 +1,25 @@
# Changelog # Changelog
## [0.7.2] -- 2020-06-12 ## [Unreleased]
### Highlights
- Reactions
- React to a message with an emoji! 🎉
- Reactions are shown below a message in a small bubble with a counter.
- By clicking on that, others can add to the reaction count.
- It may help you celebrating a new Nheko Release or react with a 👎 to a failed build to express your frustration.
- This uses a new emoji picker. The picker will be improved in the near future (better scrolling, sections, favorites, recently used or similar) and then probably replace the current picker.
- Support for tagging rooms `[tag]`
- Assign custom tags to rooms from the context menu in the room list.
- This allows filtering rooms via the group list. This puts you in a focus mode showing only the selected tags.
- You can assign multiple tags to group rooms however you like.
- SSO Login
- With this you can now login on servers, that only provide SSO.
- Just enter any mxid on the server. Nheko will figure out that you need to use SSO and redirect your browser to the login page.
- Complete the login in your browser and Nheko should automatically log you in.
- Presence
- Shows online status of the people you are talking to.
- You can define a custom status message to tell others what you are currently up to.
- The status message appears next to the usernames in the timeline.
- Your server needs to have presence enabled for this to work.
### Features ### [0.7.0] -- Unreleased
- Respect exif rotation of images
- An italian translation (contributed by Lorenzo Ancora)
- Optional alerts in your taskbar (contributed by z33ky)
- Optional bigger emoji only messages in the timeline (contributed by lkito)
- Optional hover feedback on messages (contributed by lkito)
- `/roomnick` to change your displayname in a single room.
- Preliminary support for showing inline images.
- Warn about unencrypted messages in encrypted rooms.
### Improvements
- perf: Use less CPU to sort the room list.
- Limit size of replies. This currently looks a bit rough, but should improve in the future with a gradient or at some other transition.
- perf: Only clean out old messages from the database every 500 syncs. (There is usually more than one sync every second)
- Improve the login and register masks a bit with hints and validation.
- Descriptions for settings (contributed by lkito)
- A visual indicator, that nheko is fetching messages and improved scrolling (contributed by Lasath Fernando)
### Bugfixes
- Fix not being able to join rooms
- Fix scale factor setting
- Buildfixes against gcc10 and Qt5.15 (missing includes)
- Settings now apply immediately again after changing them (only exception should be the scale factor)
- Join messages should never have empty texts now
- Timeline should now fail to render less often on platforms with native sibling windows.
- Don't rescale images on every frame on highdpi screens.
### Upgrade Notes
<span style="color: red;">This updates includes some changes to the database. Older versions don't handle that gracefully and will delete your database. It is therefore recommended to not downgrade below this version!</span>
## [0.7.1] -- 2020-04-24
### Features
- Show decrypted message source (helps debugging)
- Allow user to show / hide messages in encrypted rooms in sidebar
### Bugfixes
- Fix display of images sent by the user (thank you, wnereiz and not-chicken for reporting)
- Fix crash when trying to maximize image, that wasn't downloaded yet.
- Fix Binding restoreMode flooding logs on Qt 5.14.2+
- Fix with some qml styles hidden menu items leave empty space
- Fix encrypted messages not showing a user in the sidebar
- Fix hangs when generating colors with some system theme color schemes (#172)
## [0.7.0] -- 2020-04-19
0.7.0 *requires* mtxclient 0.3.0. Make sure you compile against 0.3.0 0.7.0 *requires* mtxclient 0.3.0. Make sure you compile against 0.3.0
if you do not use the mtxclient bundled with nheko. if you do not use the mtxclient bundled with nheko.
### Features #### Features
- Make nheko session import / export format match riot. Fixes #48 - Make nheko session import / export format match riot. Fixes #48 (WIP)
- Implement proper replies - Implement proper replies (WIP)
- Add .well-known support for auto-completing homeserver information - Add .well-known support for auto-completing homeserver information
- Add mentions viewer so you can see all the messages you have been mentioned in - Add mentions viewer so you can see all the messages you have been mentioned in (WIP)
- Currently broken due to QML changes. Will be fixed in the future.
- Add emoji font selection preference - Add emoji font selection preference
- Encryption and decryption of media in E2EE rooms
- Square avatars
- Support for muting and unmuting rooms
- Basic support for playing audio and video messages in the timeline
- Support for a lot more event types (hiding them will come in the future)
- Support for sending all messages as plain text
- Support for inviting, kicking, banning and unbanning users
- Sort the room list by importance of messages (thanks @Alch-Emi)
- Experimental support for [blurhashes](https://github.com/matrix-org/matrix-doc/pull/2448)
### Improvements #### Improvements
- Add dedicated reply button to Timeline items. Add button for other options so - Add dedicated reply button to Timeline items. Add button for other options so
that right click isn't always required. that right click isn't always required.
- Fix various things with regards to emoji rendering and the emoji picker - Fix various things with regards to emoji rendering and the emoji picker (WIP)
- Lots and lots and lots of localization updates. - Lots and lots and lots of localization updates.
- Additional tweaks to the system theme - Additional tweaks to the system theme
- Render timeline in Qml to drop memory usage
- Reduce memory usage of avatars
- Close notifications after they have been read on Linux
- Escape html properly in most places
- A lot of improvements around the image overlay
- The settings page now resizes properly for small screens
- Miscellaneous styling improvements
- Simplify and speedup build
- Display more emojis in the selected emoji font
- Use 'system' theme as default if QT_QPA_PLATFORMTHEME is set
### Bugfixes
- Fix messages stuck on unread
- Reduce the amount of messages shown as "xxx sent an encrypted message"
- Fix various race conditions and crashes
- Fix some compatibility issues with the construct homeserver
Be aware, that Nheko now requires Qt 5.10 and boost 1.70 or higher.
## [0.6.4] - 2019-05-22 ## [0.6.4] - 2019-05-22

@ -4,23 +4,21 @@ option(APPVEYOR_BUILD "Build on appveyor" OFF)
option(CI_BUILD "Set when building in CI. Enables -Werror where possible" OFF) option(CI_BUILD "Set when building in CI. Enables -Werror where possible" OFF)
option(ASAN "Compile with address sanitizers" OFF) option(ASAN "Compile with address sanitizers" OFF)
option(QML_DEBUGGING "Enable qml debugging" OFF) option(QML_DEBUGGING "Enable qml debugging" OFF)
option(COMPILE_QML "Compile Qml. It will make Nheko faster, but you will need to recompile it, when you update Qt." OFF)
set( set(
CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/toolchain.cmake" CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/toolchain.cmake"
CACHE CACHE
FILEPATH "Default toolchain" FILEPATH "Default toolchain"
) )
set(CMAKE_CXX_STANDARD 17 CACHE STRING "C++ standard")
set(CMAKE_CXX_STANDARD_REQUIRED ON CACHE BOOL "Require C++ standard to be supported")
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "compile as PIC by default")
option(HUNTER_ENABLED "Enable Hunter package manager" OFF) option(HUNTER_ENABLED "Enable Hunter package manager" OFF)
include("cmake/HunterGate.cmake") include("cmake/HunterGate.cmake")
HunterGate( HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.23.260.tar.gz" URL "https://github.com/cpp-pm/hunter/archive/v0.23.244.tar.gz"
SHA1 "13775235910a3fa85644568d1c5be8271de72e1c" SHA1 "2c0f491fd0b80f7b09e3d21adb97237161ef9835"
) LOCAL
)
option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${HUNTER_ENABLED}) option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${HUNTER_ENABLED})
option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog." option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog."
@ -35,6 +33,10 @@ option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json."
option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL." option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL."
${HUNTER_ENABLED}) ${HUNTER_ENABLED})
option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED}) option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED})
option(USE_BUNDLED_SODIUM "Use the bundled version of libsodium."
${HUNTER_ENABLED})
option(USE_BUNDLED_ZLIB "Use the bundled version of zlib."
${HUNTER_ENABLED})
option(USE_BUNDLED_LMDB "Use the bundled version of lmdb." option(USE_BUNDLED_LMDB "Use the bundled version of lmdb."
${HUNTER_ENABLED}) ${HUNTER_ENABLED})
option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++." option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++."
@ -78,7 +80,7 @@ include(QtCommon)
project(nheko LANGUAGES CXX C) project(nheko LANGUAGES CXX C)
set(CPACK_PACKAGE_VERSION_MAJOR "0") set(CPACK_PACKAGE_VERSION_MAJOR "0")
set(CPACK_PACKAGE_VERSION_MINOR "7") set(CPACK_PACKAGE_VERSION_MINOR "7")
set(CPACK_PACKAGE_VERSION_PATCH "2") set(CPACK_PACKAGE_VERSION_PATCH "0")
set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR}) set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR})
set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR}) set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR})
set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH}) set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH})
@ -142,9 +144,9 @@ if (APPLE)
endif(APPLE) endif(APPLE)
if (Qt5Widgets_FOUND) if (Qt5Widgets_FOUND)
if (Qt5Widgets_VERSION VERSION_LESS 5.10.0) if (Qt5Widgets_VERSION VERSION_LESS 5.9.0)
message(STATUS "Qt version ${Qt5Widgets_VERSION}") message(STATUS "Qt version ${Qt5Widgets_VERSION}")
message(WARNING "Minimum supported Qt5 version is 5.10!") message(WARNING "Minimum supported Qt5 version is 5.9!")
endif() endif()
endif(Qt5Widgets_FOUND) endif(Qt5Widgets_FOUND)
@ -225,7 +227,6 @@ configure_file(cmake/nheko.h config/nheko.h)
# #
set(SRC_FILES set(SRC_FILES
# Dialogs # Dialogs
src/dialogs/AcceptCall.cpp
src/dialogs/CreateRoom.cpp src/dialogs/CreateRoom.cpp
src/dialogs/FallbackAuth.cpp src/dialogs/FallbackAuth.cpp
src/dialogs/ImageOverlay.cpp src/dialogs/ImageOverlay.cpp
@ -234,25 +235,20 @@ set(SRC_FILES
src/dialogs/LeaveRoom.cpp src/dialogs/LeaveRoom.cpp
src/dialogs/Logout.cpp src/dialogs/Logout.cpp
src/dialogs/MemberList.cpp src/dialogs/MemberList.cpp
src/dialogs/PlaceCall.cpp
src/dialogs/PreviewUploadOverlay.cpp src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp src/dialogs/ReCaptcha.cpp
src/dialogs/ReadReceipts.cpp src/dialogs/ReadReceipts.cpp
src/dialogs/RoomSettings.cpp src/dialogs/RoomSettings.cpp
src/dialogs/UserProfile.cpp
# Emoji # Emoji
src/emoji/Category.cpp src/emoji/Category.cpp
src/emoji/EmojiModel.cpp
src/emoji/ItemDelegate.cpp src/emoji/ItemDelegate.cpp
src/emoji/Panel.cpp src/emoji/Panel.cpp
src/emoji/PickButton.cpp src/emoji/PickButton.cpp
src/emoji/Provider.cpp src/emoji/Provider.cpp
src/emoji/Provider_new.cpp
# Timeline # Timeline
src/timeline/EventStore.cpp
src/timeline/Reaction.cpp
src/timeline/TimelineViewManager.cpp src/timeline/TimelineViewManager.cpp
src/timeline/TimelineModel.cpp src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp src/timeline/DelegateChooser.cpp
@ -277,17 +273,14 @@ set(SRC_FILES
src/ui/ToggleButton.cpp src/ui/ToggleButton.cpp
src/ui/Theme.cpp src/ui/Theme.cpp
src/ui/ThemeManager.cpp src/ui/ThemeManager.cpp
src/ui/UserProfile.cpp
src/AvatarProvider.cpp src/AvatarProvider.cpp
src/BlurhashProvider.cpp src/BlurhashProvider.cpp
src/Cache.cpp src/Cache.cpp
src/CallManager.cpp
src/ChatPage.cpp src/ChatPage.cpp
src/ColorImageProvider.cpp src/ColorImageProvider.cpp
src/CommunitiesList.cpp src/CommunitiesList.cpp
src/CommunitiesListItem.cpp src/CommunitiesListItem.cpp
src/DeviceVerificationFlow.cpp
src/EventAccessors.cpp src/EventAccessors.cpp
src/InviteeItem.cpp src/InviteeItem.cpp
src/Logging.cpp src/Logging.cpp
@ -300,17 +293,17 @@ set(SRC_FILES
src/RegisterPage.cpp src/RegisterPage.cpp
src/RoomInfoListItem.cpp src/RoomInfoListItem.cpp
src/RoomList.cpp src/RoomList.cpp
src/SSOHandler.cpp
src/SideBarActions.cpp src/SideBarActions.cpp
src/Splitter.cpp src/Splitter.cpp
src/TextInputWidget.cpp src/TextInputWidget.cpp
src/TopRoomBar.cpp
src/TrayIcon.cpp src/TrayIcon.cpp
src/UserInfoWidget.cpp src/UserInfoWidget.cpp
src/UserSettingsPage.cpp src/UserSettingsPage.cpp
src/Utils.cpp src/Utils.cpp
src/WebRTCSession.cpp
src/WelcomePage.cpp src/WelcomePage.cpp
src/popups/PopupItem.cpp src/popups/PopupItem.cpp
src/popups/ReplyPopup.cpp
src/popups/SuggestionsPopup.cpp src/popups/SuggestionsPopup.cpp
src/popups/UserMentions.cpp src/popups/UserMentions.cpp
src/main.cpp src/main.cpp
@ -328,11 +321,14 @@ find_package(Boost 1.70 REQUIRED
COMPONENTS iostreams COMPONENTS iostreams
system system
thread) thread)
if(USE_BUNDLED_ZLIB)
hunter_add_package(ZLIB)
endif()
find_package(ZLIB REQUIRED)
if(USE_BUNDLED_OPENSSL) if(USE_BUNDLED_OPENSSL)
hunter_add_package(OpenSSL) hunter_add_package(OpenSSL)
endif() endif()
find_package(OpenSSL 1.1.0 REQUIRED) find_package(OpenSSL REQUIRED)
if(USE_BUNDLED_MTXCLIENT) if(USE_BUNDLED_MTXCLIENT)
include(FetchContent) include(FetchContent)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
@ -340,11 +336,11 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG ad5575bc24089dc385e97d9ace026414b618775c GIT_TAG c1ccd6c6cdaead3ff1c2bf336b719ca45fee2d33
) )
FetchContent_MakeAvailable(MatrixClient) FetchContent_MakeAvailable(MatrixClient)
else() else()
find_package(MatrixClient 0.3.1 REQUIRED) find_package(MatrixClient 0.3.0 REQUIRED)
endif() endif()
if(USE_BUNDLED_OLM) if(USE_BUNDLED_OLM)
include(FetchContent) include(FetchContent)
@ -383,7 +379,7 @@ if(USE_BUNDLED_CMARK)
add_library(cmark::cmark ALIAS libcmark_static) add_library(cmark::cmark ALIAS libcmark_static)
endif() endif()
else() else()
find_package(cmark REQUIRED 0.29.0) find_package(cmark REQUIRED)
endif() endif()
if(USE_BUNDLED_JSON) if(USE_BUNDLED_JSON)
@ -426,18 +422,14 @@ else()
find_package(Tweeny REQUIRED) find_package(Tweeny REQUIRED)
endif() endif()
include(FindPkgConfig)
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.14 gstreamer-webrtc-1.0>=1.14)
# single instance functionality # single instance functionality
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
add_subdirectory(third_party/SingleApplication-3.1.3.1/) add_subdirectory(third_party/SingleApplication-3.0.19/)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
qt5_wrap_cpp(MOC_HEADERS qt5_wrap_cpp(MOC_HEADERS
# Dialogs # Dialogs
src/dialogs/AcceptCall.h
src/dialogs/CreateRoom.h src/dialogs/CreateRoom.h
src/dialogs/FallbackAuth.h src/dialogs/FallbackAuth.h
src/dialogs/ImageOverlay.h src/dialogs/ImageOverlay.h
@ -446,24 +438,20 @@ qt5_wrap_cpp(MOC_HEADERS
src/dialogs/LeaveRoom.h src/dialogs/LeaveRoom.h
src/dialogs/Logout.h src/dialogs/Logout.h
src/dialogs/MemberList.h src/dialogs/MemberList.h
src/dialogs/PlaceCall.h
src/dialogs/PreviewUploadOverlay.h src/dialogs/PreviewUploadOverlay.h
src/dialogs/RawMessage.h src/dialogs/RawMessage.h
src/dialogs/ReCaptcha.h src/dialogs/ReCaptcha.h
src/dialogs/ReadReceipts.h src/dialogs/ReadReceipts.h
src/dialogs/RoomSettings.h src/dialogs/RoomSettings.h
src/dialogs/UserProfile.h
# Emoji # Emoji
src/emoji/Category.h src/emoji/Category.h
src/emoji/EmojiModel.h
src/emoji/ItemDelegate.h src/emoji/ItemDelegate.h
src/emoji/Panel.h src/emoji/Panel.h
src/emoji/PickButton.h src/emoji/PickButton.h
src/emoji/Provider.h
# Timeline # Timeline
src/timeline/EventStore.h
src/timeline/Reaction.h
src/timeline/TimelineViewManager.h src/timeline/TimelineViewManager.h
src/timeline/TimelineModel.h src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h src/timeline/DelegateChooser.h
@ -487,18 +475,15 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/ToggleButton.h src/ui/ToggleButton.h
src/ui/Theme.h src/ui/Theme.h
src/ui/ThemeManager.h src/ui/ThemeManager.h
src/ui/UserProfile.h
src/notifications/Manager.h src/notifications/Manager.h
src/AvatarProvider.h src/AvatarProvider.h
src/BlurhashProvider.h src/BlurhashProvider.h
src/Cache_p.h src/Cache_p.h
src/CallManager.h
src/ChatPage.h src/ChatPage.h
src/CommunitiesList.h src/CommunitiesList.h
src/CommunitiesListItem.h src/CommunitiesListItem.h
src/DeviceVerificationFlow.h
src/InviteeItem.h src/InviteeItem.h
src/LoginPage.h src/LoginPage.h
src/MainWindow.h src/MainWindow.h
@ -507,16 +492,16 @@ qt5_wrap_cpp(MOC_HEADERS
src/RegisterPage.h src/RegisterPage.h
src/RoomInfoListItem.h src/RoomInfoListItem.h
src/RoomList.h src/RoomList.h
src/SSOHandler.h
src/SideBarActions.h src/SideBarActions.h
src/Splitter.h src/Splitter.h
src/TextInputWidget.h src/TextInputWidget.h
src/TopRoomBar.h
src/TrayIcon.h src/TrayIcon.h
src/UserInfoWidget.h src/UserInfoWidget.h
src/UserSettingsPage.h src/UserSettingsPage.h
src/WebRTCSession.h
src/WelcomePage.h src/WelcomePage.h
src/popups/PopupItem.h src/popups/PopupItem.h
src/popups/ReplyPopup.h
src/popups/SuggestionsPopup.h src/popups/SuggestionsPopup.h
src/popups/UserMentions.h src/popups/UserMentions.h
) )
@ -530,9 +515,6 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
if (APPLE) if (APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm) set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
endif()
elseif (WIN32) elseif (WIN32)
file(DOWNLOAD file(DOWNLOAD
"https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.cpp" "https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.cpp"
@ -560,12 +542,7 @@ if(ASAN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined")
endif() endif()
if(WIN32) add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
add_executable (nheko WIN32 ${OS_BUNDLE} ${NHEKO_DEPS})
else()
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
endif()
if(APPLE) if(APPLE)
target_link_libraries (nheko PRIVATE Qt5::MacExtras) target_link_libraries (nheko PRIVATE Qt5::MacExtras)
elseif(WIN32) elseif(WIN32)
@ -574,7 +551,7 @@ elseif(WIN32)
else() else()
target_link_libraries (nheko PRIVATE Qt5::DBus) target_link_libraries (nheko PRIVATE Qt5::DBus)
endif() endif()
target_include_directories(nheko PRIVATE src includes third_party/blurhash third_party/cpp-httplib-0.5.12) target_include_directories(nheko PRIVATE src includes third_party/blurhash)
target_link_libraries(nheko PRIVATE target_link_libraries(nheko PRIVATE
MatrixClient::MatrixClient MatrixClient::MatrixClient
@ -596,18 +573,6 @@ target_link_libraries(nheko PRIVATE
tweeny tweeny
SingleApplication::SingleApplication) SingleApplication::SingleApplication)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
target_precompile_headers(nheko
PRIVATE
<string>
)
endif()
if (TARGET PkgConfig::GSTREAMER)
target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER)
target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE)
endif()
if(MSVC) if(MSVC)
target_link_libraries(nheko PRIVATE ntdll) target_link_libraries(nheko PRIVATE ntdll)
endif() endif()

@ -2,7 +2,7 @@ nheko
---- ----
[![Build Status](https://travis-ci.org/Nheko-Reborn/nheko.svg?branch=master)](https://travis-ci.org/Nheko-Reborn/nheko) [![Build Status](https://travis-ci.org/Nheko-Reborn/nheko.svg?branch=master)](https://travis-ci.org/Nheko-Reborn/nheko)
[![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master) [![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master)
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.7.2) [![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.6.4)
[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/) [![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/)
[![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org) [![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org)
[![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko) [![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko)
@ -51,14 +51,12 @@ can be found in the [Github releases](https://github.com/Nheko-Reborn/nheko/rele
### Repositories ### Repositories
[![Packaging status](https://repology.org/badge/tiny-repos/nheko.svg)](https://repology.org/project/nheko/versions)
#### Arch Linux #### Arch Linux
```bash ```bash
pacaur -S nheko # nheko-git pacaur -S nheko # nheko-git
``` ```
#### Debian (10 and above) / Ubuntu (18.04 and above) #### Debian (10 and above)
```bash ```bash
sudo apt install nheko sudo apt install nheko
@ -75,14 +73,6 @@ sudo eselect repository enable matrix
sudo emerge -a nheko sudo emerge -a nheko
``` ```
#### Nix(os)
```bash
nix-env -iA nixpkgs.nheko
# or
nix-shell -p nheko --run nheko
```
#### Alpine Linux (and postmarketOS) #### Alpine Linux (and postmarketOS)
Make sure you have the testing repositories from `edge` enabled. Note that this is not needed on postmarketOS. Make sure you have the testing repositories from `edge` enabled. Note that this is not needed on postmarketOS.
@ -105,10 +95,11 @@ guix install nheko
#### macOS (10.14 and above) #### macOS (10.14 and above)
with [homebrew](https://brew.sh/):
with [macports](https://www.macports.org/) :
```sh ```sh
brew cask install nheko sudo port install nheko
``` ```
### Build Requirements ### Build Requirements
@ -121,7 +112,8 @@ brew cask install nheko
- [LMDB](https://symas.com/lightning-memory-mapped-database/) - [LMDB](https://symas.com/lightning-memory-mapped-database/)
- [cmark](https://github.com/commonmark/cmark) 0.29 or greater. - [cmark](https://github.com/commonmark/cmark) 0.29 or greater.
- Boost 1.70 or greater. - Boost 1.70 or greater.
- [libolm](https://gitlab.matrix.org/matrix-org/olm) - [libolm](https://git.matrix.org/git/olm)
- [libsodium](https://github.com/jedisct1/libsodium)
- [spdlog](https://github.com/gabime/spdlog) - [spdlog](https://github.com/gabime/spdlog)
- A compiler that supports C++ 17: - A compiler that supports C++ 17:
- Clang 6 (tested on Travis CI) - Clang 6 (tested on Travis CI)
@ -132,7 +124,6 @@ Nheko can use bundled version for most of those libraries automatically, if the
To use them, you can enable the hunter integration by passing `-DHUNTER_ENABLED=ON`. To use them, you can enable the hunter integration by passing `-DHUNTER_ENABLED=ON`.
It is probably wise to link those dependencies statically by passing `-DBUILD_SHARED_LIBS=OFF` It is probably wise to link those dependencies statically by passing `-DBUILD_SHARED_LIBS=OFF`
You can select which bundled dependencies you want to use py passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter. You can select which bundled dependencies you want to use py passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter.
If you experience build issues and you are trying to link `mtxclient` library without hunter, make sure the library version(commit) as mentioned in the `CMakeList.txt` is used. Sometimes we have to make breaking changes in `mtxclient` and for that period the master branch of both repos may not be compatible.
The bundle flags are currently: The bundle flags are currently:
@ -144,13 +135,15 @@ The bundle flags are currently:
- USE_BUNDLED_JSON - USE_BUNDLED_JSON
- USE_BUNDLED_OPENSSL - USE_BUNDLED_OPENSSL
- USE_BUNDLED_MTXCLIENT - USE_BUNDLED_MTXCLIENT
- USE_BUNDLED_SODIUM
- USE_BUNDLED_ZLIB
- USE_BUNDLED_LMDB - USE_BUNDLED_LMDB
- USE_BUNDLED_LMDBXX - USE_BUNDLED_LMDBXX
- USE_BUNDLED_TWEENY - USE_BUNDLED_TWEENY
#### Linux #### Linux
If you don't want to install any external dependencies, you can generate an AppImage locally using docker. It is not that well maintained though... If you don't want to install any external dependencies, you can generate an AppImage locally using docker.
```bash ```bash
make docker-app-image make docker-app-image
@ -168,30 +161,39 @@ sudo pacman -S qt5-base \
fontconfig \ fontconfig \
lmdb \ lmdb \
cmark \ cmark \
boost boost \
libsodium
``` ```
##### Gentoo Linux ##### Gentoo Linux
```bash ```bash
sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig
```
##### Ubuntu 16.04
```bash
sudo add-apt-repository ppa:beineri/opt-qt592-xenial
sudo add-apt-repository ppa:george-edison55/cmake-3.x
sudo add-apt-repository ppa:ubuntu-toolchain-r-test
sudo apt-get update
sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev libsodium-dev
``` ```
##### Ubuntu 20.04 ##### Ubuntu 19.10
```bash ```bash
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports): # Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,graphicaleffects,quick-controls2} sudo apt install g++-7 cmake liblmdb-dev libsodium-dev qt{base,tools,multimedia}5-dev qml-module-qt{gstreamer,multimedia,quick-extras} libqt5svg5-dev qt{script,quickcontrols2-}5-dev
``` ```
This will install all dependencies, except for tweeny (use bundled tweeny)
and mtxclient (needs to be build separately).
##### Debian Buster (or higher probably) ##### Debian Buster (or higher probably)
(User report, not sure if all of those are needed) (User report, not sure if all of those are needed)
```bash ```bash
sudo apt install cmake gcc make automake liblmdb-dev \ sudo apt install cmake gcc make automake liblmdb-dev libsodium-dev \
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \ qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools \ qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools \
qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts
@ -208,7 +210,7 @@ guix environment nheko
```bash ```bash
brew update brew update
brew install qt5 lmdb cmake llvm spdlog boost cmark libolm brew install qt5 lmdb cmake llvm libsodium spdlog boost cmark
``` ```
##### Windows ##### Windows
@ -227,14 +229,14 @@ Make sure to install the `MSVC 2017 64-bit` toolset for at least Qt 5.10
We can now build nheko: We can now build nheko:
```bash ```bash
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build cmake --build build
``` ```
To use bundled dependencies you can use hunter, i.e.: To use bundled dependencies you can use hunter, i.e.:
```bash ```bash
cmake -S. -Bbuild -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=OFF cmake -H. -Bbuild -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=OFF
cmake --build build --config Release cmake --build build --config Release
``` ```
@ -253,7 +255,7 @@ You might need to pass `-DCMAKE_PREFIX_PATH` to cmake to point it at your qt5 in
e.g on macOS e.g on macOS
``` ```
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5) cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5)
cmake --build build cmake --build build
``` ```
@ -273,7 +275,7 @@ Examples for the paths are:
You should also enable hunter by setting `HUNTER_ENABLED` to `ON` and `BUILD_SHARED_LIBS` to `OFF`. You should also enable hunter by setting `HUNTER_ENABLED` to `ON` and `BUILD_SHARED_LIBS` to `OFF`.
Now right click into the root nheko source directory and choose `Open in Visual Studio`. Now right click into the root nheko source directory and choose `Open in Visual Studio`.
You can choose the build type Release and Debug in the top toolbar. You can choose the build type Release and Debug in the top toolbar.
After a successful CMake generation you can select the `nheko.exe` as the run target. After a successful CMake generation you can select the `nheko.exe` as the run target.
Now choose `Build all` in the CMake menu or press `F7` to compile the executable. Now choose `Build all` in the CMake menu or press `F7` to compile the executable.

@ -1,6 +1,6 @@
--- ---
version: 0.7.2-{build} version: 0.7.0-{build}
configuration: Release configuration: Release
image: Visual Studio 2017 image: Visual Studio 2017
@ -30,8 +30,8 @@ build_script:
# VERSION format: branch-master/branch-1.2 # VERSION format: branch-master/branch-1.2
# INSTVERSION format: x.y.z # INSTVERSION format: x.y.z
# WINVERSION format: 9999.0.0.123/1.2.0.234 # WINVERSION format: 9999.0.0.123/1.2.0.234
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.7.2 - if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.6.4
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.7.2 - if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.6.4
- if "%APPVEYOR_REPO_TAG%"=="false" if "%APPVEYOR_REPO_BRANCH%"=="master" set INSTVERSION=9999.0 - if "%APPVEYOR_REPO_TAG%"=="false" if "%APPVEYOR_REPO_BRANCH%"=="master" set INSTVERSION=9999.0
- if "%APPVEYOR_REPO_TAG%"=="false" set WINVERSION=%INSTVERSION%.0.%APPVEYOR_BUILD_NUMBER% - if "%APPVEYOR_REPO_TAG%"=="false" set WINVERSION=%INSTVERSION%.0.%APPVEYOR_BUILD_NUMBER%
# VERSION format: v1.2.3/v1.3.4 # VERSION format: v1.2.3/v1.3.4
@ -74,31 +74,31 @@ after_build:
- mkdir installer - mkdir installer
- mkdir installer\config - mkdir installer\config
- mkdir installer\packages - mkdir installer\packages
- mkdir installer\packages\io.github.nhekoreborn.nheko - mkdir installer\packages\com.mujx.nheko
- mkdir installer\packages\io.github.nhekoreborn.nheko\data - mkdir installer\packages\com.mujx.nheko\data
- mkdir installer\packages\io.github.nhekoreborn.nheko\meta - mkdir installer\packages\com.mujx.nheko\meta
- mkdir installer\packages\io.github.nhekoreborn.nheko.cleanup\meta - mkdir installer\packages\com.mujx.nheko.cleanup\meta
# Copy installer data # Copy installer data
- copy %BUILD%\resources\nheko.ico installer\config - copy %BUILD%\resources\nheko.ico installer\config
- copy %BUILD%\resources\nheko.png installer\config - copy %BUILD%\resources\nheko.png installer\config
- copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko\meta\license.txt - copy %BUILD%\COPYING installer\packages\com.mujx.nheko\meta\license.txt
- copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\license.txt - copy %BUILD%\COPYING installer\packages\com.mujx.nheko.cleanup\meta\license.txt
- copy %BUILD%\deploy\installer\config.xml installer\config - copy %BUILD%\deploy\installer\config.xml installer\config
- copy %BUILD%\deploy\installer\controlscript.qs installer\config - copy %BUILD%\deploy\installer\controlscript.qs installer\config
- copy %BUILD%\deploy\installer\uninstall.qs installer\packages\io.github.nhekoreborn.nheko\data - copy %BUILD%\deploy\installer\uninstall.qs installer\packages\com.mujx.nheko\data
- copy %BUILD%\deploy\installer\gui\package.xml installer\packages\io.github.nhekoreborn.nheko\meta - copy %BUILD%\deploy\installer\gui\package.xml installer\packages\com.mujx.nheko\meta
- copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\io.github.nhekoreborn.nheko\meta - copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\com.mujx.nheko\meta
- copy %BUILD%\deploy\installer\cleanup\package.xml installer\packages\io.github.nhekoreborn.nheko.cleanup\meta - copy %BUILD%\deploy\installer\cleanup\package.xml installer\packages\com.mujx.nheko.cleanup\meta
- copy %BUILD%\deploy\installer\cleanup\installscript.qs installer\packages\io.github.nhekoreborn.nheko.cleanup\meta - copy %BUILD%\deploy\installer\cleanup\installscript.qs installer\packages\com.mujx.nheko.cleanup\meta
# Amend version and date # Amend version and date
- sed -i "s/__VERSION__/0.7.2/" installer\config\config.xml - sed -i "s/__VERSION__/0.6.4/" installer\config\config.xml
- sed -i "s/__VERSION__/0.7.2/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml - sed -i "s/__VERSION__/0.6.4/" installer\packages\com.mujx.nheko\meta\package.xml
- sed -i "s/__VERSION__/0.7.2/" installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\package.xml - sed -i "s/__VERSION__/0.6.4/" installer\packages\com.mujx.nheko.cleanup\meta\package.xml
- sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml - sed -i "s/__DATE__/%DATE%/" installer\packages\com.mujx.nheko\meta\package.xml
- sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\package.xml - sed -i "s/__DATE__/%DATE%/" installer\packages\com.mujx.nheko.cleanup\meta\package.xml
# Copy nheko data # Copy nheko data
- xcopy NhekoData\*.* installer\packages\io.github.nhekoreborn.nheko\data\*.* /s /e /c /y - xcopy NhekoData\*.* installer\packages\com.mujx.nheko\data\*.* /s /e /c /y
- move NhekoRelease\nheko.exe installer\packages\io.github.nhekoreborn.nheko\data - move NhekoRelease\nheko.exe installer\packages\com.mujx.nheko\data
- mkdir tools - mkdir tools
- curl -L -O https://download.qt.io/official_releases/qt-installer-framework/3.0.4/QtInstallerFramework-win-x86.exe - curl -L -O https://download.qt.io/official_releases/qt-installer-framework/3.0.4/QtInstallerFramework-win-x86.exe
- 7z x QtInstallerFramework-win-x86.exe -otools -aoa - 7z x QtInstallerFramework-win-x86.exe -otools -aoa

@ -1,5 +1,5 @@
hunter_config( hunter_config(
Boost Boost
VERSION "1.70.0-p1" VERSION "1.70.0-p0"
CMAKE_ARGS IOSTREAMS_NO_BZIP2=1 CMAKE_ARGS IOSTREAMS_NO_BZIP2=1
) )

@ -133,14 +133,10 @@ function(hunter_gate_self root version sha1 result)
string(SUBSTRING "${sha1}" 0 7 archive_id) string(SUBSTRING "${sha1}" 0 7 archive_id)
if(EXISTS "${root}/cmake/Hunter") set(
set(hunter_self "${root}") hunter_self
else() "${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked"
set( )
hunter_self
"${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked"
)
endif()
set("${result}" "${hunter_self}" PARENT_SCOPE) set("${result}" "${hunter_self}" PARENT_SCOPE)
endfunction() endfunction()
@ -494,44 +490,37 @@ macro(HunterGate)
) )
set(_master_location "${_hunter_self}/cmake/Hunter") set(_master_location "${_hunter_self}/cmake/Hunter")
if(EXISTS "${HUNTER_GATE_ROOT}/cmake/Hunter") get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE)
# Hunter downloaded manually (e.g. by 'git clone') set(_done_location "${_archive_id_location}/DONE")
set(_unused "xxxxxxxxxx") set(_sha1_location "${_archive_id_location}/SHA1")
set(HUNTER_GATE_SHA1 "${_unused}")
set(HUNTER_GATE_VERSION "${_unused}")
else()
get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE)
set(_done_location "${_archive_id_location}/DONE")
set(_sha1_location "${_archive_id_location}/SHA1")
# Check Hunter already downloaded by HunterGate
if(NOT EXISTS "${_done_location}")
hunter_gate_download("${_archive_id_location}")
endif()
if(NOT EXISTS "${_done_location}") # Check Hunter already downloaded by HunterGate
hunter_gate_internal_error("hunter_gate_download failed") if(NOT EXISTS "${_done_location}")
endif() hunter_gate_download("${_archive_id_location}")
endif()
if(NOT EXISTS "${_sha1_location}") if(NOT EXISTS "${_done_location}")
hunter_gate_internal_error("${_sha1_location} not found") hunter_gate_internal_error("hunter_gate_download failed")
endif() endif()
file(READ "${_sha1_location}" _sha1_value)
string(COMPARE EQUAL "${_sha1_value}" "${HUNTER_GATE_SHA1}" _is_equal) if(NOT EXISTS "${_sha1_location}")
if(NOT _is_equal) hunter_gate_internal_error("${_sha1_location} not found")
hunter_gate_internal_error( endif()
"Short SHA1 collision:" file(READ "${_sha1_location}" _sha1_value)
" ${_sha1_value} (from ${_sha1_location})" string(COMPARE EQUAL "${_sha1_value}" "${HUNTER_GATE_SHA1}" _is_equal)
" ${HUNTER_GATE_SHA1} (HunterGate)" if(NOT _is_equal)
) hunter_gate_internal_error(
endif() "Short SHA1 collision:"
if(NOT EXISTS "${_master_location}") " ${_sha1_value} (from ${_sha1_location})"
hunter_gate_user_error( " ${HUNTER_GATE_SHA1} (HunterGate)"
"Master file not found:" )
" ${_master_location}" endif()
"try to update Hunter/HunterGate" if(NOT EXISTS "${_master_location}")
) hunter_gate_user_error(
endif() "Master file not found:"
" ${_master_location}"
"try to update Hunter/HunterGate"
)
endif() endif()
include("${_master_location}") include("${_master_location}")
set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES)

@ -21,7 +21,7 @@ if(NOT EXISTS ${_qrc})
endif() endif()
qt5_add_resources(LANG_QRC ${_qrc}) qt5_add_resources(LANG_QRC ${_qrc})
if(Qt5QuickCompiler_FOUND AND COMPILE_QML) if(Qt5QuickCompiler_FOUND)
qtquick_compiler_add_resources(QRC resources/res.qrc) qtquick_compiler_add_resources(QRC resources/res.qrc)
else() else()
qt5_add_resources(QRC resources/res.qrc) qt5_add_resources(QRC resources/res.qrc)

@ -1,7 +1,7 @@
{ {
"id": "io.github.NhekoReborn.Nheko", "id": "io.github.NhekoReborn.Nheko",
"command": "nheko", "command": "nheko",
"branch": "master", "branch": "0.7.0-dev",
"runtime": "org.kde.Platform", "runtime": "org.kde.Platform",
"runtime-version": "5.14", "runtime-version": "5.14",
"sdk": "org.kde.Sdk", "sdk": "org.kde.Sdk",
@ -73,9 +73,9 @@
], ],
"sources": [ "sources": [
{ {
"sha256": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb", "sha256": "3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19",
"type": "archive", "type": "archive",
"url": "https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz" "url": "https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz"
} }
] ]
}, },
@ -131,7 +131,7 @@
{ {
"sha256": "59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722", "sha256": "59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722",
"type": "archive", "type": "archive",
"url": "https://sourceforge.net/projects/boost/files/boost/1.72.0/boost_1_72_0.tar.bz2" "url": "https://dl.bintray.com/boostorg/release/1.72.0/source/boost_1_72_0.tar.bz2"
} }
] ]
}, },
@ -146,9 +146,9 @@
"name": "mtxclient", "name": "mtxclient",
"sources": [ "sources": [
{ {
"commit": "ad5575bc24089dc385e97d9ace026414b618775c", "sha256": "d77eab1a8af98f185194ee6dcd94f4c9dff84cb6a5692394318a78e632752a81",
"type": "git", "type": "archive",
"url": "https://github.com/Nheko-Reborn/mtxclient.git" "url": "https://github.com/Nheko-Reborn/mtxclient/archive/c1ccd6c6cdaead3ff1c2bf336b719ca45fee2d33.tar.gz"
} }
] ]
}, },
@ -171,8 +171,7 @@
{ {
"config-opts": [ "config-opts": [
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_BUILD_TYPE=Release",
"-DLMDBXX_INCLUDE_DIR=.deps/lmdbxx", "-DLMDBXX_INCLUDE_DIR=.deps/lmdbxx"
"-DCOMPILE_QML=ON"
], ],
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"name": "nheko", "name": "nheko",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,5 +0,0 @@
The below media files were obtained from https://github.com/matrix-org/matrix-react-sdk/tree/develop/res/media
callend.ogg
ringback.ogg
ring.ogg

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,67 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 mujx, nheko reborn developers --> <!-- Copyright 2019 mujx, nheko reborn developers -->
<component type="desktop"> <component type="desktop">
<id>nheko.desktop</id> <id>nheko.desktop</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later and CC-BY</project_license> <project_license>GPL-3.0-or-later and CC-BY</project_license>
<name>nheko</name> <name>nheko</name>
<summary>Desktop client for the Matrix protocol</summary> <summary>Desktop client for the Matrix protocol</summary>
<description> <description>
<p>The motivation behind the project is to provide a native <p>The motivation behind the project is to provide a native
desktop app for Matrix that feels more like a mainstream desktop app for Matrix that feels more like a mainstream
chat app.</p> chat app.</p>
</description> </description>
<translation/> <translation/>
<languages> <languages>
<lang>de</lang> <lang>de</lang>
<lang>el</lang> <lang>el</lang>
<lang>en</lang> <lang>en</lang>
<lang>fr</lang> <lang>fr</lang>
<lang>nl</lang> <lang>nl</lang>
<lang>pl</lang> <lang>pl</lang>
<lang>ru</lang> <lang>ru</lang>
<lang>zh_CN</lang> <lang>zh_CN</lang>
</languages> </languages>
<content_rating type="oars-1.0"> <content_rating type="oars-1.0">
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute> <content_attribute id="social-audio">intense</content_attribute>
</content_rating> </content_rating>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://nheko-reborn.github.io/images/screenshots/chat-resized.png</image> <image>https://nheko-reborn.github.io/images/screenshots/chat-resized.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://nheko-reborn.github.io/images/screenshots/Start-resized.png</image> <image>https://nheko-reborn.github.io/images/screenshots/Start-resized.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://nheko-reborn.github.io/images/screenshots/settings-resized.png</image> <image>https://nheko-reborn.github.io/images/screenshots/settings-resized.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://nheko-reborn.github.io/images/screenshots/login-resized.png</image> <image>https://nheko-reborn.github.io/images/screenshots/login-resized.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<url type="homepage">https://github.com/Nheko-Reborn/nheko</url> <url type="homepage">https://github.com/Nheko-Reborn/nheko</url>
<update_contact>https://github.com/Nheko-Reborn</update_contact> <update_contact>https://github.com/Nheko-Reborn</update_contact>
<releases> <releases>
<release date="2020-06-12" version="0.7.2"/> <release version="0.6.4" date="2019-05-22" />
<release date="2020-04-24" version="0.7.1"/> <release version="0.6.3" date="2019-02-08" />
<release date="2020-04-19" version="0.7.0"/> <release version="0.6.2" date="2018-10-07" />
<release date="2019-05-22" version="0.6.4"/> <release version="0.6.1" date="2018-09-26" />
<release date="2019-02-08" version="0.6.3"/> <release version="0.6.0" date="2018-09-21" />
<release date="2018-10-07" version="0.6.2"/> <release version="0.5.5" date="2018-09-01" />
<release date="2018-09-26" version="0.6.1"/> <release version="0.5.4" date="2018-08-21" />
<release date="2018-09-21" version="0.6.0"/> <release version="0.5.3" date="2018-08-12" />
<release date="2018-09-01" version="0.5.5"/> <release version="0.5.2" date="2018-07-28" />
<release date="2018-08-21" version="0.5.4"/> </releases>
<release date="2018-08-12" version="0.5.3"/>
<release date="2018-07-28" version="0.5.2"/>
</releases>
<developer_name>Nheko Reborn</developer_name>
<url type="bugtracker">https://github.com/Nheko-Reborn/nheko/issues</url>
<url type="help">https://github.com/Nheko-Reborn/nheko/</url>
<url type="translate">https://weblate.nheko.im/projects/nheko/</url>
</component> </component>

@ -1,113 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import im.nheko 1.0
Rectangle {
id: activeCallBar
visible: TimelineManager.callState != WebRTCState.DISCONNECTED
color: "#2ECC71"
implicitHeight: rowLayout.height + 8
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
Avatar {
width: avatarSize
height: avatarSize
url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: TimelineManager.callPartyName
}
Label {
font.pointSize: fontMetrics.font.pointSize * 1.1
text: " " + TimelineManager.callPartyName + " "
}
Image {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
source: "qrc:/icons/icons/ui/place-call.png"
}
Label {
id: callStateLabel
font.pointSize: fontMetrics.font.pointSize * 1.1
}
Connections {
function onCallStateChanged(state) {
switch (state) {
case WebRTCState.INITIATING:
callStateLabel.text = qsTr("Initiating...");
break;
case WebRTCState.OFFERSENT:
callStateLabel.text = qsTr("Calling...");
break;
case WebRTCState.CONNECTING:
callStateLabel.text = qsTr("Connecting...");
break;
case WebRTCState.CONNECTED:
callStateLabel.text = "00:00";
var d = new Date();
callTimer.startTime = Math.floor(d.getTime() / 1000);
break;
case WebRTCState.DISCONNECTED:
callStateLabel.text = "";
}
}
target: TimelineManager
}
Timer {
id: callTimer
property int startTime
function pad(n) {
return (n < 10) ? ("0" + n) : n;
}
interval: 1000
running: TimelineManager.callState == WebRTCState.CONNECTED
repeat: true
onTriggered: {
var d = new Date();
let seconds = Math.floor(d.getTime() / 1000 - startTime);
let s = Math.floor(seconds % 60);
let m = Math.floor(seconds / 60) % 60;
let h = Math.floor(seconds / 3600);
callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s);
}
}
Item {
Layout.fillWidth: true
}
ImageButton {
width: 24
height: 24
buttonTextColor: "#000000"
image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
onClicked: TimelineManager.toggleMicMute()
}
Item {
implicitWidth: 16
}
}
}

@ -1,75 +1,46 @@
import QtGraphicalEffects 1.0
import QtQuick 2.6 import QtQuick 2.6
import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0
import im.nheko 1.0
Rectangle { Rectangle {
id: avatar id: avatar
width: 48
property alias url: img.source height: 48
property string userid radius: settings.avatar_circles ? height/2 : 3
property string displayName
property alias url: img.source
width: 48 property string displayName
height: 48
radius: Settings.avatarCircles ? height / 2 : 3 Text {
color: colors.base anchors.fill: parent
text: chat.model.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0)))
Label { textFormat: Text.RichText
anchors.fill: parent color: colors.text
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") font.pixelSize: avatar.height/2
textFormat: Text.RichText verticalAlignment: Text.AlignVCenter
font.pixelSize: avatar.height / 2 horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter visible: img.status != Image.Ready
horizontalAlignment: Text.AlignHCenter }
visible: img.status != Image.Ready
color: colors.text Image {
} id: img
anchors.fill: parent
Image { asynchronous: true
id: img fillMode: Image.PreserveAspectCrop
mipmap: true
anchors.fill: parent smooth: false
asynchronous: true
fillMode: Image.PreserveAspectCrop sourceSize.width: avatar.width
mipmap: true sourceSize.height: avatar.height
smooth: false
sourceSize.width: avatar.width layer.enabled: true
sourceSize.height: avatar.height layer.effect: OpacityMask {
layer.enabled: true maskSource: Rectangle {
anchors.fill: parent
layer.effect: OpacityMask { width: avatar.width
height: avatar.height
maskSource: Rectangle { radius: settings.avatar_circles ? height/2 : 3
anchors.fill: parent }
width: avatar.width }
height: avatar.height }
radius: Settings.avatarCircles ? height / 2 : 3 color: colors.base
}
}
}
Rectangle {
anchors.bottom: avatar.bottom
anchors.right: avatar.right
visible: !!userid
height: avatar.height / 6
width: height
radius: Settings.avatarCircles ? height / 2 : height / 4
color: {
switch (TimelineManager.userPresence(userid)) {
case "online":
return "#00cc66";
case "unavailable":
return "#ff9933";
case "offline":
default:
// return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled
"transparent";
}
}
}
} }

@ -3,42 +3,24 @@ import QtQuick.Controls 2.1
import im.nheko 1.0 import im.nheko 1.0
Rectangle { Rectangle {
id: indicator id: indicator
color: "transparent"
property bool encrypted: false width: 16
height: 16
function getEncryptionImage() {
if (encrypted) ToolTip.visible: ma.containsMouse && indicator.visible
return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText; ToolTip.text: qsTr("Encrypted")
else
return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d"; MouseArea{
} id: ma
anchors.fill: parent
function getEncryptionTooltip() { hoverEnabled: true
if (encrypted) }
return qsTr("Encrypted");
else Image {
return qsTr("This message is not encrypted!"); id: stateImg
} anchors.fill: parent
source: "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText
color: "transparent" }
width: 16
height: 16
ToolTip.visible: ma.containsMouse && indicator.visible
ToolTip.text: getEncryptionTooltip()
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
}
Image {
id: stateImg
anchors.fill: parent
source: getEncryptionImage()
}
} }

@ -1,30 +1,29 @@
import QtQuick 2.3 import QtQuick 2.3
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
AbstractButton { Button {
id: button property string image: undefined
property string image: undefined id: button
property color highlightColor: colors.highlight
property color buttonTextColor: colors.buttonText flat: true
width: 16 // disable background, because we don't want a border on hover
height: 16 background: Item {
}
Image {
id: buttonImg Image {
id: buttonImg
// Workaround, can't get icon.source working for now... // Workaround, can't get icon.source working for now...
anchors.fill: parent anchors.fill: parent
source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText)
} }
MouseArea { MouseArea
id: mouseArea {
id: mouseArea
anchors.fill: parent anchors.fill: parent
onPressed: mouse.accepted = false onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
} }

@ -1,37 +1,32 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import im.nheko 1.0
TextEdit { TextEdit {
textFormat: TextEdit.RichText textFormat: TextEdit.RichText
readOnly: true readOnly: true
wrapMode: Text.Wrap wrapMode: Text.Wrap
selectByMouse: true selectByMouse: true
activeFocusOnPress: false color: colors.text
color: colors.text
onLinkActivated: {
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) {
chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]);
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) {
TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]);
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link);
TimelineManager.setHistoryView(match[1]);
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain);
} else {
TimelineManager.openLink(link);
}
}
ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink
MouseArea { onLinkActivated: {
id: ma if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1])
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
anchors.fill: parent else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
propagateComposedEvents: true var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link)
acceptedButtons: Qt.NoButton timelineManager.setHistoryView(match[1])
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain)
} }
else Qt.openUrlExternally(link)
}
MouseArea
{
id: ma
anchors.fill: parent
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink
} }

@ -1,95 +0,0 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import im.nheko 1.0
// This class is for showing Reactions in the timeline row, not for
// adding new reactions via the emoji picker
Flow {
id: reactionFlow
// highlight colors for selfReactedEvent background
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
property string eventId
property alias reactions: repeater.model
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
Repeater {
id: repeater
delegate: AbstractButton {
id: reaction
hoverEnabled: true
implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding * 2
implicitHeight: contentItem.childrenRect.height
ToolTip.visible: hovered
ToolTip.text: modelData.users
onClicked: {
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent);
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key);
}
contentItem: Row {
anchors.centerIn: parent
spacing: reactionText.implicitHeight / 4
leftPadding: reactionText.implicitHeight / 2
rightPadding: reactionText.implicitHeight / 2
TextMetrics {
id: textMetrics
font.family: Settings.emojiFont
elide: Text.ElideRight
elideWidth: 150
text: modelData.key
}
Text {
id: reactionText
anchors.baseline: reactionCounter.baseline
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…")
font.family: Settings.emojiFont
color: reaction.hovered ? colors.highlight : colors.text
maximumLineCount: 1
}
Rectangle {
id: divider
height: Math.floor(reactionCounter.implicitHeight * 1.4)
width: 1
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text
}
Text {
id: reactionCounter
anchors.verticalCenter: divider.verticalCenter
text: modelData.count
font: reaction.font
color: reaction.hovered ? colors.highlight : colors.text
}
}
background: Rectangle {
anchors.centerIn: parent
implicitWidth: reaction.implicitWidth
implicitHeight: reaction.implicitHeight
border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text
color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.base
border.width: 1
radius: reaction.height / 2
}
}
}
}

@ -1,108 +0,0 @@
/*
* Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net>
* Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com>
*
* 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 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*
* Shamelessly stolen from:
* https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml
*
* The MouseArea + interactive: false + maximumFlickVelocity are required
* to fix scrolling for desktop systems where we don't want flicking behaviour.
*
* See also:
* ScrollView.qml in qtquickcontrols
* qquickwheelarea.cpp in qtquickcontrols
*/
import QtQuick 2.9
import QtQuick.Controls 2.3
MouseArea {
// console.warn("Delta: ", wheel.pixelDelta.y);
// console.warn("Old position: ", flickable.contentY);
// console.warn("New position: ", newPos);
id: root
property Flickable flickable
property alias enabled: root.enabled
function calculateNewPosition(flickableItem, wheel) {
//Nothing to scroll
if (flickableItem.contentHeight < flickableItem.height)
return flickableItem.contentY;
//Ignore 0 events (happens at least with Christians trackpad)
if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0)
return flickableItem.contentY;
//pixelDelta seems to be the same as angleDelta/8
var pixelDelta = 0;
//The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta
if (wheel.angleDelta.y) {
var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property
var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit
var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps.
pixelDelta = ticks * pixelPerLine * wheelScrollLines;
} else {
pixelDelta = wheel.pixelDelta.y;
}
pixelDelta = Math.round(pixelDelta);
if (!pixelDelta)
return flickableItem.contentY;
var minYExtent = flickableItem.originY + flickableItem.topMargin;
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
minYExtent += flickableItem.headerItem.height;
//Avoid overscrolling
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
}
propagateComposedEvents: true
//Place the mouse area under the flickable
z: -1
onFlickableChanged: {
if (enabled) {
flickable.maximumFlickVelocity = 100000;
flickable.boundsBehavior = Flickable.StopAtBounds;
root.parent = flickable;
}
}
acceptedButtons: Qt.NoButton
onWheel: {
var newPos = calculateNewPosition(flickable, wheel);
// Show the scrollbars
flickable.flick(0, 0);
flickable.contentY = newPos;
cancelFlickStateTimer.start();
}
Timer {
id: cancelFlickStateTimer
//How long the scrollbar will remain visible
interval: 500
// Hide the scrollbars
onTriggered: {
flickable.cancelFlick();
flickable.movementEnded();
}
}
}

@ -3,55 +3,37 @@ import QtQuick.Controls 2.1
import im.nheko 1.0 import im.nheko 1.0
Rectangle { Rectangle {
id: indicator id: indicator
property int state: 0
property int state: 0 color: "transparent"
width: 16
color: "transparent" height: 16
width: 16
height: 16 ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty
ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty ToolTip.text: switch (state) {
ToolTip.text: { case MtxEvent.Failed: return qsTr("Failed")
switch (state) { case MtxEvent.Sent: return qsTr("Sent")
case MtxEvent.Failed: case MtxEvent.Received: return qsTr("Received")
return qsTr("Failed"); case MtxEvent.Read: return qsTr("Read")
case MtxEvent.Sent: default: return ""
return qsTr("Sent"); }
case MtxEvent.Received: MouseArea{
return qsTr("Received"); id: ma
case MtxEvent.Read: anchors.fill: parent
return qsTr("Read"); hoverEnabled: true
default: }
return "";
} Image {
} id: stateImg
// Workaround, can't get icon.source working for now...
MouseArea { anchors.fill: parent
id: ma source: switch (indicator.state) {
case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText
anchors.fill: parent case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText
hoverEnabled: true case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText
} case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText
default: return ""
Image { }
id: stateImg }
// Workaround, can't get icon.source working for now...
anchors.fill: parent
source: {
switch (indicator.state) {
case MtxEvent.Failed:
return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText;
case MtxEvent.Sent:
return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText;
case MtxEvent.Received:
return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText;
case MtxEvent.Read:
return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText;
default:
return "";
}
}
}
} }

@ -1,148 +1,120 @@
import "./delegates"
import "./emoji"
import QtQuick 2.6 import QtQuick 2.6
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import im.nheko 1.0 import im.nheko 1.0
Item { import "./delegates"
anchors.left: parent.left
anchors.right: parent.right
height: row.height
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
preventStealing: true
hoverEnabled: true
acceptedButtons: Qt.AllButtons
onClicked: {
if (mouse.button === Qt.RightButton)
messageContextMenu.show(model.id, model.type, model.isEncrypted, row);
}
onPressAndHold: {
messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y));
}
}
Rectangle {
color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent"
anchors.fill: row
}
RowLayout {
id: row
anchors.leftMargin: avatarSize + 16
anchors.left: parent.left
anchors.right: parent.right
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 4
// fancy reply, if this is a reply
Reply {
visible: model.replyTo
modelData: chat.model.getDump(model.replyTo, model.id)
userColor: TimelineManager.userColor(modelData.userId, colors.window)
}
// actual message content
MessageDelegate {
id: contentItem
width: parent.width
modelData: model
}
Reactions {
id: reactionRow
reactions: model.reactions
eventId: model.id
}
}
StatusIndicator {
state: model.state
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
}
EncryptionIndicator {
visible: model.isRoomEncrypted
encrypted: model.isEncrypted
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
}
EmojiButton {
id: reactButton
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
emojiPicker: emojiPopup
event_id: model.id
}
ImageButton {
id: replyButton
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/mail-reply.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Reply")
onClicked: chat.model.replyAction(model.id)
}
ImageButton {
id: optionsButton
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Options")
onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton)
}
Label {
Layout.alignment: Qt.AlignRight | Qt.AlignTop
text: model.timestamp.toLocaleTimeString("HH:mm")
width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
color: inactiveColors.text
ToolTip.visible: ma.containsMouse
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
}
}
MouseArea {
anchors.left: parent.left
anchors.right: parent.right
height: row.height
propagateComposedEvents: true
preventStealing: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton)
messageContextMenu.show(model.id, model.type, row)
}
onPressAndHold: {
if (mouse.source === Qt.MouseEventNotSynthesized)
messageContextMenu.show(model.id, model.type, row)
}
RowLayout {
id: row
anchors.leftMargin: avatarSize + 4
anchors.left: parent.left
anchors.right: parent.right
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 4
// fancy reply, if this is a reply
Reply {
visible: model.replyTo
modelData: chat.model.getDump(model.replyTo)
userColor: timelineManager.userColor(modelData.userId, colors.window)
}
// actual message content
MessageDelegate {
id: contentItem
width: parent.width
modelData: model
}
}
ImageButton {
visible: timelineSettings.buttons
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
id: replyButton
hoverEnabled: true
image: ":/icons/icons/ui/mail-reply.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Reply")
onClicked: chat.model.replyAction(model.id)
}
ImageButton {
visible: timelineSettings.buttons
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
id: optionsButton
hoverEnabled: true
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Options")
onClicked: messageContextMenu.show(model.id, model.type, optionsButton)
}
StatusIndicator {
state: model.state
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
}
EncryptionIndicator {
visible: model.isEncrypted
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
}
Text {
Layout.alignment: Qt.AlignRight | Qt.AlignTop
text: model.timestamp.toLocaleTimeString("HH:mm")
color: inactiveColors.text
MouseArea{
id: ma
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
ToolTip.visible: ma.containsMouse
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
}
}
} }

@ -1,582 +1,328 @@
import "./delegates"
import "./device-verification"
import "./emoji"
import QtGraphicalEffects 1.0
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import im.nheko 1.0 import Qt.labs.settings 1.0
import im.nheko.EmojiModel 1.0
Page {
id: timelineRoot
property var colors: currentActivePalette
property var systemInactive
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
property int avatarSize: 40
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
palette: colors
FontMetrics {
id: fontMetrics
}
EmojiPicker {
id: emojiPopup
width: 7 * 52 + 20
height: 6 * 52
colors: palette
model: EmojiProxyModel {
category: EmojiCategory.People
sourceModel: EmojiModel {
}
}
}
Menu {
id: messageContextMenu
property string eventId
property int eventType
property bool isEncrypted
function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
if (position)
popup(position, showAt_);
else
popup(showAt_);
}
modal: true
MenuItem {
text: qsTr("React")
onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Reply")
onClicked: chat.model.replyAction(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Read receipts")
onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Mark as read")
}
MenuItem {
text: qsTr("View raw message")
onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
}
MenuItem {
visible: messageContextMenu.isEncrypted
height: visible ? implicitHeight : 0
text: qsTr("View decrypted raw message")
onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Redact message")
onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
}
MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
height: visible ? implicitHeight : 0
text: qsTr("Save as")
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
}
Rectangle {
anchors.fill: parent
color: colors.window
Component {
id: deviceVerificationDialog
DeviceVerification {
}
}
Connections {
function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) {
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
"flow": flow
});
dialog.show();
}
target: TimelineManager
}
Connections {
function onOpenProfile(profile) {
var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile
});
userProfile.show();
}
target: TimelineManager.timeline
}
Label {
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24
color: colors.text
}
BusyIndicator {
visible: running
anchors.centerIn: parent
running: TimelineManager.isInitialSync
height: 200
width: 200
z: 3
}
ColumnLayout {
anchors.fill: parent
Rectangle {
id: topBar
Layout.fillWidth: true
implicitHeight: topLayout.height + 16
z: 3
color: colors.base
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
GridLayout {
//Layout.margins: 8
id: topLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
anchors.verticalCenter: parent.verticalCenter
ImageButton {
id: backToRoomsButton
Layout.column: 0
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
visible: TimelineManager.isNarrowView
image: ":/icons/icons/ui/angle-pointing-to-left.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Back to room list")
onClicked: TimelineManager.backToRooms()
}
Avatar {
Layout.column: 1
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
width: avatarSize
height: avatarSize
url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
displayName: chat.model ? chat.model.roomName : qsTr("No room selected")
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
}
Label {
Layout.fillWidth: true
Layout.column: 2
Layout.row: 0
color: colors.text
font.pointSize: fontMetrics.font.pointSize * 1.1
text: chat.model ? chat.model.roomName : qsTr("No room selected")
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
}
MatrixText {
Layout.fillWidth: true
Layout.column: 2
Layout.row: 1
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
clip: true
text: chat.model ? chat.model.roomTopic : ""
}
ImageButton {
id: roomOptionsButton
Layout.column: 3
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Room options")
onClicked: roomOptionsMenu.popup(roomOptionsButton)
Menu {
id: roomOptionsMenu
MenuItem {
text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsersDialog()
}
MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openMemberListDialog()
}
MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog()
}
MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings()
}
}
}
}
}
ListView {
id: chat
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
visible: TimelineManager.timeline != null
cacheBuffer: 400
Layout.fillWidth: true
Layout.fillHeight: true
model: TimelineManager.timeline
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
spacing: 4
verticalLayoutDirection: ListView.BottomToTop
onCountChanged: {
if (atYEnd)
model.currentIndex = 0;
} // Mark last event as read, since we are at the bottom
ScrollHelper {
flickable: parent
anchors.fill: parent
}
Shortcut {
sequence: StandardKey.MoveToPreviousPage
onActivated: {
chat.contentY = chat.contentY - chat.height / 2;
chat.returnToBounds();
}
}
Shortcut {
sequence: StandardKey.MoveToNextPage
onActivated: {
chat.contentY = chat.contentY + chat.height / 2;
chat.returnToBounds();
}
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: chat.model.reply = undefined
}
Shortcut {
sequence: "Alt+Up"
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
}
Shortcut {
sequence: "Alt+Down"
onActivated: {
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
}
}
Component {
id: userProfileComponent
UserProfile {
}
}
section {
property: "section"
}
Component {
id: sectionHeader
Column {
property var modelData
property string section
property string nextSection
topPadding: 4
bottomPadding: 4
spacing: 8
visible: !!modelData
width: parent.width
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: section.includes(" ")
text: chat.model.formatDateSeparator(modelData.timestamp)
color: colors.text
height: fontMetrics.height * 1.4
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
background: Rectangle {
radius: parent.height / 2
color: colors.base
}
}
Row {
height: userName.height
spacing: 8
Avatar {
width: avatarSize
height: avatarSize
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
displayName: modelData.userName
userid: modelData.userId
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(modelData.userId)
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
Label {
id: userName
text: TimelineManager.escapeEmoji(modelData.userName)
color: TimelineManager.userColor(modelData.userId, colors.window)
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
Layout.alignment: Qt.AlignHCenter
onClicked: chat.model.openUserProfile(modelData.userId)
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
}
delegate: Item {
id: wrapper
// This would normally be previousSection, but our model's order is inverted.
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
property Item section
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
width: chat.delegateMaxWidth
height: section ? section.height + timelinerow.height : timelinerow.height
onSectionBoundaryChanged: {
if (sectionBoundary) {
var properties = {
"modelData": model.dump,
"section": ListView.section,
"nextSection": ListView.nextSection
};
section = sectionHeader.createObject(wrapper, properties);
} else {
section.destroy();
section = null;
}
}
TimelineRow {
id: timelinerow
y: section ? section.y + section.height : 0
}
Connections {
function onMovementEnded() {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
}
target: chat
}
}
footer: BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: chat.model && chat.model.paginationInProgress
height: 50
width: 50
z: 3
}
}
Item {
id: chatFooter
implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height)
Layout.fillWidth: true
z: 3
Column {
id: footerContent
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Rectangle {
id: typingRect
anchors.left: parent.left
anchors.right: parent.right
color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent"
height: typingDisplay.height
Label {
id: typingDisplay
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
color: colors.text
text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : ""
textFormat: Text.RichText
}
}
Rectangle {
id: replyPopup
anchors.left: parent.left
anchors.right: parent.right
visible: chat.model && chat.model.reply
// Height of child, plus margins, plus border
height: replyPreview.height + 10
color: colors.base
Reply {
id: replyPreview
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: closeReplyButton.left
anchors.rightMargin: 20
anchors.bottom: parent.bottom
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {
}
userColor: TimelineManager.userColor(modelData.userId, colors.window)
}
ImageButton {
id: closeReplyButton
anchors.right: parent.right
anchors.rightMargin: 15
anchors.top: replyPreview.top
hoverEnabled: true
width: 16
height: 16
image: ":/icons/icons/ui/remove-symbol.png"
ToolTip.visible: closeReplyButton.hovered
ToolTip.text: qsTr("Close")
onClicked: chat.model.reply = undefined
}
} import im.nheko 1.0
}
}
ActiveCallBar {
Layout.fillWidth: true
z: 3
}
}
} import "./delegates"
systemInactive: SystemPalette {
colorGroup: SystemPalette.Disabled
}
Item {
property var colors: currentActivePalette
property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled }
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
property int avatarSize: 40
Settings {
id: settings
category: "user"
property bool avatar_circles: true
}
Settings {
id: timelineSettings
category: "user/timeline"
property bool buttons: true
}
Menu {
id: messageContextMenu
palette: colors
modal: true
function show(eventId_, eventType_, showAt) {
eventId = eventId_
eventType = eventType_
popup(showAt)
}
property string eventId
property int eventType
MenuItem {
text: qsTr("Reply")
onClicked: chat.model.replyAction(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Read receipts")
onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Mark as read")
}
MenuItem {
text: qsTr("View raw message")
onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Redact message")
onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
}
MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
text: qsTr("Save as")
onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
}
id: timelineRoot
Rectangle {
anchors.fill: parent
color: colors.window
Text {
visible: !timelineManager.timeline && !timelineManager.isInitialSync
anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24
color: colors.windowText
}
BusyIndicator {
anchors.centerIn: parent
running: timelineManager.isInitialSync
height: 200
width: 200
z: 3
}
ListView {
id: chat
visible: timelineManager.timeline != null
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: chatFooter.top
anchors.leftMargin: 4
anchors.rightMargin: scrollbar.width
model: timelineManager.timeline
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
onWheel: {
if (wheel.angleDelta != 0) {
chat.contentY = chat.contentY - wheel.angleDelta.y
wheel.accepted = true
chat.returnToBounds()
}
}
}
Shortcut {
sequence: StandardKey.MoveToPreviousPage
onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); }
}
Shortcut {
sequence: StandardKey.MoveToNextPage
onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); }
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
parent: chat.parent
anchors.top: chat.top
anchors.left: chat.right
anchors.bottom: chat.bottom
}
spacing: 4
verticalLayoutDirection: ListView.BottomToTop
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom
delegate: Rectangle {
// This would normally be previousSection, but our model's order is inverted.
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
id: wrapper
property Item section
width: chat.width
height: section ? section.height + timelinerow.height : timelinerow.height
color: "transparent"
TimelineRow {
id: timelinerow
y: section ? section.y + section.height : 0
}
onSectionBoundaryChanged: {
if (sectionBoundary) {
var properties = {
'modelData': model.dump,
'section': ListView.section,
'nextSection': ListView.nextSection
}
section = sectionHeader.createObject(wrapper, properties)
} else {
section.destroy()
section = null
}
}
Binding {
target: chat.model
property: "currentIndex"
when: y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height
value: index
delayed: true
}
}
section {
property: "section"
}
Component {
id: sectionHeader
Column {
property var modelData
property string section
property string nextSection
topPadding: 4
bottomPadding: 4
spacing: 8
visible: !!modelData
width: parent.width
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: section.includes(" ")
text: chat.model.formatDateSeparator(modelData.timestamp)
color: colors.windowText
height: contentHeight * 1.2
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
radius: parent.height / 2
color: colors.base
}
}
Row {
height: userName.height
spacing: 4
Avatar {
width: avatarSize
height: avatarSize
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
displayName: modelData.userName
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(modelData.userId)
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
Text {
id: userName
text: chat.model.escapeEmoji(modelData.userName)
color: timelineManager.userColor(modelData.userId, colors.window)
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(section.split(" ")[0])
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
}
}
}
}
Rectangle {
id: chatFooter
height: Math.max(16, footerContent.height)
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 3
color: "transparent"
Column {
id: footerContent
anchors.left: parent.left
anchors.right: parent.right
Text {
id: typingDisplay
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 10
anchors.rightMargin: 10
text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : ""
textFormat: Text.RichText
color: colors.windowText
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
id: replyPopup
visible: timelineManager.replyingEvent && chat.model
// Height of child, plus margins, plus border
height: replyPreview.height + 10
color: colors.base
Reply {
id: replyPreview
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: closeReplyButton.left
anchors.rightMargin: 20
anchors.bottom: parent.bottom
modelData: chat.model ? chat.model.getDump(timelineManager.replyingEvent) : {}
userColor: timelineManager.userColor(modelData.userId, colors.window)
}
ImageButton {
id: closeReplyButton
anchors.right: parent.right
anchors.rightMargin: 15
anchors.top: replyPreview.top
hoverEnabled: true
width: 16
height: 16
image: ":/icons/icons/ui/remove-symbol.png"
ToolTip.visible: closeReplyButton.hovered
ToolTip.text: qsTr("Close")
onClicked: timelineManager.closeReply()
}
}
}
}
}
} }

@ -1,175 +0,0 @@
import "./device-verification"
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.3
import im.nheko 1.0
ApplicationWindow {
id: userProfileDialog
property var profile
height: 650
width: 420
minimumHeight: 420
palette: colors
Component {
id: deviceVerificationDialog
DeviceVerification {
}
}
ColumnLayout {
id: contentL
anchors.fill: parent
anchors.margins: 10
spacing: 10
Avatar {
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
height: 130
width: 130
displayName: profile.displayName
userid: profile.userid
Layout.alignment: Qt.AlignHCenter
}
Label {
text: profile.displayName
fontSizeMode: Text.HorizontalFit
font.pixelSize: 20
color: TimelineManager.userColor(profile.userid, colors.window)
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
MatrixText {
text: profile.userid
font.pixelSize: 15
Layout.alignment: Qt.AlignHCenter
}
Button {
id: verifyUserButton
text: qsTr("Verify")
Layout.alignment: Qt.AlignHCenter
enabled: !profile.isUserVerified
visible: !profile.isUserVerified
onClicked: profile.verify()
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 8
ImageButton {
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Ban the user")
onClicked: profile.banUser()
}
// ImageButton{
// image:":/icons/icons/ui/volume-off-indicator.png"
// Layout.margins: {
// left: 5
// right: 5
// }
// ToolTip.visible: hovered
// ToolTip.text: qsTr("Ignore messages from this user")
// onClicked : {
// profile.ignoreUser()
// }
// }
ImageButton {
image: ":/icons/icons/ui/black-bubble-speech.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Start a private chat")
onClicked: profile.startChat()
}
ImageButton {
image: ":/icons/icons/ui/round-remove-button.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Kick the user")
onClicked: profile.kickUser()
}
}
ListView {
id: devicelist
Layout.fillHeight: true
Layout.minimumHeight: 200
Layout.fillWidth: true
clip: true
spacing: 8
boundsBehavior: Flickable.StopAtBounds
model: profile.deviceList
delegate: RowLayout {
width: devicelist.width
spacing: 4
ColumnLayout {
spacing: 0
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight
font.bold: true
color: colors.text
text: model.deviceId
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
elide: Text.ElideRight
color: colors.text
text: model.deviceName
}
}
Image {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red"))
}
Button {
id: verifyButton
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify"
onClicked: {
if (model.verificationStatus == VerificationStatus.VERIFIED)
profile.unverify(model.deviceId);
else
profile.verify(model.deviceId);
}
}
}
}
}
footer: DialogButtonBox {
standardButtons: DialogButtonBox.Ok
onAccepted: userProfileDialog.close()
}
}

@ -1,75 +1,57 @@
import QtQuick 2.6 import QtQuick 2.6
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import im.nheko 1.0
Item {
height: row.height + 24
width: parent ? parent.width : undefined
RowLayout {
id: row
anchors.centerIn: parent
width: parent.width - 24
spacing: 15
Rectangle {
id: button
color: colors.light
radius: 22
height: 44
width: 44
Image {
id: img
anchors.centerIn: parent
source: "qrc:/icons/icons/ui/arrow-pointing-down.png"
fillMode: Image.Pad
}
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.timeline.saveMedia(model.data.id)
cursorShape: Qt.PointingHandCursor
}
}
ColumnLayout {
id: col
Text {
id: filename
Layout.fillWidth: true
text: model.data.filename
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
Text {
id: filesize
Layout.fillWidth: true
text: model.data.filesize
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
}
}
Rectangle {
color: colors.dark
z: -1
radius: 10
height: row.height + 24
width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth))
}
Rectangle {
radius: 10
color: colors.base
height: row.height + 24
width: parent ? parent.width : undefined
RowLayout {
id: row
anchors.centerIn: parent
width: parent.width - 24
spacing: 15
Rectangle {
id: button
color: colors.light
radius: 22
height: 44
width: 44
Image {
id: img
anchors.centerIn: parent
source: "qrc:/icons/icons/ui/arrow-pointing-down.png"
fillMode: Image.Pad
}
MouseArea {
anchors.fill: parent
onClicked: timelineManager.timeline.saveMedia(model.data.id)
cursorShape: Qt.PointingHandCursor
}
}
ColumnLayout {
id: col
Text {
Layout.fillWidth: true
text: model.data.body
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
Text {
Layout.fillWidth: true
text: model.data.filesize
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
}
}
} }

@ -1,70 +1,41 @@
import QtQuick 2.6 import QtQuick 2.6
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width)
property double tempHeight: tempWidth * model.data.proportionalHeight property double tempHeight: tempWidth * model.data.proportionalHeight
property double divisor: model.isReply ? 4 : 2
property bool tooHigh: tempHeight > timelineRoot.height / divisor
height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight)
width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth)
Image { property bool tooHigh: tempHeight > timelineRoot.height / 2
id: blurhash
anchors.fill: parent height: tooHigh ? timelineRoot.height / 2 : tempHeight
visible: img.status != Image.Ready width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth
source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText)
asynchronous: true
fillMode: Image.PreserveAspectFit
sourceSize.width: parent.width
sourceSize.height: parent.height
}
Image { Image {
id: img id: blurhash
anchors.fill: parent
visible: img.status != Image.Ready
anchors.fill: parent source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?"+colors.buttonText)
source: model.data.url.replace("mxc://", "image://MxcImage/") asynchronous: true
asynchronous: true fillMode: Image.PreserveAspectFit
fillMode: Image.PreserveAspectFit
MouseArea { sourceSize.width: parent.width
id: mouseArea sourceSize.height: parent.height
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready }
hoverEnabled: true
anchors.fill: parent
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id)
}
Item { Image {
id: overlay id: img
anchors.fill: parent
anchors.fill: parent
visible: mouseArea.containsMouse
Rectangle { source: model.data.url.replace("mxc://", "image://MxcImage/")
id: container asynchronous: true
fillMode: Image.PreserveAspectFit
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: colors.window
opacity: 0.75
}
Text { MouseArea {
id: imgcaption enabled: model.data.type == MtxEvent.ImageMessage
anchors.fill: parent
anchors.fill: container onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id)
elide: Text.ElideMiddle }
horizontalAlignment: Text.AlignHCenter }
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: model.data.filename ? model.data.filename : model.data.body
color: colors.text
}
}
}
} }

@ -2,334 +2,124 @@ import QtQuick 2.6
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
property alias modelData: model.data // Workaround to have an assignable global property
property alias isReply: model.isReply Item {
property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width id: model
property var data;
height: chooser.childrenRect.height }
// Workaround to have an assignable global property property alias modelData: model.data
Item {
id: model height: chooser.childrenRect.height
property var data DelegateChooser {
property bool isReply: false id: chooser
} //role: "type" //< not supported in our custom implementation, have to use roleValue
roleValue: model.data.type
DelegateChooser { anchors.fill: parent
id: chooser
DelegateChoice {
//role: "type" //< not supported in our custom implementation, have to use roleValue roleValue: MtxEvent.UnknownMessage
roleValue: model.data.type Placeholder { text: "Unretrieved event" }
anchors.fill: parent }
DelegateChoice {
DelegateChoice { roleValue: MtxEvent.TextMessage
roleValue: MtxEvent.UnknownMessage TextMessage {}
}
Placeholder { DelegateChoice {
text: "Unretrieved event" roleValue: MtxEvent.NoticeMessage
} NoticeMessage {}
}
} DelegateChoice {
roleValue: MtxEvent.EmoteMessage
DelegateChoice { NoticeMessage {
roleValue: MtxEvent.TextMessage formatted: chat.model.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
color: timelineManager.userColor(modelData.userId, colors.window)
TextMessage { }
} }
DelegateChoice {
} roleValue: MtxEvent.ImageMessage
ImageMessage {}
DelegateChoice { }
roleValue: MtxEvent.NoticeMessage DelegateChoice {
roleValue: MtxEvent.Sticker
NoticeMessage { ImageMessage {}
} }
DelegateChoice {
} roleValue: MtxEvent.FileMessage
FileMessage {}
DelegateChoice { }
roleValue: MtxEvent.EmoteMessage DelegateChoice {
roleValue: MtxEvent.VideoMessage
NoticeMessage { PlayableMediaMessage {}
formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody }
color: TimelineManager.userColor(modelData.userId, colors.window) DelegateChoice {
} roleValue: MtxEvent.AudioMessage
PlayableMediaMessage {}
} }
DelegateChoice {
DelegateChoice { roleValue: MtxEvent.Redacted
roleValue: MtxEvent.ImageMessage Pill {
text: qsTr("redacted")
ImageMessage { }
} }
DelegateChoice {
} roleValue: MtxEvent.Encryption
Pill {
DelegateChoice { text: qsTr("Encryption enabled")
roleValue: MtxEvent.Sticker }
}
ImageMessage { DelegateChoice {
} roleValue: MtxEvent.Name
NoticeMessage {
} text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
}
DelegateChoice { }
roleValue: MtxEvent.FileMessage DelegateChoice {
roleValue: MtxEvent.Topic
FileMessage { NoticeMessage {
} text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
}
} }
DelegateChoice {
DelegateChoice { roleValue: MtxEvent.RoomCreate
roleValue: MtxEvent.VideoMessage NoticeMessage {
text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId)
PlayableMediaMessage { }
} }
DelegateChoice {
} // TODO: make a more complex formatter for the power levels.
roleValue: MtxEvent.PowerLevels
DelegateChoice { NoticeMessage {
roleValue: MtxEvent.AudioMessage text: timelineManager.timeline.formatPowerLevelEvent(model.data.id)
}
PlayableMediaMessage { }
} DelegateChoice {
roleValue: MtxEvent.RoomJoinRules
} NoticeMessage {
text: timelineManager.timeline.formatJoinRuleEvent(model.data.id)
DelegateChoice { }
roleValue: MtxEvent.Redacted }
DelegateChoice {
Pill { roleValue: MtxEvent.RoomHistoryVisibility
text: qsTr("redacted") NoticeMessage {
} text: timelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
}
} }
DelegateChoice {
DelegateChoice { roleValue: MtxEvent.RoomGuestAccess
roleValue: MtxEvent.Redaction NoticeMessage {
text: timelineManager.timeline.formatGuestAccessEvent(model.data.id)
Pill { }
text: qsTr("redacted") }
} DelegateChoice {
roleValue: MtxEvent.Member
} NoticeMessage {
text: timelineManager.timeline.formatMemberEvent(model.data.id);
DelegateChoice { }
roleValue: MtxEvent.Encryption }
DelegateChoice {
Pill { Placeholder {}
text: qsTr("Encryption enabled") }
} }
}
DelegateChoice {
roleValue: MtxEvent.Name
NoticeMessage {
text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
}
}
DelegateChoice {
roleValue: MtxEvent.Topic
NoticeMessage {
text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
}
}
DelegateChoice {
roleValue: MtxEvent.RoomCreate
NoticeMessage {
text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId)
}
}
DelegateChoice {
roleValue: MtxEvent.CallInvite
NoticeMessage {
text: {
switch (model.data.callType) {
case "voice":
return qsTr("%1 placed a voice call.").arg(model.data.userName);
case "video":
return qsTr("%1 placed a video call.").arg(model.data.userName);
default:
return qsTr("%1 placed a call.").arg(model.data.userName);
}
}
}
}
DelegateChoice {
roleValue: MtxEvent.CallAnswer
NoticeMessage {
text: qsTr("%1 answered the call.").arg(model.data.userName)
}
}
DelegateChoice {
roleValue: MtxEvent.CallHangUp
NoticeMessage {
text: qsTr("%1 ended the call.").arg(model.data.userName)
}
}
DelegateChoice {
roleValue: MtxEvent.CallCandidates
NoticeMessage {
text: qsTr("Negotiating call...")
}
}
DelegateChoice {
// TODO: make a more complex formatter for the power levels.
roleValue: MtxEvent.PowerLevels
NoticeMessage {
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.RoomJoinRules
NoticeMessage {
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.RoomHistoryVisibility
NoticeMessage {
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.RoomGuestAccess
NoticeMessage {
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.Member
NoticeMessage {
text: TimelineManager.timeline.formatMemberEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationRequest
NoticeMessage {
text: "KeyVerificationRequest"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationStart
NoticeMessage {
text: "KeyVerificationStart"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationReady
NoticeMessage {
text: "KeyVerificationReady"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationCancel
NoticeMessage {
text: "KeyVerificationCancel"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationKey
NoticeMessage {
text: "KeyVerificationKey"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationMac
NoticeMessage {
text: "KeyVerificationMac"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationDone
NoticeMessage {
text: "KeyVerificationDone"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationDone
NoticeMessage {
text: "KeyVerificationDone"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationAccept
NoticeMessage {
text: "KeyVerificationAccept"
}
}
DelegateChoice {
Placeholder {
}
}
}
} }

@ -1,6 +1,4 @@
TextMessage { TextMessage {
font.italic: true font.italic: true
color: colors.buttonText color: inactiveColors.text
height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined
clip: true
} }

@ -2,14 +2,13 @@ import QtQuick 2.5
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
Label { Label {
color: colors.brightText color: inactiveColors.text
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
height: contentHeight * 1.2
width: contentWidth * 1.2
background: Rectangle {
radius: parent.height / 2
color: colors.dark
}
height: contentHeight * 1.2
width: contentWidth * 1.2
background: Rectangle {
radius: parent.height / 2
color: colors.base
}
} }

@ -1,7 +1,7 @@
import ".." import ".."
MatrixText { MatrixText {
text: qsTr("unimplemented event: ") + model.data.typeString text: qsTr("unimplemented event: ") + model.data.typeString
width: parent ? parent.width : undefined width: parent ? parent.width : undefined
color: inactiveColors.text color: inactiveColors.text
} }

@ -1,216 +1,167 @@
import QtMultimedia 5.6
import QtQuick 2.6 import QtQuick 2.6
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Controls 2.1
import QtMultimedia 5.6
import im.nheko 1.0 import im.nheko 1.0
Rectangle { Rectangle {
id: bg id: bg
radius: 10
radius: 10 color: colors.base
color: colors.dark height: content.height + 24
height: Math.round(content.height + 24) width: parent ? parent.width : undefined
width: parent ? parent.width : undefined
Column {
Column { id: content
id: content width: parent.width - 24
anchors.centerIn: parent
width: parent.width - 24
anchors.centerIn: parent Rectangle {
id: videoContainer
Rectangle { visible: model.data.type == MtxEvent.VideoMessage
id: videoContainer width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size...
height: width*model.data.proportionalHeight
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) Image {
property double tempHeight: tempWidth * model.data.proportionalHeight anchors.fill: parent
property double divisor: model.isReply ? 4 : 2 source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/")
property bool tooHigh: tempHeight > timelineRoot.height / divisor asynchronous: true
fillMode: Image.PreserveAspectFit
visible: model.data.type == MtxEvent.VideoMessage
height: tooHigh ? timelineRoot.height / divisor : tempHeight VideoOutput {
width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth anchors.fill: parent
fillMode: VideoOutput.PreserveAspectFit
Image { source: media
anchors.fill: parent }
source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") }
asynchronous: true }
fillMode: Image.PreserveAspectFit
RowLayout {
VideoOutput { width: parent.width
anchors.fill: parent Text {
fillMode: VideoOutput.PreserveAspectFit id: positionText
source: media text: "--:--:--"
} color: colors.text
}
} Slider {
Layout.fillWidth: true
} id: progress
value: media.position
RowLayout { from: 0
width: parent.width to: media.duration
Text { onMoved: media.seek(value)
id: positionText //indeterminate: true
function updatePositionTexts() {
text: "--:--:--" function formatTime(date) {
color: colors.text var hh = date.getUTCHours();
} var mm = date.getUTCMinutes();
var ss = date.getSeconds();
Slider { if (hh < 10) {hh = "0"+hh;}
id: progress if (mm < 10) {mm = "0"+mm;}
if (ss < 10) {ss = "0"+ss;}
//indeterminate: true return hh+":"+mm+":"+ss;
function updatePositionTexts() { }
function formatTime(date) { positionText.text = formatTime(new Date(media.position))
var hh = date.getUTCHours(); durationText.text = formatTime(new Date(media.duration))
var mm = date.getUTCMinutes(); }
var ss = date.getSeconds(); onValueChanged: updatePositionTexts()
if (hh < 10)
hh = "0" + hh; palette: colors
}
if (mm < 10) Text {
mm = "0" + mm; id: durationText
text: "--:--:--"
if (ss < 10) color: colors.text
ss = "0" + ss; }
}
return hh + ":" + mm + ":" + ss;
} RowLayout {
width: parent.width
positionText.text = formatTime(new Date(media.position));
durationText.text = formatTime(new Date(media.duration)); spacing: 15
}
Rectangle {
Layout.fillWidth: true id: button
value: media.position color: colors.window
from: 0 radius: 22
to: media.duration height: 44
onMoved: media.seek(value) width: 44
onValueChanged: updatePositionTexts() Image {
palette: colors id: img
} anchors.centerIn: parent
z: 3
Text {
id: durationText source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text
fillMode: Image.Pad
text: "--:--:--"
color: colors.text }
} MouseArea {
anchors.fill: parent
} onClicked: {
switch (button.state) {
RowLayout { case "": timelineManager.timeline.cacheMedia(model.data.id); break;
width: parent.width case "stopped":
spacing: 15 media.play(); console.log("play");
button.state = "playing"
Rectangle { break
id: button case "playing":
media.pause(); console.log("pause");
color: colors.window button.state = "stopped"
radius: 22 break
height: 44 }
width: 44 }
states: [ cursorShape: Qt.PointingHandCursor
State { }
name: "stopped" MediaPlayer {
id: media
PropertyChanges { onError: console.log(errorString)
target: img onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts()
source: "image://colorimage/:/icons/icons/ui/play-sign.png?" + colors.text onStopped: button.state = "stopped"
} }
}, Connections {
State { target: timelineManager.timeline
name: "playing" onMediaCached: {
if (mxcUrl == model.data.url) {
PropertyChanges { media.source = "file://" + cacheUrl
target: img button.state = "stopped"
source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + colors.text console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
} }
console.log("media cached: " + mxcUrl + " at " + cacheUrl)
} }
] }
Image { states: [
id: img State {
name: "stopped"
anchors.centerIn: parent PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text }
z: 3 },
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text State {
fillMode: Image.Pad name: "playing"
} PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text }
}
MouseArea { ]
anchors.fill: parent }
onClicked: { ColumnLayout {
switch (button.state) { id: col
case "":
TimelineManager.timeline.cacheMedia(model.data.id); Text {
break; Layout.fillWidth: true
case "stopped": text: model.data.body
media.play(); textFormat: Text.PlainText
console.log("play"); elide: Text.ElideRight
button.state = "playing"; color: colors.text
break; }
case "playing": Text {
media.pause(); Layout.fillWidth: true
console.log("pause"); text: model.data.filesize
button.state = "stopped"; textFormat: Text.PlainText
break; elide: Text.ElideRight
} color: colors.text
} }
cursorShape: Qt.PointingHandCursor }
} }
}
MediaPlayer {
id: media
onError: console.log(errorString)
onStatusChanged: {
if (status == MediaPlayer.Loaded)
progress.updatePositionTexts();
}
onStopped: button.state = "stopped"
}
Connections {
target: TimelineManager.timeline
onMediaCached: {
if (mxcUrl == model.data.url) {
media.source = "file://" + cacheUrl;
button.state = "stopped";
console.log("media loaded: " + mxcUrl + " at " + cacheUrl);
}
console.log("media cached: " + mxcUrl + " at " + cacheUrl);
}
}
}
ColumnLayout {
id: col
Text {
Layout.fillWidth: true
text: model.data.body
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
Text {
Layout.fillWidth: true
text: model.data.filesize
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
}
}
}
} }

@ -2,71 +2,57 @@ import QtQuick 2.6
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import im.nheko 1.0
Item {
id: replyComponent
property alias modelData: reply.modelData
property color userColor: "red"
width: parent.width
height: replyContainer.height
MouseArea {
anchors.fill: parent
preventStealing: true
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
cursorShape: Qt.PointingHandCursor
}
Rectangle {
id: colorLine
anchors.top: replyContainer.top
anchors.bottom: replyContainer.bottom
width: 4
color: TimelineManager.userColor(reply.modelData.userId, colors.window)
}
Column {
id: replyContainer
anchors.left: colorLine.right
anchors.leftMargin: 4
width: parent.width - 8
Text {
id: userName
text: TimelineManager.escapeEmoji(reply.modelData.userName)
color: replyComponent.userColor
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(reply.modelData.userId)
cursorShape: Qt.PointingHandCursor
}
}
MessageDelegate {
id: reply
width: parent.width
isReply: true
}
}
Rectangle {
id: backgroundItem
z: -1
height: replyContainer.height
width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width)
color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2)
}
Rectangle {
id: replyComponent
property alias modelData: reply.modelData
property color userColor: "red"
width: parent.width
height: replyContainer.height
MouseArea {
anchors.fill: parent
preventStealing: true
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(timelineManager.replyingEvent), ListView.Contain)
cursorShape: Qt.PointingHandCursor
}
Rectangle {
id: colorLine
anchors.top: replyContainer.top
anchors.bottom: replyContainer.bottom
width: 4
color: timelineManager.userColor(reply.modelData.userId, colors.window)
}
Column {
id: replyContainer
anchors.left: colorLine.right
anchors.leftMargin: 4
width: parent.width - 8
Text {
id: userName
text: chat.model ? chat.model.escapeEmoji(reply.modelData.userName) : ""
color: replyComponent.userColor
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(reply.modelData.userId)
cursorShape: Qt.PointingHandCursor
}
}
MessageDelegate {
id: reply
width: parent.width
}
}
color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2)
} }

@ -1,12 +1,7 @@
import ".." import ".."
import im.nheko 1.0
MatrixText { MatrixText {
property string formatted: model.data.formattedBody property string formatted: model.data.formattedBody
text: "<style type=\"text/css\">a { color:"+colors.link+";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>")
text: "<style type=\"text/css\">a { color:" + colors.link + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>") width: parent ? parent.width : undefined
width: parent ? parent.width : undefined
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
clip: true
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
} }

@ -1,46 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Awaiting Confirmation")
ColumnLayout {
spacing: 16
Label {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Waiting for other side to complete verification.")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("Cancel")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
}
}
}

@ -1,144 +0,0 @@
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Window 2.10
import im.nheko 1.0
ApplicationWindow {
id: dialog
property var flow
onClosing: TimelineManager.removeVerificationFlow(flow)
title: stack.currentItem.title
flags: Qt.Dialog
palette: colors
height: stack.implicitHeight
width: stack.implicitWidth
StackView {
id: stack
initialItem: newVerificationRequest
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
}
Component {
id: newVerificationRequest
NewVerificationRequest {
}
}
Component {
id: waiting
Waiting {
}
}
Component {
id: success
Success {
}
}
Component {
id: failed
Failed {
}
}
Component {
id: digitVerification
DigitVerification {
}
}
Component {
id: emojiVerification
EmojiVerification {
}
}
Item {
state: flow.state
states: [
State {
name: "PromptStartVerification"
StateChangeScript {
script: stack.replace(newVerificationRequest)
}
},
State {
name: "CompareEmoji"
StateChangeScript {
script: stack.replace(emojiVerification)
}
},
State {
name: "CompareNumber"
StateChangeScript {
script: stack.replace(digitVerification)
}
},
State {
name: "WaitingForKeys"
StateChangeScript {
script: stack.replace(waiting)
}
},
State {
name: "WaitingForOtherToAccept"
StateChangeScript {
script: stack.replace(waiting)
}
},
State {
name: "WaitingForMac"
StateChangeScript {
script: stack.replace(waiting)
}
},
State {
name: "Success"
StateChangeScript {
script: stack.replace(success)
}
},
State {
name: "Failed"
StateChangeScript {
script: stack.replace(failed)
}
}
]
}
}

@ -1,69 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Verification Code")
ColumnLayout {
spacing: 16
Label {
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[0]
color: colors.text
}
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[1]
color: colors.text
}
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[2]
color: colors.text
}
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("They do not match!")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("They match!")
onClicked: flow.next()
}
}
}
}

@ -1,33 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
Rectangle {
color: "red"
implicitHeight: Qt.application.font.pixelSize * 4
implicitWidth: col.width
height: Qt.application.font.pixelSize * 4
width: col.width
ColumnLayout {
id: col
property var emoji: emojis.mapping[Math.floor(Math.random() * 64)]
anchors.bottom: parent.bottom
Label {
height: font.pixelSize * 2
Layout.alignment: Qt.AlignHCenter
text: col.emoji.emoji
font.pixelSize: Qt.application.font.pixelSize * 2
}
Label {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
text: col.emoji.description
}
}
}

@ -1,414 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Verification Code")
ColumnLayout {
spacing: 16
Label {
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
id: emojis
property var mapping: [{
"number": 0,
"emoji": "🐶",
"description": "Dog",
"unicode": "U+1F436"
}, {
"number": 1,
"emoji": "🐱",
"description": "Cat",
"unicode": "U+1F431"
}, {
"number": 2,
"emoji": "🦁",
"description": "Lion",
"unicode": "U+1F981"
}, {
"number": 3,
"emoji": "🐎",
"description": "Horse",
"unicode": "U+1F40E"
}, {
"number": 4,
"emoji": "🦄",
"description": "Unicorn",
"unicode": "U+1F984"
}, {
"number": 5,
"emoji": "🐷",
"description": "Pig",
"unicode": "U+1F437"
}, {
"number": 6,
"emoji": "🐘",
"description": "Elephant",
"unicode": "U+1F418"
}, {
"number": 7,
"emoji": "🐰",
"description": "Rabbit",
"unicode": "U+1F430"
}, {
"number": 8,
"emoji": "🐼",
"description": "Panda",
"unicode": "U+1F43C"
}, {
"number": 9,
"emoji": "🐓",
"description": "Rooster",
"unicode": "U+1F413"
}, {
"number": 10,
"emoji": "🐧",
"description": "Penguin",
"unicode": "U+1F427"
}, {
"number": 11,
"emoji": "🐢",
"description": "Turtle",
"unicode": "U+1F422"
}, {
"number": 12,
"emoji": "🐟",
"description": "Fish",
"unicode": "U+1F41F"
}, {
"number": 13,
"emoji": "🐙",
"description": "Octopus",
"unicode": "U+1F419"
}, {
"number": 14,
"emoji": "🦋",
"description": "Butterfly",
"unicode": "U+1F98B"
}, {
"number": 15,
"emoji": "🌷",
"description": "Flower",
"unicode": "U+1F337"
}, {
"number": 16,
"emoji": "🌳",
"description": "Tree",
"unicode": "U+1F333"
}, {
"number": 17,
"emoji": "🌵",
"description": "Cactus",
"unicode": "U+1F335"
}, {
"number": 18,
"emoji": "🍄",
"description": "Mushroom",
"unicode": "U+1F344"
}, {
"number": 19,
"emoji": "🌏",
"description": "Globe",
"unicode": "U+1F30F"
}, {
"number": 20,
"emoji": "🌙",
"description": "Moon",
"unicode": "U+1F319"
}, {
"number": 21,
"emoji": "☁",
"description": "Cloud",
"unicode": "U+2601U+FE0F"
}, {
"number": 22,
"emoji": "🔥",
"description": "Fire",
"unicode": "U+1F525"
}, {
"number": 23,
"emoji": "🍌",
"description": "Banana",
"unicode": "U+1F34C"
}, {
"number": 24,
"emoji": "🍎",
"description": "Apple",
"unicode": "U+1F34E"
}, {
"number": 25,
"emoji": "🍓",
"description": "Strawberry",
"unicode": "U+1F353"
}, {
"number": 26,
"emoji": "🌽",
"description": "Corn",
"unicode": "U+1F33D"
}, {
"number": 27,
"emoji": "🍕",
"description": "Pizza",
"unicode": "U+1F355"
}, {
"number": 28,
"emoji": "🎂",
"description": "Cake",
"unicode": "U+1F382"
}, {
"number": 29,
"emoji": "❤",
"description": "Heart",
"unicode": "U+2764U+FE0F"
}, {
"number": 30,
"emoji": "😀",
"description": "Smiley",
"unicode": "U+1F600"
}, {
"number": 31,
"emoji": "🤖",
"description": "Robot",
"unicode": "U+1F916"
}, {
"number": 32,
"emoji": "🎩",
"description": "Hat",
"unicode": "U+1F3A9"
}, {
"number": 33,
"emoji": "👓",
"description": "Glasses",
"unicode": "U+1F453"
}, {
"number": 34,
"emoji": "🔧",
"description": "Spanner",
"unicode": "U+1F527"
}, {
"number": 35,
"emoji": "🎅",
"description": "Santa",
"unicode": "U+1F385"
}, {
"number": 36,
"emoji": "👍",
"description": "Thumbs Up",
"unicode": "U+1F44D"
}, {
"number": 37,
"emoji": "☂",
"description": "Umbrella",
"unicode": "U+2602U+FE0F"
}, {
"number": 38,
"emoji": "⌛",
"description": "Hourglass",
"unicode": "U+231B"
}, {
"number": 39,
"emoji": "⏰",
"description": "Clock",
"unicode": "U+23F0"
}, {
"number": 40,
"emoji": "🎁",
"description": "Gift",
"unicode": "U+1F381"
}, {
"number": 41,
"emoji": "💡",
"description": "Light Bulb",
"unicode": "U+1F4A1"
}, {
"number": 42,
"emoji": "📕",
"description": "Book",
"unicode": "U+1F4D5"
}, {
"number": 43,
"emoji": "✏",
"description": "Pencil",
"unicode": "U+270FU+FE0F"
}, {
"number": 44,
"emoji": "📎",
"description": "Paperclip",
"unicode": "U+1F4CE"
}, {
"number": 45,
"emoji": "✂",
"description": "Scissors",
"unicode": "U+2702U+FE0F"
}, {
"number": 46,
"emoji": "🔒",
"description": "Lock",
"unicode": "U+1F512"
}, {
"number": 47,
"emoji": "🔑",
"description": "Key",
"unicode": "U+1F511"
}, {
"number": 48,
"emoji": "🔨",
"description": "Hammer",
"unicode": "U+1F528"
}, {
"number": 49,
"emoji": "☎",
"description": "Telephone",
"unicode": "U+260EU+FE0F"
}, {
"number": 50,
"emoji": "🏁",
"description": "Flag",
"unicode": "U+1F3C1"
}, {
"number": 51,
"emoji": "🚂",
"description": "Train",
"unicode": "U+1F682"
}, {
"number": 52,
"emoji": "🚲",
"description": "Bicycle",
"unicode": "U+1F6B2"
}, {
"number": 53,
"emoji": "✈",
"description": "Aeroplane",
"unicode": "U+2708U+FE0F"
}, {
"number": 54,
"emoji": "🚀",
"description": "Rocket",
"unicode": "U+1F680"
}, {
"number": 55,
"emoji": "🏆",
"description": "Trophy",
"unicode": "U+1F3C6"
}, {
"number": 56,
"emoji": "⚽",
"description": "Ball",
"unicode": "U+26BD"
}, {
"number": 57,
"emoji": "🎸",
"description": "Guitar",
"unicode": "U+1F3B8"
}, {
"number": 58,
"emoji": "🎺",
"description": "Trumpet",
"unicode": "U+1F3BA"
}, {
"number": 59,
"emoji": "🔔",
"description": "Bell",
"unicode": "U+1F514"
}, {
"number": 60,
"emoji": "⚓",
"description": "Anchor",
"unicode": "U+2693"
}, {
"number": 61,
"emoji": "🎧",
"description": "Headphones",
"unicode": "U+1F3A7"
}, {
"number": 62,
"emoji": "📁",
"description": "Folder",
"unicode": "U+1F4C1"
}, {
"number": 63,
"emoji": "📌",
"description": "Pin",
"unicode": "U+1F4CC"
}]
Layout.alignment: Qt.AlignHCenter
Repeater {
id: repeater
model: 7
delegate: Rectangle {
color: "transparent"
implicitHeight: Qt.application.font.pixelSize * 8
implicitWidth: col.width
ColumnLayout {
id: col
property var emoji: emojis.mapping[flow.sasList[index]]
Layout.fillWidth: true
anchors.bottom: parent.bottom
Label {
//height: font.pixelSize * 2
Layout.alignment: Qt.AlignHCenter
text: col.emoji.emoji
font.pixelSize: Qt.application.font.pixelSize * 2
font.family: Settings.emojiFont
color: colors.text
}
Label {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
text: col.emoji.description
color: colors.text
}
}
}
}
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("They do not match!")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("They match!")
onClicked: flow.next()
}
}
}
}

@ -1,56 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Verification failed")
ColumnLayout {
spacing: 16
Text {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: {
switch (flow.error) {
case DeviceVerificationFlow.UnknownMethod:
return qsTr("Other client does not support our verification protocol.");
case DeviceVerificationFlow.MismatchedCommitment:
case DeviceVerificationFlow.MismatchedSAS:
case DeviceVerificationFlow.KeyMismatch:
return qsTr("Key mismatch detected!");
case DeviceVerificationFlow.Timeout:
return qsTr("Device verification timed out.");
case DeviceVerificationFlow.User:
return qsTr("Other party canceled the verification.");
case DeviceVerificationFlow.OutOfOrder:
return qsTr("Device verification timed out.");
default:
return "Unknown verification error.";
}
}
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("Close")
onClicked: dialog.close()
}
}
}
}

@ -1,46 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
ColumnLayout {
spacing: 16
Label {
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: flow.sender ? qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") : qsTr("The device was requested to be verified")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
onClicked: flow.next()
}
}
}
}

@ -1,38 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
Pane {
property string title: qsTr("Successful Verification")
ColumnLayout {
spacing: 16
Label {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Verification successful! Both sides verified their devices!")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("Close")
onClicked: dialog.close()
}
}
}
}

@ -1,56 +0,0 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Waiting for other party")
ColumnLayout {
spacing: 16
Label {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: {
switch (flow.state) {
case "WaitingForOtherToAccept":
return qsTr("Waiting for other side to accept the verification request.");
case "WaitingForKeys":
return qsTr("Waiting for other side to continue the verification request.");
case "WaitingForMac":
return qsTr("Waiting for other side to complete the verification request.");
}
}
color: colors.text
verticalAlignment: Text.AlignVCenter
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
palette: colors
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("Cancel")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
}
}
}

@ -1,66 +0,0 @@
[
{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
{"number": 21, "emoji": "☁", "description": "Cloud", "unicode": "U+2601U+FE0F"},
{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
{"number": 29, "emoji": "❤", "description": "Heart", "unicode": "U+2764U+FE0F"},
{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
{"number": 37, "emoji": "☂", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
{"number": 43, "emoji": "✏", "description": "Pencil", "unicode": "U+270FU+FE0F"},
{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
{"number": 45, "emoji": "✂", "description": "Scissors", "unicode": "U+2702U+FE0F"},
{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
{"number": 49, "emoji": "☎", "description": "Telephone", "unicode": "U+260EU+FE0F"},
{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
{"number": 53, "emoji": "✈", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
]

@ -1,16 +0,0 @@
import "../"
import QtQuick 2.10
import QtQuick.Controls 2.1
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
ImageButton {
id: emojiButton
property var colors: currentActivePalette
property var emojiPicker
property string event_id
image: ":/icons/icons/ui/smile.png"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id)
}

@ -1,332 +0,0 @@
import "../"
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
Popup {
id: emojiPopup
property string event_id
property var colors
property alias model: gridView.model
property var textArea
property string emojiCategory: "people"
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
function show(showAt, event_id) {
console.debug("Showing emojiPicker for " + event_id);
if (showAt) {
parent = showAt;
x = Math.round((showAt.width - width) / 2);
y = showAt.height;
}
emojiPopup.event_id = event_id;
open();
}
margins: 0
bottomPadding: 1
leftPadding: 1
rightPadding: 1
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
ColumnLayout {
id: columnView
anchors.fill: parent
spacing: 0
Layout.bottomMargin: 0
Layout.leftMargin: 3
Layout.rightMargin: 3
Layout.topMargin: 2
// emoji grid
GridView {
id: gridView
Layout.preferredHeight: emojiPopup.height
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 4
cellWidth: 52
cellHeight: 52
boundsBehavior: Flickable.StopAtBounds
clip: true
currentIndex: -1 // prevent sorting from stealing focus
// Individual emoji
delegate: AbstractButton {
width: 48
height: 48
hoverEnabled: true
ToolTip.text: model.shortName
ToolTip.visible: hovered
// TODO: maybe add favorites at some point?
onClicked: {
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id);
emojiPopup.close();
TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode);
}
// give the emoji a little oomf
DropShadow {
width: parent.width
height: parent.height
horizontalOffset: 3
verticalOffset: 3
radius: 8
samples: 17
color: "#80000000"
source: parent.contentItem
}
contentItem: Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: Settings.emojiFont
font.pixelSize: 36
text: model.unicode
}
background: Rectangle {
anchors.fill: parent
color: hovered ? colors.highlight : 'transparent'
radius: 5
}
}
// Search field
header: TextField {
id: emojiSearch
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: emojiScroll.width + 4
placeholderText: qsTr("Search")
selectByMouse: true
rightPadding: clearSearch.width
onTextChanged: searchTimer.restart()
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
Timer {
id: searchTimer
interval: 350 // tweak as needed?
onTriggered: {
emojiPopup.model.filter = emojiSearch.text;
emojiPopup.model.category = EmojiCategory.Search;
}
}
ToolButton {
id: clearSearch
visible: emojiSearch.text !== ''
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
focusPolicy: Qt.NoFocus
onClicked: emojiSearch.clear()
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
// clear the default hover effects.
background: Item {
}
}
}
ScrollBar.vertical: ScrollBar {
id: emojiScroll
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: emojiPopup.colors.dark
}
// Category picker row
RowLayout {
Layout.bottomMargin: 0
Layout.preferredHeight: 42
implicitHeight: 42
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
// Display the normal categories
Repeater {
model: ListModel {
// TODO: Would like to get 'simple' icons for the categories
ListElement {
image: ":/icons/icons/emoji-categories/people.png"
category: EmojiCategory.People
}
ListElement {
image: ":/icons/icons/emoji-categories/nature.png"
category: EmojiCategory.Nature
}
ListElement {
image: ":/icons/icons/emoji-categories/foods.png"
category: EmojiCategory.Food
}
ListElement {
image: ":/icons/icons/emoji-categories/activity.png"
category: EmojiCategory.Activity
}
ListElement {
image: ":/icons/icons/emoji-categories/travel.png"
category: EmojiCategory.Travel
}
ListElement {
image: ":/icons/icons/emoji-categories/objects.png"
category: EmojiCategory.Objects
}
ListElement {
image: ":/icons/icons/emoji-categories/symbols.png"
category: EmojiCategory.Symbols
}
ListElement {
image: ":/icons/icons/emoji-categories/flags.png"
category: EmojiCategory.Flags
}
}
delegate: AbstractButton {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
hoverEnabled: true
ToolTip.text: {
switch (model.category) {
case EmojiCategory.People:
return qsTr('People');
case EmojiCategory.Nature:
return qsTr('Nature');
case EmojiCategory.Food:
return qsTr('Food');
case EmojiCategory.Activity:
return qsTr('Activity');
case EmojiCategory.Travel:
return qsTr('Travel');
case EmojiCategory.Objects:
return qsTr('Objects');
case EmojiCategory.Symbols:
return qsTr('Symbols');
case EmojiCategory.Flags:
return qsTr('Flags');
}
}
ToolTip.visible: hovered
onClicked: {
emojiPopup.model.category = model.category;
}
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}
contentItem: Image {
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
fillMode: Image.Pad
sourceSize.width: 32
sourceSize.height: 32
source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText)
}
background: Rectangle {
anchors.fill: parent
color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent'
radius: 5
border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent'
}
}
}
// Separator
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 1
implicitWidth: 1
height: parent.height
color: emojiPopup.colors.dark
}
// Search Button is special
AbstractButton {
id: searchBtn
hoverEnabled: true
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: 0
ToolTip.text: qsTr("Search")
ToolTip.visible: hovered
onClicked: {
// clear any filters
emojiPopup.model.category = EmojiCategory.Search;
gridView.positionViewAtBeginning();
emojiSearch.forceActiveFocus();
}
Layout.preferredWidth: 36
Layout.preferredHeight: 36
implicitWidth: 36
implicitHeight: 36
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}
contentItem: Image {
anchors.right: parent.right
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
sourceSize.width: 32
sourceSize.height: 32
fillMode: Image.Pad
smooth: true
source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText)
}
}
}
}
}

@ -1,2 +0,0 @@
[Controls]
FallbackStyle=Fusion

@ -14,16 +14,12 @@
<file>icons/ui/double-tick-indicator@2x.png</file> <file>icons/ui/double-tick-indicator@2x.png</file>
<file>icons/ui/lock.png</file> <file>icons/ui/lock.png</file>
<file>icons/ui/lock@2x.png</file> <file>icons/ui/lock@2x.png</file>
<file>icons/ui/unlock.png</file>
<file>icons/ui/unlock@2x.png</file>
<file>icons/ui/clock.png</file> <file>icons/ui/clock.png</file>
<file>icons/ui/clock@2x.png</file> <file>icons/ui/clock@2x.png</file>
<file>icons/ui/checkmark.png</file> <file>icons/ui/checkmark.png</file>
<file>icons/ui/checkmark@2x.png</file> <file>icons/ui/checkmark@2x.png</file>
<file>icons/ui/cursor.png</file> <file>icons/ui/cursor.png</file>
<file>icons/ui/cursor@2x.png</file> <file>icons/ui/cursor@2x.png</file>
<file>icons/ui/search.png</file>
<file>icons/ui/search@2x.png</file>
<file>icons/ui/settings.png</file> <file>icons/ui/settings.png</file>
<file>icons/ui/settings@2x.png</file> <file>icons/ui/settings@2x.png</file>
<file>icons/ui/smile.png</file> <file>icons/ui/smile.png</file>
@ -70,11 +66,6 @@
<file>icons/ui/mail-reply.png</file> <file>icons/ui/mail-reply.png</file>
<file>icons/ui/place-call.png</file>
<file>icons/ui/end-call.png</file>
<file>icons/ui/microphone-mute.png</file>
<file>icons/ui/microphone-unmute.png</file>
<file>icons/emoji-categories/people.png</file> <file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/people@2x.png</file> <file>icons/emoji-categories/people@2x.png</file>
<file>icons/emoji-categories/nature.png</file> <file>icons/emoji-categories/nature.png</file>
@ -118,21 +109,13 @@
<file>styles/nheko-dark.qss</file> <file>styles/nheko-dark.qss</file>
</qresource> </qresource>
<qresource prefix="/"> <qresource prefix="/">
<file>qtquickcontrols2.conf</file>
<file>qml/TimelineView.qml</file> <file>qml/TimelineView.qml</file>
<file>qml/ActiveCallBar.qml</file>
<file>qml/Avatar.qml</file> <file>qml/Avatar.qml</file>
<file>qml/ImageButton.qml</file> <file>qml/ImageButton.qml</file>
<file>qml/MatrixText.qml</file> <file>qml/MatrixText.qml</file>
<file>qml/StatusIndicator.qml</file> <file>qml/StatusIndicator.qml</file>
<file>qml/EncryptionIndicator.qml</file> <file>qml/EncryptionIndicator.qml</file>
<file>qml/Reactions.qml</file>
<file>qml/ScrollHelper.qml</file>
<file>qml/TimelineRow.qml</file> <file>qml/TimelineRow.qml</file>
<file>qml/emoji/EmojiButton.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/UserProfile.qml</file>
<file>qml/delegates/MessageDelegate.qml</file> <file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file> <file>qml/delegates/TextMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file> <file>qml/delegates/NoticeMessage.qml</file>
@ -142,17 +125,5 @@
<file>qml/delegates/Pill.qml</file> <file>qml/delegates/Pill.qml</file>
<file>qml/delegates/Placeholder.qml</file> <file>qml/delegates/Placeholder.qml</file>
<file>qml/delegates/Reply.qml</file> <file>qml/delegates/Reply.qml</file>
<file>qml/device-verification/Waiting.qml</file>
<file>qml/device-verification/DeviceVerification.qml</file>
<file>qml/device-verification/DigitVerification.qml</file>
<file>qml/device-verification/EmojiVerification.qml</file>
<file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Failed.qml</file>
<file>qml/device-verification/Success.qml</file>
</qresource>
<qresource prefix="/media">
<file>media/ring.ogg</file>
<file>media/ringback.ogg</file>
<file>media/callend.ogg</file>
</qresource> </qresource>
</RCC> </RCC>

@ -31,6 +31,7 @@ UserMentionsWidget > * {
} }
QLineEdit, QLineEdit,
QListWidget,
WelcomePage, WelcomePage,
LoginPage, LoginPage,
RegisterPage, RegisterPage,
@ -159,7 +160,7 @@ CommunitiesListItem {
qproperty-backgroundColor: #2d3139; qproperty-backgroundColor: #2d3139;
qproperty-avatarBgColor: #202228; qproperty-avatarBgColor: #202228;
qproperty-avatarFgColor: palette(window); qproperty-avatarFgColor: white;
} }
LoadingIndicator { LoadingIndicator {
@ -176,6 +177,14 @@ UserInfoWidget {
border-bottom: 1px solid #202228; border-bottom: 1px solid #202228;
} }
#UserSettingScrollWidget > QComboBox {
color: #202228;
}
#UserSettingScrollWidget > QComboBox {
color: #202228;
}
Avatar { Avatar {
qproperty-textColor: white; qproperty-textColor: white;
qproperty-backgroundColor: #2d3139; qproperty-backgroundColor: #2d3139;
@ -205,6 +214,11 @@ TextField {
qproperty-labelColor: #caccd1; qproperty-labelColor: #caccd1;
} }
ScrollBar {
qproperty-handleColor: #2d3139;
qproperty-backgroundColor: #202228;
}
SideBarActions, SideBarActions,
TopRoomBar TopRoomBar
{ {
@ -232,11 +246,6 @@ Toggle {
qproperty-trackColor: rgb(240, 240, 240); qproperty-trackColor: rgb(240, 240, 240);
} }
QListWidget {
color: #caccd1;
background-color: #202228;
}
SnackBar { SnackBar {
qproperty-textColor: #caccd1; qproperty-textColor: #caccd1;
qproperty-bgColor: #202228; qproperty-bgColor: #202228;

@ -183,8 +183,6 @@ TopSection {
WelcomePage, WelcomePage,
LoginPage, LoginPage,
QComboBox,
QPushButton,
RegisterPage { RegisterPage {
background-color: white; background-color: white;
color: #333; color: #333;
@ -223,19 +221,16 @@ TextField {
qproperty-labelColor: #333; qproperty-labelColor: #333;
} }
QListWidget,
TextInputWidget,
QTextEdit,
QLineEdit {
background-color: white;
color: #333;
}
TextInputWidget { TextInputWidget {
border: none; border: none;
border-top: 1px solid #dcdcdc; border-top: 1px solid #dcdcdc;
} }
ScrollBar {
qproperty-handleColor: #ccc;
qproperty-backgroundColor: #efefef;
}
SideBarActions { SideBarActions {
border: none; border: none;
border-top: 1px solid #dcdcdc; border-top: 1px solid #dcdcdc;

@ -70,7 +70,7 @@ FileItem {
} }
RaisedButton { RaisedButton {
qproperty-foregroundColor: palette(button-text); qproperty-foregroundColor: palette(buttonText);
} }
TextField { TextField {
@ -95,18 +95,18 @@ UserMentionsWidget {
qproperty-titleColor: palette(text); qproperty-titleColor: palette(text);
qproperty-subtitleColor: palette(text); qproperty-subtitleColor: palette(text);
qproperty-highlightedTitleColor: palette(highlighted-text); qproperty-highlightedTitleColor: palette(highlightedtext);
qproperty-highlightedSubtitleColor: palette(highlighted-text); qproperty-highlightedSubtitleColor: palette(highlightedtext);
qproperty-hoverTitleColor: palette(dark); qproperty-hoverTitleColor: palette(highlightedtext);
qproperty-hoverSubtitleColor: palette(dark); qproperty-hoverSubtitleColor: palette(highlightedtext);
qproperty-btnColor: palette(dark); qproperty-btnColor: palette(button);
qproperty-btnTextColor: palette(bright-text); qproperty-btnTextColor: palette(buttonText);
qproperty-timestampColor: palette(text); qproperty-timestampColor: palette(text);
qproperty-highlightedTimestampColor: palette(highlighted-text); qproperty-highlightedTimestampColor: palette(highlightedtext);
qproperty-hoverTimestampColor: palette(dark); qproperty-hoverTimestampColor: palette(highlightedtext);
qproperty-bubbleBgColor: palette(base); qproperty-bubbleBgColor: palette(base);
qproperty-bubbleFgColor: palette(text); qproperty-bubbleFgColor: palette(text);

@ -14,9 +14,8 @@ class Emoji(object):
def generate_code(emojis, category): def generate_code(emojis, category):
tmpl = Template(''' tmpl = Template('''
const std::vector<Emoji> emoji::Provider::{{ category }} = { const std::vector<Emoji> emoji::Provider::{{ category }} = {
// {{ category.capitalize() }}
{%- for e in emoji %} {%- for e in emoji %}
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ category.capitalize() }}}, Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}"},
{%- endfor %} {%- endfor %}
}; };
''') ''')
@ -24,19 +23,6 @@ const std::vector<Emoji> emoji::Provider::{{ category }} = {
d = dict(category=category, emoji=emojis) d = dict(category=category, emoji=emojis)
print(tmpl.render(d)) print(tmpl.render(d))
def generate_qml_list(**kwargs):
tmpl = Template('''
const QVector<Emoji> emoji::Provider::emoji = {
{%- for c in kwargs.items() %}
// {{ c[0].capitalize() }}
{%- for e in c[1] %}
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ c[0].capitalize() }}},
{%- endfor %}
{%- endfor %}
};
''')
d = dict(kwargs=kwargs)
print(tmpl.render(d))
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) < 2: if len(sys.argv) < 2:
@ -101,4 +87,3 @@ if __name__ == '__main__':
generate_code(objects, 'objects') generate_code(objects, 'objects')
generate_code(symbols, 'symbols') generate_code(symbols, 'symbols')
generate_code(flags, 'flags') generate_code(flags, 'flags')
generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags)

@ -9,35 +9,19 @@ set -eu
INPUT=$1 INPUT=$1
OUTPUT=nheko OUTPUT=nheko
filename=$(basename -- "$1")
extension="${filename##*.}"
mkdir ${OUTPUT}.iconset mkdir ${OUTPUT}.iconset
if [ extension = "svg" ]; then sips -z 16 16 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16.png
rsvg-convert -h 16 "${INPUT}" > ${OUTPUT}.iconset/icon_16x16.png sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16@2x.png
rsvg-convert -h 32 "${INPUT}" > ${OUTPUT}.iconset/icon_16x16@2x.png sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32.png
rsvg-convert -h 32 "${INPUT}" > ${OUTPUT}.iconset/icon_32x32.png sips -z 64 64 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32@2x.png
rsvg-convert -h 64 "${INPUT}" > ${OUTPUT}.iconset/icon_32x32@2x.png sips -z 128 128 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128.png
rsvg-convert -h 128 "${INPUT}" > ${OUTPUT}.iconset/icon_128x128.png sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128@2x.png
rsvg-convert -h 256 "${INPUT}" > ${OUTPUT}.iconset/icon_128x128@2x.png sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256.png
rsvg-convert -h 256 "${INPUT}" > ${OUTPUT}.iconset/icon_256x256.png sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256@2x.png
rsvg-convert -h 512 "${INPUT}" > ${OUTPUT}.iconset/icon_256x256@2x.png sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_512x512.png
rsvg-convert -h 512 "${INPUT}" > ${OUTPUT}.iconset/icon_512x512.png
rsvg-convert -h 1024 "${INPUT}" > ${OUTPUT}.iconset/icon_512x512@2x.png cp "${INPUT}" ${OUTPUT}.iconset/icon_512x512@2x.png
else
sips -z 16 16 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16.png
sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16@2x.png
sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32.png
sips -z 64 64 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32@2x.png
sips -z 128 128 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128.png
sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128@2x.png
sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256.png
sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256@2x.png
sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_512x512.png
cp "${INPUT}" ${OUTPUT}.iconset/icon_512x512@2x.png
fi
iconutil -c icns ${OUTPUT}.iconset iconutil -c icns ${OUTPUT}.iconset

@ -24,7 +24,6 @@
#include "Cache.h" #include "Cache.h"
#include "Logging.h" #include "Logging.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Utils.h"
static QPixmapCache avatar_cache; static QPixmapCache avatar_cache;
@ -34,12 +33,10 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
{ {
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
QPixmap pixmap; if (avatarUrl.isEmpty())
if (avatarUrl.isEmpty()) {
callback(pixmap);
return; return;
}
QPixmap pixmap;
if (avatar_cache.find(cacheKey, &pixmap)) { if (avatar_cache.find(cacheKey, &pixmap)) {
callback(pixmap); callback(pixmap);
return; return;
@ -47,7 +44,7 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
auto data = cache::image(cacheKey); auto data = cache::image(cacheKey);
if (!data.isNull()) { if (!data.isNull()) {
pixmap = QPixmap::fromImage(utils::readImage(&data)); pixmap.loadFromData(data);
avatar_cache.insert(cacheKey, pixmap); avatar_cache.insert(cacheKey, pixmap);
callback(pixmap); callback(pixmap);
return; return;
@ -57,8 +54,9 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
QObject::connect(proxy.get(), QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded, &AvatarProxy::avatarDownloaded,
receiver, receiver,
[callback, cacheKey](QByteArray data) { [callback, cacheKey](const QByteArray &data) {
QPixmap pm = QPixmap::fromImage(utils::readImage(&data)); QPixmap pm;
pm.loadFromData(data);
avatar_cache.insert(cacheKey, pm); avatar_cache.insert(cacheKey, pm);
callback(pm); callback(pm);
}); });
@ -77,10 +75,11 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
opts.mxc_url, opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode), mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error); err->matrix_error.error);
} else { return;
cache::saveImage(cacheKey.toStdString(), res);
} }
cache::saveImage(cacheKey.toStdString(), res);
emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); emit proxy->avatarDownloaded(QByteArray(res.data(), res.size()));
}); });
} }

File diff suppressed because it is too large Load Diff

@ -54,26 +54,6 @@ insertDisplayName(const QString &room_id, const QString &user_id, const QString
void void
insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url);
// presence
mtx::presence::PresenceState
presenceState(const std::string &user_id);
std::string
statusMessage(const std::string &user_id);
// user cache stores user keys
std::optional<UserKeyCache>
userKeys(const std::string &user_id);
void
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
// device & user verification cache
std::optional<VerificationStatus>
verificationStatus(const std::string &user_id);
void
markDeviceVerified(const std::string &user_id, const std::string &device);
void
markDeviceUnverified(const std::string &user_id, const std::string &device);
//! Load saved data for the display names & avatars. //! Load saved data for the display names & avatars.
void void
populateMembers(); populateMembers();
@ -131,15 +111,10 @@ removeRoom(const QString &roomid);
void void
setup(); setup();
//! returns if the format is current, older or newer bool
cache::CacheVersion isFormatValid();
formatVersion();
//! set the format version to the current version
void void
setCurrentFormat(); setCurrentFormat();
//! migrates db to the current format
bool
runMigrations();
std::map<QString, mtx::responses::Timeline> std::map<QString, mtx::responses::Timeline>
roomMessages(); roomMessages();
@ -179,6 +154,21 @@ using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>
UserReceipts UserReceipts
readReceipts(const QString &event_id, const QString &room_id); readReceipts(const QString &event_id, const QString &room_id);
//! Filter the events that have at least one read receipt.
std::vector<QString>
filterReadEvents(const QString &room_id,
const std::vector<QString> &event_ids,
const std::string &excluded_user);
//! Add event for which we are expecting some read receipts.
void
addPendingReceipt(const QString &room_id, const QString &event_id);
void
removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id);
void
notifyForReadReceipts(const std::string &room_id);
std::vector<QString>
pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id);
QByteArray QByteArray
image(const QString &url); image(const QString &url);
QByteArray QByteArray
@ -269,8 +259,6 @@ bool
outboundMegolmSessionExists(const std::string &room_id) noexcept; outboundMegolmSessionExists(const std::string &room_id) noexcept;
void void
updateOutboundMegolmSession(const std::string &room_id, int message_index); updateOutboundMegolmSession(const std::string &room_id, int message_index);
void
dropOutboundMegolmSession(const std::string &room_id);
void void
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);

@ -65,53 +65,3 @@ struct OlmSessionStorage
std::mutex group_outbound_mtx; std::mutex group_outbound_mtx;
std::mutex group_inbound_mtx; std::mutex group_inbound_mtx;
}; };
//! Verification status of a single user
struct VerificationStatus
{
//! True, if the users master key is verified
bool user_verified = false;
//! List of all devices marked as verified
std::vector<std::string> verified_devices;
};
//! In memory cache of verification status
struct VerificationStorage
{
//! mapping of user to verification status
std::map<std::string, VerificationStatus> status;
std::mutex verification_storage_mtx;
};
// this will store the keys of the user with whom a encrypted room is shared with
struct UserKeyCache
{
//! Device id to device keys
std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
//! corss signing keys
mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
//! Sync token when nheko last fetched the keys
std::string updated_at;
//! Sync token when the keys last changed. updated != last_changed means they are outdated.
std::string last_changed;
};
void
to_json(nlohmann::json &j, const UserKeyCache &info);
void
from_json(const nlohmann::json &j, UserKeyCache &info);
// the reason these are stored in a seperate cache rather than storing it in the user cache is
// UserKeyCache stores only keys of users with which encrypted room is shared
struct VerificationCache
{
//! list of verified device_ids with device-verification
std::vector<std::string> device_verified;
//! list of devices the user blocks
std::vector<std::string> device_blocked;
};
void
to_json(nlohmann::json &j, const VerificationCache &info);
void
from_json(const nlohmann::json &j, VerificationCache &info);

@ -8,15 +8,6 @@
#include <mtx/events/join_rules.hpp> #include <mtx/events/join_rules.hpp>
namespace cache {
enum class CacheVersion : int
{
Older = -1,
Current = 0,
Newer = 1,
};
}
struct RoomMember struct RoomMember
{ {
QString user_id; QString user_id;
@ -48,8 +39,7 @@ struct DescInfo
QString event_id; QString event_id;
QString userid; QString userid;
QString body; QString body;
QString descriptiveTime; QString timestamp;
uint64_t timestamp;
QDateTime datetime; QDateTime datetime;
}; };

@ -18,7 +18,6 @@
#pragma once #pragma once
#include <limits>
#include <optional> #include <optional>
#include <QDateTime> #include <QDateTime>
@ -39,6 +38,9 @@
#include "CacheCryptoStructs.h" #include "CacheCryptoStructs.h"
#include "CacheStructs.h" #include "CacheStructs.h"
int
numeric_key_comparison(const MDB_val *a, const MDB_val *b);
class Cache : public QObject class Cache : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -50,27 +52,6 @@ public:
static QString displayName(const QString &room_id, const QString &user_id); static QString displayName(const QString &room_id, const QString &user_id);
static QString avatarUrl(const QString &room_id, const QString &user_id); static QString avatarUrl(const QString &room_id, const QString &user_id);
// presence
mtx::presence::PresenceState presenceState(const std::string &user_id);
std::string statusMessage(const std::string &user_id);
// user cache stores user keys
std::optional<UserKeyCache> userKeys(const std::string &user_id);
void updateUserKeys(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery);
void markUserKeysOutOfDate(lmdb::txn &txn,
lmdb::dbi &db,
const std::vector<std::string> &user_ids,
const std::string &sync_token);
void deleteUserKeys(lmdb::txn &txn,
lmdb::dbi &db,
const std::vector<std::string> &user_ids);
// device & user verification cache
VerificationStatus verificationStatus(const std::string &user_id);
void markDeviceVerified(const std::string &user_id, const std::string &device);
void markDeviceUnverified(const std::string &user_id, const std::string &device);
static void removeDisplayName(const QString &room_id, const QString &user_id); static void removeDisplayName(const QString &room_id, const QString &user_id);
static void removeAvatarUrl(const QString &room_id, const QString &user_id); static void removeAvatarUrl(const QString &room_id, const QString &user_id);
@ -121,9 +102,8 @@ public:
void removeRoom(const std::string &roomid); void removeRoom(const std::string &roomid);
void setup(); void setup();
cache::CacheVersion formatVersion(); bool isFormatValid();
void setCurrentFormat(); void setCurrentFormat();
bool runMigrations();
std::map<QString, mtx::responses::Timeline> roomMessages(); std::map<QString, mtx::responses::Timeline> roomMessages();
@ -157,6 +137,18 @@ public:
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
UserReceipts readReceipts(const QString &event_id, const QString &room_id); UserReceipts readReceipts(const QString &event_id, const QString &room_id);
//! Filter the events that have at least one read receipt.
std::vector<QString> filterReadEvents(const QString &room_id,
const std::vector<QString> &event_ids,
const std::string &excluded_user);
//! Add event for which we are expecting some read receipts.
void addPendingReceipt(const QString &room_id, const QString &event_id);
void removePendingReceipt(lmdb::txn &txn,
const std::string &room_id,
const std::string &event_id);
void notifyForReadReceipts(const std::string &room_id);
std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id);
QByteArray image(const QString &url) const; QByteArray image(const QString &url) const;
QByteArray image(lmdb::txn &txn, const std::string &url) const; QByteArray image(lmdb::txn &txn, const std::string &url) const;
void saveImage(const std::string &url, const std::string &data); void saveImage(const std::string &url, const std::string &data);
@ -187,47 +179,6 @@ public:
//! Add all notifications containing a user mention to the db. //! Add all notifications containing a user mention to the db.
void saveTimelineMentions(const mtx::responses::Notifications &res); void saveTimelineMentions(const mtx::responses::Notifications &res);
//! retrieve events in timeline and related functions
struct Messages
{
mtx::responses::Timeline timeline;
uint64_t next_index;
bool end_of_cache = false;
};
Messages getTimelineMessages(lmdb::txn &txn,
const std::string &room_id,
uint64_t index = std::numeric_limits<uint64_t>::max(),
bool forward = false);
std::optional<mtx::events::collections::TimelineEvent> getEvent(
const std::string &room_id,
const std::string &event_id);
void storeEvent(const std::string &room_id,
const std::string &event_id,
const mtx::events::collections::TimelineEvent &event);
std::vector<std::string> relatedEvents(const std::string &room_id,
const std::string &event_id);
struct TimelineRange
{
uint64_t first, last;
};
std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
std::optional<uint64_t> getTimelineIndex(const std::string &room_id,
std::string_view event_id);
std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
std::string previousBatchToken(const std::string &room_id);
uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
void savePendingMessage(const std::string &room_id,
const mtx::events::collections::TimelineEvent &message);
std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage(
const std::string &room_id);
void removePendingStatus(const std::string &room_id, const std::string &txn_id);
//! clear timeline keeping only the latest batch
void clearTimeline(const std::string &room_id);
//! Remove old unused data. //! Remove old unused data.
void deleteOldMessages(); void deleteOldMessages();
void deleteOldData() noexcept; void deleteOldData() noexcept;
@ -250,7 +201,6 @@ public:
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
bool outboundMegolmSessionExists(const std::string &room_id) noexcept; bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
void updateOutboundMegolmSession(const std::string &room_id, int message_index); void updateOutboundMegolmSession(const std::string &room_id, int message_index);
void dropOutboundMegolmSession(const std::string &room_id);
void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
mtx::crypto::ExportedSessionKeys exportSessionKeys(); mtx::crypto::ExportedSessionKeys exportSessionKeys();
@ -279,10 +229,6 @@ public:
signals: signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void roomReadStatus(const std::map<QString, bool> &status); void roomReadStatus(const std::map<QString, bool> &status);
void removeNotification(const QString &room_id, const QString &event_id);
void userKeysUpdate(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery);
void verificationStatusChanged(const std::string &userid);
private: private:
//! Save an invited room. //! Save an invited room.
@ -310,13 +256,7 @@ private:
const std::string &room_id, const std::string &room_id,
const mtx::responses::Timeline &res); const mtx::responses::Timeline &res);
//! retrieve a specific event from account data mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id);
//! pass empty room_id for global account data
std::optional<mtx::events::collections::RoomAccountDataEvents>
getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
bool isHiddenEvent(lmdb::txn &txn,
mtx::events::collections::TimelineEvents e,
const std::string &room_id);
//! Remove a room from the cache. //! Remove a room from the cache.
// void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
@ -447,10 +387,6 @@ private:
void saveInvites(lmdb::txn &txn, void saveInvites(lmdb::txn &txn,
const std::map<std::string, mtx::responses::InvitedRoom> &rooms); const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
void savePresence(
lmdb::txn &txn,
const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates);
//! Sends signals for the rooms that are removed. //! Sends signals for the rooms that are removed.
void removeLeftRooms(lmdb::txn &txn, void removeLeftRooms(lmdb::txn &txn,
const std::map<std::string, mtx::responses::LeftRoom> &rooms) const std::map<std::string, mtx::responses::LeftRoom> &rooms)
@ -468,46 +404,13 @@ private:
return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
} }
lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE); auto db =
} lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE);
lmdb::dbi_set_compare(txn, db, numeric_key_comparison);
lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id) return db;
{
return lmdb::dbi::open(
txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
// inverse of EventOrderDb
lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
}
lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
}
lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
} }
lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
@ -527,12 +430,6 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
} }
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
}
lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
@ -543,21 +440,6 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
} }
lmdb::dbi getPresenceDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
}
lmdb::dbi getUserKeysDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
}
lmdb::dbi getVerificationDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "verified", MDB_CREATE);
}
//! Retrieves or creates the database that stores the open OLM sessions between our device //! Retrieves or creates the database that stores the open OLM sessions between our device
//! and the given curve25519 key which represents another device. //! and the given curve25519 key which represents another device.
//! //!
@ -576,8 +458,6 @@ private:
return QString::fromStdString(event.state_key); return QString::fromStdString(event.state_key);
} }
std::optional<VerificationCache> verificationCache(const std::string &user_id);
void setNextBatchToken(lmdb::txn &txn, const std::string &token); void setNextBatchToken(lmdb::txn &txn, const std::string &token);
void setNextBatchToken(lmdb::txn &txn, const QString &token); void setNextBatchToken(lmdb::txn &txn, const QString &token);
@ -602,7 +482,6 @@ private:
static QHash<QString, QString> AvatarUrls; static QHash<QString, QString> AvatarUrls;
OlmSessionStorage session_storage; OlmSessionStorage session_storage;
VerificationStorage verification_storage;
}; };
namespace cache { namespace cache {

@ -1,457 +0,0 @@
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <QMediaPlaylist>
#include <QUrl>
#include "Cache.h"
#include "CallManager.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "UserSettingsPage.h"
#include "WebRTCSession.h"
#include "dialogs/AcceptCall.h"
#include "mtx/responses/turn_server.hpp"
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
Q_DECLARE_METATYPE(mtx::responses::TurnServer)
using namespace mtx::events;
using namespace mtx::events::msg;
// https://github.com/vector-im/riot-web/issues/10173
#define STUN_SERVER "stun://turn.matrix.org:3478"
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer);
}
CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
: QObject()
, session_(WebRTCSession::instance())
, turnServerTimer_(this)
, settings_(userSettings)
{
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
qRegisterMetaType<mtx::responses::TurnServer>();
connect(
&session_,
&WebRTCSession::offerCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_});
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
QTimer::singleShot(timeoutms_, this, [this]() {
if (session_.state() == webrtc::State::OFFERSENT) {
hangUp(CallHangUp::Reason::InviteTimeOut);
emit ChatPage::instance()->showNotification(
"The remote side failed to pick up.");
}
});
});
connect(
&session_,
&WebRTCSession::answerCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
emit newMessage(roomid_, CallAnswer{callid_, sdp, 0});
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
});
connect(&session_,
&WebRTCSession::newICECandidate,
this,
[this](const CallCandidates::Candidate &candidate) {
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0});
});
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
connect(this,
&CallManager::turnServerRetrieved,
this,
[this](const mtx::responses::TurnServer &res) {
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
nhlog::net()->info("username: {}", res.username);
nhlog::net()->info("ttl: {} seconds", res.ttl);
for (const auto &u : res.uris)
nhlog::net()->info("uri: {}", u);
// Request new credentials close to expiry
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
turnURIs_ = getTurnURIs(res);
uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
if (res.ttl < 3600)
nhlog::net()->warn("Setting ttl to 1 hour");
turnServerTimer_.setInterval(ttl * 1000 * 0.9);
});
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
switch (state) {
case webrtc::State::DISCONNECTED:
playRingtone("qrc:/media/media/callend.ogg", false);
clear();
break;
case webrtc::State::ICEFAILED: {
QString error("Call connection failed.");
if (turnURIs_.empty())
error += " Your homeserver has no configured TURN server.";
emit ChatPage::instance()->showNotification(error);
hangUp(CallHangUp::Reason::ICEFailed);
break;
}
default:
break;
}
});
connect(&player_,
&QMediaPlayer::mediaStatusChanged,
this,
[this](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::LoadedMedia)
player_.play();
});
}
void
CallManager::sendInvite(const QString &roomid)
{
if (onActiveCall())
return;
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
if (roomInfo.member_count != 2) {
emit ChatPage::instance()->showNotification(
"Voice calls are limited to 1:1 rooms.");
return;
}
std::string errorMessage;
if (!session_.init(&errorMessage)) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
return;
}
roomid_ = roomid;
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
session_.setTurnServers(turnURIs_);
generateCallID();
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
const RoomMember &callee =
members.front().user_id == utils::localUser() ? members.back() : members.front();
callPartyName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
emit newCallParty();
playRingtone("qrc:/media/media/ringback.ogg", true);
if (!session_.createOffer()) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall();
}
}
namespace {
std::string
callHangUpReasonString(CallHangUp::Reason reason)
{
switch (reason) {
case CallHangUp::Reason::ICEFailed:
return "ICE failed";
case CallHangUp::Reason::InviteTimeOut:
return "Invite time out";
default:
return "User";
}
}
}
void
CallManager::hangUp(CallHangUp::Reason reason)
{
if (!callid_.empty()) {
nhlog::ui()->debug(
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
endCall();
}
}
bool
CallManager::onActiveCall() const
{
return session_.state() != webrtc::State::DISCONNECTED;
}
void
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
{
#ifdef GSTREAMER_AVAILABLE
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) ||
handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event))
return;
#else
(void)event;
#endif
}
template<typename T>
bool
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event)
{
if (std::holds_alternative<RoomEvent<T>>(event)) {
handleEvent(std::get<RoomEvent<T>>(event));
return true;
}
return false;
}
void
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
{
const char video[] = "m=video";
const std::string &sdp = callInviteEvent.content.sdp;
bool isVideo = std::search(sdp.cbegin(),
sdp.cend(),
std::cbegin(video),
std::cend(video) - 1,
[](unsigned char c1, unsigned char c2) {
return std::tolower(c1) == std::tolower(c2);
}) != sdp.cend();
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
callInviteEvent.content.call_id,
(isVideo ? "video" : "voice"),
callInviteEvent.sender);
if (callInviteEvent.content.call_id.empty())
return;
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
if (onActiveCall() || roomInfo.member_count != 2 || isVideo) {
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
0,
CallHangUp::Reason::InviteTimeOut});
return;
}
playRingtone("qrc:/media/media/ring.ogg", true);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
const RoomMember &caller =
members.front().user_id == utils::localUser() ? members.back() : members.front();
callPartyName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
emit newCallParty();
auto dialog = new dialogs::AcceptCall(caller.user_id,
caller.display_name,
QString::fromStdString(roomInfo.name),
QString::fromStdString(roomInfo.avatar_url),
settings_,
MainWindow::instance());
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() {
MainWindow::instance()->hideOverlay();
answerInvite(callInviteEvent.content);
});
connect(dialog, &dialogs::AcceptCall::reject, this, [this]() {
MainWindow::instance()->hideOverlay();
hangUp();
});
MainWindow::instance()->showSolidOverlayModal(dialog);
}
void
CallManager::answerInvite(const CallInvite &invite)
{
stopRingtone();
std::string errorMessage;
if (!session_.init(&errorMessage)) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
hangUp();
return;
}
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
session_.setTurnServers(turnURIs_);
if (!session_.acceptOffer(invite.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
return;
}
session_.acceptICECandidates(remoteICECandidates_);
remoteICECandidates_.clear();
}
void
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
{
if (callCandidatesEvent.sender == utils::localUser().toStdString())
return;
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
callCandidatesEvent.content.call_id,
callCandidatesEvent.sender);
if (callid_ == callCandidatesEvent.content.call_id) {
if (onActiveCall())
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
else {
// CallInvite has been received and we're awaiting localUser to accept or
// reject the call
for (const auto &c : callCandidatesEvent.content.candidates)
remoteICECandidates_.push_back(c);
}
}
}
void
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
{
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
callAnswerEvent.content.call_id,
callAnswerEvent.sender);
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
callid_ == callAnswerEvent.content.call_id) {
emit ChatPage::instance()->showNotification("Call answered on another device.");
stopRingtone();
MainWindow::instance()->hideOverlay();
return;
}
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
}
}
}
void
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
{
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
callHangUpEvent.content.call_id,
callHangUpReasonString(callHangUpEvent.content.reason),
callHangUpEvent.sender);
if (callid_ == callHangUpEvent.content.call_id) {
MainWindow::instance()->hideOverlay();
endCall();
}
}
void
CallManager::generateCallID()
{
using namespace std::chrono;
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
callid_ = "c" + std::to_string(ms);
}
void
CallManager::clear()
{
roomid_.clear();
callPartyName_.clear();
callPartyAvatarUrl_.clear();
callid_.clear();
remoteICECandidates_.clear();
}
void
CallManager::endCall()
{
stopRingtone();
clear();
session_.end();
}
void
CallManager::refreshTurnServer()
{
turnURIs_.clear();
turnServerTimer_.start(2000);
}
void
CallManager::retrieveTurnServer()
{
http::client()->get_turn_server(
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
if (err) {
turnServerTimer_.setInterval(5000);
return;
}
emit turnServerRetrieved(res);
});
}
void
CallManager::playRingtone(const QString &ringtone, bool repeat)
{
static QMediaPlaylist playlist;
playlist.clear();
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
: QMediaPlaylist::CurrentItemOnce);
playlist.addMedia(QUrl(ringtone));
player_.setVolume(100);
player_.setPlaylist(&playlist);
}
void
CallManager::stopRingtone()
{
player_.setPlaylist(nullptr);
}
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer)
{
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
// where username and password are percent-encoded
std::vector<std::string> ret;
for (const auto &uri : turnServer.uris) {
if (auto c = uri.find(':'); c == std::string::npos) {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
} else {
std::string scheme = std::string(uri, 0, c);
if (scheme != "turn" && scheme != "turns") {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
QString encodedUri =
QString::fromStdString(scheme) + "://" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
":" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
"@" + QString::fromStdString(std::string(uri, ++c));
ret.push_back(encodedUri.toStdString());
}
}
return ret;
}
}

@ -1,76 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <QMediaPlayer>
#include <QObject>
#include <QSharedPointer>
#include <QString>
#include <QTimer>
#include "mtx/events/collections.hpp"
#include "mtx/events/voip.hpp"
namespace mtx::responses {
struct TurnServer;
}
class UserSettings;
class WebRTCSession;
class CallManager : public QObject
{
Q_OBJECT
public:
CallManager(QSharedPointer<UserSettings>);
void sendInvite(const QString &roomid);
void hangUp(
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
bool onActiveCall() const;
QString callPartyName() const { return callPartyName_; }
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
void refreshTurnServer();
public slots:
void syncEvent(const mtx::events::collections::TimelineEvents &event);
signals:
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
void newCallParty();
void turnServerRetrieved(const mtx::responses::TurnServer &);
private slots:
void retrieveTurnServer();
private:
WebRTCSession &session_;
QString roomid_;
QString callPartyName_;
QString callPartyAvatarUrl_;
std::string callid_;
const uint32_t timeoutms_ = 120000;
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
std::vector<std::string> turnURIs_;
QTimer turnServerTimer_;
QSharedPointer<UserSettings> settings_;
QMediaPlayer player_;
template<typename T>
bool handleEvent_(const mtx::events::collections::TimelineEvents &event);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
void answerInvite(const mtx::events::msg::CallInvite &);
void generateCallID();
void clear();
void endCall();
void playRingtone(const QString &ringtone, bool repeat);
void stopRingtone();
};

File diff suppressed because it is too large Load Diff

@ -19,7 +19,6 @@
#include <atomic> #include <atomic>
#include <optional> #include <optional>
#include <stack>
#include <variant> #include <variant>
#include <mtx/common.hpp> #include <mtx/common.hpp>
@ -35,9 +34,7 @@
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
#include "CacheCryptoStructs.h"
#include "CacheStructs.h" #include "CacheStructs.h"
#include "CallManager.h"
#include "CommunitiesList.h" #include "CommunitiesList.h"
#include "Utils.h" #include "Utils.h"
#include "notifications/Manager.h" #include "notifications/Manager.h"
@ -50,10 +47,10 @@ class SideBarActions;
class Splitter; class Splitter;
class TextInputWidget; class TextInputWidget;
class TimelineViewManager; class TimelineViewManager;
class TopRoomBar;
class UserInfoWidget; class UserInfoWidget;
class UserSettings; class UserSettings;
class NotificationsManager; class NotificationsManager;
class TimelineModel;
constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int CONSENSUS_TIMEOUT = 1000;
constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int SHOW_CONTENT_TIMEOUT = 3000;
@ -80,23 +77,14 @@ public:
QSharedPointer<UserSettings> userSettings() { return userSettings_; } QSharedPointer<UserSettings> userSettings() { return userSettings_; }
void deleteConfigs(); void deleteConfigs();
CommunitiesList *communitiesList() { return communitiesList_; }
//! Calculate the width of the message timeline. //! Calculate the width of the message timeline.
uint64_t timelineWidth(); uint64_t timelineWidth();
bool isSideBarExpanded();
//! Hide the room & group list (if it was visible). //! Hide the room & group list (if it was visible).
void hideSideBars(); void hideSideBars();
//! Show the room/group list (if it was visible). //! Show the room/group list (if it was visible).
void showSideBars(); void showSideBars();
void initiateLogout(); void initiateLogout();
void query_keys(const std::string &req,
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
void focusMessageInput();
QString status() const;
void setStatus(const QString &status);
mtx::presence::PresenceState currentPresence() const;
public slots: public slots:
void leaveRoom(const QString &room_id); void leaveRoom(const QString &room_id);
@ -111,6 +99,8 @@ signals:
void connectionLost(); void connectionLost();
void connectionRestored(); void connectionRestored();
void messageReply(const RelatedInfo &related);
void notificationsRetrieved(const mtx::responses::Notifications &); void notificationsRetrieved(const mtx::responses::Notifications &);
void highlightedNotifsRetrieved(const mtx::responses::Notifications &, void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
const QPoint widgetPos); const QPoint widgetPos);
@ -124,7 +114,8 @@ signals:
const QString &mime, const QString &mime,
qint64 dsize, qint64 dsize,
const QSize &dimensions, const QSize &dimensions,
const QString &blurhash); const QString &blurhash,
const std::optional<RelatedInfo> &related);
void contentLoaded(); void contentLoaded();
void closing(); void closing();
@ -143,7 +134,6 @@ signals:
void trySyncCb(); void trySyncCb();
void tryDelayedSyncCb(); void tryDelayedSyncCb();
void tryInitialSyncCb(); void tryInitialSyncCb();
void newSyncResponse(mtx::responses::Sync res);
void leftRoom(const QString &room_id); void leftRoom(const QString &room_id);
void initializeRoomList(QMap<QString, RoomInfo>); void initializeRoomList(QMap<QString, RoomInfo>);
@ -153,6 +143,7 @@ signals:
void syncUI(const mtx::responses::Rooms &rooms); void syncUI(const mtx::responses::Rooms &rooms);
void syncRoomlist(const std::map<QString, RoomInfo> &updates); void syncRoomlist(const std::map<QString, RoomInfo> &updates);
void syncTags(const std::map<QString, RoomInfo> &updates); void syncTags(const std::map<QString, RoomInfo> &updates);
void syncTopBar(const std::map<QString, RoomInfo> &updates);
void dropToLoginPageCb(const QString &msg); void dropToLoginPageCb(const QString &msg);
void notifyMessage(const QString &roomid, void notifyMessage(const QString &roomid,
@ -163,37 +154,18 @@ signals:
const QImage &icon); const QImage &icon);
void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
void themeChanged(); void themeChanged();
void decryptSidebarChanged();
//! Signals for device verificaiton
void receivedDeviceVerificationAccept(
const mtx::events::msg::KeyVerificationAccept &message);
void receivedDeviceVerificationRequest(
const mtx::events::msg::KeyVerificationRequest &message,
std::string sender);
void receivedRoomDeviceVerificationRequest(
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
TimelineModel *model);
void receivedDeviceVerificationCancel(
const mtx::events::msg::KeyVerificationCancel &message);
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
std::string sender);
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
private slots: private slots:
void showUnreadMessageNotification(int count); void showUnreadMessageNotification(int count);
void updateTopBarAvatar(const QString &roomid, const QString &img);
void changeTopRoomInfo(const QString &room_id);
void logout(); void logout();
void removeRoom(const QString &room_id); void removeRoom(const QString &room_id);
void dropToLoginPage(const QString &msg); void dropToLoginPage(const QString &msg);
void joinRoom(const QString &room); void joinRoom(const QString &room);
void sendTypingNotifications(); void sendTypingNotifications();
void handleSyncResponse(mtx::responses::Sync res);
private: private:
static ChatPage *instance_; static ChatPage *instance_;
@ -233,13 +205,10 @@ private:
uint16_t notification_count, uint16_t notification_count,
uint16_t highlight_count); uint16_t highlight_count);
//! Send desktop notification for the received messages. //! Send desktop notification for the received messages.
void sendNotifications(const mtx::responses::Notifications &); void sendDesktopNotifications(const mtx::responses::Notifications &);
void showNotificationsDialog(const QPoint &point); void showNotificationsDialog(const QPoint &point);
template<typename T>
void connectCallMessage();
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_;
Splitter *splitter; Splitter *splitter;
@ -257,6 +226,7 @@ private:
TimelineViewManager *view_manager_; TimelineViewManager *view_manager_;
SideBarActions *sidebarActions_; SideBarActions *sidebarActions_;
TopRoomBar *top_bar_;
TextInputWidget *text_input_; TextInputWidget *text_input_;
QTimer connectivityTimer_; QTimer connectivityTimer_;
@ -275,7 +245,6 @@ private:
QSharedPointer<UserSettings> userSettings_; QSharedPointer<UserSettings> userSettings_;
NotificationsManager notificationsManager; NotificationsManager notificationsManager;
CallManager callManager_;
}; };
template<class Collection> template<class Collection>

@ -257,18 +257,6 @@ CommunitiesList::roomList(const QString &id) const
return {}; return {};
} }
std::vector<std::string>
CommunitiesList::currentTags() const
{
std::vector<std::string> tags;
for (auto &entry : communities_) {
CommunitiesListItem *item = entry.second.data();
if (item->is_tag())
tags.push_back(entry.first.mid(4).toStdString());
}
return tags;
}
void void
CommunitiesList::sortEntries() CommunitiesList::sortEntries()
{ {

@ -28,7 +28,6 @@ public:
void syncTags(const std::map<QString, RoomInfo> &info); void syncTags(const std::map<QString, RoomInfo> &info);
void setTagsForRoom(const QString &id, const std::vector<std::string> &tags); void setTagsForRoom(const QString &id, const std::vector<std::string> &tags);
std::vector<std::string> currentTags() const;
signals: signals:
void communityChanged(const QString &id); void communityChanged(const QString &id);

@ -137,8 +137,6 @@ CommunitiesListItem::updateTooltip()
setToolTip(tr("Favourite rooms")); setToolTip(tr("Favourite rooms"));
else if (tag == "m.lowpriority") else if (tag == "m.lowpriority")
setToolTip(tr("Low priority rooms")); setToolTip(tr("Low priority rooms"));
else if (tag == "m.server_notice")
setToolTip(tr("Server Notices", "Tag translation for m.server_notice"));
else if (tag.startsWith("u.")) else if (tag.startsWith("u."))
setToolTip(tag.right(tag.size() - 2) + tr(" (tag)")); setToolTip(tag.right(tag.size() - 2) + tr(" (tag)"));
else else

@ -7,6 +7,7 @@
#include "ui/Theme.h" #include "ui/Theme.h"
class RippleOverlay; class RippleOverlay;
class QPainter;
class QMouseEvent; class QMouseEvent;
class CommunitiesListItem : public QWidget class CommunitiesListItem : public QWidget

@ -1,20 +0,0 @@
#pragma once
// Class for showing a limited amount of completions at a time
#include <QSortFilterProxyModel>
class CompletionModel : public QSortFilterProxyModel
{
public:
CompletionModel(QAbstractItemModel *model, QObject *parent = nullptr)
: QSortFilterProxyModel(parent)
{
setSourceModel(model);
}
int rowCount(const QModelIndex &parent) const override
{
auto row_count = QSortFilterProxyModel::rowCount(parent);
return (row_count < 7) ? row_count : 7;
}
};

@ -53,9 +53,9 @@ namespace strings {
const QString url_html = "<a href=\"\\1\">\\1</a>"; const QString url_html = "<a href=\"\\1\">\\1</a>";
const QRegularExpression url_regex( const QRegularExpression url_regex(
// match an URL, that is not quoted, i.e. // match an URL, that is not quoted, i.e.
// vvvvvv match quote via negative lookahead/lookbehind vv // vvvvvv match quote via negative lookahead/lookbehind vv
// vvvv atomic match url -> fail if there is a " before or after vvv // vvvv atomic match url -> fail if there is a " before or after vvv
R"((?<!["'])(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!["']))"); R"((?<!")(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!"))");
} }
// Window geometry. // Window geometry.

@ -1,794 +0,0 @@
#include "DeviceVerificationFlow.h"
#include "Cache.h"
#include "ChatPage.h"
#include "Logging.h"
#include "timeline/TimelineModel.h"
#include <QDateTime>
#include <QTimer>
#include <iostream>
static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
namespace msgs = mtx::events::msg;
static mtx::events::msg::KeyVerificationMac
key_verification_mac(mtx::crypto::SAS *sas,
mtx::identifiers::User sender,
const std::string &senderDevice,
mtx::identifiers::User receiver,
const std::string &receiverDevice,
const std::string &transactionId,
std::map<std::string, std::string> keys);
DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
DeviceVerificationFlow::Type flow_type,
TimelineModel *model,
QString userID,
QString deviceId_)
: sender(false)
, type(flow_type)
, deviceId(deviceId_)
, model_(model)
{
timeout = new QTimer(this);
timeout->setSingleShot(true);
this->sas = olm::client()->sas_init();
this->isMacVerified = false;
auto user_id = userID.toStdString();
this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
ChatPage::instance()->query_keys(
user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
err->matrix_error.errcode,
static_cast<int>(err->status_code));
return;
}
if (!this->deviceId.isEmpty() &&
(res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
nhlog::net()->warn("no devices retrieved {}", user_id);
return;
}
this->their_keys = res;
});
ChatPage::instance()->query_keys(
http::client()->user_id().to_string(),
[this](const UserKeyCache &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
err->matrix_error.errcode,
static_cast<int>(err->status_code));
return;
}
if (res.master_keys.keys.empty())
return;
if (auto status =
cache::verificationStatus(http::client()->user_id().to_string());
status && status->user_verified)
this->our_trusted_master_key = res.master_keys.keys.begin()->second;
});
if (model) {
connect(this->model_,
&TimelineModel::updateFlowEventId,
this,
[this](std::string event_id_) {
this->relation.rel_type = mtx::common::RelationType::Reference;
this->relation.event_id = event_id_;
this->transaction_id = event_id_;
});
}
connect(timeout, &QTimer::timeout, this, [this]() {
if (state_ != Success && state_ != Failed)
this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationStart,
this,
&DeviceVerificationFlow::handleStartMessage);
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationAccept,
this,
[this](const mtx::events::msg::KeyVerificationAccept &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
(msg.hash == "sha256") &&
(msg.message_authentication_code == "hkdf-hmac-sha256")) {
this->commitment = msg.commitment;
if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Emoji) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Emoji;
} else {
this->method = mtx::events::msg::SASMethods::Decimal;
}
this->mac_method = msg.message_authentication_code;
this->sendVerificationKey();
} else {
this->cancelVerification(
DeviceVerificationFlow::Error::UnknownMethod);
}
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationCancel,
this,
[this](const mtx::events::msg::KeyVerificationCancel &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
error_ = User;
emit errorChanged();
setState(Failed);
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationKey,
this,
[this](const mtx::events::msg::KeyVerificationKey &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
if (sender) {
if (state_ != WaitingForOtherToAccept) {
this->cancelVerification(OutOfOrder);
return;
}
} else {
if (state_ != WaitingForKeys) {
this->cancelVerification(OutOfOrder);
return;
}
}
this->sas->set_their_key(msg.key);
std::string info;
if (this->sender == true) {
info = "MATRIX_KEY_VERIFICATION_SAS|" +
http::client()->user_id().to_string() + "|" +
http::client()->device_id() + "|" + this->sas->public_key() +
"|" + this->toClient.to_string() + "|" +
this->deviceId.toStdString() + "|" + msg.key + "|" +
this->transaction_id;
} else {
info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
"|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
http::client()->user_id().to_string() + "|" +
http::client()->device_id() + "|" + this->sas->public_key() +
"|" + this->transaction_id;
}
nhlog::ui()->info("Info is: '{}'", info);
if (this->sender == false) {
this->sendVerificationKey();
} else {
if (this->commitment !=
mtx::crypto::bin2base64_unpadded(
mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
this->cancelVerification(
DeviceVerificationFlow::Error::MismatchedCommitment);
return;
}
}
if (this->method == mtx::events::msg::SASMethods::Emoji) {
this->sasList = this->sas->generate_bytes_emoji(info);
setState(CompareEmoji);
} else if (this->method == mtx::events::msg::SASMethods::Decimal) {
this->sasList = this->sas->generate_bytes_decimal(info);
setState(CompareNumber);
}
});
connect(
ChatPage::instance(),
&ChatPage::receivedDeviceVerificationMac,
this,
[this](const mtx::events::msg::KeyVerificationMac &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
std::map<std::string, std::string> key_list;
std::string key_string;
for (const auto &mac : msg.mac) {
for (const auto &[deviceid, key] : their_keys.device_keys) {
(void)deviceid;
if (key.keys.count(mac.first))
key_list[mac.first] = key.keys.at(mac.first);
}
if (their_keys.master_keys.keys.count(mac.first))
key_list[mac.first] = their_keys.master_keys.keys[mac.first];
if (their_keys.user_signing_keys.keys.count(mac.first))
key_list[mac.first] =
their_keys.user_signing_keys.keys[mac.first];
if (their_keys.self_signing_keys.keys.count(mac.first))
key_list[mac.first] =
their_keys.self_signing_keys.keys[mac.first];
}
auto macs = key_verification_mac(sas.get(),
toClient,
this->deviceId.toStdString(),
http::client()->user_id(),
http::client()->device_id(),
this->transaction_id,
key_list);
for (const auto &[key, mac] : macs.mac) {
if (mac != msg.mac.at(key)) {
this->cancelVerification(
DeviceVerificationFlow::Error::KeyMismatch);
return;
}
}
if (msg.keys == macs.keys) {
mtx::requests::KeySignaturesUpload req;
if (utils::localUser().toStdString() == this->toClient.to_string()) {
// self verification, sign master key with device key, if we
// verified it
for (const auto &mac : msg.mac) {
if (their_keys.master_keys.keys.count(mac.first)) {
json j = their_keys.master_keys;
j.erase("signatures");
j.erase("unsigned");
mtx::crypto::CrossSigningKeys master_key = j;
master_key
.signatures[utils::localUser().toStdString()]
["ed25519:" +
http::client()->device_id()] =
olm::client()->sign_message(j.dump());
req.signatures[utils::localUser().toStdString()]
[master_key.keys.at(mac.first)] =
master_key;
}
}
// TODO(Nico): Sign their device key with self signing key
} else {
// TODO(Nico): Sign their master key with user signing key
}
if (!req.signatures.empty()) {
http::client()->keys_signatures_upload(
req,
[](const mtx::responses::KeySignaturesUpload &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"failed to upload signatures: {},{}",
err->matrix_error.errcode,
static_cast<int>(err->status_code));
}
for (const auto &[user_id, tmp] : res.errors)
for (const auto &[key_id, e] : tmp)
nhlog::net()->error(
"signature error for user {} and key "
"id {}: {}, {}",
user_id,
key_id,
e.errcode,
e.error);
});
}
this->isMacVerified = true;
this->acceptDevice();
} else {
this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
}
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationReady,
this,
[this](const mtx::events::msg::KeyVerificationReady &msg) {
if (!sender) {
if (msg.from_device != http::client()->device_id()) {
error_ = User;
emit errorChanged();
setState(Failed);
}
return;
}
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if ((msg.relates_to.has_value() && sender)) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
else {
this->deviceId = QString::fromStdString(msg.from_device);
}
}
this->startVerificationRequest();
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationDone,
this,
[this](const mtx::events::msg::KeyVerificationDone &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
nhlog::ui()->info("Flow done on other side");
});
timeout->start(TIMEOUT);
}
QString
DeviceVerificationFlow::state()
{
switch (state_) {
case PromptStartVerification:
return "PromptStartVerification";
case CompareEmoji:
return "CompareEmoji";
case CompareNumber:
return "CompareNumber";
case WaitingForKeys:
return "WaitingForKeys";
case WaitingForOtherToAccept:
return "WaitingForOtherToAccept";
case WaitingForMac:
return "WaitingForMac";
case Success:
return "Success";
case Failed:
return "Failed";
default:
return "";
}
}
void
DeviceVerificationFlow::next()
{
if (sender) {
switch (state_) {
case PromptStartVerification:
sendVerificationRequest();
break;
case CompareEmoji:
case CompareNumber:
sendVerificationMac();
break;
case WaitingForKeys:
case WaitingForOtherToAccept:
case WaitingForMac:
case Success:
case Failed:
nhlog::db()->error("verification: Invalid state transition!");
break;
}
} else {
switch (state_) {
case PromptStartVerification:
if (canonical_json.is_null())
sendVerificationReady();
else // legacy path without request and ready
acceptVerificationRequest();
break;
case CompareEmoji:
[[fallthrough]];
case CompareNumber:
sendVerificationMac();
break;
case WaitingForKeys:
case WaitingForOtherToAccept:
case WaitingForMac:
case Success:
case Failed:
nhlog::db()->error("verification: Invalid state transition!");
break;
}
}
}
QString
DeviceVerificationFlow::getUserId()
{
return QString::fromStdString(this->toClient.to_string());
}
QString
DeviceVerificationFlow::getDeviceId()
{
return this->deviceId;
}
bool
DeviceVerificationFlow::getSender()
{
return this->sender;
}
std::vector<int>
DeviceVerificationFlow::getSasList()
{
return this->sasList;
}
void
DeviceVerificationFlow::setEventId(std::string event_id_)
{
this->relation.rel_type = mtx::common::RelationType::Reference;
this->relation.event_id = event_id_;
this->transaction_id = event_id_;
}
void
DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
std::string)
{
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
if ((std::find(msg.key_agreement_protocols.begin(),
msg.key_agreement_protocols.end(),
"curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
(std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
(std::find(msg.message_authentication_codes.begin(),
msg.message_authentication_codes.end(),
"hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Emoji) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Emoji;
} else if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Decimal) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Decimal;
} else {
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
return;
}
if (!sender)
this->canonical_json = nlohmann::json(msg);
else {
if (utils::localUser().toStdString() < this->toClient.to_string()) {
this->canonical_json = nlohmann::json(msg);
}
}
if (state_ != PromptStartVerification)
this->acceptVerificationRequest();
} else {
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
}
}
//! accepts a verification
void
DeviceVerificationFlow::acceptVerificationRequest()
{
mtx::events::msg::KeyVerificationAccept req;
req.method = mtx::events::msg::VerificationMethods::SASv1;
req.key_agreement_protocol = "curve25519-hkdf-sha256";
req.hash = "sha256";
req.message_authentication_code = "hkdf-hmac-sha256";
if (this->method == mtx::events::msg::SASMethods::Emoji)
req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
else if (this->method == mtx::events::msg::SASMethods::Decimal)
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
req.commitment = mtx::crypto::bin2base64_unpadded(
mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
send(req);
setState(WaitingForKeys);
}
//! responds verification request
void
DeviceVerificationFlow::sendVerificationReady()
{
mtx::events::msg::KeyVerificationReady req;
req.from_device = http::client()->device_id();
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
send(req);
setState(WaitingForKeys);
}
//! accepts a verification
void
DeviceVerificationFlow::sendVerificationDone()
{
mtx::events::msg::KeyVerificationDone req;
send(req);
}
//! starts the verification flow
void
DeviceVerificationFlow::startVerificationRequest()
{
mtx::events::msg::KeyVerificationStart req;
req.from_device = http::client()->device_id();
req.method = mtx::events::msg::VerificationMethods::SASv1;
req.key_agreement_protocols = {"curve25519-hkdf-sha256"};
req.hashes = {"sha256"};
req.message_authentication_codes = {"hkdf-hmac-sha256"};
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal,
mtx::events::msg::SASMethods::Emoji};
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
req.transaction_id = this->transaction_id;
this->canonical_json = nlohmann::json(req);
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.relates_to = this->relation;
this->canonical_json = nlohmann::json(req);
}
send(req);
setState(WaitingForOtherToAccept);
}
//! sends a verification request
void
DeviceVerificationFlow::sendVerificationRequest()
{
mtx::events::msg::KeyVerificationRequest req;
req.from_device = http::client()->device_id();
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
QDateTime currentTime = QDateTime::currentDateTimeUtc();
req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.to = this->toClient.to_string();
req.msgtype = "m.key.verification.request";
req.body = "User is requesting to verify keys with you. However, your client does "
"not support this method, so you will need to use the legacy method of "
"key verification.";
}
send(req);
setState(WaitingForOtherToAccept);
}
//! cancels a verification flow
void
DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
{
mtx::events::msg::KeyVerificationCancel req;
if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
req.code = "m.unknown_method";
req.reason = "unknown method received";
} else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
req.code = "m.mismatched_commitment";
req.reason = "commitment didn't match";
} else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
req.code = "m.mismatched_sas";
req.reason = "sas didn't match";
} else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
req.code = "m.key_match";
req.reason = "keys did not match";
} else if (error_code == DeviceVerificationFlow::Error::Timeout) {
req.code = "m.timeout";
req.reason = "timed out";
} else if (error_code == DeviceVerificationFlow::Error::User) {
req.code = "m.user";
req.reason = "user cancelled the verification";
} else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
req.code = "m.unexpected_message";
req.reason = "received messages out of order";
}
this->error_ = error_code;
emit errorChanged();
this->setState(Failed);
send(req);
}
//! sends the verification key
void
DeviceVerificationFlow::sendVerificationKey()
{
mtx::events::msg::KeyVerificationKey req;
req.key = this->sas->public_key();
send(req);
}
mtx::events::msg::KeyVerificationMac
key_verification_mac(mtx::crypto::SAS *sas,
mtx::identifiers::User sender,
const std::string &senderDevice,
mtx::identifiers::User receiver,
const std::string &receiverDevice,
const std::string &transactionId,
std::map<std::string, std::string> keys)
{
mtx::events::msg::KeyVerificationMac req;
std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
receiver.to_string() + receiverDevice + transactionId;
std::string key_list;
bool first = true;
for (const auto &[key_id, key] : keys) {
req.mac[key_id] = sas->calculate_mac(key, info + key_id);
if (!first)
key_list += ",";
key_list += key_id;
first = false;
}
req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
return req;
}
//! sends the mac of the keys
void
DeviceVerificationFlow::sendVerificationMac()
{
std::map<std::string, std::string> key_list;
key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
// send our master key, if we trust it
if (!this->our_trusted_master_key.empty())
key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
mtx::events::msg::KeyVerificationMac req =
key_verification_mac(sas.get(),
http::client()->user_id(),
http::client()->device_id(),
this->toClient,
this->deviceId.toStdString(),
this->transaction_id,
key_list);
send(req);
setState(WaitingForMac);
acceptDevice();
}
//! Completes the verification flow
void
DeviceVerificationFlow::acceptDevice()
{
if (!isMacVerified) {
setState(WaitingForMac);
} else if (state_ == WaitingForMac) {
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
this->sendVerificationDone();
setState(Success);
}
}
void
DeviceVerificationFlow::unverify()
{
cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
emit refreshProfile();
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
TimelineModel *timelineModel_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString event_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_,
Type::RoomMsg,
timelineModel_,
other_user_,
QString::fromStdString(msg.from_device)));
flow->setEventId(event_id_.toStdString());
if (std::find(msg.methods.begin(),
msg.methods.end(),
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
flow->cancelVerification(UnknownMethod);
}
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString txn_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
flow->transaction_id = txn_id_.toStdString();
if (std::find(msg.methods.begin(),
msg.methods.end(),
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
flow->cancelVerification(UnknownMethod);
}
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
const mtx::events::msg::KeyVerificationStart &msg,
QString other_user_,
QString txn_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
flow->transaction_id = txn_id_.toStdString();
flow->handleStartMessage(msg, "");
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
TimelineModel *timelineModel_,
QString userid)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
flow->sender = true;
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
flow->sender = true;
flow->transaction_id = http::client()->generate_txn_id();
return flow;
}

@ -1,235 +0,0 @@
#pragma once
#include <QObject>
#include <mtx/responses/crypto.hpp>
#include "CacheCryptoStructs.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Olm.h"
#include "timeline/TimelineModel.h"
class QTimer;
using sas_ptr = std::unique_ptr<mtx::crypto::SAS>;
// clang-format off
/*
* Stolen from fluffy chat :D
*
* State | +-------------+ +-----------+ |
* | | AliceDevice | | BobDevice | |
* | | (sender) | | | |
* | +-------------+ +-----------+ |
* promptStartVerify | | | |
* | o | (m.key.verification.request) | |
* | p |-------------------------------->| (ASK FOR VERIFICATION REQUEST) |
* waitForOtherAccept | t | | | promptStartVerify
* && | i | (m.key.verification.ready) | |
* no commitment | o |<--------------------------------| |
* && | n | | |
* no canonical_json | a | (m.key.verification.start) | | waitingForKeys
* | l |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
* | | | | && no canonical_json
* | | m.key.verification.start | |
* waitForOtherAccept | |-------------------------------->| (IF NOT ALREADY ASKED, |
* && | | | ASK FOR VERIFICATION REQUEST) | promptStartVerify, if not accepted
* canonical_json | | m.key.verification.accept | |
* | |<--------------------------------| |
* waitForOtherAccept | | | | waitingForKeys
* && | | m.key.verification.key | | && canonical_json
* commitment | |-------------------------------->| | && commitment
* | | | |
* | | m.key.verification.key | |
* | |<--------------------------------| |
* compareEmoji/Number| | | | compareEmoji/Number
* | | COMPARE EMOJI / NUMBERS | |
* | | | |
* waitingForMac | | m.key.verification.mac | | waitingForMac
* | success |<------------------------------->| success |
* | | | |
* success/fail | | m.key.verification.done | | success/fail
* | |<------------------------------->| |
*/
// clang-format on
class DeviceVerificationFlow : public QObject
{
Q_OBJECT
Q_PROPERTY(QString state READ state NOTIFY stateChanged)
Q_PROPERTY(Error error READ error NOTIFY errorChanged)
Q_PROPERTY(QString userId READ getUserId CONSTANT)
Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
Q_PROPERTY(bool sender READ getSender CONSTANT)
Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT)
public:
enum State
{
PromptStartVerification,
WaitingForOtherToAccept,
WaitingForKeys,
CompareEmoji,
CompareNumber,
WaitingForMac,
Success,
Failed,
};
Q_ENUM(State)
enum Type
{
ToDevice,
RoomMsg
};
enum Error
{
UnknownMethod,
MismatchedCommitment,
MismatchedSAS,
KeyMismatch,
Timeout,
User,
OutOfOrder,
};
Q_ENUM(Error)
static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification(
QObject *parent_,
TimelineModel *timelineModel_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString event_id_);
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
QObject *parent_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString txn_id_);
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
QObject *parent_,
const mtx::events::msg::KeyVerificationStart &msg,
QString other_user_,
QString txn_id_);
static QSharedPointer<DeviceVerificationFlow>
InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent,
QString userid,
QString device);
// getters
QString state();
Error error() { return error_; }
QString getUserId();
QString getDeviceId();
bool getSender();
std::vector<int> getSasList();
QString transactionId() { return QString::fromStdString(this->transaction_id); }
// setters
void setDeviceId(QString deviceID);
void setEventId(std::string event_id);
void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
public slots:
//! unverifies a device
void unverify();
//! Continues the flow
void next();
//! Cancel the flow
void cancel() { cancelVerification(User); }
signals:
void refreshProfile();
void stateChanged();
void errorChanged();
private:
DeviceVerificationFlow(QObject *,
DeviceVerificationFlow::Type flow_type,
TimelineModel *model,
QString userID,
QString deviceId_);
void setState(State state)
{
if (state != state_) {
state_ = state;
emit stateChanged();
}
}
void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
//! sends a verification request
void sendVerificationRequest();
//! accepts a verification request
void sendVerificationReady();
//! completes the verification flow();
void sendVerificationDone();
//! accepts a verification
void acceptVerificationRequest();
//! starts the verification flow
void startVerificationRequest();
//! cancels a verification flow
void cancelVerification(DeviceVerificationFlow::Error error_code);
//! sends the verification key
void sendVerificationKey();
//! sends the mac of the keys
void sendVerificationMac();
//! Completes the verification flow
void acceptDevice();
std::string transaction_id;
bool sender;
Type type;
mtx::identifiers::User toClient;
QString deviceId;
// public part of our master key, when trusted or empty
std::string our_trusted_master_key;
mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
QTimer *timeout = nullptr;
sas_ptr sas;
std::string mac_method;
std::string commitment;
nlohmann::json canonical_json;
std::vector<int> sasList;
UserKeyCache their_keys;
TimelineModel *model_;
mtx::common::RelatesTo relation;
State state_ = PromptStartVerification;
Error error_ = UnknownMethod;
bool isMacVerified = false;
template<typename T>
void send(T msg)
{
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
mtx::requests::ToDeviceMessages<T> body;
msg.transaction_id = this->transaction_id;
body[this->toClient][deviceId.toStdString()] = msg;
http::client()->send_to_device<T>(
this->transaction_id, body, [](mtx::http::RequestErr err) {
if (err)
nhlog::net()->warn(
"failed to send verification to_device message: {} {}",
err->matrix_error.error,
static_cast<int>(err->status_code));
});
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>)
msg.relates_to = this->relation;
(model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
}
nhlog::net()->debug(
"Sent verification step: {} in state: {}",
mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
state().toStdString());
}
};

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

Loading…
Cancel
Save