Compare commits

..

No commits in common. "master" and "move-state" have entirely different histories.

280 changed files with 18862 additions and 37494 deletions

View File

@ -7,24 +7,11 @@
set -eu
FILES=$(find src -type f \( -iname "*.cpp" -o -iname "*.h" \))
FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \))
for f in $FILES
do
clang-format -i "$f"
done;
QMLFORMAT_PATH=$(command -v qmlformat || true)
if [ -n "$QMLFORMAT_PATH" ]; then
QML_FILES=$(find resources -type f -iname "*.qml")
for f in $QML_FILES
do
$QMLFORMAT_PATH -i "$f"
done;
else
echo "qmlformat not found; skipping qml formatting"
fi
git diff --exit-code

View File

@ -1,14 +0,0 @@
#!/usr/bin/env sh
# Runs the license update
# Return codes:
# - 1 there are files to be formatted
# - 0 everything looks fine
set -eu
FILES=$(find src resources/qml -type f \( -iname "*.cpp" -o -iname "*.h" -o -iname "*.qml" \))
reuse addheader --copyright="Nheko Contributors" --license="GPL-3.0-or-later" $FILES
git diff --exit-code

View File

@ -11,13 +11,13 @@ build-gcc7:
variables:
CXX: g++-7
CC: gcc-7
QT_PKG: 512
QT_PKG: 510
TRAVIS_OS_NAME: linux
before_script:
- apt-get update
- apt-get install -y software-properties-common
- add-apt-repository ppa:ubuntu-toolchain-r/test -y
- add-apt-repository ppa:beineri/opt-qt-5.12.9-xenial -y
- add-apt-repository ppa:beineri/opt-qt-5.10.1-xenial -y
- apt-get update && apt-get -y install --no-install-recommends g++-7 build-essential ninja-build qt${QT_PKG}{base,declarative,tools,multimedia,script,quickcontrols2,svg} liblmdb-dev libgl1-mesa-dev libssl-dev git ccache
# need recommended deps for wget
- apt-get -y install wget
@ -37,7 +37,7 @@ build-gcc7:
- cmake -GNinja -H. -Bbuild
-DCMAKE_INSTALL_PREFIX=.deps/usr
-DHUNTER_ROOT=".hunter"
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON -DUSE_BUNDLED_LMDB=OFF
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF
-DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release
-DCI_BUILD=ON
- cmake --build build
@ -88,7 +88,7 @@ build-flatpak-amd64:
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
tags: [docker]
before_script:
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
- apt-get update && apt-get -y install flatpak-builder git python curl
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
@ -96,10 +96,9 @@ build-flatpak-amd64:
- export VERSION=$(git describe)
- mkdir -p build-flatpak
- cd build-flatpak
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.yaml
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json
- flatpak build-bundle repo nheko-amd64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
after_script:
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-amd64.flatpak
cache:
key: "$CI_JOB_NAME"
@ -116,7 +115,7 @@ build-flatpak-arm64:
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
tags: [docker-arm64]
before_script:
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
- apt-get update && apt-get -y install flatpak-builder git python curl
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
@ -124,10 +123,9 @@ build-flatpak-arm64:
- export VERSION=$(git describe)
- mkdir -p build-flatpak
- cd build-flatpak
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for arm64" app ../io.github.NhekoReborn.Nheko.yaml
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for arm64" app ../io.github.NhekoReborn.Nheko.json
- flatpak build-bundle repo nheko-arm64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
after_script:
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-arm64.flatpak
cache:
key: "$CI_JOB_NAME"
@ -143,12 +141,9 @@ linting:
image: alpine:latest
tags: [docker]
before_script:
- apk update && apk add clang make git python3 py3-pip
- export PATH="$PATH:/root/.local/bin"
- pip3 install --user reuse
- apk update && apk add clang make git
script:
- make lint
- make license
appimage-amd64:
stage: build

View File

@ -79,7 +79,6 @@ AppDir:
- libxv1
- libxxf86vm1
- libzstd1
- qml-module-qt-labs-platform
- qml-module-qtgraphicaleffects
- qml-module-qtmultimedia
- qml-module-qtquick-controls2

View File

@ -1,151 +1,5 @@
# Changelog
## [0.8.2] -- 2021-04-23
### Highlights
- Edits
- If you made a typo, just press the `Up` key and edit what you wrote.
- Messages other users edited will get updated automatically and have a small
pen symbol next to them.
- Privacy Screen
- Blur your messages, when Nheko looses focus, which prevents others from
peeking at your messages.
- You can configure the timeout of when this happens.
- Improved notifications (contributed by lorendb)
- No more breakage, because the message included a > on KDE based DEs.
- Render html and images where possible in the notification.
- Render if a message is a reply or someone sent an emote message more nicely
where possible.
- Encrypted notifications now show, that the content is encrypted instead of
being empty.
- Screenshare support in calls on X11 (contributed by trilene)
- Share your screen in a call!
- Select if your mouse cursor should be shown or not and if your webcam should
be included.
- SEND MESSAGES AS RAINBOWS! (contributed by LordMZTE)
- YES MESSAGES, EMOTES AND NOTICES!
### Features
- Set your displayname and avatar from Nheko either globally or per room.
(contributed by jedi18)
- Show room topic in the room settings.
- Double tap a message to reply to it.
- Leave a room using `/part` or `/leave`. (contributed by lorendb)
- Show mxid when hovering a username or avatar.
- Allow opening matrix: uris on Windows.
- Disable room pings caused by replies sent via Nheko (unless you are using
Element Web/Desktop).
### Improvements
- Userprofile can be closed via the Escape key. No more hotel california!
(contributed by lorendb)
- Most dialogs are now centered on the Nheko window. (contributed by lorendb)
- Update Hungarian translations. (contributed by maxigaz)
- Update Estonian translations. (contributed by Priit)
- Update Russian translations. (contributed by Alexey Murz and Artem)
- Update Swedish translations. (contributed by Emilie)
- Update French translations. (contributed by MayeulC, Nicolas Guichard and Carl Schwan)
- Allow drag and drop of files on the whole timeline. (contributed by lorendb)
- Enable notifications on Haiku. (contributed by kallisti5)
- Update scheme handler to the latest matrix: scheme proposal.
- Close completers when typing a space after the colon. (contributed by jedi18)
- Port room settings to Qml. (contributed by jedi18)
- Improved read marker handling. Read marker should now get stuck less often.
- Various changes around hover and tap handling in the timeline, which hopefully
now works more predicatably.
- Buttons in the timeline are now rendered in a box on hover on desktop
platforms.
- Complete room links in the timeline after typing a # character. (contributed
by jedi18)
- An improved quick switcher with better rendering and search. (contributed by jedi18)
- Some fixes around inline emoji and images.
- Jump into new rooms, after you created them. (contrubuted by jedi18)
- Improved search in the emoji picker.
- Allow disabling certificate checks via the config file.
- Use native menus where possible.
- Fix video playback on Windows. (contrubuted by jedi18)
- Send image messages by pressing Enter. (contributed by salahmak)
- Escape closes the upload widget. (contributed by salahmak)
- Improve session rotation and sharing in E2EE rooms.
### Bugfixes
- Emojis joined from separate emojis with a 0xfe0f in the middle should now
render correctly.
- Fix a bug when logging out of a non default profile clearing the wrong
profile. (contrubuted by lorendb)
- Various fixed around profile handling. (contributed by lorendb)
- Focus message input after a reaction. (contributed by jedi18)
- Disable native rendering to prevent kerning bugs on non integer scale factors.
- Fix duplex call devices not showing up. (contributed by trilene)
- Fix a few crashes when leaving a room. (contributed by jedi18)
- Fix hidden tags not updating properly. (contributed by jedi18)
- Fix some issues with login, when a server had SSO as well as password login
enabled (for example matrix.org).
- Properly set the dialog flag for dialogs on most platforms. (Wayland does not
support that.)
- Properly add license to source files.
- Fix fingerprint increasing the minimum window size.
- Don't send markdown links in the plain text body of events when autocompleting
user or room names.
- Fix webcam not working in flatpaks.
- Fix markdown override in replies.
- Fix unsupported events causing errors when saving them. (contributed by
anjanik)
- Fix exif rotation not being respected anymore in E2EE rooms.
- Remove unused qml plugins in the windows package.
- Fix broken olm channels automatically when noticed.
- Fix pasting not overwriting the selection.
- Fix Nheko sometimes overwriting received keys with keys it requested, even if
they have a higher minimum index.
### Packaging changes
- Added xcb dependency on X11 based platforms for screensharing (optional)
- Bumped lmdbxx version from 0.9.14.0 to 1.0.0, which is a BREAKING change. You
can get the new version here: https://github.com/hoytech/lmdbxx/releases
(repo changed)
- Removed tweeny as a dependency.
## [0.8.1] -- 2021-01-27
### Features
- `/plain` and `/md` commands to override the current markdown setting. (contributed by lorendb)
- Allow persistent hiding of rooms with a specific tag (or from a community) via a context menu.
- Allow open media messages in an external program immediately. (contributed by rnhmjoj)
### Improvements
- Use async dbus connection for notifications. (contributed by lorendb)
- Update Hungarian translations. (contributed by maxigaz)
- Update Finnish translations. (contributed by Priit)
- Update Malayalam translations. (contributed by vachan-maker)
- Update Dutch translations. (contributed by Glael)
- Store splitter size across restarts.
- Add a border around the completer. (contributed by lorendb)
- Request keys for messages with unknown message indices (once per restart, when they are shown).
- Move the database location to XDG_DATA_DIR. (contributed by rnhmjoj)
- Reload the timeline after key backup import.
- Autoclose completer on `space`, when there are no matches.
- Make completer only react, when the mouse cursor is moved.
### Bugfixes
- Fix unhandled exception, when a device has no keys.
- Fix some cmake warnings regarding GNUInstallDirs.
- Fix tags being broken. If you have no tags showing up, you may want to logout and login again.
- Fix versionOk being called on the wrong thread. (contributed by Jedi18)
- Fix font tags showing up in media message filenames.
- Fix user profile in dark themes showing the wrong colors. (contributed by lorendb)
- Fix emoji category switching on old Qt versions. (contributed by lorendb)
- Fix old messages being replayed after a limited timeline.
- Fix empty secrets being returned from the wallet breaking verification.
- Make matrix link chat invites create a direct chat.
- Fix focus handling on room change or reply button clicks.
- Fix username completion deleting the character before it.
## [0.8.0] -- 2021-01-21
### Highlights

View File

@ -33,12 +33,14 @@ option(USE_BUNDLED_CMARK "Use the bundled version of cmark."
option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json."
${HUNTER_ENABLED})
option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL."
OFF)
${HUNTER_ENABLED})
option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED})
option(USE_BUNDLED_LMDB "Use the bundled version of lmdb."
${HUNTER_ENABLED})
option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++."
${HUNTER_ENABLED})
option(USE_BUNDLED_TWEENY "Use the bundled version of tweeny."
${HUNTER_ENABLED})
option(USE_BUNDLED_QTKEYCHAIN "Use the bundled version of Qt5Keychain."
${HUNTER_ENABLED})
@ -70,16 +72,16 @@ if(${CMAKE_VERSION} VERSION_LESS "3.14.0")
endmacro()
endif()
# Include Qt basic functions
include(QtCommon)
project(nheko LANGUAGES CXX C)
include(GNUInstallDirs)
# Include Qt basic functions
include(QtCommon)
set(CPACK_PACKAGE_VERSION_MAJOR "0")
set(CPACK_PACKAGE_VERSION_MINOR "8")
set(CPACK_PACKAGE_VERSION_PATCH "2")
set(CPACK_PACKAGE_VERSION_PATCH "0")
set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR})
set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR})
set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH})
@ -127,11 +129,8 @@ endif()
if(USE_BUNDLED_LMDB)
hunter_add_package(lmdb)
find_package(liblmdb CONFIG REQUIRED)
target_include_directories(liblmdb::lmdb INTERFACE
"${HUNTER_INSTALL_PREFIX}/include/lmdb")
else()
find_package(LMDB REQUIRED)
find_package(LMDB)
endif()
#
@ -211,7 +210,7 @@ set(SPDLOG_DEBUG_ON false)
# Windows doesn't handle CMAKE_BUILD_TYPE.
if(NOT WIN32)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
if(${CMAKE_BUILD_TYPE} STREQUAL "Debug")
set(SPDLOG_DEBUG_ON true)
else()
set(SPDLOG_DEBUG_ON false)
@ -258,6 +257,7 @@ set(SRC_FILES
src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp
src/dialogs/ReadReceipts.cpp
src/dialogs/RoomSettings.cpp
# Emoji
src/emoji/EmojiModel.cpp
@ -271,7 +271,6 @@ set(SRC_FILES
src/timeline/TimelineViewManager.cpp
src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp
src/timeline/Permissions.cpp
# UI components
src/ui/Avatar.cpp
@ -282,7 +281,6 @@ set(SRC_FILES
src/ui/InfoMessage.cpp
src/ui/Label.cpp
src/ui/LoadingIndicator.cpp
src/ui/NhekoCursorShape.cpp
src/ui/NhekoDropArea.cpp
src/ui/OverlayModal.cpp
src/ui/OverlayWidget.cpp
@ -296,18 +294,12 @@ set(SRC_FILES
src/ui/ThemeManager.cpp
src/ui/ToggleButton.cpp
src/ui/UserProfile.cpp
src/ui/RoomSettings.cpp
# Generic notification stuff
src/notifications/Manager.cpp
src/AvatarProvider.cpp
src/BlurhashProvider.cpp
src/Cache.cpp
src/CallDevices.cpp
src/CallManager.cpp
src/ChatPage.cpp
src/Clipboard.cpp
src/ColorImageProvider.cpp
src/CommunitiesList.cpp
src/CommunitiesListItem.cpp
@ -321,6 +313,7 @@ set(SRC_FILES
src/MatrixClient.cpp
src/MxcImageProvider.cpp
src/Olm.cpp
src/QuickSwitcher.cpp
src/RegisterPage.cpp
src/RoomInfoListItem.cpp
src/RoomList.cpp
@ -331,7 +324,6 @@ set(SRC_FILES
src/UserInfoWidget.cpp
src/UserSettingsPage.cpp
src/UsersModel.cpp
src/RoomsModel.cpp
src/Utils.cpp
src/WebRTCSession.cpp
src/WelcomePage.cpp
@ -363,13 +355,13 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 5d2f055ea9403770039ddf66b1900f890cd5cde7
GIT_TAG v0.4.0
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(MatrixClient)
else()
find_package(MatrixClient 0.5.1 REQUIRED)
find_package(MatrixClient 0.4.0 REQUIRED)
endif()
if(USE_BUNDLED_OLM)
include(FetchContent)
@ -381,7 +373,7 @@ if(USE_BUNDLED_OLM)
set(OLM_TESTS OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(Olm)
else()
find_package(Olm 3 REQUIRED)
find_package(Olm 3)
set_package_properties(Olm PROPERTIES
DESCRIPTION "An implementation of the Double Ratchet cryptographic ratchet"
URL "https://git.matrix.org/git/olm/about/"
@ -422,18 +414,8 @@ set_package_properties(nlohmann_json PROPERTIES
)
if(USE_BUNDLED_LMDBXX)
include(FetchContent)
FetchContent_Declare(
lmdbxx
URL "https://raw.githubusercontent.com/hoytech/lmdbxx/1.0.0/lmdb++.h"
DOWNLOAD_NO_EXTRACT TRUE
)
if(NOT lmdbxx_POPULATED)
FetchContent_Populate(lmdbxx)
endif()
add_library(lmdbxx INTERFACE)
target_include_directories(lmdbxx INTERFACE ${lmdbxx_SOURCE_DIR})
add_library(lmdbxx::lmdbxx ALIAS lmdbxx)
hunter_add_package(lmdbxx)
find_package(lmdbxx CONFIG REQUIRED)
else()
if(NOT LMDBXX_INCLUDE_DIR)
find_path(LMDBXX_INCLUDE_DIR
@ -449,18 +431,24 @@ else()
add_library(lmdbxx::lmdbxx ALIAS lmdbxx)
endif()
if(USE_BUNDLED_TWEENY)
include(FetchContent)
FetchContent_Declare(
Tweeny
GIT_REPOSITORY https://github.com/mobius3/tweeny.git
GIT_TAG 6a5033372fe53c4c731c66c8a2d56261746cd85c #v3 <- v3 has unfixed warnings
)
FetchContent_MakeAvailable(Tweeny)
else()
find_package(Tweeny REQUIRED)
endif()
include(FindPkgConfig)
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18)
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.16 gstreamer-webrtc-1.0>=1.16)
if (TARGET PkgConfig::GSTREAMER)
add_feature_info(voip ON "GStreamer found. Call support is enabled automatically.")
pkg_check_modules(XCB IMPORTED_TARGET xcb xcb-ewmh)
if (TARGET PkgConfig::XCB)
add_feature_info("Window selection when screen sharing (X11)" ON "XCB-EWMH found. Window selection is enabled when screen sharing (X11).")
else()
add_feature_info("Window selection when screen sharing (X11)" OFF "XCB-EWMH could not be found on your system. Screen sharing (X11) is limited to the entire screen only. To enable window selection, make sure xcb and xcb-ewmh can be found via pkgconfig.")
endif()
else()
add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18 can be found via pkgconfig.")
add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.16 gstreamer-webrtc-1.0>=1.16 can be found via pkgconfig.")
endif()
# single instance functionality
@ -483,6 +471,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/dialogs/RawMessage.h
src/dialogs/ReCaptcha.h
src/dialogs/ReadReceipts.h
src/dialogs/RoomSettings.h
# Emoji
src/emoji/EmojiModel.h
@ -495,7 +484,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/timeline/TimelineViewManager.h
src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h
src/timeline/Permissions.h
# UI components
src/ui/Avatar.h
@ -506,7 +494,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/Label.h
src/ui/FloatingButton.h
src/ui/Menu.h
src/ui/NhekoCursorShape.h
src/ui/NhekoDropArea.h
src/ui/OverlayWidget.h
src/ui/SnackBar.h
@ -519,18 +506,14 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/Theme.h
src/ui/ThemeManager.h
src/ui/UserProfile.h
src/ui/RoomSettings.h
src/notifications/Manager.h
src/AvatarProvider.h
src/BlurhashProvider.h
src/Cache_p.h
src/CacheCryptoStructs.h
src/CallDevices.h
src/CallManager.h
src/ChatPage.h
src/Clipboard.h
src/CommunitiesList.h
src/CommunitiesListItem.h
src/CompletionProxyModel.h
@ -539,6 +522,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/LoginPage.h
src/MainWindow.h
src/MxcImageProvider.h
src/QuickSwitcher.h
src/RegisterPage.h
src/RoomInfoListItem.h
src/RoomList.h
@ -549,7 +533,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/UserInfoWidget.h
src/UserSettingsPage.h
src/UsersModel.h
src/RoomsModel.h
src/WebRTCSession.h
src/WelcomePage.h
src/popups/PopupItem.h
@ -565,7 +548,7 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
if (APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp 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()
@ -608,9 +591,6 @@ if(APPLE)
elseif(WIN32)
target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN)
target_link_libraries (nheko PRIVATE ${NTDLIB} Qt5::WinMain)
if(MSVC)
target_compile_options(nheko PUBLIC "/Zc:__cplusplus")
endif()
else()
target_link_libraries (nheko PRIVATE Qt5::DBus)
endif()
@ -639,6 +619,7 @@ target_link_libraries(nheko PRIVATE
nlohmann_json::nlohmann_json
lmdbxx::lmdbxx
liblmdb::lmdb
tweeny
SingleApplication::SingleApplication)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
@ -652,10 +633,6 @@ endif()
if (TARGET PkgConfig::GSTREAMER)
target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER)
target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE)
if (TARGET PkgConfig::XCB)
target_link_libraries(nheko PRIVATE PkgConfig::XCB)
target_compile_definitions(nheko PRIVATE XCB_AVAILABLE)
endif()
endif()
if(MSVC)
@ -668,7 +645,7 @@ if(QML_DEBUGGING)
endif()
if(NOT MSVC AND NOT HAIKU)
if(NOT MSVC)
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR CI_BUILD)
target_compile_options(nheko PRIVATE "-Werror")
endif()

View File

@ -41,9 +41,6 @@ macos-app-install:
lint:
./.ci/format.sh
license:
./.ci/licenses.sh
image:
docker build -t nheko-app-image .

View File

@ -2,9 +2,8 @@ nheko
----
[![Build Status](https://nheko.im/nheko-reborn/nheko/badges/master/pipeline.svg)](https://nheko.im/nheko-reborn/nheko/-/pipelines/latest)
[![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.8.2-RC)
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.8.0)
[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/)
<a href='https://flatpak.neko.dev/repo/nightly/appstream/io.github.NhekoReborn.Nheko.flatpakref' download='nheko-nightly.flatpakref'><img alt='Download Nightly Flatpak' src='https://img.shields.io/badge/download-flatpak--nightly-green'/></a>
[![#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)
<a href='https://flathub.org/apps/details/io.github.NhekoReborn.Nheko'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
@ -50,7 +49,7 @@ Specifically there is support for:
### Releases
Releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer)
can be found in the [GitHub releases](https://github.com/Nheko-Reborn/nheko/releases).
can be found in the [Github releases](https://github.com/Nheko-Reborn/nheko/releases).
### Repositories
@ -75,7 +74,7 @@ sudo dnf install nheko
#### Gentoo Linux
```bash
sudo eselect repository enable guru
sudo eselect repository enable matrix
sudo emerge -a nheko
```
@ -115,29 +114,14 @@ with [homebrew](https://brew.sh/):
brew install --cask nheko
```
#### Windows
with [Chocolatey](https://chocolatey.org/):
```posh
choco install nheko-reborn
```
### FAQ
##
**Q:** Why don't videos run for me on Windows?
**A:** You're probably missing the required video codecs, download [K-Lite Codec Pack](https://codecguide.com/download_kl.htm).
##
### Build Requirements
- Qt5 (5.12 or greater). Required for overlapping hover handlers in Qml.
- Qt5 (5.10 or greater). Qt 5.7 adds support for color font rendering with
Freetype, which is essential to properly support emoji, 5.8 adds some features
to make interopability with Qml easier, 5.10 makes sliders actually visible with different palettes.
- CMake 3.15 or greater. (Lower version may work, but may break boost linking)
- [mtxclient](https://github.com/Nheko-Reborn/mtxclient)
- [LMDB](https://symas.com/lightning-memory-mapped-database/)
- [lmdb++](https://github.com/hoytech/lmdbxx)
- [cmark](https://github.com/commonmark/cmark) 0.29 or greater.
- Boost 1.70 or greater.
- [libolm](https://gitlab.matrix.org/matrix-org/olm)
@ -158,7 +142,7 @@ choco install nheko-reborn
Nheko can use bundled version for most of those libraries automatically, if the versions in your distro are too old.
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`
You can select which bundled dependencies you want to use by passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter. (The exception to that is OpenSSL, which is always disabled by default.)
You can select which bundled dependencies you want to use by 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:
@ -175,8 +159,6 @@ The bundle flags are currently:
- USE_BUNDLED_LMDBXX
- USE_BUNDLED_TWEENY
A note on bundled OpenSSL: You need to explicitly enable it and it will not be using your system certificate directory by default, if you enable it. You need to override that at runtime with the SSL_CERT_FILE variable. On Windows it will still be using your system certificates though, since it loads them from the system store instead of the OpenSSL directory.
#### 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...
@ -211,7 +193,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
```bash
# 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,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev
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} qt5keychain-dev
```
This will install all dependencies, except for tweeny (use bundled tweeny)
and mtxclient (needs to be build separately).
@ -223,8 +205,8 @@ and mtxclient (needs to be build separately).
```bash
sudo apt install cmake gcc make automake liblmdb-dev \
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev \
qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform\
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 \
qt5keychain-dev
```

View File

@ -15,7 +15,7 @@ environment:
cache:
- c:\hunter\ -> appveyor.yml
- c:\hunter\
- build\_deps -> appveyor.yml
build:
@ -23,16 +23,15 @@ build:
install:
- set QT_DIR=C:\Qt\5.15\msvc2019_64
- set PATH=Path=C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin;C:\Program Files\Git\cmd;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\PowerShell\7\;C:\Program Files\7-Zip;C:\Program Files\Microsoft\Web Platform Installer\;C:\Tools\NuGet;C:\Tools\PsTools;C:\Program Files\Git\usr\bin;C:\Program Files\Git LFS;C:\Program Files\Mercurial\;C:\Program Files (x86)\Subversion\bin;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files\dotnet\;C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\150;C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin;C:\Tools\xUnit;C:\Tools\xUnit20;C:\Tools\NUnit\bin;C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\Extensions\TestPlatform;C:\Ruby193\bin;C:\Tools\WebDriver;C:\Python27;C:\Python27\Scripts;C:\Program Files\erl10.7\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files (x86)\Microsoft DirectX SDK;C:\Program Files\Microsoft Service Fabric\bin\Fabric\Fabric.Code;C:\Program Files\Microsoft SDKs\Service Fabric\Tools\ServiceFabricLocalClusterManager;C:\Tools\Doxygen;C:\Program Files (x86)\CMake\bin;C:\ProgramData\chocolatey\bin;C:\Tools\vcpkg;C:\Tools\Coverity\bin;C:\Program Files (x86)\NSIS;C:\Tools\Octopus;C:\Program Files\Meson\;C:\Tools\GitVersion;C:\Tools\NUnit3\bin;C:\Users\appveyor\AppData\Local\Microsoft\WindowsApps;C:\Users\appveyor\.dotnet\tools;C:\Program Files\AppVeyor\BuildAgent\
- set PATH=C:\Strawberry\perl\bin;C:\Python39-x64;%QT_DIR%\bin;%PATH%
- set PATH=C:\Strawberry\perl\bin;%PATH%;%QT_DIR%\bin
- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
build_script:
# VERSION format: branch-master/branch-1.2
# INSTVERSION format: x.y.z
# WINVERSION format: 9999.0.0.123/1.2.0.234
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.8.2
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.8.2
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.8.0
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.8.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%
# VERSION format: v1.2.3/v1.3.4
@ -47,9 +46,10 @@ build_script:
- echo %DATE%
# Build nheko
#- cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild
- cmake -G "Visual Studio 16 2019" -A x64 -H. -Bbuild
-DHUNTER_ROOT="C:\hunter"
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF
-DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release
- cmake --build build --config Release
@ -61,7 +61,7 @@ after_build:
- mkdir NhekoRelease
- copy build\Release\nheko.exe NhekoRelease\nheko.exe
- copy build\_deps\cmark-build\src\Release\cmark.dll NhekoRelease\cmark.dll
- windeployqt --qmldir resources\qml\ NhekoRelease\nheko.exe
- windeployqt --qmldir %QT_DIR%\qml\ --release NhekoRelease\nheko.exe
- 7z a nheko_win_64.zip .\NhekoRelease\*
- ls -lh build\Release\
@ -77,19 +77,25 @@ after_build:
- mkdir installer\packages\io.github.nhekoreborn.nheko
- mkdir installer\packages\io.github.nhekoreborn.nheko\data
- mkdir installer\packages\io.github.nhekoreborn.nheko\meta
- mkdir installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
# Copy installer data
- copy %BUILD%\resources\nheko.ico 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\io.github.nhekoreborn.nheko.cleanup\meta\license.txt
- copy %BUILD%\deploy\installer\config.xml 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\gui\package.xml installer\packages\io.github.nhekoreborn.nheko\meta
- copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\io.github.nhekoreborn.nheko\meta
- copy %BUILD%\deploy\installer\cleanup\package.xml installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
- copy %BUILD%\deploy\installer\cleanup\installscript.qs installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
# Amend version and date
- sed -i "s/__VERSION__/0.8.2/" installer\config\config.xml
- sed -i "s/__VERSION__/0.8.2/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
- sed -i "s/__VERSION__/0.8.0/" installer\config\config.xml
- sed -i "s/__VERSION__/0.8.0/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
- sed -i "s/__VERSION__/0.8.0/" installer\packages\io.github.nhekoreborn.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\io.github.nhekoreborn.nheko.cleanup\meta\package.xml
# Copy nheko data
- xcopy NhekoData\*.* installer\packages\io.github.nhekoreborn.nheko\data\*.* /s /e /c /y
- move NhekoRelease\nheko.exe installer\packages\io.github.nhekoreborn.nheko\data
@ -101,13 +107,13 @@ after_build:
- set PATH=%BUILD%\tools\bin;%PATH%
- binarycreator.exe -f -c installer\config\config.xml -p installer\packages nheko-installer.exe
- copy nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe
- copy nheko-installer.exe nheko-%APPVEYOR_PULL_REQUEST_HEAD_COMMIT%-installer.exe
- ps: .\.ci\upload-nightly.ps1
- mv nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe
on_success:
- if "%APPVEYOR_REPO_TAG%" == "true" (curl -T nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe -uredsky17:%BINTRAY_APIKEY% https://api.bintray.com/content/nheko-reborn/nheko/%APPVEYOR_REPO_TAG_NAME%/nheko/%APPVEYOR_REPO_TAG_NAME%/)
before_deploy:
- ps: .\.ci\upload-nightly.ps1
deploy:
- description: "Development builds"
provider: GitHub
@ -122,4 +128,3 @@ deploy:
artifacts:
- path: nheko_win_64.zip
- path: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
- path: nheko-$(APPVEYOR_PULL_REQUEST_HEAD_COMMIT)-installer.exe

View File

@ -0,0 +1,28 @@
function Component()
{
}
Component.prototype.createOperations = function()
{
component.createOperations();
try
{
if( installer.value("os") === "win" )
{
/**
* Cleanup AppData and registry
*/
component.addElevatedOperation("Execute","UNDOEXECUTE","cmd /C reg delete HKEY_CURRENT_USER\Software\nheko\nheko /f");
var localappdata = installer.environmentVariable("LOCALAPPDATA");
if( localappdata != "" )
{
component.addElevatedOperation("Execute","UNDOEXECUTE","cmd /C rmdir "+localappdata+"\nheko /f");
}
}
}
catch( e )
{
print( e );
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package>
<DisplayName>Cleanup AppData and Registry</DisplayName>
<Description>Cleans up AppData and Registry when selected (logs you out) - Broken</Description>
<Version>__VERSION__</Version>
<ReleaseDate>__DATE__</ReleaseDate>
<SortingPriority>80</SortingPriority>
<Script>installscript.qs</Script>
<Checkable>false</Checkable>
</Package>

View File

@ -23,18 +23,6 @@ Component.prototype.createOperations = function()
component.addOperation( "CreateShortcut", "@TargetDir@\\nheko.exe", "@DesktopDir@\\nheko.lnk",
"workingDirectory=@TargetDir@", "iconPath=@TargetDir@\\nheko.exe",
"iconId=0", "description=Desktop client for the Matrix protocol");
var reg = installer.environmentVariable("SystemRoot") + "\\System32\\reg.exe";
var key= "HKEY_CLASSES_ROOT\\matrix";
component.addOperation("Execute", reg, "ADD", key, "/f", "/t", "REG_SZ", "/d", "URL:Matrix Protocol");
component.addOperation("Execute", reg, "ADD", key, "/f", "/v", "URL Protocol", "/t", "REG_SZ");
var iconkey = "HKEY_CLASSES_ROOT\\matrix\\DefaultIcon";
component.addOperation("Execute", reg, "ADD", iconkey, "/f", "/t", "REG_SZ", "/d", "@TargetDir@\\nheko.exe,1");
component.addOperation("Execute", reg, "ADD", "HKEY_CLASSES_ROOT\\matrix\\shell", "/f");
component.addOperation("Execute", reg, "ADD", "HKEY_CLASSES_ROOT\\matrix\\shell\\open", "/f");
var commandkey = "HKEY_CLASSES_ROOT\\matrix\\shell\\open\\command"
component.addOperation("Execute", reg, "ADD", commandkey, "/f");
component.addOperation("Execute", reg, "ADD", commandkey, "/f", "/t", "REG_SZ", "/d", "\"@TargetDir@\\nheko.exe\" \"%1\"");
}
}
catch( e )

View File

@ -0,0 +1,269 @@
{
"id": "io.github.NhekoReborn.Nheko",
"command": "nheko",
"runtime": "org.kde.Platform",
"runtime-version": "5.15",
"sdk": "org.kde.Sdk",
"rename-icon": "nheko",
"rename-desktop-file": "nheko.desktop",
"rename-appdata-file": "nheko.appdata.xml",
"finish-args": [
"--device=dri",
"--share=ipc",
"--share=network",
"--socket=pulseaudio",
"--socket=wayland",
"--socket=x11",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.StatusNotifierWatcher"
],
"cleanup": [
"/include",
"/bin/mdb*",
"*.a"
],
"build-options" : {
"arch": {
"aarch64": {
"cxxflags": "-DBOOST_ASIO_DISABLE_EPOLL"
}
}
},
"modules": [
{
"name": "lmdb",
"sources": [
{
"sha256": "f3927859882eb608868c8c31586bb7eb84562a40a6bf5cc3e13b6b564641ea28",
"type": "archive",
"url": "https://github.com/LMDB/lmdb/archive/LMDB_0.9.22.tar.gz"
}
],
"make-install-args": [
"prefix=/app"
],
"no-autogen": true,
"subdir": "libraries/liblmdb"
},
{
"name": "cmark",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DCMARK_TESTS=OFF"
],
"sources": [
{
"sha256": "2558ace3cbeff85610de3bda32858f722b359acdadf0c4691851865bb84924a6",
"type": "archive",
"url": "https://github.com/commonmark/cmark/archive/0.29.0.tar.gz"
}
]
},
{
"name": "spdlog",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DSPDLOG_BUILD_EXAMPLES=0",
"-DSPDLOG_BUILD_BENCH=0",
"-DSPDLOG_BUILD_TESTING=0"
],
"sources": [
{
"sha256": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb",
"type": "archive",
"url": "https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz"
}
]
},
{
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release"
],
"buildsystem": "cmake-ninja",
"name": "olm",
"sources": [
{
"commit": "6753595300767dd70150831dbbe6f92d64e75038",
"disable-shallow-clone": true,
"tag": "3.1.4",
"type": "git",
"url": "https://gitlab.matrix.org/matrix-org/olm.git"
}
]
},
{
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DBUILD_TEST_APPLICATION=OFF",
"-DQTKEYCHAIN_STATIC=ON"
],
"buildsystem": "cmake-ninja",
"name": "QtKeychain",
"sources": [
{
"commit": "815fe610353ff8ad7e2f1121c368a74df8db5eb7",
"tag": "v0.12.0",
"type": "git",
"url": "https://github.com/frankosterfeld/qtkeychain.git"
}
]
},
{
"config-opts":[
"-DJSON_BuildTests=OFF"
],
"buildsystem":"cmake",
"name": "nlohmann",
"sources":[
{
"sha256": "d51a3a8d3efbb1139d7608e28782ea9efea7e7933157e8ff8184901efd8ee760",
"type": "archive",
"url": "https://github.com/nlohmann/json/archive/v3.7.0.tar.gz"
}
]
},
{
"build-commands": [
"./bootstrap.sh --with-libraries=thread,system,iostreams --prefix=/app",
"./b2 -d0 variant=release link=static threading=multi --layout=system",
"./b2 -d0 install"
],
"buildsystem": "simple",
"name": "boost",
"sources": [
{
"sha256": "59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722",
"type": "archive",
"url": "https://sourceforge.net/projects/boost/files/boost/1.72.0/boost_1_72_0.tar.bz2"
}
]
},
{
"buildsystem": "meson",
"name": "gstreamer",
"sources": [
{
"commit": "a42fe476d3ee5576921f67a331464065ec33b9a4",
"tag": "1.18.3",
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git"
}
]
},
{
"config-opts": [
"-Dcompositor=enabled",
"-Dgl=enabled"
],
"buildsystem": "meson",
"name": "gstreamer-plugins-base",
"sources": [
{
"commit": "2cc319ee13f6b72df3d432b7c75aca81feb260e5",
"tag": "1.18.3",
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git"
}
]
},
{
"config-opts": [
"-Dpulse=enabled",
"-Dqt5=enabled",
"-Drtp=enabled",
"-Drtpmanager=enabled",
"-Dvpx=enabled"
],
"buildsystem": "meson",
"name": "gstreamer-plugins-good",
"sources": [
{
"commit": "e816c6cd73c9e0676828c9e227a049ebad3d019f",
"tag": "1.18.3",
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git"
}
]
},
{
"config-opts": [
"-Ddtls=enabled",
"-Dgl=enabled",
"-Dopenh264=enabled",
"-Dopus=enabled",
"-Dsrtp=enabled",
"-Dwebrtc=enabled",
"-Dflite=disabled"
],
"buildsystem": "meson",
"name": "gstreamer-plugins-bad",
"sources": [
{
"commit": "382e373d9be363f1e21b12990a4d12f1ecb6df41",
"tag": "1.18.3",
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git"
}
]
},
{
"config-opts": [
"-DBUILD_LIB_TESTS=OFF",
"-DBUILD_LIB_EXAMPLES=OFF",
"-DCMAKE_BUILD_TYPE=Release",
"-DBUILD_SHARED_LIBS=OFF"
],
"buildsystem": "cmake-ninja",
"name": "mtxclient",
"sources": [
{
"commit": "2d6e3f79917ce2065a54ca32e6a9f9d42c0b6347",
"tag": "v0.4.0",
"type": "git",
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
}
]
},
{
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DTWEENY_BUILD_DOCUMENTATION=OFF",
"-DTWEENY_BUILD_EXAMPLES=OFF"
],
"buildsystem": "cmake-ninja",
"name": "tweeny",
"sources": [
{
"sha256": "482857256a7235646004682912badb6521d361ed6987c8ebdae7986bf64ce694",
"type": "archive",
"url": "https://github.com/mobius3/tweeny/archive/43f4130f7e4a67c19d870b60864bc2862c19b81f.tar.gz"
}
]
},
{
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DLMDBXX_INCLUDE_DIR=.deps/lmdbxx",
"-DCOMPILE_QML=ON"
],
"buildsystem": "cmake-ninja",
"name": "nheko",
"sources": [
{
"path": ".",
"type": "dir",
"skip": ["build-flatpak"]
},
{
"dest": ".deps/lmdbxx",
"sha256": "93721132bbf5045d38ad62de2997655e9984c48ea5c9886746d42128f4b26fbd",
"type": "archive",
"url": "https://github.com/bendiken/lmdbxx/archive/0b43ca87d8cfabba392dfe884eb1edb83874de02.tar.gz"
}
]
}
]
}

View File

@ -1,180 +0,0 @@
id: io.github.NhekoReborn.Nheko
command: nheko
runtime: org.kde.Platform
runtime-version: '5.15'
sdk: org.kde.Sdk
rename-icon: nheko
rename-desktop-file: nheko.desktop
rename-appdata-file: nheko.appdata.xml
finish-args:
- --device=dri
# needed for webcams, see #517
- --device=all
- --share=ipc
- --share=network
- --socket=pulseaudio
- --socket=wayland
- --socket=x11
- --talk-name=org.freedesktop.Notifications
- --talk-name=org.freedesktop.secrets
- --talk-name=org.freedesktop.StatusNotifierItem
- --talk-name=org.kde.*
cleanup:
- /include
- /bin/mdb*
- '*.a'
build-options:
arch:
aarch64:
cxxflags: -DBOOST_ASIO_DISABLE_EPOLL
modules:
- name: lmdb
sources:
- sha256: f3927859882eb608868c8c31586bb7eb84562a40a6bf5cc3e13b6b564641ea28
type: archive
url: https://github.com/LMDB/lmdb/archive/LMDB_0.9.22.tar.gz
make-install-args:
- prefix=/app
no-autogen: true
subdir: libraries/liblmdb
- name: cmark
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMARK_TESTS=OFF
sources:
- sha256: 2558ace3cbeff85610de3bda32858f722b359acdadf0c4691851865bb84924a6
type: archive
url: https://github.com/commonmark/cmark/archive/0.29.0.tar.gz
- name: spdlog
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DSPDLOG_BUILD_EXAMPLES=0
- -DSPDLOG_BUILD_BENCH=0
- -DSPDLOG_BUILD_TESTING=0
sources:
- sha256: 5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb
type: archive
url: https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz
- config-opts:
- -DCMAKE_BUILD_TYPE=Release
buildsystem: cmake-ninja
name: olm
sources:
- commit: 6753595300767dd70150831dbbe6f92d64e75038
disable-shallow-clone: true
tag: 3.1.4
type: git
url: https://gitlab.matrix.org/matrix-org/olm.git
- config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TEST_APPLICATION=OFF
- -DQTKEYCHAIN_STATIC=ON
buildsystem: cmake-ninja
name: QtKeychain
sources:
- commit: 815fe610353ff8ad7e2f1121c368a74df8db5eb7
tag: v0.12.0
type: git
url: https://github.com/frankosterfeld/qtkeychain.git
- config-opts:
- -DJSON_BuildTests=OFF
buildsystem: cmake
name: nlohmann
sources:
- sha256: d51a3a8d3efbb1139d7608e28782ea9efea7e7933157e8ff8184901efd8ee760
type: archive
url: https://github.com/nlohmann/json/archive/v3.7.0.tar.gz
- build-commands:
- ./bootstrap.sh --with-libraries=thread,system,iostreams --prefix=/app
- ./b2 -d0 variant=release link=static threading=multi --layout=system
- ./b2 -d0 install
buildsystem: simple
name: boost
sources:
- sha256: 59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722
type: archive
url: https://sourceforge.net/projects/boost/files/boost/1.72.0/boost_1_72_0.tar.bz2
- buildsystem: meson
name: gstreamer
sources:
- commit: a42fe476d3ee5576921f67a331464065ec33b9a4
tag: 1.18.3
type: git
url: https://gitlab.freedesktop.org/gstreamer/gstreamer.git
- config-opts:
- -Dcompositor=enabled
- -Dgl=enabled
buildsystem: meson
name: gstreamer-plugins-base
sources:
- commit: 2cc319ee13f6b72df3d432b7c75aca81feb260e5
tag: 1.18.3
type: git
url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git
- config-opts:
- -Dpulse=enabled
- -Dqt5=enabled
- -Drtp=enabled
- -Drtpmanager=enabled
- -Dvpx=enabled
buildsystem: meson
name: gstreamer-plugins-good
sources:
- commit: e816c6cd73c9e0676828c9e227a049ebad3d019f
tag: 1.18.3
type: git
url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git
- config-opts:
- -Ddtls=enabled
- -Dgl=enabled
- -Dopenh264=enabled
- -Dopus=enabled
- -Dsrtp=enabled
- -Dwebrtc=enabled
- -Dflite=disabled
buildsystem: meson
name: gstreamer-plugins-bad
sources:
- commit: 382e373d9be363f1e21b12990a4d12f1ecb6df41
tag: 1.18.3
type: git
url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git
- config-opts:
- -DBUILD_LIB_TESTS=OFF
- -DBUILD_LIB_EXAMPLES=OFF
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_SHARED_LIBS=OFF
buildsystem: cmake-ninja
name: mtxclient
sources:
- commit: 5d2f055ea9403770039ddf66b1900f890cd5cde7
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DTWEENY_BUILD_DOCUMENTATION=OFF
- -DTWEENY_BUILD_EXAMPLES=OFF
buildsystem: cmake-ninja
name: tweeny
sources:
- sha256: 482857256a7235646004682912badb6521d361ed6987c8ebdae7986bf64ce694
type: archive
url: https://github.com/mobius3/tweeny/archive/43f4130f7e4a67c19d870b60864bc2862c19b81f.tar.gz
- config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DLMDBXX_INCLUDE_DIR=.deps/lmdbxx
- -DCOMPILE_QML=ON
buildsystem: cmake-ninja
name: nheko
sources:
- path: .
type: dir
skip:
- build-flatpak
- dest: .deps/lmdbxx
sha256: 5e12eb3aefe9050068af7df2c663edabc977ef34c9e7ba7b9d2c43e0ad47d8df
type: archive
url: https://github.com/hoytech/lmdbxx/archive/1.0.0.tar.gz

View File

@ -1,10 +0,0 @@
[Flatpak Ref]
Title=Nheko Nightly
Name=io.github.NhekoReborn.Nheko
Branch=master
Url=https://flatpak.neko.dev/repo/nightly
Homepage=https://nheko-reborn.github.io/
Icon=https://nheko.im/nheko-reborn/nheko/-/raw/master/resources/nheko.svg
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
IsRuntime=false
GPGKey=mDMEXENMphYJKwYBBAHaRw8BAQdAqn+Eo42lPoGpJ5HaOf4nFGfxR0QtOggJTCfsdbOyL4e0Kk5pY29sYXMgV2VybmVyIDxuaWNvbGFzLndlcm5lckBob3RtYWlsLmRlPoiWBBMWCAA+FiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAlxDTVUCGwMFCQtJjooFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQkgauGyMeBbs2rQD/dAEoOGT21BL85A8LmPK743EboBAjoRbWcI1hHnvS28AA/3b3HYGwgvTC6hQLyz75zjpeO5ZaUtbezRyDUR4xabMAtCROaWNvbGFzIFdlcm5lciA8bmljb2xhc0BuZWtvZGV2Lm5ldD6IlgQTFggAPhYhBNWLRiQlpqNxJcb+25IGrhsjHgW7BQJcQ01GAhsDBQkLSY6KBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJIGrhsjHgW7GxwBANT4gL03Uu9N5KmTBihC7B71+0r7M/azPbUh86NthCeIAQCF2JXa0axBKhgQF5fWC5ncL+m8ZpH7C5rzDqVgO82WALQnTmljb2xhcyBXZXJuZXIgPG5pY29sYXMud2VybmVyQGdteC5uZXQ+iJYEExYIAD4WIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbAwUJC0mOigULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCSBq4bIx4FuxU5APoCRDYlJW0oTsJs3lcTTB5Nsqb3X4iCEDCjIgsA3wtsIwEAlGBzD8ElCYi2+8m8esSRNlmpRcGoqgXbceLxPUXFpQu4OARcQ0ymEgorBgEEAZdVAQUBAQdAD8dBmT3iqrqdlxSw90L0SIH11fVxiX9MdWfBkTi6PzUDAQgHiH4EGBYIACYWIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbDAUJC0mOigAKCRCSBq4bIx4Fu/LNAQDhH64IBic6h7H3uvtSAFT4xNn7Epobt2baIaDp7uKsQQEAyI+oc5dLknABwIOMrQQuZCmGejx9e4/8HEqLCdszhgG4MwRgNICHFgkrBgEEAdpHDwEBB0DR9eFFzfR62FIi7g+txcQenLvKFzhlyTz0wo3icOy6RYj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0gIcCGwIFCQlmAYAAgQkQkgauGyMeBbt2IAQZFggAHRYhBGz14re9h4cNPaFEKMjXXmEHc/LZBQJgNICHAAoJEMjXXmEHc/LZhVMBAPdYRspdeFh6E9BDxGubT705e/pZFdCHjCToDyxgdW5KAP9sU0hFI5VDHD1h98RzxSt7hc3jxyPSzbG1MBUJ9gbfCVhcAPsFfeZc3v5UBgmn4uICFEGjlzAWCQ7WctE6QTSkY5aL/wD9ETJH5lB+i/8km/sOBKQozXR0yHHw46gB6ZWMeN1wfgq4MwRgNPutFgkrBgEEAdpHDwEBB0APwMn0FJmnAds8IO8iCl/RHr7fz8xnpGd7E4zVgCNZpIj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0+60CGwIFCQANLwAAgQkQkgauGyMeBbt2IAQZFggAHRYhBAH7QBkzNfVIZJM93RNnXzGtBKQcBQJgNPutAAoJEBNnXzGtBKQcHnUA/0E2H5sxmfZ+EWFTso3X4NWu3uN2xF+MdNaY8C72f9H6AP91XaNmlB9gV61rg6wcB5E/j0998yWS9gltY1XY1ImqDPvlAP4sHFs5zuDazgKYxZ/kFhENCgEStdpnvJjt/DxmQPVT3AD/QK5vGoMTIeYjihv0QCnnRDfboTTZHlaEqJW8i02PQww=

View File

@ -1,8 +0,0 @@
[Flatpak Repo]
Title=Nheko Nightly
Url=https://flatpak.neko.dev/repo/nightly
Homepage=https://nheko.im/
Comment=Nheko nightly release repository
Description=Nheko nightly release repository
Icon=https://nheko.im/nheko-reborn/nheko/-/raw/master/resources/nheko.svg
GPGKey=mDMEXENMphYJKwYBBAHaRw8BAQdAqn+Eo42lPoGpJ5HaOf4nFGfxR0QtOggJTCfsdbOyL4e0Kk5pY29sYXMgV2VybmVyIDxuaWNvbGFzLndlcm5lckBob3RtYWlsLmRlPoiWBBMWCAA+FiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAlxDTVUCGwMFCQtJjooFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQkgauGyMeBbs2rQD/dAEoOGT21BL85A8LmPK743EboBAjoRbWcI1hHnvS28AA/3b3HYGwgvTC6hQLyz75zjpeO5ZaUtbezRyDUR4xabMAtCROaWNvbGFzIFdlcm5lciA8bmljb2xhc0BuZWtvZGV2Lm5ldD6IlgQTFggAPhYhBNWLRiQlpqNxJcb+25IGrhsjHgW7BQJcQ01GAhsDBQkLSY6KBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJIGrhsjHgW7GxwBANT4gL03Uu9N5KmTBihC7B71+0r7M/azPbUh86NthCeIAQCF2JXa0axBKhgQF5fWC5ncL+m8ZpH7C5rzDqVgO82WALQnTmljb2xhcyBXZXJuZXIgPG5pY29sYXMud2VybmVyQGdteC5uZXQ+iJYEExYIAD4WIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbAwUJC0mOigULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCSBq4bIx4FuxU5APoCRDYlJW0oTsJs3lcTTB5Nsqb3X4iCEDCjIgsA3wtsIwEAlGBzD8ElCYi2+8m8esSRNlmpRcGoqgXbceLxPUXFpQu4OARcQ0ymEgorBgEEAZdVAQUBAQdAD8dBmT3iqrqdlxSw90L0SIH11fVxiX9MdWfBkTi6PzUDAQgHiH4EGBYIACYWIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbDAUJC0mOigAKCRCSBq4bIx4Fu/LNAQDhH64IBic6h7H3uvtSAFT4xNn7Epobt2baIaDp7uKsQQEAyI+oc5dLknABwIOMrQQuZCmGejx9e4/8HEqLCdszhgG4MwRgNICHFgkrBgEEAdpHDwEBB0DR9eFFzfR62FIi7g+txcQenLvKFzhlyTz0wo3icOy6RYj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0gIcCGwIFCQlmAYAAgQkQkgauGyMeBbt2IAQZFggAHRYhBGz14re9h4cNPaFEKMjXXmEHc/LZBQJgNICHAAoJEMjXXmEHc/LZhVMBAPdYRspdeFh6E9BDxGubT705e/pZFdCHjCToDyxgdW5KAP9sU0hFI5VDHD1h98RzxSt7hc3jxyPSzbG1MBUJ9gbfCVhcAPsFfeZc3v5UBgmn4uICFEGjlzAWCQ7WctE6QTSkY5aL/wD9ETJH5lB+i/8km/sOBKQozXR0yHHw46gB6ZWMeN1wfgq4MwRgNPutFgkrBgEEAdpHDwEBB0APwMn0FJmnAds8IO8iCl/RHr7fz8xnpGd7E4zVgCNZpIj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0+60CGwIFCQANLwAAgQkQkgauGyMeBbt2IAQZFggAHRYhBAH7QBkzNfVIZJM93RNnXzGtBKQcBQJgNPutAAoJEBNnXzGtBKQcHnUA/0E2H5sxmfZ+EWFTso3X4NWu3uN2xF+MdNaY8C72f9H6AP91XaNmlB9gV61rg6wcB5E/j0998yWS9gltY1XY1ImqDPvlAP4sHFs5zuDazgKYxZ/kFhENCgEStdpnvJjt/DxmQPVT3AD/QK5vGoMTIeYjihv0QCnnRDfboTTZHlaEqJW8i02PQww=

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 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

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

View File

@ -13,23 +13,13 @@
</description>
<translation/>
<languages>
<lang>cs</lang>
<lang>de</lang>
<lang>el</lang>
<lang>en</lang>
<lang>eo</lang>
<lang>et</lang>
<lang>fi</lang>
<lang>fr</lang>
<lang>hu</lang>
<lang>it</lang>
<lang>ja</lang>
<lang>ml</lang>
<lang>nl</lang>
<lang>pl</lang>
<lang>ro</lang>
<lang>ru</lang>
<lang>sv</lang>
<lang>zh_CN</lang>
</languages>
<content_rating type="oars-1.0">
@ -53,8 +43,6 @@
<url type="homepage">https://github.com/Nheko-Reborn/nheko</url>
<update_contact>https://github.com/Nheko-Reborn</update_contact>
<releases>
<release date="2021-04-23" version="0.8.2"/>
<release date="2021-01-27" version="0.8.1"/>
<release date="2021-01-21" version="0.8.0"/>
<release date="2020-06-12" version="0.7.2"/>
<release date="2020-04-24" version="0.7.1"/>
@ -69,11 +57,12 @@
<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>
<!--FIXME: where to donate to the application-->
<url type="donation"/>
<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>

View File

@ -2,7 +2,7 @@
Name=nheko
Version=1.0
Comment=Desktop client for Matrix
Exec=nheko %u
Exec=nheko
Icon=nheko
Type=Application
Categories=Network;InstantMessaging;Qt;

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./ui"
import QtGraphicalEffects 1.0
import QtQuick 2.6
@ -61,7 +57,6 @@ Rectangle {
}
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
anchors.fill: parent
@ -95,9 +90,4 @@ Rectangle {
}
}
CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./ui"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -15,16 +11,8 @@ Popup {
property string completerName
property var completer
property bool bottomToTop: true
property bool fullWidth: false
property bool centerRowContent: true
property int avatarHeight: 24
property int avatarWidth: 24
property int rowMargin: 0
property int rowSpacing: 5
property alias count: listView.count
signal completionClicked(string completion)
signal completionSelected(string id)
function up() {
if (bottomToTop)
@ -61,18 +49,9 @@ Popup {
return null;
}
function finishCompletion() {
if (popup.completerName == "room")
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
}
onCompleterNameChanged: {
if (completerName) {
if (completerName == "user")
completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId());
else
completer = TimelineManager.completerFor(completerName);
completer = TimelineManager.timeline.input.completerFor(completerName);
completer.setSearchString("");
} else {
completer = undefined;
@ -91,31 +70,22 @@ Popup {
id: listView
anchors.fill: parent
implicitWidth: fullWidth ? parent.width : contentItem.childrenRect.width
implicitWidth: contentItem.childrenRect.width
model: completer
verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
spacing: rowSpacing
pixelAligned: true
delegate: Rectangle {
property variant modelData: model
color: model.index == popup.currentIndex ? colors.highlight : colors.base
height: chooser.childrenRect.height + 2 * popup.rowMargin
implicitWidth: fullWidth ? popup.width : chooser.childrenRect.width + 4
height: chooser.childrenRect.height + 4
implicitWidth: chooser.childrenRect.width + 4
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPositionChanged: popup.currentIndex = model.index
onClicked: {
popup.completionClicked(completer.completionAt(model.index));
if (popup.completerName == "room")
popup.completionSelected(model.roomid);
}
onEntered: popup.currentIndex = model.index
onClicked: popup.completionClicked(completer.completionAt(model.index))
Ripple {
rippleTarget: mouseArea
@ -128,8 +98,7 @@ Popup {
id: chooser
roleValue: popup.completerName
anchors.fill: parent
anchors.margins: popup.rowMargin
anchors.centerIn: parent
DelegateChoice {
roleValue: "user"
@ -138,11 +107,10 @@ Popup {
id: del
anchors.centerIn: parent
spacing: rowSpacing
Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
height: 24
width: 24
displayName: model.displayName
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
onClicked: popup.completionClicked(completer.completionAt(model.index))
@ -169,7 +137,6 @@ Popup {
id: del
anchors.centerIn: parent
spacing: rowSpacing
Label {
text: model.unicode
@ -186,67 +153,6 @@ Popup {
}
DelegateChoice {
roleValue: "room"
RowLayout {
id: del
anchors.centerIn: centerRowContent ? parent : undefined
spacing: rowSpacing
Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.roomName
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
onClicked: {
popup.completionClicked(completer.completionAt(model.index));
popup.completionSelected(model.roomid);
}
}
Label {
text: model.roomName
font.pixelSize: popup.avatarHeight * 0.5
color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
}
}
}
DelegateChoice {
roleValue: "roomAliases"
RowLayout {
id: del
anchors.centerIn: parent
spacing: rowSpacing
Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.roomName
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
onClicked: popup.completionClicked(completer.completionAt(model.index))
}
Label {
text: model.roomName
color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
}
Label {
text: "(" + model.roomAlias + ")"
color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText
}
}
}
}
}

View File

@ -1,50 +1,44 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.12
import QtQuick 2.5
import QtQuick.Controls 2.1
import im.nheko 1.0
Image {
id: stateImg
Rectangle {
id: indicator
property bool encrypted: false
property int trust: Crypto.Unverified
function getEncryptionImage() {
if (encrypted)
return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText;
else
return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d";
}
function getEncryptionTooltip() {
if (encrypted)
return qsTr("Encrypted");
else
return qsTr("This message is not encrypted!");
}
color: "transparent"
width: 16
height: 16
source: {
if (encrypted) {
switch (trust) {
case Crypto.Verified:
return "image://colorimage/:/icons/icons/ui/lock.png?green";
case Crypto.TOFU:
return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText;
default:
return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d";
}
} else {
return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d";
}
}
ToolTip.visible: ma.hovered
ToolTip.text: {
if (!encrypted)
return qsTr("This message is not encrypted!");
ToolTip.visible: ma.containsMouse && indicator.visible
ToolTip.text: getEncryptionTooltip()
switch (trust) {
case Crypto.Verified:
return qsTr("Encrypted by a verified device");
case Crypto.TOFU:
return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
default:
return qsTr("Encrypted by an unverified device");
}
}
HoverHandler {
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
}
Image {
id: stateImg
anchors.fill: parent
source: getEncryptionImage()
}
}

View File

@ -1,117 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./delegates/"
import QtQuick 2.9
import QtQuick.Controls 2.3
import im.nheko 1.0
Popup {
id: forwardMessagePopup
property var mid
function setMessageEventId(mid_in) {
mid = mid_in;
}
x: Math.round(parent.width / 2 - width / 2)
y: Math.round(parent.height / 2 - height / 2)
modal: true
palette: colors
parent: Overlay.overlay
width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8)
height: implicitHeight + completerPopup.height + padding * 2
leftPadding: 10
rightPadding: 10
onOpened: {
completerPopup.open();
roomTextInput.forceActiveFocus();
}
onClosed: {
completerPopup.close();
}
Column {
id: forwardColumn
spacing: 5
Label {
id: titleLabel
text: qsTr("Forward Message")
font.bold: true
bottomPadding: 10
color: colors.text
}
Reply {
id: replyPreview
modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : {
}
userColor: TimelineManager.userColor(modelData.userId, colors.window)
}
MatrixTextField {
id: roomTextInput
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
color: colors.text
onTextEdited: {
completerPopup.completer.searchString = text;
}
Keys.onPressed: {
if (event.key == Qt.Key_Up && completerPopup.opened) {
event.accepted = true;
completerPopup.up();
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion();
event.accepted = true;
}
}
}
}
Completer {
id: completerPopup
y: titleLabel.height + replyPreview.height + roomTextInput.height + roomTextInput.bottomPadding + forwardColumn.spacing * 3
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
completerName: "room"
fullWidth: true
centerRowContent: false
avatarHeight: 24
avatarWidth: 24
bottomToTop: false
closePolicy: Popup.NoAutoClose
}
Connections {
onCompletionSelected: {
TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id);
forwardMessagePopup.close();
}
onCountChanged: {
if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
completerPopup.currentIndex = 0;
}
target: completerPopup
}
background: Rectangle {
color: colors.window
}
Overlay.modal: Rectangle {
color: Qt.rgba(colors.window.r, colors.window.g, colors.window.b, 0.7)
}
}

View File

@ -1,11 +1,6 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./ui"
import QtQuick 2.3
import QtQuick.Controls 2.3
import im.nheko 1.0 // for cursor shape
AbstractButton {
id: button
@ -28,10 +23,11 @@ AbstractButton {
source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : ""
}
CursorShape {
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.5
import QtQuick.Controls 2.3
import im.nheko 1.0
@ -12,15 +8,29 @@ TextEdit {
focus: false
wrapMode: Text.Wrap
selectByMouse: !Settings.mobileMode
enabled: selectByMouse
color: colors.text
onLinkActivated: TimelineManager.openLink(link)
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
CursorShape {
MouseArea {
id: ma
anchors.fill: parent
cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}

View File

@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
TextField {
id: input
palette: colors
Rectangle {
id: blueBar
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: colors.highlight
height: 1
width: parent.width
Rectangle {
id: blackBar
anchors.verticalCenter: blueBar.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
height: parent.height + 1
width: 0
color: colors.text
states: State {
name: "focused"
when: input.activeFocus == true
PropertyChanges {
target: blackBar
width: blueBar.width
}
}
transitions: Transition {
from: ""
to: "focused"
reversible: true
NumberAnimation {
target: blackBar
properties: "width"
duration: 500
easing.type: Easing.InOutQuad
alwaysRunToEnd: true
}
}
}
}
background: Rectangle {
color: colors.base
}
}

View File

@ -1,20 +1,14 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./voip"
import QtQuick 2.12
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import im.nheko 1.0
Rectangle {
id: inputBar
color: colors.window
Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight
Layout.preferredHeight: textInput.height + 16
Layout.minimumHeight: 40
Component {
@ -26,10 +20,11 @@ Rectangle {
}
RowLayout {
id: row
id: inputBar
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) || messageContextMenu.isSender
anchors.fill: parent
anchors.margins: 8
spacing: 16
ImageButton {
visible: CallManager.callsSupported
@ -41,7 +36,7 @@ Rectangle {
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
ToolTip.visible: hovered
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
Layout.margins: 8
Layout.leftMargin: 8
onClicked: {
if (TimelineManager.timeline) {
if (CallManager.haveCallInvite) {
@ -49,6 +44,7 @@ Rectangle {
} else if (CallManager.isOnCall) {
CallManager.hangUp();
} else {
CallManager.refreshDevices();
var dialog = placeCallDialog.createObject(timelineRoot);
dialog.open();
}
@ -62,7 +58,7 @@ Rectangle {
width: 22
height: 22
image: ":/icons/icons/ui/paper-clip-outline.png"
Layout.margins: 8
Layout.leftMargin: CallManager.callsSupported ? 0 : 8
onClicked: TimelineManager.timeline.input.openFileSelection()
ToolTip.visible: hovered
ToolTip.text: qsTr("Send a file")
@ -81,53 +77,71 @@ Rectangle {
}
ScrollView {
Flickable {
id: textInput
Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX + width <= r.x + r.width)
contentX = r.x + r.width - width;
if (contentY >= r.y)
contentY = r.y;
else if (contentY + height <= r.y + r.height)
contentY = r.y + r.height - height;
}
Layout.alignment: Qt.AlignBottom
Layout.maximumHeight: Window.height / 4
Layout.minimumHeight: Settings.fontSize
implicitWidth: inputBar.width - 4 * (22 + 16) - 24
Layout.fillWidth: true
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
implicitWidth: textArea.width
implicitHeight: textArea.height
contentWidth: textArea.width
contentHeight: textArea.height
TextArea {
id: messageInput
id: textArea
property int completerTriggeredAt: -1
function insertCompletion(completion) {
messageInput.remove(completerTriggeredAt, cursorPosition);
messageInput.insert(cursorPosition, completion);
textArea.remove(completerTriggeredAt, cursorPosition);
textArea.insert(cursorPosition, completion);
}
function openCompleter(pos, type) {
completerTriggeredAt = pos;
popup.completerName = type;
popup.open();
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
}
function positionCursorAtEnd() {
cursorPosition = messageInput.length;
}
function positionCursorAtStart() {
cursorPosition = 0;
popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
}
text: "asfkajsdf"
selectByMouse: true
placeholderText: qsTr("Write a message...")
placeholderTextColor: colors.buttonText
//placeholderTextColor: colors.buttonText
// only set the anchors on Qt 5.12 or higher
// see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
Component.onCompleted: {
if (placeholderTextColor !== undefined)
placeholderTextColor = colors.buttonText;
}
color: colors.text
width: textInput.width
wrapMode: TextEdit.Wrap
padding: 8
padding: 0
focus: true
onTextChanged: {
if (TimelineManager.timeline)
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
forceActiveFocus();
}
onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle)
onCursorPositionChanged: {
if (!TimelineManager.timeline)
return ;
@ -138,7 +152,7 @@ Rectangle {
popup.close();
}
if (popup.opened)
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
}
onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
@ -149,28 +163,17 @@ Rectangle {
if (event.matches(StandardKey.Paste)) {
TimelineManager.timeline.input.paste(false);
event.accepted = true;
} else if (event.key == Qt.Key_Space) {
// close popup if user enters space after colon
if (cursorPosition == completerTriggeredAt + 1)
popup.close();
if (popup.opened && popup.count <= 0)
popup.close();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
messageInput.clear();
textArea.clear();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
messageInput.text = TimelineManager.timeline.input.previousText();
textArea.text = TimelineManager.timeline.input.previousText();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
messageInput.text = TimelineManager.timeline.input.nextText();
textArea.text = TimelineManager.timeline.input.nextText();
} else if (event.key == Qt.Key_At) {
messageInput.openCompleter(cursorPosition, "user");
textArea.openCompleter(cursorPosition, "user");
popup.open();
} else if (event.key == Qt.Key_Colon) {
messageInput.openCompleter(cursorPosition, "emoji");
popup.open();
} else if (event.key == Qt.Key_NumberSign) {
messageInput.openCompleter(cursorPosition, "roomAliases");
textArea.openCompleter(cursorPosition, "emoji");
popup.open();
} else if (event.key == Qt.Key_Escape && popup.opened) {
completerTriggeredAt = -1;
@ -183,12 +186,13 @@ Rectangle {
popup.completerName = "";
popup.close();
if (currentCompletion) {
messageInput.insertCompletion(currentCompletion);
textArea.insertCompletion(currentCompletion);
event.accepted = true;
return ;
}
}
TimelineManager.timeline.input.send();
textArea.clear();
event.accepted = true;
} else if (event.key == Qt.Key_Tab) {
event.accepted = true;
@ -197,22 +201,19 @@ Rectangle {
} else {
var pos = cursorPosition - 1;
while (pos > -1) {
var t = messageInput.getText(pos, pos + 1);
var t = textArea.getText(pos, pos + 1);
console.log('"' + t + '"');
if (t == '@') {
messageInput.openCompleter(pos, "user");
return ;
} else if (t == ' ' || t == '\t') {
messageInput.openCompleter(pos + 1, "user");
if (t == '@' || t == ' ' || t == '\t') {
textArea.openCompleter(pos, "user");
return ;
} else if (t == ':') {
messageInput.openCompleter(pos, "emoji");
textArea.openCompleter(pos, "emoji");
return ;
}
pos = pos - 1;
}
// At start of input
messageInput.openCompleter(0, "user");
textArea.openCompleter(0, "user");
}
} else if (event.key == Qt.Key_Up && popup.opened) {
event.accepted = true;
@ -220,93 +221,38 @@ Rectangle {
} else if (event.key == Qt.Key_Down && popup.opened) {
event.accepted = true;
popup.down();
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
if (cursorPosition == 0) {
event.accepted = true;
var idx = TimelineManager.timeline.edit ? TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) + 1 : 0;
while (true) {
var id = TimelineManager.timeline.indexToId(idx);
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) {
TimelineManager.timeline.edit = id;
cursorPosition = 0;
Qt.callLater(positionCursorAtEnd);
break;
}
idx++;
}
} else if (positionAt(0, cursorRectangle.y) === 0) {
event.accepted = true;
positionCursorAtStart();
}
} else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) {
if (cursorPosition == messageInput.length && TimelineManager.timeline.edit) {
event.accepted = true;
var idx = TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) - 1;
while (true) {
var id = TimelineManager.timeline.indexToId(idx);
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) {
TimelineManager.timeline.edit = id;
Qt.callLater(positionCursorAtStart);
break;
}
idx--;
}
} else if (positionAt(width, cursorRectangle.y + 2) === messageInput.length) {
event.accepted = true;
positionCursorAtEnd();
}
}
}
background: null
Connections {
onActiveTimelineChanged: {
messageInput.clear();
messageInput.append(TimelineManager.timeline.input.text());
messageInput.completerTriggeredAt = -1;
onTimelineChanged: {
textArea.clear();
textArea.append(TimelineManager.timeline.input.text());
textArea.completerTriggeredAt = -1;
popup.completerName = "";
messageInput.forceActiveFocus();
}
target: TimelineManager
}
Connections {
onCompletionClicked: messageInput.insertCompletion(completion)
onCompletionClicked: textArea.insertCompletion(completion)
target: popup
}
Completer {
id: popup
x: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).x : 0
y: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height : 0
x: textArea.completerTriggeredAt >= 0 ? textArea.positionToRectangle(textArea.completerTriggeredAt).x : 0
y: textArea.completerTriggeredAt >= 0 ? textArea.positionToRectangle(textArea.completerTriggeredAt).y - height : 0
}
Connections {
ignoreUnknownSignals: true
onInsertText: {
messageInput.remove(messageInput.selectionStart, messageInput.selectionEnd);
messageInput.insert(messageInput.cursorPosition, text);
}
onTextChanged: {
messageInput.text = newText;
messageInput.cursorPosition = newText.length;
}
onInsertText: textArea.insert(textArea.cursorPosition, text)
target: TimelineManager.timeline ? TimelineManager.timeline.input : null
}
Connections {
ignoreUnknownSignals: true
onReplyChanged: messageInput.forceActiveFocus()
onEditChanged: messageInput.forceActiveFocus()
target: TimelineManager.timeline
}
Connections {
target: TimelineManager
onFocusInput: messageInput.forceActiveFocus()
}
MouseArea {
// workaround for wrong cursor shape on some platforms
anchors.fill: parent
@ -315,6 +261,14 @@ Rectangle {
onClicked: TimelineManager.timeline.input.paste(true)
}
NhekoDropArea {
anchors.fill: parent
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
}
}
ScrollBar.vertical: ScrollBar {
}
}
@ -323,7 +277,6 @@ Rectangle {
id: emojiButton
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8
hoverEnabled: true
width: 22
height: 22
@ -331,14 +284,12 @@ Rectangle {
ToolTip.visible: hovered
ToolTip.text: qsTr("Emoji")
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
messageInput.insert(messageInput.cursorPosition, emoji);
TimelineManager.focusMessageInput();
textArea.insert(textArea.cursorPosition, emoji);
})
}
ImageButton {
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8
hoverEnabled: true
width: 22
height: 22
@ -348,16 +299,10 @@ Rectangle {
ToolTip.text: qsTr("Send")
onClicked: {
TimelineManager.timeline.input.send();
textArea.clear();
}
}
}
Text {
anchors.centerIn: parent
visible: TimelineManager.timeline ? (!TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage)) : false
text: qsTr("You don't have permission to send messages in this room")
color: colors.text
}
}

View File

@ -1,416 +1,192 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./delegates"
import "./emoji"
import QtGraphicalEffects 1.0
import QtQuick 2.12
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import im.nheko 1.0
ScrollView {
clip: false
palette: colors
padding: 8
ScrollBar.horizontal.visible: false
ListView {
id: chat
ListView {
id: chat
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2 - 8)
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
Layout.fillWidth: true
Layout.fillHeight: true
cacheBuffer: 400
model: TimelineManager.timeline
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
spacing: 4
verticalLayoutDirection: ListView.BottomToTop
onCountChanged: {
// Mark timeline as read
if (atYEnd)
model.currentIndex = 0;
model: TimelineManager.timeline
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
spacing: 4
verticalLayoutDirection: ListView.BottomToTop
onCountChanged: {
// Mark timeline as read
if (atYEnd)
model.currentIndex = 0;
}
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
Shortcut {
sequence: StandardKey.MoveToPreviousPage
onActivated: {
chat.contentY = chat.contentY - chat.height / 2;
chat.returnToBounds();
}
}
Rectangle {
//closePolicy: Popup.NoAutoClose
Shortcut {
sequence: StandardKey.MoveToNextPage
onActivated: {
chat.contentY = chat.contentY + chat.height / 2;
chat.returnToBounds();
}
}
id: messageActions
Shortcut {
sequence: StandardKey.Cancel
onActivated: chat.model.reply = undefined
}
property Item attached: null
property alias model: row.model
// use comma to update on scroll
property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
readonly property int padding: 4
Shortcut {
sequence: "Alt+Up"
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
}
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || messageActionHover.hovered)
x: attached ? attachedPos.x : 0
y: attached ? attachedPos.y : 0
z: 10
height: row.implicitHeight + padding * 2
width: row.implicitWidth + padding * 2
color: colors.window
border.color: colors.buttonText
border.width: 1
radius: padding
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;
}
}
HoverHandler {
id: messageActionHover
Component {
id: sectionHeader
Column {
topPadding: 4
bottomPadding: 4
spacing: 8
visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
width: parentWidth
height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: modelData && modelData.previousMessageDay !== modelData.day
text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
color: colors.text
height: Math.round(fontMetrics.height * 1.4)
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
background: Rectangle {
radius: parent.height / 2
color: colors.window
}
grabPermissions: PointerHandler.CanTakeOverFromAnything
}
Row {
id: row
property var model
anchors.centerIn: parent
spacing: messageActions.padding
ImageButton {
id: editButton
visible: !!row.model && row.model.isEditable
buttonTextColor: colors.buttonText
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/edit.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Edit")
onClicked: {
if (row.model.isEditable)
chat.model.editAction(row.model.id);
}
}
EmojiButton {
id: reactButton
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
width: 16
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
emojiPicker: emojiPopup
event_id: row.model ? row.model.id : ""
}
ImageButton {
id: replyButton
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.TextMessage) : false
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/mail-reply.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Reply")
onClicked: chat.model.replyAction(row.model.id)
}
ImageButton {
id: optionsButton
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Options")
onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
}
}
}
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
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: {
if (chat.model.reply)
chat.model.reply = undefined;
else
chat.model.edit = 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) : null;
}
}
Shortcut {
sequence: "Alt+F"
onActivated: {
if (chat.model.reply) {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(chat.model.reply);
forwardMess.open();
chat.model.reply = null;
}
}
}
Shortcut {
sequence: "Ctrl+E"
onActivated: {
chat.model.edit = chat.model.reply;
}
}
Connections {
target: TimelineManager
onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
}
Timer {
id: readTimer
// force current read index to update
onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
interval: 1000
}
Component {
id: sectionHeader
Column {
topPadding: 4
bottomPadding: 4
height: userName.height
spacing: 8
visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
width: parentWidth
height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
Avatar {
width: avatarSize
height: avatarSize
url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
displayName: modelData ? modelData.userName : ""
userid: modelData ? modelData.userId : ""
onClicked: chat.model.openUserProfile(modelData.userId)
}
Label {
id: dateBubble
id: userName
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: modelData && modelData.previousMessageDay !== modelData.day
text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
color: colors.text
height: Math.round(fontMetrics.height * 1.4)
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
textFormat: Text.RichText
background: Rectangle {
radius: parent.height / 2
color: colors.window
}
}
Row {
height: userName.height
spacing: 8
Avatar {
id: messageUserAvatar
width: avatarSize
height: avatarSize
url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
displayName: modelData ? modelData.userName : ""
userid: modelData ? modelData.userId : ""
MouseArea {
anchors.fill: parent
Layout.alignment: Qt.AlignHCenter
onClicked: chat.model.openUserProfile(modelData.userId)
ToolTip.visible: avatarHover.hovered
ToolTip.text: userid
HoverHandler {
id: avatarHover
}
}
Connections {
target: chat.model
onRoomAvatarUrlChanged: {
messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
}
onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible)
}
Label {
id: userName
text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
textFormat: Text.RichText
ToolTip.visible: displayNameHover.hovered
ToolTip.text: modelData ? modelData.userId : ""
TapHandler {
onSingleTapped: chat.model.openUserProfile(modelData.userId)
}
CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
HoverHandler {
id: displayNameHover
}
}
Label {
color: colors.buttonText
text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
textFormat: Text.PlainText
elide: Text.ElideRight
width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize
font.italic: true
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
}
}
delegate: Item {
id: wrapper
property bool scrolledToThis: model.id === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
width: chat.delegateMaxWidth
height: section ? section.height + timelinerow.height : timelinerow.height
Rectangle {
id: scrollHighlight
opacity: 0
visible: true
anchors.fill: timelinerow
color: colors.highlight
states: State {
name: "revealed"
when: wrapper.scrolledToThis
}
transitions: Transition {
from: ""
to: "revealed"
SequentialAnimation {
PropertyAnimation {
target: scrollHighlight
properties: "opacity"
easing.type: Easing.InOutQuad
from: 0
to: 1
duration: 500
}
PropertyAnimation {
target: scrollHighlight
properties: "opacity"
easing.type: Easing.InOutQuad
from: 1
to: 0
duration: 500
}
ScriptAction {
script: chat.model.eventShown()
}
}
Label {
color: colors.buttonText
text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
textFormat: Text.PlainText
elide: Text.ElideRight
width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize
font.italic: true
}
}
Loader {
id: section
property var modelData: model
property int parentWidth: parent.width
active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
}
TimelineRow {
id: timelinerow
property alias hovered: hoverHandler.hovered
y: section.visible && section.active ? section.y + section.height : 0
HoverHandler {
id: hoverHandler
enabled: !Settings.mobileMode
onHoveredChanged: {
if (hovered) {
if (!messageActionHover.hovered) {
messageActions.attached = timelinerow;
messageActions.model = model;
}
}
}
}
}
Connections {
target: chat
onMovementEnded: {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
}
}
}
footer: BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: chat.model && chat.model.paginationInProgress
height: 50
width: 50
z: 3
}
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
}
delegate: Item {
id: wrapper
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
width: chat.delegateMaxWidth
height: section ? section.height + timelinerow.height : timelinerow.height
Loader {
id: section
property var modelData: model
property int parentWidth: parent.width
active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
}
TimelineRow {
id: timelinerow
y: section.active && section.visible ? section.y + section.height : 0
}
Connections {
target: chat
onMovementEnded: {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
}
}
}
footer: BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: chat.model && chat.model.paginationInProgress
height: 50
width: 50
z: 3
}
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2

View File

@ -1,130 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtGraphicalEffects 1.0
import QtQuick 2.12
import im.nheko 1.0
Item {
id: privacyScreen
property var timelineRoot
property int screenTimeout
Connections {
target: TimelineManager
onFocusChanged: {
if (TimelineManager.isWindowFocused) {
screenSaverTimer.stop();
screenSaver.state = "Invisible";
} else {
if (timelineRoot.visible)
screenSaverTimer.start();
}
}
}
Timer {
id: screenSaverTimer
interval: screenTimeout * 1000
running: true
onTriggered: {
screenSaver.state = "Visible";
}
}
Item {
id: screenSaver
state: "Invisible"
anchors.fill: parent
visible: false
states: [
State {
name: "Visible"
PropertyChanges {
target: screenSaver
visible: true
}
PropertyChanges {
target: screenSaver
opacity: 1
}
},
State {
name: "Invisible"
PropertyChanges {
target: screenSaver
opacity: 0
}
PropertyChanges {
target: screenSaver
visible: false
}
}
]
transitions: [
Transition {
from: "Visible"
to: "Invisible"
SequentialAnimation {
NumberAnimation {
target: screenSaver
property: "opacity"
duration: 250
easing.type: Easing.InQuad
}
NumberAnimation {
target: screenSaver
property: "visible"
duration: 0
}
}
},
Transition {
from: "Invisible"
to: "Visible"
SequentialAnimation {
NumberAnimation {
target: screenSaver
property: "visible"
duration: 0
}
NumberAnimation {
target: screenSaver
property: "opacity"
duration: 500
easing.type: Easing.InQuad
}
}
}
]
FastBlur {
id: blur
anchors.fill: parent
source: timelineRoot
radius: 50
}
}
}

View File

@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import QtQuick.Controls 2.3
import im.nheko 1.0
Popup {
id: quickSwitcher
property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4)
property int textMargin: Math.round(textHeight / 8)
background: null
width: Math.round(parent.width / 2)
x: Math.round(parent.width / 2 - width / 2)
y: Math.round(parent.height / 4 - height / 2)
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
parent: Overlay.overlay
palette: colors
onOpened: {
completerPopup.open();
roomTextInput.forceActiveFocus();
}
onClosed: {
completerPopup.close();
}
MatrixTextField {
id: roomTextInput
anchors.fill: parent
font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
padding: textMargin
color: colors.text
onTextEdited: {
completerPopup.completer.searchString = text;
}
Keys.onPressed: {
if (event.key == Qt.Key_Up && completerPopup.opened) {
event.accepted = true;
completerPopup.up();
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion();
event.accepted = true;
}
}
}
Completer {
id: completerPopup
x: roomTextInput.x
y: roomTextInput.y + quickSwitcher.textHeight + quickSwitcher.textMargin
visible: roomTextInput.length > 0
width: parent.width
completerName: "room"
bottomToTop: false
fullWidth: true
avatarHeight: quickSwitcher.textHeight
avatarWidth: quickSwitcher.textHeight
centerRowContent: false
rowMargin: Math.round(quickSwitcher.textMargin / 2)
rowSpacing: quickSwitcher.textMargin
closePolicy: Popup.NoAutoClose
}
Connections {
onCompletionSelected: {
TimelineManager.setHistoryView(id);
TimelineManager.highlightRoom(id);
quickSwitcher.close();
}
onCountChanged: {
if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
completerPopup.currentIndex = 0;
}
target: completerPopup
}
Overlay.modal: Rectangle {
color: "#aa1E1E1E"
}
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.6
import QtQuick.Controls 2.2
import im.nheko 1.0

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./delegates/"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -14,16 +10,15 @@ Rectangle {
property var room: TimelineManager.timeline
Layout.fillWidth: true
visible: room && (room.reply || room.edit)
visible: room && room.reply
// Height of child, plus margins, plus border
implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10
implicitHeight: replyPreview.height + 10
color: colors.window
z: 3
Reply {
id: replyPreview
visible: room && room.reply
anchors.left: parent.left
anchors.leftMargin: 2 * 22 + 3 * 16
anchors.right: closeReplyButton.left
@ -37,9 +32,8 @@ Rectangle {
ImageButton {
id: closeReplyButton
visible: room && room.reply
anchors.right: parent.right
anchors.rightMargin: 16
anchors.rightMargin: 15
anchors.top: replyPreview.top
hoverEnabled: true
width: 16
@ -50,17 +44,4 @@ Rectangle {
onClicked: room.reply = undefined
}
Button {
id: closeEditButton
visible: room && room.edit
anchors.left: parent.left
anchors.rightMargin: 16
anchors.topMargin: 10
anchors.top: parent.top
//height: 16
text: qsTr("Cancel edit")
onClicked: room.edit = undefined
}
}

View File

@ -1,287 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
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: roomSettingsDialog
property var roomSettings
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
minimumWidth: 420
minimumHeight: 650
palette: colors
color: colors.window
modality: Qt.WindowModal
flags: Qt.Dialog
title: qsTr("Room Settings")
Shortcut {
sequence: StandardKey.Cancel
onActivated: roomSettingsDialog.close()
}
ColumnLayout {
id: contentLayout1
anchors.fill: parent
anchors.margins: 10
spacing: 10
Avatar {
url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: roomSettings.roomName
height: 130
width: 130
Layout.alignment: Qt.AlignHCenter
onClicked: {
if (roomSettings.canChangeAvatar)
roomSettings.updateAvatar();
}
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: roomSettings.isLoading
visible: roomSettings.isLoading
}
Text {
id: errorText
color: "red"
visible: opacity > 0
opacity: 0
Layout.alignment: Qt.AlignHCenter
}
SequentialAnimation {
id: hideErrorAnimation
running: false
PauseAnimation {
duration: 4000
}
NumberAnimation {
target: errorText
property: 'opacity'
to: 0
duration: 1000
}
}
Connections {
target: roomSettings
onDisplayError: {
errorText.text = errorMessage;
errorText.opacity = 1;
hideErrorAnimation.restart();
}
}
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
MatrixText {
text: roomSettings.roomName
font.pixelSize: 24
Layout.alignment: Qt.AlignHCenter
}
MatrixText {
text: qsTr("%1 member(s)").arg(roomSettings.memberCount)
Layout.alignment: Qt.AlignHCenter
}
}
ImageButton {
Layout.alignment: Qt.AlignHCenter
image: ":/icons/icons/ui/edit.png"
visible: roomSettings.canChangeNameAndTopic
onClicked: roomSettings.openEditModal()
}
ScrollView {
Layout.maximumHeight: 75
Layout.alignment: Qt.AlignHCenter
width: parent.width
TextArea {
text: TimelineManager.escapeEmoji(roomSettings.roomTopic)
wrapMode: TextEdit.WordWrap
textFormat: TextEdit.RichText
readOnly: true
background: null
selectByMouse: true
color: colors.text
horizontalAlignment: TextEdit.AlignHCenter
onLinkActivated: TimelineManager.openLink(link)
CursorShape {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
GridLayout {
columns: 2
rowSpacing: 10
MatrixText {
text: qsTr("SETTINGS")
font.bold: true
}
Item {
Layout.fillWidth: true
}
MatrixText {
text: qsTr("Notifications")
Layout.fillWidth: true
}
ComboBox {
model: [qsTr("Muted"), qsTr("Mentions only"), qsTr("All messages")]
currentIndex: roomSettings.notifications
onActivated: {
roomSettings.changeNotifications(index);
}
Layout.fillWidth: true
}
MatrixText {
text: "Room access"
Layout.fillWidth: true
}
ComboBox {
enabled: roomSettings.canChangeJoinRules
model: [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")]
currentIndex: roomSettings.accessJoinRules
onActivated: {
roomSettings.changeAccessRules(index);
}
Layout.fillWidth: true
}
MatrixText {
text: qsTr("Encryption")
}
ToggleButton {
id: encryptionToggle
checked: roomSettings.isEncryptionEnabled
onClicked: {
if (roomSettings.isEncryptionEnabled) {
checked = true;
return ;
}
confirmEncryptionDialog.open();
}
Layout.alignment: Qt.AlignRight
}
Platform.MessageDialog {
id: confirmEncryptionDialog
title: qsTr("End-to-End Encryption")
text: qsTr("Encryption is currently experimental and things might break unexpectedly. <br>
Please take note that it can't be disabled afterwards.")
modality: Qt.WindowModal
onAccepted: {
if (roomSettings.isEncryptionEnabled)
return ;
roomSettings.enableEncryption();
}
onRejected: {
encryptionToggle.checked = false;
}
buttons: Dialog.Ok | Dialog.Cancel
}
MatrixText {
visible: roomSettings.isEncryptionEnabled
text: qsTr("Respond to key requests")
}
ToggleButton {
visible: roomSettings.isEncryptionEnabled
ToolTip.text: qsTr("Whether or not the client should respond automatically with the session keys
upon request. Use with caution, this is a temporary measure to test the
E2E implementation until device verification is completed.")
checked: roomSettings.respondsToKeyRequests
onClicked: {
roomSettings.changeKeyRequestsPreference(checked);
}
Layout.alignment: Qt.AlignRight
}
Item {
// for adding extra space between sections
Layout.fillWidth: true
}
Item {
// for adding extra space between sections
Layout.fillWidth: true
}
MatrixText {
text: qsTr("INFO")
font.bold: true
}
Item {
Layout.fillWidth: true
}
MatrixText {
text: qsTr("Internal ID")
}
MatrixText {
text: roomSettings.roomId
font.pixelSize: 14
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Room Version")
}
MatrixText {
text: roomSettings.roomVersion
font.pixelSize: 14
Layout.alignment: Qt.AlignRight
}
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("OK")
onClicked: close()
}
}
}

View File

@ -1,9 +1,21 @@
// Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net>
// Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
/*
* 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
@ -78,7 +90,7 @@ MouseArea {
// Show the scrollbars
flickable.flick(0, 0);
flickable.contentY = newPos;
cancelFlickStateTimer.restart();
cancelFlickStateTimer.start();
}
Timer {

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.5
import QtQuick.Controls 2.1
import im.nheko 1.0

View File

@ -1,10 +1,6 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./delegates"
import "./emoji"
import QtQuick 2.12
import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
@ -16,32 +12,32 @@ Item {
height: row.height
Rectangle {
color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent"
color: (Settings.messageHoverHighlight && hoverHandler.containsMouse) ? colors.alternateBase : "transparent"
anchors.fill: row
}
HoverHandler {
MouseArea {
id: hoverHandler
acceptedDevices: PointerDevice.GenericPointer
}
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
gesturePolicy: TapHandler.ReleaseWithinBounds
}
TapHandler {
onLongPressed: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
onDoubleTapped: chat.model.reply = model.id
gesturePolicy: TapHandler.ReleaseWithinBounds
anchors.fill: parent
propagateComposedEvents: true
preventStealing: false
hoverEnabled: true
acceptedButtons: Qt.AllButtons
onClicked: {
if (mouse.button === Qt.RightButton)
messageContextMenu.show(model.id, model.type, model.isEncrypted, row);
else
event.accepted = false;
}
onPressAndHold: {
messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y));
}
}
RowLayout {
id: row
anchors.rightMargin: 1
anchors.leftMargin: avatarSize + 16
anchors.left: parent.left
anchors.right: parent.right
@ -50,8 +46,6 @@ Item {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 4
Layout.topMargin: 1
Layout.bottomMargin: 1
// fancy reply, if this is a reply
Reply {
@ -86,41 +80,67 @@ Item {
EncryptionIndicator {
visible: model.isRoomEncrypted
encrypted: model.isEncrypted
trust: model.trustlevel
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
Layout.preferredWidth: 16
width: 16
}
Image {
visible: model.isEdited || model.id == chat.model.edit
EmojiButton {
id: reactButton
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
Layout.preferredWidth: 16
height: 16
width: 16
sourceSize.width: 16
sourceSize.height: 16
source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? colors.highlight : colors.buttonText)
ToolTip.visible: editHovered.hovered
ToolTip.text: qsTr("Edited")
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
emojiPicker: emojiPopup
event_id: model.id
}
HoverHandler {
id: editHovered
}
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(Locale.ShortFormat)
text: model.timestamp.toLocaleTimeString("HH:mm")
width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
color: inactiveColors.text
ToolTip.visible: ma.hovered
ToolTip.visible: ma.containsMouse
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
HoverHandler {
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
}

View File

@ -1,12 +1,7 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./delegates"
import "./device-verification"
import "./emoji"
import "./voip"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -21,7 +16,7 @@ Page {
property var colors: currentActivePalette
property var systemInactive
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
readonly property int avatarSize: 40
property int avatarSize: 40
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
@ -35,8 +30,18 @@ Page {
EmojiPicker {
id: emojiPopup
width: 7 * 52 + 20
height: 6 * 52
colors: palette
model: TimelineManager.completerFor("allemoji", "")
model: EmojiProxyModel {
category: EmojiCategory.People
sourceModel: EmojiModel {
}
}
}
Component {
@ -47,14 +52,6 @@ Page {
}
Component {
id: roomSettingsComponent
RoomSettings {
}
}
Component {
id: mobileCallInviteDialog
@ -63,158 +60,71 @@ Page {
}
Component {
id: quickSwitcherComponent
QuickSwitcher {
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: {
var quickSwitch = quickSwitcherComponent.createObject(timelineRoot);
TimelineManager.focusTimeline();
quickSwitch.open();
}
}
Platform.Menu {
Menu {
id: messageContextMenu
property string eventId
property string link
property string text
property int eventType
property bool isEncrypted
property bool isEditable
property bool isSender
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
text = text_;
if (position)
popup(position, showAt_);
else
text = "";
if (link_)
link = link_;
else
link = "";
if (showAt_)
open(showAt_);
else
open();
popup(showAt_);
}
Platform.MenuItem {
visible: messageContextMenu.text
enabled: visible
text: qsTr("&Copy")
onTriggered: Clipboard.text = messageContextMenu.text
}
modal: true
Platform.MenuItem {
visible: messageContextMenu.link
enabled: visible
text: qsTr("Copy &link location")
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false
text: qsTr("Re&act")
onTriggered: emojiPopup.show(null, function(emoji) {
MenuItem {
text: qsTr("React")
onClicked: emojiPopup.show(messageContextMenu.parent, function(emoji) {
TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji);
})
}
Platform.MenuItem {
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false
text: qsTr("Repl&y")
onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
MenuItem {
text: qsTr("Reply")
onClicked: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
text: qsTr("&Edit")
onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("Read receip&ts")
MenuItem {
text: qsTr("Read receipts")
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
text: qsTr("&Forward")
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
}
MenuItem {
text: qsTr("Mark as read")
}
Platform.MenuItem {
text: qsTr("&Mark as read")
}
Platform.MenuItem {
MenuItem {
text: qsTr("View raw message")
onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
enabled: visible
height: visible ? implicitHeight : 0
text: qsTr("View decrypted raw message")
onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender
text: qsTr("Remo&ve message")
MenuItem {
text: qsTr("Remove message")
onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId)
}
Platform.MenuItem {
MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Save as")
height: visible ? implicitHeight : 0
text: qsTr("Save as")
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Open in external program")
onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventId
enabled: visible
text: qsTr("Copy link to eve&nt")
onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
}
}
Rectangle {
@ -237,6 +147,10 @@ Page {
});
dialog.show();
}
}
Connections {
target: TimelineManager.timeline
onOpenProfile: {
var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile
@ -245,16 +159,6 @@ Page {
}
}
Connections {
target: TimelineManager.timeline
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
}
}
Connections {
target: CallManager
onNewInviteState: {
@ -283,8 +187,6 @@ Page {
}
ColumnLayout {
id: timelineLayout
visible: TimelineManager.timeline != null
anchors.fill: parent
spacing: 0
@ -329,7 +231,7 @@ Page {
}
Loader {
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
source: CallManager.isOnCall && CallManager.isVideo ? "voip/VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem()
}
@ -369,18 +271,6 @@ Page {
}
NhekoDropArea {
anchors.fill: parent
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
}
}
PrivacyScreen {
anchors.fill: parent
visible: Settings.privacyScreen
screenTimeout: Settings.privacyScreenTimeout
timelineRoot: timelineLayout
}
systemInactive: SystemPalette {

View File

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.5
import QtQuick 2.12
import QtQuick.Controls 2.12
import im.nheko 1.0
Switch {
id: toggleButton
implicitWidth: indicatorItem.width
indicator: Item {
id: indicatorItem
implicitWidth: 48
implicitHeight: 24
y: parent.height / 2 - height / 2
Rectangle {
height: 3 * parent.height / 4
radius: height / 2
width: parent.width - height
x: radius
y: parent.height / 2 - height / 2
color: toggleButton.checked ? "skyblue" : "grey"
border.color: "#cccccc"
}
Rectangle {
x: toggleButton.checked ? parent.width - width : 0
y: parent.height / 2 - height / 2
width: parent.height
height: width
radius: width / 2
color: toggleButton.down ? "whitesmoke" : "whitesmoke"
border.color: "#ebebeb"
}
}
}

View File

@ -1,10 +1,5 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import im.nheko 1.0
@ -18,12 +13,9 @@ Rectangle {
z: 3
color: colors.window
TapHandler {
onSingleTapped: {
TimelineManager.timeline.openRoomSettings();
eventPoint.accepted = true;
}
gesturePolicy: TapHandler.ReleaseWithinBounds
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
GridLayout {
@ -61,7 +53,7 @@ Rectangle {
height: avatarSize
url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
displayName: room ? room.roomName : qsTr("No room selected")
onClicked: TimelineManager.timeline.openRoomSettings()
onClicked: TimelineManager.openRoomSettings()
}
Label {
@ -73,7 +65,12 @@ Rectangle {
text: room ? room.roomName : qsTr("No room selected")
maximumLineCount: 1
elide: Text.ElideRight
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
}
MatrixText {
@ -95,30 +92,29 @@ Rectangle {
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Room options")
onClicked: roomOptionsMenu.open(roomOptionsButton)
onClicked: roomOptionsMenu.popup(roomOptionsButton)
Platform.Menu {
Menu {
id: roomOptionsMenu
Platform.MenuItem {
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false
MenuItem {
text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsersDialog()
}
Platform.MenuItem {
MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openMemberListDialog()
}
Platform.MenuItem {
MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog()
}
Platform.MenuItem {
MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.timeline.openRoomSettings()
onTriggered: TimelineManager.openRoomSettings()
}
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./device-verification"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -14,21 +10,11 @@ ApplicationWindow {
property var profile
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
height: 650
width: 420
minimumHeight: 420
palette: colors
color: colors.window
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
modality: Qt.WindowModal
flags: Qt.Dialog
Shortcut {
sequence: StandardKey.Cancel
onActivated: userProfileDialog.close()
}
ColumnLayout {
id: contentL
@ -44,86 +30,16 @@ ApplicationWindow {
displayName: profile.displayName
userid: profile.userid
Layout.alignment: Qt.AlignHCenter
onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(profile.avatarUrl, "")
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: profile.isLoading
visible: profile.isLoading
}
Text {
id: errorText
color: "red"
visible: opacity > 0
opacity: 0
Layout.alignment: Qt.AlignHCenter
}
SequentialAnimation {
id: hideErrorAnimation
running: false
PauseAnimation {
duration: 4000
}
NumberAnimation {
target: errorText
property: 'opacity'
to: 0
duration: 1000
}
}
Connections {
target: profile
onDisplayError: {
errorText.text = errorMessage;
errorText.opacity = 1;
hideErrorAnimation.restart();
}
}
TextInput {
id: displayUsername
property bool isUsernameEditingAllowed
readOnly: !isUsernameEditingAllowed
Label {
text: profile.displayName
fontSizeMode: Text.HorizontalFit
font.pixelSize: 20
color: TimelineManager.userColor(profile.userid, colors.window)
font.bold: true
Layout.alignment: Qt.AlignHCenter
selectByMouse: true
onAccepted: {
profile.changeUsername(displayUsername.text);
displayUsername.isUsernameEditingAllowed = false;
}
ImageButton {
visible: profile.isSelf
anchors.leftMargin: 5
anchors.left: displayUsername.right
anchors.verticalCenter: displayUsername.verticalCenter
image: displayUsername.isUsernameEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
onClicked: {
if (displayUsername.isUsernameEditingAllowed) {
profile.changeUsername(displayUsername.text);
displayUsername.isUsernameEditingAllowed = false;
} else {
displayUsername.isUsernameEditingAllowed = true;
displayUsername.focus = true;
displayUsername.selectAll();
}
}
}
}
MatrixText {
@ -137,21 +53,32 @@ ApplicationWindow {
text: qsTr("Verify")
Layout.alignment: Qt.AlignHCenter
enabled: profile.userVerified != Crypto.Verified
visible: profile.userVerified != Crypto.Verified && !profile.isSelf && profile.userVerificationEnabled
enabled: !profile.isUserVerified
visible: !profile.isUserVerified && !profile.isSelf && profile.userVerificationEnabled
onClicked: profile.verify()
}
Image {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : colors.buttonText)
visible: profile.userVerified != Crypto.Unverified
source: "image://colorimage/:/icons/icons/ui/lock.png?green"
visible: profile.isUserVerified
Layout.alignment: Qt.AlignHCenter
}
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
@ -163,10 +90,6 @@ ApplicationWindow {
// profile.ignoreUser()
// }
// }
Layout.alignment: Qt.AlignHCenter
spacing: 8
ImageButton {
image: ":/icons/icons/ui/black-bubble-speech.png"
hoverEnabled: true
@ -181,16 +104,6 @@ ApplicationWindow {
ToolTip.visible: hovered
ToolTip.text: qsTr("Kick the user")
onClicked: profile.kickUser()
visible: profile.room ? profile.room.permissions.canKick() : false
}
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()
visible: profile.room ? profile.room.permissions.canBan() : false
}
}
@ -242,7 +155,7 @@ ApplicationWindow {
id: verifyButton
visible: (!profile.userVerificationEnabled && !profile.isSelf) || (profile.isSelf && (model.verificationStatus != VerificationStatus.VERIFIED || !profile.userVerificationEnabled))
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify"
onClicked: {
if (model.verificationStatus == VerificationStatus.VERIFIED)
profile.unverify(model.deviceId);

View File

@ -1,8 +1,4 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.12
import QtQuick 2.6
import QtQuick.Layouts 1.2
import im.nheko 1.0
@ -33,13 +29,9 @@ Item {
fillMode: Image.Pad
}
TapHandler {
onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id)
gesturePolicy: TapHandler.ReleaseWithinBounds
}
CursorShape {
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.timeline.saveMedia(model.data.id)
cursorShape: Qt.PointingHandCursor
}

View File

@ -1,14 +1,10 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.12
import QtQuick 2.6
import im.nheko 1.0
Item {
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 divisor: model.isReply ? 5 : 3
property double divisor: model.isReply ? 4 : 2
property bool tooHigh: tempHeight > timelineRoot.height / divisor
height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight)
@ -36,24 +32,20 @@ Item {
smooth: true
mipmap: true
TapHandler {
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
onSingleTapped: {
TimelineManager.openImageOverlay(model.data.url, model.data.id);
eventPoint.accepted = true;
}
gesturePolicy: TapHandler.ReleaseWithinBounds
}
HoverHandler {
MouseArea {
id: mouseArea
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 {
id: overlay
anchors.fill: parent
visible: mouseArea.hovered
visible: mouseArea.containsMouse
Rectangle {
id: container

View File

@ -1,14 +1,9 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.6
import im.nheko 1.0
Item {
property alias modelData: model.data
property alias isReply: model.isReply
property alias child: chooser.child
property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
height: chooser.childrenRect.height

View File

@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
TextMessage {
font.italic: true
color: colors.buttonText
height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined
clip: isReply
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.5
import QtQuick.Controls 2.1

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
MatrixText {

View File

@ -1,10 +1,5 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtMultimedia 5.6
import QtQuick 2.12
import QtQuick 2.6
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.2
import im.nheko 1.0
@ -108,40 +103,20 @@ Rectangle {
width: parent.width
spacing: 15
ImageButton {
Rectangle {
id: button
Layout.alignment: Qt.AlignVCenter
//color: colors.window
//radius: 22
height: 32
width: 32
z: 3
image: ":/icons/icons/ui/arrow-pointing-down.png"
onClicked: {
switch (button.state) {
case "":
TimelineManager.timeline.cacheMedia(model.data.id);
break;
case "stopped":
media.play();
console.log("play");
button.state = "playing";
break;
case "playing":
media.pause();
console.log("pause");
button.state = "stopped";
break;
}
}
color: colors.window
radius: 22
height: 44
width: 44
states: [
State {
name: "stopped"
PropertyChanges {
target: button
image: ":/icons/icons/ui/play-sign.png"
target: img
source: "image://colorimage/:/icons/icons/ui/play-sign.png?" + colors.text
}
},
@ -149,15 +124,41 @@ Rectangle {
name: "playing"
PropertyChanges {
target: button
image: ":/icons/icons/ui/pause-symbol.png"
target: img
source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + colors.text
}
}
]
CursorShape {
Image {
id: img
anchors.centerIn: parent
z: 3
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text
fillMode: Image.Pad
}
MouseArea {
anchors.fill: parent
onClicked: {
switch (button.state) {
case "":
TimelineManager.timeline.cacheMedia(model.data.id);
break;
case "stopped":
media.play();
console.log("play");
button.state = "playing";
break;
case "playing":
media.pause();
console.log("pause");
button.state = "stopped";
break;
}
}
cursorShape: Qt.PointingHandCursor
}
@ -177,7 +178,7 @@ Rectangle {
target: TimelineManager.timeline
onMediaCached: {
if (mxcUrl == model.data.url) {
media.source = cacheUrl;
media.source = "file://" + cacheUrl;
button.state = "stopped";
console.log("media loaded: " + mxcUrl + " at " + cacheUrl);
}

View File

@ -1,8 +1,4 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.12
import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
@ -17,13 +13,10 @@ Item {
width: parent.width
height: replyContainer.height
TapHandler {
onSingleTapped: chat.model.showEvent(modelData.id)
gesturePolicy: TapHandler.ReleaseWithinBounds
}
CursorShape {
MouseArea {
anchors.fill: parent
preventStealing: false
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
cursorShape: Qt.PointingHandCursor
}
@ -50,9 +43,10 @@ Item {
color: replyComponent.userColor
textFormat: Text.RichText
TapHandler {
onSingleTapped: chat.model.openUserProfile(reply.modelData.userId)
gesturePolicy: TapHandler.ReleaseWithinBounds
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(reply.modelData.userId)
cursorShape: Qt.PointingHandCursor
}
}

View File

@ -1,18 +1,12 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import im.nheko 1.0
MatrixText {
property string formatted: model.data.formattedBody
property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body
text: "<style type=\"text/css\">a { color:" + colors.link + ";}\ncode { background-color: " + colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + colors.alternateBase + "'>")
text: "<span style='white-space: pre-wrap'><style type=\"text/css\">a { color:" + colors.link + ";}\ncode { background-color: " + colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + colors.alternateBase + "'>") + "</span>"
width: parent ? parent.width : undefined
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
clip: isReply
selectByMouse: !Settings.mobileMode && !isReply
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Window 2.10
@ -15,12 +11,9 @@ ApplicationWindow {
onClosing: TimelineManager.removeVerificationFlow(flow)
title: stack.currentItem.title
flags: Qt.Dialog
modality: Qt.WindowModal
palette: colors
height: stack.implicitHeight
width: stack.implicitWidth
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
StackView {
id: stack

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.10
import QtQuick.Controls 2.1
@ -18,6 +14,5 @@ ImageButton {
image: ":/icons/icons/ui/smile.png"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
TimelineManager.queueReactionMessage(event_id, emoji);
TimelineManager.focusMessageInput();
})
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtGraphicalEffects 1.0
import QtQuick 2.9
@ -10,7 +6,7 @@ import QtQuick.Layouts 1.3
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
Menu {
Popup {
id: emojiPopup
property var callback
@ -24,8 +20,13 @@ Menu {
function show(showAt, callback) {
console.debug("Showing emojiPicker");
if (showAt) {
parent = showAt;
x = Math.round((showAt.width - width) / 2);
y = showAt.height;
}
emojiPopup.callback = callback;
popup(showAt ? showAt : null);
open();
}
margins: 0
@ -35,74 +36,24 @@ Menu {
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
height: columnView.implicitHeight + 4
width: columnView.implicitWidth
ColumnLayout {
id: columnView
anchors.fill: parent
spacing: 0
anchors.leftMargin: 3
anchors.rightMargin: 3
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 2
// Search field
TextField {
id: emojiSearch
//width: gridView.width - 6
Layout.topMargin: 3
Layout.preferredWidth: 7 * 52 + 20 - 6
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.searchString = emojiSearch.text;
emojiPopup.model.category = Emoji.Category.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 {
}
}
}
Layout.bottomMargin: 0
Layout.leftMargin: 3
Layout.rightMargin: 3
Layout.topMargin: 2
// emoji grid
GridView {
id: gridView
Layout.preferredHeight: cellHeight * 5
Layout.preferredWidth: 7 * 52 + 20
Layout.preferredHeight: emojiPopup.height
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 4
cellWidth: 52
cellHeight: 52
@ -152,6 +103,54 @@ Menu {
}
// 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
}
@ -160,7 +159,6 @@ Menu {
// Separator
Rectangle {
visible: emojiSearch.text === ''
Layout.fillWidth: true
Layout.preferredHeight: 1
color: emojiPopup.colors.alternateBase
@ -168,7 +166,6 @@ Menu {
// Category picker row
RowLayout {
visible: emojiSearch.text === ''
Layout.bottomMargin: 0
Layout.preferredHeight: 42
implicitHeight: 42
@ -181,42 +178,42 @@ Menu {
// TODO: Would like to get 'simple' icons for the categories
ListElement {
image: ":/icons/icons/emoji-categories/people.png"
category: Emoji.Category.People
category: EmojiCategory.People
}
ListElement {
image: ":/icons/icons/emoji-categories/nature.png"
category: Emoji.Category.Nature
category: EmojiCategory.Nature
}
ListElement {
image: ":/icons/icons/emoji-categories/foods.png"
category: Emoji.Category.Food
category: EmojiCategory.Food
}
ListElement {
image: ":/icons/icons/emoji-categories/activity.png"
category: Emoji.Category.Activity
category: EmojiCategory.Activity
}
ListElement {
image: ":/icons/icons/emoji-categories/travel.png"
category: Emoji.Category.Travel
category: EmojiCategory.Travel
}
ListElement {
image: ":/icons/icons/emoji-categories/objects.png"
category: Emoji.Category.Objects
category: EmojiCategory.Objects
}
ListElement {
image: ":/icons/icons/emoji-categories/symbols.png"
category: Emoji.Category.Symbols
category: EmojiCategory.Symbols
}
ListElement {
image: ":/icons/icons/emoji-categories/flags.png"
category: Emoji.Category.Flags
category: EmojiCategory.Flags
}
}
@ -227,28 +224,27 @@ Menu {
hoverEnabled: true
ToolTip.text: {
switch (model.category) {
case Emoji.Category.People:
case EmojiCategory.People:
return qsTr('People');
case Emoji.Category.Nature:
case EmojiCategory.Nature:
return qsTr('Nature');
case Emoji.Category.Food:
case EmojiCategory.Food:
return qsTr('Food');
case Emoji.Category.Activity:
case EmojiCategory.Activity:
return qsTr('Activity');
case Emoji.Category.Travel:
case EmojiCategory.Travel:
return qsTr('Travel');
case Emoji.Category.Objects:
case EmojiCategory.Objects:
return qsTr('Objects');
case Emoji.Category.Symbols:
case EmojiCategory.Symbols:
return qsTr('Symbols');
case Emoji.Category.Flags:
case EmojiCategory.Flags:
return qsTr('Flags');
}
}
ToolTip.visible: hovered
onClicked: {
//emojiPopup.model.category = model.category;
gridView.positionViewAtIndex(emojiPopup.model.sourceModel.categoryToIndex(model.category), GridView.Beginning);
emojiPopup.model.category = model.category;
}
MouseArea {
@ -279,6 +275,56 @@ Menu {
}
// Separator
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 1
implicitWidth: 1
height: parent.height
color: emojiPopup.colors.alternateBase
}
// 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)
}
}
}
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtGraphicalEffects 1.0
import QtQuick 2.10
import QtQuick.Controls 2.3
@ -13,9 +9,9 @@ Item {
property real radius: 0
property color color: "#22000000"
property real maxRadius: Math.max(width, height)
readonly property real radiusAnimationRate: 0.05
readonly property real radiusTailAnimationRate: 0.5
readonly property real opacityAnimationDuration: 300
property real radiusAnimationRate: 0.05
property real radiusTailAnimationRate: 0.5
property real opacityAnimationDuration: 300
readonly property real diameter: radius * 2
property real centerX
property real centerY

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -16,7 +12,7 @@ Rectangle {
MouseArea {
anchors.fill: parent
onClicked: {
if (CallManager.callType != CallType.VOICE)
if (CallManager.isVideo)
stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
}
@ -46,46 +42,10 @@ Rectangle {
}
Image {
id: callTypeIcon
Layout.leftMargin: 4
Layout.preferredWidth: 24
Layout.preferredHeight: 24
}
Item {
states: [
State {
name: "VOICE"
when: CallManager.callType == CallType.VOICE
PropertyChanges {
target: callTypeIcon
source: "qrc:/icons/icons/ui/place-call.png"
}
},
State {
name: "VIDEO"
when: CallManager.callType == CallType.VIDEO
PropertyChanges {
target: callTypeIcon
source: "qrc:/icons/icons/ui/video-call.png"
}
},
State {
name: "SCREEN"
when: CallManager.callType == CallType.SCREEN
PropertyChanges {
target: callTypeIcon
source: "qrc:/icons/icons/ui/screen-share.png"
}
}
]
source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
}
Label {
@ -143,7 +103,7 @@ Rectangle {
PropertyChanges {
target: stackLayout
currentIndex: CallManager.callType != CallType.VOICE ? 1 : 0
currentIndex: CallManager.isVideo ? 1 : 0
}
},
@ -187,28 +147,20 @@ Rectangle {
}
}
Label {
Layout.leftMargin: 16
visible: CallManager.callType == CallType.SCREEN && CallManager.callState == WebRTCState.CONNECTED
text: qsTr("You are screen sharing")
font.pointSize: fontMetrics.font.pointSize * 1.1
color: "#000000"
}
Item {
Layout.fillWidth: true
}
ImageButton {
visible: CallManager.haveLocalPiP
visible: CallManager.haveLocalVideo
width: 24
height: 24
buttonTextColor: "#000000"
image: ":/icons/icons/ui/toggle-camera-view.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Hide/Show Picture-in-Picture")
onClicked: CallManager.toggleLocalPiP()
ToolTip.text: qsTr("Toggle camera view")
onClicked: CallManager.toggleCameraView()
}
ImageButton {

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
@ -44,7 +40,7 @@ Popup {
}
RowLayout {
visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0
visible: CallManager.isVideo && CallManager.cameras.length > 0
Image {
Layout.preferredWidth: 22

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -57,7 +53,7 @@ Popup {
Layout.bottomMargin: msgView.height / 25
Image {
property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
property string image: CallManager.isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: msgView.height / 10
@ -67,7 +63,7 @@ Popup {
Label {
Layout.alignment: Qt.AlignCenter
text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
text: CallManager.isVideo ? qsTr("Video Call") : qsTr("Voice Call")
font.pointSize: fontMetrics.font.pointSize * 2
color: colors.windowText
}
@ -101,7 +97,7 @@ Popup {
}
RowLayout {
visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0
visible: CallManager.isVideo && CallManager.cameras.length > 0
Layout.alignment: Qt.AlignCenter
Image {
@ -163,7 +159,7 @@ Popup {
RoundButton {
id: acceptButton
property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
property string image: CallManager.isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
implicitWidth: buttonLayout.buttonSize
implicitHeight: buttonLayout.buttonSize

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -56,12 +52,12 @@ Rectangle {
Layout.leftMargin: 4
Layout.preferredWidth: 24
Layout.preferredHeight: 24
source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
}
Label {
font.pointSize: fontMetrics.font.pointSize * 1.1
text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
text: CallManager.isVideo ? qsTr("Video Call") : qsTr("Voice Call")
color: "#000000"
}
@ -79,6 +75,7 @@ Rectangle {
ToolTip.visible: hovered
ToolTip.text: qsTr("Devices")
onClicked: {
CallManager.refreshDevices();
var dialog = devicesDialog.createObject(timelineRoot);
dialog.open();
}
@ -86,7 +83,7 @@ Rectangle {
Button {
Layout.rightMargin: 4
icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
icon.source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
text: qsTr("Accept")
palette: colors
onClicked: {
@ -105,7 +102,7 @@ Rectangle {
dialog.open();
return ;
}
if (CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
if (CallManager.isVideo && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
var dialog = deviceError.createObject(timelineRoot, {
"errorString": qsTr("Unknown camera: %1").arg(Settings.camera),
"image": ":/icons/icons/ui/video-call.png"

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.9
import QtQuick.Controls 2.3
@ -27,14 +23,6 @@ Popup {
}
Component {
id: screenShareDialog
ScreenShare {
}
}
ColumnLayout {
id: columnLayout
@ -88,7 +76,7 @@ Popup {
onClicked: {
if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE);
CallManager.sendInvite(TimelineManager.timeline.roomId(), false);
close();
}
}
@ -102,23 +90,12 @@ Popup {
if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText;
Settings.camera = cameraCombo.currentText;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO);
CallManager.sendInvite(TimelineManager.timeline.roomId(), true);
close();
}
}
}
Button {
visible: CallManager.screenShareSupported
text: qsTr("Screen")
icon.source: "qrc:/icons/icons/ui/screen-share.png"
onClicked: {
var dialog = screenShareDialog.createObject(timelineRoot);
dialog.open();
close();
}
}
Button {
text: qsTr("Cancel")
onClicked: {

View File

@ -1,168 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "../"
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import im.nheko 1.0
Popup {
modal: true
// only set the anchors on Qt 5.12 or higher
// see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
Component.onCompleted: {
if (anchors)
anchors.centerIn = parent;
frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate);
}
palette: colors
ColumnLayout {
Label {
Layout.topMargin: 16
Layout.bottomMargin: 16
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.alignment: Qt.AlignLeft
text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
color: colors.windowText
}
RowLayout {
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 8
Label {
Layout.alignment: Qt.AlignLeft
text: qsTr("Window:")
color: colors.windowText
}
ComboBox {
id: windowCombo
Layout.fillWidth: true
model: CallManager.windowList()
}
}
RowLayout {
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 8
Label {
Layout.alignment: Qt.AlignLeft
text: qsTr("Frame rate:")
color: colors.windowText
}
ComboBox {
id: frameRateCombo
Layout.fillWidth: true
model: ["25", "20", "15", "10", "5", "2", "1"]
}
}
GridLayout {
columns: 2
rowSpacing: 10
Layout.margins: 8
MatrixText {
text: qsTr("Include your camera picture-in-picture")
}
ToggleButton {
id: pipCheckBox
enabled: CallManager.cameras.length > 0
checked: Settings.screenSharePiP
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Request remote camera")
ToolTip.text: qsTr("View your callee's camera like a regular video call")
ToolTip.visible: hovered
}
ToggleButton {
id: remoteVideoCheckBox
Layout.alignment: Qt.AlignRight
checked: Settings.screenShareRemoteVideo
ToolTip.text: qsTr("View your callee's camera like a regular video call")
ToolTip.visible: hovered
}
MatrixText {
text: qsTr("Hide mouse cursor")
}
ToggleButton {
id: hideCursorCheckBox
Layout.alignment: Qt.AlignRight
checked: Settings.screenShareHideCursor
}
}
RowLayout {
Layout.margins: 8
Item {
Layout.fillWidth: true
}
Button {
text: qsTr("Share")
icon.source: "qrc:/icons/icons/ui/screen-share.png"
onClicked: {
if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText;
if (pipCheckBox.checked)
Settings.camera = cameraCombo.currentText;
Settings.screenShareFrameRate = frameRateCombo.currentText;
Settings.screenSharePiP = pipCheckBox.checked;
Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
Settings.screenShareHideCursor = hideCursorCheckBox.checked;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex);
close();
}
}
}
Button {
text: qsTr("Preview")
onClicked: {
CallManager.previewWindow(windowCombo.currentIndex);
}
}
Button {
text: qsTr("Cancel")
onClicked: {
close();
}
}
}
}
background: Rectangle {
color: colors.window
border.color: colors.windowText
}
}

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import org.freedesktop.gstreamer.GLVideoItem 1.0

View File

@ -74,7 +74,6 @@
<file>icons/ui/end-call.png</file>
<file>icons/ui/microphone-mute.png</file>
<file>icons/ui/microphone-unmute.png</file>
<file>icons/ui/screen-share.png</file>
<file>icons/ui/toggle-camera-view.png</file>
<file>icons/ui/video-call.png</file>
@ -129,22 +128,16 @@
<file>qml/EncryptionIndicator.qml</file>
<file>qml/ImageButton.qml</file>
<file>qml/MatrixText.qml</file>
<file>qml/MatrixTextField.qml</file>
<file>qml/ToggleButton.qml</file>
<file>qml/MessageInput.qml</file>
<file>qml/MessageView.qml</file>
<file>qml/NhekoBusyIndicator.qml</file>
<file>qml/PrivacyScreen.qml</file>
<file>qml/Reactions.qml</file>
<file>qml/ReplyPopup.qml</file>
<file>qml/ScrollHelper.qml</file>
<file>qml/StatusIndicator.qml</file>
<file>qml/TimelineRow.qml</file>
<file>qml/TopBar.qml</file>
<file>qml/QuickSwitcher.qml</file>
<file>qml/ForwardCompleter.qml</file>
<file>qml/TypingIndicator.qml</file>
<file>qml/RoomSettings.qml</file>
<file>qml/emoji/EmojiButton.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/UserProfile.qml</file>
@ -171,7 +164,6 @@
<file>qml/voip/CallInviteBar.qml</file>
<file>qml/voip/DeviceError.qml</file>
<file>qml/voip/PlaceCall.qml</file>
<file>qml/voip/ScreenShare.qml</file>
<file>qml/voip/VideoCall.qml</file>
</qresource>
<qresource prefix="/media">

View File

@ -11,13 +11,26 @@ class Emoji(object):
self.code = repr(code.encode('utf-8'))[1:].strip("'")
self.shortname = shortname
def generate_code(emojis, category):
tmpl = Template('''
const std::vector<Emoji> emoji::Provider::{{ category }} = {
// {{ category.capitalize() }}
{%- for e in emoji %}
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ category.capitalize() }}},
{%- endfor %}
};
''')
d = dict(category=category, emoji=emojis)
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::Emoji::Category::{{ c[0].capitalize() }}},
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ c[0].capitalize() }}},
{%- endfor %}
{%- endfor %}
};
@ -80,4 +93,12 @@ if __name__ == '__main__':
# Use xclip to pipe the output to clipboard.
# e.g ./codegen.py emoji.json | xclip -sel clip
generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags)
generate_code(people, 'people')
generate_code(nature, 'nature')
generate_code(food, 'food')
generate_code(activity, 'activity')
generate_code(travel, 'travel')
generate_code(objects, 'objects')
generate_code(symbols, 'symbols')
generate_code(flags, 'flags')
generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags)

View File

@ -1,772 +0,0 @@
#!/usr/bin/python3
# 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.
import asyncio
import base64
import binascii
import errno
import fnmatch
import gzip
import json
import logging
import os
import sys
import time
import traceback
from argparse import ArgumentParser
from functools import reduce
from urllib.parse import urljoin, urlparse, urlsplit, urlunparse, urlunsplit
import aiohttp
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
import gi
gi.require_version('OSTree', '1.0')
from gi.repository import Gio, GLib, OSTree
UPLOAD_CHUNK_LIMIT = 4 * 1024 * 1024
DEFAULT_LIMIT = 2 ** 16
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
class UsageException(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class ApiError(Exception):
def __init__(self, response, body):
self.url = str(response.url)
self.status = response.status
try:
self.body = json.loads(response);
except:
self.body = {"status": self.status, "error-type": "no-error", "message": "No json error details from server"}
def repr(self):
return {
"type": "api",
"url": self.url,
"status_code": self.status,
"details": self.body
}
def __str__(self):
return "Api call to %s failed with status %d, details: %s" % (self.url, self.status, self.body)
# This is similar to the regular payload, but opens the file lazily
class AsyncNamedFilePart(aiohttp.payload.Payload):
def __init__(self,
value,
disposition='attachment',
*args,
**kwargs):
self._file = None
if 'filename' not in kwargs:
kwargs['filename'] = os.path.basename(value)
super().__init__(value, *args, **kwargs)
if self._filename is not None and disposition is not None:
self.set_content_disposition(disposition, filename=self._filename, quote_fields=False)
self._size = os.stat(value).st_size
async def write(self, writer):
if self._file is None or self._file.closed:
self._file = open(self._value, 'rb')
try:
chunk = self._file.read(DEFAULT_LIMIT)
while chunk:
await writer.write(chunk)
chunk = self._file.read(DEFAULT_LIMIT)
finally:
self._file.close()
@property
def size(self):
return self._size
def ostree_object_path(repo, obj):
repodir = repo.get_path().get_path()
return os.path.join(repodir, 'objects', obj[0:2], obj[2:])
def ostree_get_dir_files(repo, objects, dirtree):
if dirtree.endswith(".dirtree"):
dirtree = dirtree[:-8]
dirtreev = repo.load_variant(OSTree.ObjectType.DIR_TREE, dirtree)[1]
iter = OSTree.RepoCommitTraverseIter()
iter.init_dirtree(repo, dirtreev, 0)
while True:
type = iter.next()
if type == OSTree.RepoCommitIterResult.END:
break
if type == OSTree.RepoCommitIterResult.ERROR:
break
if type == OSTree.RepoCommitIterResult.FILE:
d = iter.get_file()
objects.add(d.out_checksum + ".filez")
if type == OSTree.RepoCommitIterResult.DIR:
pass
def local_needed_files(repo, metadata_objects):
objects = set()
for c in metadata_objects:
if c.endswith(".dirtree"):
ostree_get_dir_files(repo, objects, c)
return objects
def local_needed_metadata_dirtree(repo, objects, dirtree_content, dirtree_meta):
objects.add(dirtree_meta + ".dirmeta")
dirtree_content_name = dirtree_content + ".dirtree"
if dirtree_content_name in objects:
return
objects.add(dirtree_content_name)
dirtreev = repo.load_variant(OSTree.ObjectType.DIR_TREE, dirtree_content)[1]
iter = OSTree.RepoCommitTraverseIter()
iter.init_dirtree(repo, dirtreev, 0)
while True:
type = iter.next()
if type == OSTree.RepoCommitIterResult.END:
break
if type == OSTree.RepoCommitIterResult.ERROR:
break
if type == OSTree.RepoCommitIterResult.FILE:
pass
if type == OSTree.RepoCommitIterResult.DIR:
d = iter.get_dir()
local_needed_metadata_dirtree(repo, objects, d.out_content_checksum, d.out_meta_checksum)
def local_needed_metadata(repo, commits):
objects = set()
for rev in commits:
objects.add(rev + ".commit")
commitv = repo.load_variant(OSTree.ObjectType.COMMIT, rev)[1]
iter = OSTree.RepoCommitTraverseIter()
iter.init_commit(repo, commitv, 0)
while True:
type = iter.next()
if type == OSTree.RepoCommitIterResult.END:
break
if type == OSTree.RepoCommitIterResult.ERROR:
break
if type == OSTree.RepoCommitIterResult.FILE:
pass
if type == OSTree.RepoCommitIterResult.DIR:
d = iter.get_dir()
local_needed_metadata_dirtree(repo, objects, d.out_content_checksum, d.out_meta_checksum)
return objects
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
async def missing_objects(session, build_url, token, wanted):
missing=[]
for chunk in chunks(wanted, 2000):
wanted_json=json.dumps({'wanted': chunk}).encode('utf-8')
data=gzip.compress(wanted_json)
headers = {
'Authorization': 'Bearer ' + token,
'Content-Encoding': 'gzip',
'Content-Type': 'application/json'
}
resp = await session.get(build_url + "/missing_objects", data=data, headers=headers)
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
data = await resp.json()
missing.extend(data["missing"])
return missing
async def upload_files(session, build_url, token, files):
if len(files) == 0:
return
print("Uploading %d files (%d bytes)" % (len(files), reduce(lambda x, y: x + y, map(lambda f: f.size, files))))
with aiohttp.MultipartWriter() as writer:
for f in files:
writer.append(f)
writer.headers['Authorization'] = 'Bearer ' + token
resp = await session.request("post", build_url + '/upload', data=writer, headers=writer.headers)
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
async def upload_deltas(session, repo_path, build_url, token, deltas, refs, ignore_delta):
if not len(deltas):
return
req = []
for ref, commit in refs.items():
# Skip screenshots here
parts = ref.split("/")
if len(parts) == 4 and (parts[0] == "app" or parts[0] =="runtime") and not should_skip_delta(parts[1], ignore_delta):
for delta in deltas:
# Only upload from-scratch deltas, as these are the only reused ones
if delta == commit:
print(" %s: %s" % (ref, delta))
delta_name = delta_name_encode (delta)
delta_dir = repo_path + "/deltas/" + delta_name[:2] + "/" + delta_name[2:]
parts = os.listdir(delta_dir)
for part in parts:
req.append(AsyncNamedFilePart(delta_dir + "/" + part, filename = delta_name + "." + part + ".delta"))
if len(req):
await upload_files(session, build_url, token, req)
async def upload_objects(session, repo_path, build_url, token, objects):
req = []
total_size = 0
for file_obj in objects:
named = get_object_multipart(repo_path, file_obj)
file_size = named.size
if total_size + file_size > UPLOAD_CHUNK_LIMIT: # The new object would bring us over the chunk limit
if len(req) > 0: # We already have some objects, upload those first
next_req = [named]
total_size = file_size
else:
next_req = []
req.append(named)
total_size = 0
await upload_files(session, build_url, token, req)
req = next_req
else:
total_size = total_size + file_size
req.append(named)
# Upload any remainder
await upload_files(session, build_url, token, req)
async def create_ref(session, build_url, token, ref, commit):
print("Creating ref %s with commit %s" % (ref, commit))
resp = await session.post(build_url + "/build_ref", headers={'Authorization': 'Bearer ' + token}, json= { "ref": ref, "commit": commit} )
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
data = await resp.json()
return data
async def add_extra_ids(session, build_url, token, extra_ids):
print("Adding extra ids %s" % (extra_ids))
resp = await session.post(build_url + "/add_extra_ids", headers={'Authorization': 'Bearer ' + token}, json= { "ids": extra_ids} )
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
data = await resp.json()
return data
async def get_build(session, build_url, token):
resp = await session.get(build_url, headers={'Authorization': 'Bearer ' + token})
if resp.status != 200:
raise ApiError(resp, await resp.text())
data = await resp.json()
return data
# For stupid reasons this is a string with json, lets expand it
def reparse_job_results(job):
job["results"] = json.loads(job.get("results", "{}"))
return job
async def get_job(session, job_url, token):
resp = await session.get(job_url, headers={'Authorization': 'Bearer ' + token}, json={})
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
data = await resp.json()
return data
async def wait_for_job(session, job_url, token):
reported_delay = False
old_job_status = 0
printed_len = 0
iterations_since_change=0
error_iterations = 0
while True:
try:
resp = await session.get(job_url, headers={'Authorization': 'Bearer ' + token}, json={'log-offset': printed_len})
async with resp:
if resp.status == 200:
error_iterations = 0
job = await resp.json()
job_status = job['status']
if job_status == 0 and not reported_delay:
reported_delay = True
start_after_struct = job.get("start_after", None)
if start_after_struct:
start_after = start_after_struct.get("secs_since_epoch", None)
now = time.time()
if start_after and start_after > now:
print("Waiting %d seconds before starting job" % (int(start_after - now)))
if job_status > 0 and old_job_status == 0:
print("/ Job was started");
old_job_status = job_status
log = job['log']
if len(log) > 0:
iterations_since_change=0
for line in log.splitlines(True):
print("| %s" % line, end="")
printed_len = printed_len + len(log)
else:
iterations_since_change=iterations_since_change+1
if job_status > 1:
if job_status == 2:
print("\ Job completed successfully")
else:
print("\ Job failed")
return job
else:
iterations_since_change=4 # Start at 4 so we ramp up the delay faster
error_iterations=error_iterations + 1
if error_iterations <= 5:
print("Unexpected response %s getting job log, ignoring" % resp.status)
else:
raise ApiError(resp, await resp.text())
except OSError as e:
if e.args[0] == errno.ECONNRESET:
# Client disconnected, retry
# Not sure exactly why, but i got a lot of ConnectionResetErrors here
# in tests. I guess the server stops reusing a http2 session after a bit
# Should be fine to retry with the backof
pass
else:
raise
# Some polling backoff to avoid loading the server
if iterations_since_change <= 1:
sleep_time=1
elif iterations_since_change < 5:
sleep_time=3
elif iterations_since_change < 15:
sleep_time=5
elif iterations_since_change < 30:
sleep_time=10
else:
sleep_time=60
time.sleep(sleep_time)
async def commit_build(session, build_url, eol, eol_rebase, token_type, wait, token):
print("Committing build %s" % (build_url))
json = {
"endoflife": eol,
"endoflife_rebase": eol_rebase
}
if token_type != None:
json['token_type'] = token_type
resp = await session.post(build_url + "/commit", headers={'Authorization': 'Bearer ' + token}, json=json)
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
job = await resp.json()
job_url = resp.headers['location'];
if wait:
print("Waiting for commit job")
job = await wait_for_job(session, job_url, token);
reparse_job_results(job)
job["location"] = job_url
return job
async def publish_build(session, build_url, wait, token):
print("Publishing build %s" % (build_url))
resp = await session.post(build_url + "/publish", headers={'Authorization': 'Bearer ' + token}, json= { } )
async with resp:
if resp.status == 400:
body = await resp.text()
try:
msg = json.loads(body)
if msg.get("current-state", "") == "published":
print("the build has been already published")
return {}
except:
pass
if resp.status != 200:
raise ApiError(resp, await resp.text())
job = await resp.json()
job_url = resp.headers['location'];
if wait:
print("Waiting for publish job")
job = await wait_for_job(session, job_url, token);
reparse_job_results(job)
job["location"] = job_url
return job
async def purge_build(session, build_url, token):
print("Purging build %s" % (build_url))
resp = await session.post(build_url + "/purge", headers={'Authorization': 'Bearer ' + token}, json= {} )
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
return await resp.json()
async def create_token(session, manager_url, token, name, subject, scope, duration):
token_url = urljoin(manager_url, "api/v1/token_subset")
resp = await session.post(token_url, headers={'Authorization': 'Bearer ' + token}, json = {
"name": name,
"sub": subject,
"scope": scope,
"duration": duration,
})
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
return await resp.json()
def get_object_multipart(repo_path, object):
return AsyncNamedFilePart(repo_path + "/objects/" + object[:2] + "/" + object[2:], filename=object)
async def create_command(session, args):
build_url = urljoin(args.manager_url, "api/v1/build")
resp = await session.post(build_url, headers={'Authorization': 'Bearer ' + args.token}, json={
"repo": args.repo
})
async with resp:
if resp.status != 200:
raise ApiError(resp, await resp.text())
data = await resp.json()
data["location"] = resp.headers['location']
if not args.print_output:
print(resp.headers['location'])
return data
def delta_name_part_encode(commit):
return base64.b64encode(binascii.unhexlify(commit), b"+_")[:-1].decode("utf-8")
def delta_name_encode (delta):
return "-".join(map(delta_name_part_encode, delta.split("-")))
def should_skip_delta(id, globs):
if globs:
for glob in globs:
if fnmatch.fnmatch(id, glob):
return True
return False
def build_url_to_api(build_url):
parts = urlparse(build_url)
path = os.path.dirname(os.path.dirname(parts.path))
return urlunparse((parts.scheme, parts.netloc, path, None, None, None))
@retry(
stop=stop_after_attempt(6),
wait=wait_fixed(10),
retry=retry_if_exception_type(ApiError),
reraise=True,
)
async def push_command(session, args):
local_repo = OSTree.Repo.new(Gio.File.new_for_path(args.repo_path))
try:
local_repo.open(None)
except GLib.Error as err:
raise UsageException("Can't open repo %s: %s" % (args.repo_path, err.message)) from err
refs = {}
if len(args.branches) == 0:
_, all_refs = local_repo.list_refs(None, None)
for ref in all_refs:
if ref.startswith("app/") or ref.startswith("runtime/") or ref.startswith("screenshots/"):
refs[ref] = all_refs[ref]
else:
for branch in args.branches:
_, rev = local_repo.resolve_rev(branch, False)
refs[branch] = rev
if (args.minimal_token):
id = os.path.basename(urlparse(args.build_url).path)
token = create_token(args.build_url, args.token, "minimal-upload", "build/%s" % (id), ["upload"], 60*60)["token"]
else:
token = args.token
print("Uploading refs to %s: %s"% (args.build_url, list(refs)))
metadata_objects = local_needed_metadata(local_repo, refs.values())
print("Refs contain %d metadata objects" % (len(metadata_objects)))
missing_metadata_objects = await missing_objects(session, args.build_url, token, list(metadata_objects))
print("Remote missing %d of those" % (len(missing_metadata_objects)))
file_objects = local_needed_files(local_repo, missing_metadata_objects)
print("Has %d file objects for those" % (len(file_objects)))
missing_file_objects = await missing_objects(session, args.build_url, token, list(file_objects))
print("Remote missing %d of those" % (len(missing_file_objects)))
# First upload all missing file objects
print("Uploading file objects")
await upload_objects(session, args.repo_path, args.build_url, token, missing_file_objects)
# Then all the metadata
print("Uploading metadata objects")
await upload_objects(session, args.repo_path, args.build_url, token, missing_metadata_objects)
_, deltas = local_repo.list_static_delta_names()
print("Uploading deltas")
await upload_deltas(session, args.repo_path, args.build_url, token, deltas, refs, args.ignore_delta)
# Then the refs
for ref, commit in refs.items():
await create_ref(session, args.build_url, token, ref, commit)
# Then any extra ids
if args.extra_id:
await add_extra_ids(session, args.build_url, token, args.extra_id)
commit_job = None
publish_job = None
update_job = None
# Note, this always uses the full token, as the minimal one only has upload permissions
if args.commit or args.publish:
commit_job = await commit_build(session, args.build_url, args.end_of_life, args.end_of_life_rebase, args.token_type, args.publish or args.wait, args.token)
if args.publish:
publish_job = await publish_build(session, args.build_url, args.wait or args.wait_update, args.token)
update_job_id = publish_job.get("results", {}).get("update-repo-job", None)
if update_job_id:
print("Queued repo update job %d" %(update_job_id))
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
if args.wait_update:
print("Waiting for repo update job")
update_job = await wait_for_job (session, update_job_url, token);
else:
update_job = await get_job(session, update_job_url, token)
reparse_job_results(update_job)
update_job["location"] = update_job_url
data = await get_build(session, args.build_url, args.token)
if commit_job:
data["commit_job"] = commit_job
if publish_job:
data["publish_job"] = publish_job
if update_job:
data["update_job"] = update_job
return data
async def commit_command(session, args):
job = await commit_build(session, args.build_url, args.end_of_life, args.end_of_life_rebase, args.token_type, args.wait, args.token)
return job
async def publish_command(session, args):
job = await publish_build(session, args.build_url, args.wait or args.wait_update, args.token)
update_job_id = job.get("results", {}).get("update-repo-job", None)
if update_job_id:
print("Queued repo update job %d" %(update_job_id))
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
if args.wait_update:
print("Waiting for repo update job")
update_job = await wait_for_job(session, update_job_url, args.token);
else:
update_job = await get_job(session, update_job_url, args.token)
reparse_job_results(update_job)
update_job["location"] = update_job_url
return job
async def purge_command(session, args):
job = await purge_build(session, args.build_url, args.token)
return job
async def create_token_command(session, args):
data = await create_token(session, args.manager_url, args.token, args.name, args.subject, args.scope, args.duration)
if not args.print_output:
print(data['token'])
return data
async def follow_job_command(session, args):
job = await wait_for_job(session, args.job_url, args.token)
return job
async def run_with_session(args):
timeout = aiohttp.ClientTimeout(total=90*60)
async with aiohttp.ClientSession(timeout=timeout) as session:
result = await args.func(session, args)
return result
if __name__ == '__main__':
progname = os.path.basename(sys.argv[0])
parser = ArgumentParser(prog=progname)
parser.add_argument('-v', '--verbose', action='store_true',
help='enable verbose output')
parser.add_argument('--debug', action='store_true',
help='enable debugging output')
parser.add_argument('--output', help='Write output json to file')
parser.add_argument('--print-output', action='store_true', help='Print output json')
parser.add_argument('--token', help='use this token')
parser.add_argument('--token-file', help='use token from file')
subparsers = parser.add_subparsers(title='subcommands',
dest='subparser_name',
description='valid subcommands',
help='additional help')
create_parser = subparsers.add_parser('create', help='Create new build')
create_parser.add_argument('manager_url', help='remote repo manager url')
create_parser.add_argument('repo', help='repo name')
create_parser.set_defaults(func=create_command)
push_parser = subparsers.add_parser('push', help='Push to repo manager')
push_parser.add_argument('build_url', help='remote build url')
push_parser.add_argument('repo_path', help='local repository')
push_parser.add_argument('branches', nargs='*', help='branches to push')
push_parser.add_argument('--commit', action='store_true',
help='commit build after pushing')
push_parser.add_argument('--publish', action='store_true',
help='publish build after committing')
push_parser.add_argument('--extra-id', action='append', help='add extra collection-id')
push_parser.add_argument('--ignore-delta', action='append', help='don\'t upload deltas matching this glob')
push_parser.add_argument('--wait', action='store_true',
help='wait for commit/publish to finish')
push_parser.add_argument('--wait-update', action='store_true',
help='wait for update-repo to finish')
push_parser.add_argument('--minimal-token', action='store_true',
help='Create minimal token for the upload')
push_parser.add_argument('--end-of-life', help='Set end of life')
push_parser.add_argument('--end-of-life-rebase', help='Set new ID which will supercede the current one')
push_parser.add_argument('--token-type', help='Set token type', type=int)
push_parser.set_defaults(func=push_command)
commit_parser = subparsers.add_parser('commit', help='Commit build')
commit_parser.add_argument('--wait', action='store_true',
help='wait for commit to finish')
commit_parser.add_argument('--end-of-life', help='Set end of life')
commit_parser.add_argument('--end-of-life-rebase', help='Set new ID which will supercede the current one')
commit_parser.add_argument('--token-type', help='Set token type', type=int)
commit_parser.add_argument('build_url', help='remote build url')
commit_parser.set_defaults(func=commit_command)
publish_parser = subparsers.add_parser('publish', help='Publish build')
publish_parser.add_argument('--wait', action='store_true',
help='wait for publish to finish')
publish_parser.add_argument('--wait-update', action='store_true',
help='wait for update-repo to finish')
publish_parser.add_argument('build_url', help='remote build url')
publish_parser.set_defaults(func=publish_command)
purge_parser = subparsers.add_parser('purge', help='Purge build')
purge_parser.add_argument('build_url', help='remote build url')
purge_parser.set_defaults(func=purge_command)
create_token_parser = subparsers.add_parser('create-token', help='Create subset token')
create_token_parser.add_argument('manager_url', help='remote repo manager url')
create_token_parser.add_argument('name', help='Name')
create_token_parser.add_argument('subject', help='Subject')
create_token_parser.add_argument('scope', nargs='*', help='Scope')
create_token_parser.add_argument('--duration', help='Duration until expires, in seconds',
default=60*60*24, # Default duration is one day
type=int)
create_token_parser.set_defaults(func=create_token_command)
follow_job_parser = subparsers.add_parser('follow-job', help='Follow existing job log')
follow_job_parser.add_argument('job_url', help='url of job')
follow_job_parser.set_defaults(func=follow_job_command)
args = parser.parse_args()
loglevel = logging.WARNING
if args.verbose:
loglevel = logging.INFO
if args.debug:
loglevel = logging.DEBUG
logging.basicConfig(format='%(module)s: %(levelname)s: %(message)s',
level=loglevel, stream=sys.stderr)
if not args.subparser_name:
print("No subcommand specified, see --help for usage")
exit(1)
if not args.token:
if args.token_file:
file = open(args.token_file, 'rb')
args.token = file.read().splitlines()[0].decode("utf-8").strip()
elif "REPO_TOKEN" in os.environ:
args.token = os.environ["REPO_TOKEN"]
else:
print("No token available, pass with --token, --token-file or $REPO_TOKEN")
exit(1)
res = 1
output = None
try:
loop = asyncio.get_event_loop()
result = loop.run_until_complete(run_with_session(args))
output = {
"command": args.subparser_name,
"result": result,
}
res = 0
except SystemExit:
# Something called sys.exit(), lets just exit
res = 1
raise # Pass on regular exit callse
except ApiError as e:
eprint(str(e))
output = {
"command": args.subparser_name,
"error": e.repr(),
}
except UsageException as e:
eprint(str(e))
output = {
"error": {
"type": "usage",
"details": {
"message": str(e),
}
}
}
except:
ei = sys.exc_info()
eprint("Unexpected %s exception in %s: %s" % (ei[0].__name__, args.subparser_name, ei[1]))
eprint(traceback.format_exc())
output = {
"command": args.subparser_name,
"error": {
"type": "exception",
"details": {
"error-type": ei[0].__name__,
"message": str(ei[1]),
}
}
}
res = 1
if output:
if args.print_output:
print(json.dumps(output, indent=4))
if args.output:
f = open(args.output,"w+")
f.write(json.dumps(output, indent=4))
f.write("\n")
f.close()
exit(res)

View File

@ -1,24 +0,0 @@
#!/bin/bash
if [ -z "$1" ]; then
echo "Missing repo to upload!"
exit 1
fi
if [ -n "${CI_COMMIT_TAG}" ]; then
BUILD_URL=$(./flat-manager-client create https://flatpak.neko.dev stable)
elif [ "master" = "${CI_COMMIT_REF_NAME}" ]; then
BUILD_URL=$(./flat-manager-client create https://flatpak.neko.dev nightly)
fi
if [ -z "${BUILD_URL}" ]; then
echo "No upload to repo."
exit 0
fi
BUILD_URL=${BUILD_URL/http:/https:}
./flat-manager-client push $BUILD_URL $1
./flat-manager-client commit --wait $BUILD_URL
./flat-manager-client publish --wait $BUILD_URL

View File

@ -1,11 +1,22 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QBuffer>
#include <QPixmapCache>
#include <QPointer>
#include <memory>
#include <unordered_map>
@ -13,14 +24,13 @@
#include "Cache.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "Utils.h"
static QPixmapCache avatar_cache;
namespace AvatarProvider {
void
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback)
{
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
@ -35,32 +45,44 @@ resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
return;
}
MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
QSize(size, size),
[callback, cacheKey, recv = QPointer<QObject>(receiver)](
QString, QSize, QImage img, QString) {
if (!recv)
return;
auto data = cache::image(cacheKey);
if (!data.isNull()) {
pixmap = QPixmap::fromImage(utils::readImage(&data));
avatar_cache.insert(cacheKey, pixmap);
callback(pixmap);
return;
}
auto proxy = std::make_shared<AvatarProxy>();
QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded,
recv,
[callback, cacheKey](QPixmap pm) {
if (!pm.isNull())
avatar_cache.insert(
cacheKey, pm);
callback(pm);
});
auto proxy = std::make_shared<AvatarProxy>();
QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded,
receiver,
[callback, cacheKey](QByteArray data) {
QPixmap pm = QPixmap::fromImage(utils::readImage(&data));
avatar_cache.insert(cacheKey, pm);
callback(pm);
});
if (img.isNull()) {
emit proxy->avatarDownloaded(QPixmap{});
return;
}
mtx::http::ThumbOpts opts;
opts.width = size;
opts.height = size;
opts.mxc_url = avatarUrl.toStdString();
auto pm = QPixmap::fromImage(std::move(img));
emit proxy->avatarDownloaded(pm);
});
http::client()->get_thumbnail(
opts,
[opts, cacheKey, proxy = std::move(proxy)](const std::string &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to download avatar: {} - ({} {})",
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
} else {
cache::saveImage(cacheKey.toStdString(), res);
}
emit proxy->avatarDownloaded(QByteArray(res.data(), (int)res.size()));
});
}
void
@ -70,8 +92,8 @@ resolve(const QString &room_id,
QObject *receiver,
AvatarCallback callback)
{
auto avatarUrl = cache::avatarUrl(room_id, user_id);
const auto avatarUrl = cache::avatarUrl(room_id, user_id);
resolve(std::move(avatarUrl), size, receiver, callback);
resolve(avatarUrl, size, receiver, callback);
}
}

View File

@ -1,26 +1,38 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QPixmap>
#include <functional>
using AvatarCallback = std::function<void(QPixmap)>;
class AvatarProxy : public QObject
{
Q_OBJECT
signals:
void avatarDownloaded(QPixmap pm);
void avatarDownloaded(const QByteArray &data);
};
using AvatarCallback = std::function<void(QPixmap)>;
namespace AvatarProvider {
void
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback cb);
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb);
void
resolve(const QString &room_id,
const QString &user_id,

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