Merge remote-tracking branch 'upstream/master' into screenshare-x11
This commit is contained in:
commit
55fb00c67b
@ -88,7 +88,7 @@ build-flatpak-amd64:
|
|||||||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||||
tags: [docker]
|
tags: [docker]
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get -y install flatpak-builder git python curl
|
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||||
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
||||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
||||||
@ -99,6 +99,7 @@ build-flatpak-amd64:
|
|||||||
- 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-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//\//_}
|
- flatpak build-bundle repo nheko-amd64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
|
||||||
after_script:
|
after_script:
|
||||||
|
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
|
||||||
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-amd64.flatpak
|
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-amd64.flatpak
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_JOB_NAME"
|
key: "$CI_JOB_NAME"
|
||||||
@ -115,7 +116,7 @@ build-flatpak-arm64:
|
|||||||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||||
tags: [docker-arm64]
|
tags: [docker-arm64]
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get -y install flatpak-builder git python curl
|
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||||
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
||||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
||||||
@ -126,6 +127,7 @@ build-flatpak-arm64:
|
|||||||
- 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-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//\//_}
|
- flatpak build-bundle repo nheko-arm64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
|
||||||
after_script:
|
after_script:
|
||||||
|
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
|
||||||
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-arm64.flatpak
|
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-arm64.flatpak
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_JOB_NAME"
|
key: "$CI_JOB_NAME"
|
||||||
|
@ -79,6 +79,7 @@ AppDir:
|
|||||||
- libxv1
|
- libxv1
|
||||||
- libxxf86vm1
|
- libxxf86vm1
|
||||||
- libzstd1
|
- libzstd1
|
||||||
|
- qml-module-qt-labs-platform
|
||||||
- qml-module-qtgraphicaleffects
|
- qml-module-qtgraphicaleffects
|
||||||
- qml-module-qtmultimedia
|
- qml-module-qtmultimedia
|
||||||
- qml-module-qtquick-controls2
|
- qml-module-qtquick-controls2
|
||||||
|
@ -257,7 +257,6 @@ set(SRC_FILES
|
|||||||
src/dialogs/PreviewUploadOverlay.cpp
|
src/dialogs/PreviewUploadOverlay.cpp
|
||||||
src/dialogs/ReCaptcha.cpp
|
src/dialogs/ReCaptcha.cpp
|
||||||
src/dialogs/ReadReceipts.cpp
|
src/dialogs/ReadReceipts.cpp
|
||||||
src/dialogs/RoomSettings.cpp
|
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
src/emoji/EmojiModel.cpp
|
src/emoji/EmojiModel.cpp
|
||||||
@ -295,6 +294,7 @@ set(SRC_FILES
|
|||||||
src/ui/ThemeManager.cpp
|
src/ui/ThemeManager.cpp
|
||||||
src/ui/ToggleButton.cpp
|
src/ui/ToggleButton.cpp
|
||||||
src/ui/UserProfile.cpp
|
src/ui/UserProfile.cpp
|
||||||
|
src/ui/RoomSettings.cpp
|
||||||
|
|
||||||
src/AvatarProvider.cpp
|
src/AvatarProvider.cpp
|
||||||
src/BlurhashProvider.cpp
|
src/BlurhashProvider.cpp
|
||||||
@ -326,6 +326,7 @@ set(SRC_FILES
|
|||||||
src/UserInfoWidget.cpp
|
src/UserInfoWidget.cpp
|
||||||
src/UserSettingsPage.cpp
|
src/UserSettingsPage.cpp
|
||||||
src/UsersModel.cpp
|
src/UsersModel.cpp
|
||||||
|
src/RoomsModel.cpp
|
||||||
src/Utils.cpp
|
src/Utils.cpp
|
||||||
src/WebRTCSession.cpp
|
src/WebRTCSession.cpp
|
||||||
src/WelcomePage.cpp
|
src/WelcomePage.cpp
|
||||||
@ -357,7 +358,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
MatrixClient
|
MatrixClient
|
||||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||||
GIT_TAG fee5298f068394958c2de935836a2c145f273906
|
GIT_TAG 004d4203ceb441239aafb17e1340cd063139d029
|
||||||
)
|
)
|
||||||
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
|
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
|
||||||
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
|
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
|
||||||
@ -474,7 +475,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/dialogs/RawMessage.h
|
src/dialogs/RawMessage.h
|
||||||
src/dialogs/ReCaptcha.h
|
src/dialogs/ReCaptcha.h
|
||||||
src/dialogs/ReadReceipts.h
|
src/dialogs/ReadReceipts.h
|
||||||
src/dialogs/RoomSettings.h
|
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
src/emoji/EmojiModel.h
|
src/emoji/EmojiModel.h
|
||||||
@ -510,6 +510,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/ui/Theme.h
|
src/ui/Theme.h
|
||||||
src/ui/ThemeManager.h
|
src/ui/ThemeManager.h
|
||||||
src/ui/UserProfile.h
|
src/ui/UserProfile.h
|
||||||
|
src/ui/RoomSettings.h
|
||||||
|
|
||||||
src/notifications/Manager.h
|
src/notifications/Manager.h
|
||||||
|
|
||||||
@ -538,6 +539,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||||||
src/UserInfoWidget.h
|
src/UserInfoWidget.h
|
||||||
src/UserSettingsPage.h
|
src/UserSettingsPage.h
|
||||||
src/UsersModel.h
|
src/UsersModel.h
|
||||||
|
src/RoomsModel.h
|
||||||
src/WebRTCSession.h
|
src/WebRTCSession.h
|
||||||
src/WelcomePage.h
|
src/WelcomePage.h
|
||||||
src/popups/PopupItem.h
|
src/popups/PopupItem.h
|
||||||
|
@ -4,6 +4,7 @@ nheko
|
|||||||
[![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master)
|
[![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master)
|
||||||
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.8.1)
|
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.8.1)
|
||||||
[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/)
|
[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/)
|
||||||
|
<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)
|
[![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org)
|
||||||
[![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko)
|
[![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko)
|
||||||
<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>
|
<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>
|
||||||
@ -49,7 +50,7 @@ Specifically there is support for:
|
|||||||
### Releases
|
### Releases
|
||||||
|
|
||||||
Releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer)
|
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
|
### Repositories
|
||||||
|
|
||||||
@ -191,7 +192,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
|
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
|
||||||
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,graphicaleffects,quick-controls2} 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,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev
|
||||||
```
|
```
|
||||||
This will install all dependencies, except for tweeny (use bundled tweeny)
|
This will install all dependencies, except for tweeny (use bundled tweeny)
|
||||||
and mtxclient (needs to be build separately).
|
and mtxclient (needs to be build separately).
|
||||||
@ -204,7 +205,7 @@ and mtxclient (needs to be build separately).
|
|||||||
sudo apt install cmake gcc make automake liblmdb-dev \
|
sudo apt install cmake gcc make automake liblmdb-dev \
|
||||||
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
|
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
|
||||||
qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-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-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform\
|
||||||
qt5keychain-dev
|
qt5keychain-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@
|
|||||||
"name": "mtxclient",
|
"name": "mtxclient",
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"commit": "fee5298f068394958c2de935836a2c145f273906",
|
"commit": "004d4203ceb441239aafb17e1340cd063139d029",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
||||||
}
|
}
|
||||||
|
10
nheko-nightly.flatpakref
Normal file
10
nheko-nightly.flatpakref
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[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=
|
8
nheko-nightly.flatpakrepo
Normal file
8
nheko-nightly.flatpakrepo
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[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=
|
@ -154,6 +154,35 @@ Popup {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "room"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: del
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
height: 24
|
||||||
|
width: 24
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import "./voip"
|
import "./voip"
|
||||||
import QtQuick 2.9
|
import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: inputBar
|
||||||
|
|
||||||
color: colors.window
|
color: colors.window
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: textInput.height + 16
|
Layout.preferredHeight: row.implicitHeight
|
||||||
Layout.minimumHeight: 40
|
Layout.minimumHeight: 40
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
@ -20,11 +22,9 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: inputBar
|
id: row
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
ImageButton {
|
ImageButton {
|
||||||
visible: CallManager.callsSupported
|
visible: CallManager.callsSupported
|
||||||
@ -36,7 +36,7 @@ Rectangle {
|
|||||||
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
|
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
|
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
|
||||||
Layout.leftMargin: 8
|
Layout.margins: 8
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (TimelineManager.timeline) {
|
if (TimelineManager.timeline) {
|
||||||
if (CallManager.haveCallInvite) {
|
if (CallManager.haveCallInvite) {
|
||||||
@ -57,7 +57,7 @@ Rectangle {
|
|||||||
width: 22
|
width: 22
|
||||||
height: 22
|
height: 22
|
||||||
image: ":/icons/icons/ui/paper-clip-outline.png"
|
image: ":/icons/icons/ui/paper-clip-outline.png"
|
||||||
Layout.leftMargin: CallManager.callsSupported ? 0 : 8
|
Layout.margins: 8
|
||||||
onClicked: TimelineManager.timeline.input.openFileSelection()
|
onClicked: TimelineManager.timeline.input.openFileSelection()
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: qsTr("Send a file")
|
ToolTip.text: qsTr("Send a file")
|
||||||
@ -76,31 +76,13 @@ Rectangle {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
ScrollView {
|
||||||
id: textInput
|
id: textInput
|
||||||
|
|
||||||
function ensureVisible(r) {
|
Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
|
||||||
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.maximumHeight: Window.height / 4
|
||||||
Layout.minimumHeight: Settings.fontSize
|
Layout.minimumHeight: Settings.fontSize
|
||||||
Layout.fillWidth: true
|
implicitWidth: inputBar.width - 4 * (22 + 16) - 24
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
implicitWidth: messageInput.width
|
|
||||||
implicitHeight: messageInput.height
|
|
||||||
contentWidth: messageInput.width
|
|
||||||
contentHeight: messageInput.height
|
|
||||||
|
|
||||||
TextArea {
|
TextArea {
|
||||||
id: messageInput
|
id: messageInput
|
||||||
@ -121,18 +103,11 @@ Rectangle {
|
|||||||
|
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
placeholderText: qsTr("Write a message...")
|
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
|
color: colors.text
|
||||||
width: textInput.width
|
width: textInput.width
|
||||||
wrapMode: TextEdit.Wrap
|
wrapMode: TextEdit.Wrap
|
||||||
padding: 0
|
padding: 8
|
||||||
focus: true
|
focus: true
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
if (TimelineManager.timeline)
|
if (TimelineManager.timeline)
|
||||||
@ -140,7 +115,6 @@ Rectangle {
|
|||||||
|
|
||||||
forceActiveFocus();
|
forceActiveFocus();
|
||||||
}
|
}
|
||||||
onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle)
|
|
||||||
onCursorPositionChanged: {
|
onCursorPositionChanged: {
|
||||||
if (!TimelineManager.timeline)
|
if (!TimelineManager.timeline)
|
||||||
return ;
|
return ;
|
||||||
@ -182,6 +156,9 @@ Rectangle {
|
|||||||
} else if (event.key == Qt.Key_Colon) {
|
} else if (event.key == Qt.Key_Colon) {
|
||||||
messageInput.openCompleter(cursorPosition, "emoji");
|
messageInput.openCompleter(cursorPosition, "emoji");
|
||||||
popup.open();
|
popup.open();
|
||||||
|
} else if (event.key == Qt.Key_NumberSign) {
|
||||||
|
messageInput.openCompleter(cursorPosition, "room");
|
||||||
|
popup.open();
|
||||||
} else if (event.key == Qt.Key_Escape && popup.opened) {
|
} else if (event.key == Qt.Key_Escape && popup.opened) {
|
||||||
completerTriggeredAt = -1;
|
completerTriggeredAt = -1;
|
||||||
popup.completerName = "";
|
popup.completerName = "";
|
||||||
@ -199,7 +176,6 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TimelineManager.timeline.input.send();
|
TimelineManager.timeline.input.send();
|
||||||
messageInput.clear();
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
} else if (event.key == Qt.Key_Tab) {
|
} else if (event.key == Qt.Key_Tab) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
@ -231,6 +207,39 @@ Rectangle {
|
|||||||
} else if (event.key == Qt.Key_Down && popup.opened) {
|
} else if (event.key == Qt.Key_Down && popup.opened) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
popup.down();
|
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;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
} else if (cursorPosition == messageInput.length) {
|
||||||
|
event.accepted = true;
|
||||||
|
cursorPosition = 0;
|
||||||
|
}
|
||||||
|
} else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) {
|
||||||
|
if (cursorPosition == 0) {
|
||||||
|
event.accepted = true;
|
||||||
|
cursorPosition = messageInput.length;
|
||||||
|
} else 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;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx--;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
background: null
|
background: null
|
||||||
@ -292,15 +301,13 @@ Rectangle {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageButton {
|
ImageButton {
|
||||||
id: emojiButton
|
id: emojiButton
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||||
|
Layout.margins: 8
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
width: 22
|
width: 22
|
||||||
height: 22
|
height: 22
|
||||||
@ -315,6 +322,7 @@ Rectangle {
|
|||||||
|
|
||||||
ImageButton {
|
ImageButton {
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||||
|
Layout.margins: 8
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
width: 22
|
width: 22
|
||||||
height: 22
|
height: 22
|
||||||
@ -324,7 +332,6 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Send")
|
ToolTip.text: qsTr("Send")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
TimelineManager.timeline.input.send();
|
TimelineManager.timeline.input.send();
|
||||||
messageInput.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ ScrollView {
|
|||||||
ListView {
|
ListView {
|
||||||
id: chat
|
id: chat
|
||||||
|
|
||||||
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding
|
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
|
||||||
|
|
||||||
model: TimelineManager.timeline
|
model: TimelineManager.timeline
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
281
resources/qml/RoomSettings.qml
Normal file
281
resources/qml/RoomSettings.qml
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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/")
|
||||||
|
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
|
||||||
|
|
||||||
|
text: "Error Text"
|
||||||
|
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: "%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: "SETTINGS"
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: "Notifications"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
model: ["Muted", "Mentions only", "All messages"]
|
||||||
|
currentIndex: roomSettings.notifications
|
||||||
|
onActivated: {
|
||||||
|
roomSettings.changeNotifications(index);
|
||||||
|
}
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: "Room access"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
enabled: roomSettings.canChangeJoinRules
|
||||||
|
model: ["Anyone and guests", "Anyone", "Invited users"]
|
||||||
|
currentIndex: roomSettings.accessJoinRules
|
||||||
|
onActivated: {
|
||||||
|
roomSettings.changeAccessRules(index);
|
||||||
|
}
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: "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: "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: "INFO"
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: "Internal ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: roomSettings.roomId
|
||||||
|
font.pixelSize: 14
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: "Room Version"
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
text: roomSettings.roomVersion
|
||||||
|
font.pixelSize: 14
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: "Ok"
|
||||||
|
onClicked: close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -52,6 +52,14 @@ Page {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: roomSettingsComponent
|
||||||
|
|
||||||
|
RoomSettings {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: mobileCallInviteDialog
|
id: mobileCallInviteDialog
|
||||||
|
|
||||||
@ -175,6 +183,16 @@ Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: TimelineManager.timeline
|
||||||
|
onOpenRoomSettingsDialog: {
|
||||||
|
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
|
||||||
|
"roomSettings": settings
|
||||||
|
});
|
||||||
|
roomSettings.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CallManager
|
target: CallManager
|
||||||
onNewInviteState: {
|
onNewInviteState: {
|
||||||
|
40
resources/qml/ToggleButton.qml
Normal file
40
resources/qml/ToggleButton.qml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,7 +15,7 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: TimelineManager.openRoomSettings()
|
onClicked: TimelineManager.timeline.openRoomSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@ -68,7 +68,7 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: TimelineManager.openRoomSettings()
|
onClicked: TimelineManager.timeline.openRoomSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@ Rectangle {
|
|||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Settings")
|
text: qsTr("Settings")
|
||||||
onTriggered: TimelineManager.openRoomSettings()
|
onTriggered: TimelineManager.timeline.openRoomSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,7 @@
|
|||||||
<file>qml/EncryptionIndicator.qml</file>
|
<file>qml/EncryptionIndicator.qml</file>
|
||||||
<file>qml/ImageButton.qml</file>
|
<file>qml/ImageButton.qml</file>
|
||||||
<file>qml/MatrixText.qml</file>
|
<file>qml/MatrixText.qml</file>
|
||||||
|
<file>qml/ToggleButton.qml</file>
|
||||||
<file>qml/MessageInput.qml</file>
|
<file>qml/MessageInput.qml</file>
|
||||||
<file>qml/MessageView.qml</file>
|
<file>qml/MessageView.qml</file>
|
||||||
<file>qml/NhekoBusyIndicator.qml</file>
|
<file>qml/NhekoBusyIndicator.qml</file>
|
||||||
@ -140,6 +141,7 @@
|
|||||||
<file>qml/TimelineRow.qml</file>
|
<file>qml/TimelineRow.qml</file>
|
||||||
<file>qml/TopBar.qml</file>
|
<file>qml/TopBar.qml</file>
|
||||||
<file>qml/TypingIndicator.qml</file>
|
<file>qml/TypingIndicator.qml</file>
|
||||||
|
<file>qml/RoomSettings.qml</file>
|
||||||
<file>qml/emoji/EmojiButton.qml</file>
|
<file>qml/emoji/EmojiButton.qml</file>
|
||||||
<file>qml/emoji/EmojiPicker.qml</file>
|
<file>qml/emoji/EmojiPicker.qml</file>
|
||||||
<file>qml/UserProfile.qml</file>
|
<file>qml/UserProfile.qml</file>
|
||||||
|
772
scripts/flat-manager-client
Executable file
772
scripts/flat-manager-client
Executable file
@ -0,0 +1,772 @@
|
|||||||
|
#!/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)
|
24
scripts/upload-to-flatpak-repo.sh
Executable file
24
scripts/upload-to-flatpak-repo.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/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
|
||||||
|
|
@ -102,6 +102,20 @@ namespace {
|
|||||||
std::unique_ptr<Cache> instance_ = nullptr;
|
std::unique_ptr<Cache> instance_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
static T
|
||||||
|
to(lmdb::val &value)
|
||||||
|
{
|
||||||
|
static_assert(std::is_trivial_v<T>, "Can only convert to trivial types!");
|
||||||
|
T temp;
|
||||||
|
|
||||||
|
if (value.size() < sizeof(T))
|
||||||
|
throw lmdb::runtime_error(__func__, MDB_BAD_VALSIZE);
|
||||||
|
|
||||||
|
std::memcpy(&temp, value.data(), sizeof(T));
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
Cache::isHiddenEvent(lmdb::txn &txn,
|
Cache::isHiddenEvent(lmdb::txn &txn,
|
||||||
mtx::events::collections::TimelineEvents e,
|
mtx::events::collections::TimelineEvents e,
|
||||||
@ -1667,14 +1681,14 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
|
|||||||
auto cursor = lmdb::cursor::open(txn, orderDb);
|
auto cursor = lmdb::cursor::open(txn, orderDb);
|
||||||
if (index == std::numeric_limits<uint64_t>::max()) {
|
if (index == std::numeric_limits<uint64_t>::max()) {
|
||||||
if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
|
if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
|
||||||
index = *indexVal.data<uint64_t>();
|
index = to<uint64_t>(indexVal);
|
||||||
} else {
|
} else {
|
||||||
messages.end_of_cache = true;
|
messages.end_of_cache = true;
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cursor.get(indexVal, event_id, MDB_SET)) {
|
if (cursor.get(indexVal, event_id, MDB_SET)) {
|
||||||
index = *indexVal.data<uint64_t>();
|
index = to<uint64_t>(indexVal);
|
||||||
} else {
|
} else {
|
||||||
messages.end_of_cache = true;
|
messages.end_of_cache = true;
|
||||||
return messages;
|
return messages;
|
||||||
@ -1708,7 +1722,7 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
|
|||||||
cursor.close();
|
cursor.close();
|
||||||
|
|
||||||
// std::reverse(timeline.events.begin(), timeline.events.end());
|
// std::reverse(timeline.events.begin(), timeline.events.end());
|
||||||
messages.next_index = *indexVal.data<uint64_t>();
|
messages.next_index = to<uint64_t>(indexVal);
|
||||||
messages.end_of_cache = !ret;
|
messages.end_of_cache = !ret;
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
@ -1861,12 +1875,12 @@ Cache::getTimelineRange(const std::string &room_id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
TimelineRange range{};
|
TimelineRange range{};
|
||||||
range.last = *indexVal.data<uint64_t>();
|
range.last = to<uint64_t>(indexVal);
|
||||||
|
|
||||||
if (!cursor.get(indexVal, val, MDB_FIRST)) {
|
if (!cursor.get(indexVal, val, MDB_FIRST)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
range.first = *indexVal.data<uint64_t>();
|
range.first = to<uint64_t>(indexVal);
|
||||||
|
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
@ -1892,7 +1906,7 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return *val.data<uint64_t>();
|
return to<uint64_t>(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint64_t>
|
std::optional<uint64_t>
|
||||||
@ -1920,7 +1934,7 @@ Cache::getEventIndex(const std::string &room_id, std::string_view event_id)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return *val.data<uint64_t>();
|
return to<uint64_t>(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::pair<uint64_t, std::string>>
|
std::optional<std::pair<uint64_t, std::string>>
|
||||||
@ -1951,7 +1965,7 @@ Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view even
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
uint64_t prevIdx = *indexVal.data<uint64_t>();
|
uint64_t prevIdx = to<uint64_t>(indexVal);
|
||||||
std::string prevId{eventIdVal.data(), eventIdVal.size()};
|
std::string prevId{eventIdVal.data(), eventIdVal.size()};
|
||||||
|
|
||||||
auto cursor = lmdb::cursor::open(txn, eventOrderDb);
|
auto cursor = lmdb::cursor::open(txn, eventOrderDb);
|
||||||
@ -1964,7 +1978,7 @@ Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view even
|
|||||||
if (lmdb::dbi_get(txn, timelineDb, lmdb::val(evId.data(), evId.size()), temp)) {
|
if (lmdb::dbi_get(txn, timelineDb, lmdb::val(evId.data(), evId.size()), temp)) {
|
||||||
return std::pair{prevIdx, std::string(prevId)};
|
return std::pair{prevIdx, std::string(prevId)};
|
||||||
} else {
|
} else {
|
||||||
prevIdx = *indexVal.data<uint64_t>();
|
prevIdx = to<uint64_t>(indexVal);
|
||||||
prevId = std::move(evId);
|
prevId = std::move(evId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1994,7 +2008,7 @@ Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return *val.data<uint64_t>();
|
return to<uint64_t>(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string>
|
std::optional<std::string>
|
||||||
@ -2775,13 +2789,13 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
|
|||||||
uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
|
uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
|
||||||
auto cursor = lmdb::cursor::open(txn, orderDb);
|
auto cursor = lmdb::cursor::open(txn, orderDb);
|
||||||
if (cursor.get(indexVal, val, MDB_LAST)) {
|
if (cursor.get(indexVal, val, MDB_LAST)) {
|
||||||
index = *indexVal.data<int64_t>();
|
index = to<uint64_t>(indexVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
|
uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
|
||||||
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
|
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
|
||||||
if (msgCursor.get(indexVal, val, MDB_LAST)) {
|
if (msgCursor.get(indexVal, val, MDB_LAST)) {
|
||||||
msgIndex = *indexVal.data<uint64_t>();
|
msgIndex = to<uint64_t>(indexVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
@ -2942,7 +2956,7 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message
|
|||||||
{
|
{
|
||||||
auto cursor = lmdb::cursor::open(txn, orderDb);
|
auto cursor = lmdb::cursor::open(txn, orderDb);
|
||||||
if (cursor.get(indexVal, val, MDB_FIRST)) {
|
if (cursor.get(indexVal, val, MDB_FIRST)) {
|
||||||
index = *indexVal.data<uint64_t>();
|
index = to<uint64_t>(indexVal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2950,7 +2964,7 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message
|
|||||||
{
|
{
|
||||||
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
|
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
|
||||||
if (msgCursor.get(indexVal, val, MDB_FIRST)) {
|
if (msgCursor.get(indexVal, val, MDB_FIRST)) {
|
||||||
msgIndex = *indexVal.data<uint64_t>();
|
msgIndex = to<uint64_t>(indexVal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3258,12 +3272,12 @@ Cache::deleteOldMessages()
|
|||||||
|
|
||||||
uint64_t first, last;
|
uint64_t first, last;
|
||||||
if (cursor.get(indexVal, val, MDB_LAST)) {
|
if (cursor.get(indexVal, val, MDB_LAST)) {
|
||||||
last = *indexVal.data<uint64_t>();
|
last = to<uint64_t>(indexVal);
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cursor.get(indexVal, val, MDB_FIRST)) {
|
if (cursor.get(indexVal, val, MDB_FIRST)) {
|
||||||
first = *indexVal.data<uint64_t>();
|
first = to<uint64_t>(indexVal);
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -253,6 +253,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||||||
this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
|
this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
|
||||||
|
|
||||||
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
|
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
|
||||||
|
connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
|
||||||
connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
|
connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
|
||||||
connect(this,
|
connect(this,
|
||||||
&ChatPage::highlightedNotifsRetrieved,
|
&ChatPage::highlightedNotifsRetrieved,
|
||||||
@ -920,6 +921,13 @@ ChatPage::joinRoom(const QString &room)
|
|||||||
void
|
void
|
||||||
ChatPage::joinRoomVia(const std::string &room_id, const std::vector<std::string> &via)
|
ChatPage::joinRoomVia(const std::string &room_id, const std::vector<std::string> &via)
|
||||||
{
|
{
|
||||||
|
if (QMessageBox::Yes !=
|
||||||
|
QMessageBox::question(
|
||||||
|
this,
|
||||||
|
tr("Confirm join"),
|
||||||
|
tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
|
||||||
|
return;
|
||||||
|
|
||||||
http::client()->join_room(
|
http::client()->join_room(
|
||||||
room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
|
room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -960,8 +968,9 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit showNotification(
|
QString newRoomId = QString::fromStdString(res.room_id.to_string());
|
||||||
tr("Room %1 created.").arg(QString::fromStdString(res.room_id.to_string())));
|
emit showNotification(tr("Room %1 created.").arg(newRoomId));
|
||||||
|
emit newRoom(newRoomId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -982,6 +991,13 @@ ChatPage::leaveRoom(const QString &room_id)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ChatPage::changeRoom(const QString &room_id)
|
||||||
|
{
|
||||||
|
view_manager_->setHistoryView(room_id);
|
||||||
|
room_list_->highlightSelectedRoom(room_id);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ChatPage::inviteUser(QString userid, QString reason)
|
ChatPage::inviteUser(QString userid, QString reason)
|
||||||
{
|
{
|
||||||
@ -1308,6 +1324,13 @@ ChatPage::startChat(QString userid)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (QMessageBox::Yes !=
|
||||||
|
QMessageBox::question(
|
||||||
|
this,
|
||||||
|
tr("Confirm invite"),
|
||||||
|
tr("Do you really want to start a private chat with %1?").arg(userid)))
|
||||||
|
return;
|
||||||
|
|
||||||
mtx::requests::CreateRoom req;
|
mtx::requests::CreateRoom req;
|
||||||
req.preset = mtx::requests::Preset::PrivateChat;
|
req.preset = mtx::requests::Preset::PrivateChat;
|
||||||
req.visibility = mtx::common::RoomVisibility::Private;
|
req.visibility = mtx::common::RoomVisibility::Private;
|
||||||
@ -1326,14 +1349,14 @@ mxidFromSegments(QStringRef sigil, QStringRef mxid)
|
|||||||
|
|
||||||
auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
|
auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
|
||||||
|
|
||||||
if (sigil == "user") {
|
if (sigil == "u") {
|
||||||
return "@" + mxid_;
|
return "@" + mxid_;
|
||||||
} else if (sigil == "roomid") {
|
} else if (sigil == "roomid") {
|
||||||
return "!" + mxid_;
|
return "!" + mxid_;
|
||||||
} else if (sigil == "room") {
|
} else if (sigil == "r") {
|
||||||
return "#" + mxid_;
|
return "#" + mxid_;
|
||||||
} else if (sigil == "group") {
|
//} else if (sigil == "group") {
|
||||||
return "+" + mxid_;
|
// return "+" + mxid_;
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -1383,7 +1406,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sigil1 == "user") {
|
if (sigil1 == "u") {
|
||||||
if (action.isEmpty()) {
|
if (action.isEmpty()) {
|
||||||
view_manager_->activeTimeline()->openUserProfile(mxid1);
|
view_manager_->activeTimeline()->openUserProfile(mxid1);
|
||||||
} else if (action == "chat") {
|
} else if (action == "chat") {
|
||||||
@ -1400,10 +1423,10 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action == "join") {
|
if (action == "join" || action.isEmpty()) {
|
||||||
joinRoomVia(targetRoomId, vias);
|
joinRoomVia(targetRoomId, vias);
|
||||||
}
|
}
|
||||||
} else if (sigil1 == "room") {
|
} else if (sigil1 == "r") {
|
||||||
auto joined_rooms = cache::joinedRooms();
|
auto joined_rooms = cache::joinedRooms();
|
||||||
auto targetRoomAlias = mxid1.toStdString();
|
auto targetRoomAlias = mxid1.toStdString();
|
||||||
|
|
||||||
@ -1418,7 +1441,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action == "join") {
|
if (action == "join" || action.isEmpty()) {
|
||||||
joinRoomVia(mxid1.toStdString(), vias);
|
joinRoomVia(mxid1.toStdString(), vias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,7 @@ signals:
|
|||||||
void tryInitialSyncCb();
|
void tryInitialSyncCb();
|
||||||
void newSyncResponse(const mtx::responses::Sync &res);
|
void newSyncResponse(const mtx::responses::Sync &res);
|
||||||
void leftRoom(const QString &room_id);
|
void leftRoom(const QString &room_id);
|
||||||
|
void newRoom(const QString &room_id);
|
||||||
|
|
||||||
void initializeRoomList(QMap<QString, RoomInfo>);
|
void initializeRoomList(QMap<QString, RoomInfo>);
|
||||||
void initializeViews(const mtx::responses::Rooms &rooms);
|
void initializeViews(const mtx::responses::Rooms &rooms);
|
||||||
@ -201,6 +202,7 @@ signals:
|
|||||||
private slots:
|
private slots:
|
||||||
void logout();
|
void logout();
|
||||||
void removeRoom(const QString &room_id);
|
void removeRoom(const QString &room_id);
|
||||||
|
void changeRoom(const QString &room_id);
|
||||||
void dropToLoginPage(const QString &msg);
|
void dropToLoginPage(const QString &msg);
|
||||||
|
|
||||||
void handleSyncResponse(const mtx::responses::Sync &res);
|
void handleSyncResponse(const mtx::responses::Sync &res);
|
||||||
|
@ -147,16 +147,23 @@ LoginPage::LoginPage(QWidget *parent)
|
|||||||
error_matrixid_label_->hide();
|
error_matrixid_label_->hide();
|
||||||
|
|
||||||
button_layout_ = new QHBoxLayout();
|
button_layout_ = new QHBoxLayout();
|
||||||
button_layout_->setSpacing(0);
|
button_layout_->setSpacing(20);
|
||||||
button_layout_->setContentsMargins(0, 0, 0, 30);
|
button_layout_->setContentsMargins(0, 0, 0, 30);
|
||||||
|
|
||||||
login_button_ = new RaisedButton(tr("LOGIN"), this);
|
login_button_ = new RaisedButton(tr("LOGIN"), this);
|
||||||
login_button_->setMinimumSize(350, 65);
|
login_button_->setMinimumSize(150, 65);
|
||||||
login_button_->setFontSize(20);
|
login_button_->setFontSize(20);
|
||||||
login_button_->setCornerRadius(3);
|
login_button_->setCornerRadius(3);
|
||||||
|
|
||||||
|
sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this);
|
||||||
|
sso_login_button_->setMinimumSize(150, 65);
|
||||||
|
sso_login_button_->setFontSize(20);
|
||||||
|
sso_login_button_->setCornerRadius(3);
|
||||||
|
sso_login_button_->setVisible(false);
|
||||||
|
|
||||||
button_layout_->addStretch(1);
|
button_layout_->addStretch(1);
|
||||||
button_layout_->addWidget(login_button_);
|
button_layout_->addWidget(login_button_);
|
||||||
|
button_layout_->addWidget(sso_login_button_);
|
||||||
button_layout_->addStretch(1);
|
button_layout_->addStretch(1);
|
||||||
|
|
||||||
error_label_ = new QLabel(this);
|
error_label_ = new QLabel(this);
|
||||||
@ -179,7 +186,17 @@ LoginPage::LoginPage(QWidget *parent)
|
|||||||
this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
|
this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
|
||||||
|
|
||||||
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
|
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
|
||||||
connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
|
connect(login_button_, &RaisedButton::clicked, this, [this]() {
|
||||||
|
onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO);
|
||||||
|
});
|
||||||
|
connect(sso_login_button_, &RaisedButton::clicked, this, [this]() {
|
||||||
|
onLoginButtonClicked(LoginMethod::SSO);
|
||||||
|
});
|
||||||
|
connect(this,
|
||||||
|
&LoginPage::showErrorMessage,
|
||||||
|
this,
|
||||||
|
static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError),
|
||||||
|
Qt::QueuedConnection);
|
||||||
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
||||||
connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
||||||
connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
||||||
@ -314,16 +331,19 @@ LoginPage::checkHomeserverVersion()
|
|||||||
http::client()->get_login(
|
http::client()->get_login(
|
||||||
[this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
|
[this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
|
||||||
if (err || flows.flows.empty())
|
if (err || flows.flows.empty())
|
||||||
emit versionOkCb(LoginMethod::Password);
|
emit versionOkCb(true, false);
|
||||||
|
|
||||||
LoginMethod loginMethod_ = LoginMethod::Password;
|
bool ssoSupported_ = false;
|
||||||
|
bool passwordSupported_ = false;
|
||||||
for (const auto &flow : flows.flows) {
|
for (const auto &flow : flows.flows) {
|
||||||
if (flow.type == mtx::user_interactive::auth_types::sso) {
|
if (flow.type == mtx::user_interactive::auth_types::sso) {
|
||||||
loginMethod_ = LoginMethod::SSO;
|
ssoSupported_ = true;
|
||||||
break;
|
} else if (flow.type ==
|
||||||
|
mtx::user_interactive::auth_types::password) {
|
||||||
|
passwordSupported_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit versionOkCb(loginMethod_);
|
emit versionOkCb(passwordSupported_, ssoSupported_);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -355,28 +375,24 @@ LoginPage::versionError(const QString &error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
LoginPage::versionOk(LoginMethod loginMethod_)
|
LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_)
|
||||||
{
|
{
|
||||||
this->loginMethod = loginMethod_;
|
passwordSupported = passwordSupported_;
|
||||||
|
ssoSupported = ssoSupported_;
|
||||||
|
|
||||||
serverLayout_->removeWidget(spinner_);
|
serverLayout_->removeWidget(spinner_);
|
||||||
matrixidLayout_->removeWidget(spinner_);
|
matrixidLayout_->removeWidget(spinner_);
|
||||||
spinner_->stop();
|
spinner_->stop();
|
||||||
|
|
||||||
if (loginMethod == LoginMethod::SSO) {
|
sso_login_button_->setVisible(ssoSupported);
|
||||||
password_input_->hide();
|
login_button_->setVisible(passwordSupported);
|
||||||
login_button_->setText(tr("SSO LOGIN"));
|
|
||||||
} else {
|
|
||||||
password_input_->show();
|
|
||||||
login_button_->setText(tr("LOGIN"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverInput_->isVisible())
|
if (serverInput_->isVisible())
|
||||||
serverInput_->hide();
|
serverInput_->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
LoginPage::onLoginButtonClicked()
|
LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
|
||||||
{
|
{
|
||||||
error_label_->setText("");
|
error_label_->setText("");
|
||||||
|
|
||||||
@ -411,8 +427,8 @@ LoginPage::onLoginButtonClicked()
|
|||||||
: deviceName_->text().toStdString(),
|
: deviceName_->text().toStdString(),
|
||||||
[this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
|
[this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
showError(error_label_,
|
showErrorMessage(error_label_,
|
||||||
QString::fromStdString(err->matrix_error.error));
|
QString::fromStdString(err->matrix_error.error));
|
||||||
emit errorOccurred();
|
emit errorOccurred();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -437,7 +453,7 @@ LoginPage::onLoginButtonClicked()
|
|||||||
http::client()->login(
|
http::client()->login(
|
||||||
req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
|
req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
showError(
|
showErrorMessage(
|
||||||
error_label_,
|
error_label_,
|
||||||
QString::fromStdString(err->matrix_error.error));
|
QString::fromStdString(err->matrix_error.error));
|
||||||
emit errorOccurred();
|
emit errorOccurred();
|
||||||
@ -456,7 +472,7 @@ LoginPage::onLoginButtonClicked()
|
|||||||
sso->deleteLater();
|
sso->deleteLater();
|
||||||
});
|
});
|
||||||
connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
|
connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
|
||||||
showError(error_label_, tr("SSO login failed"));
|
showErrorMessage(error_label_, tr("SSO login failed"));
|
||||||
emit errorOccurred();
|
emit errorOccurred();
|
||||||
sso->deleteLater();
|
sso->deleteLater();
|
||||||
});
|
});
|
||||||
|
@ -56,9 +56,10 @@ signals:
|
|||||||
|
|
||||||
//! Used to trigger the corresponding slot outside of the main thread.
|
//! Used to trigger the corresponding slot outside of the main thread.
|
||||||
void versionErrorCb(const QString &err);
|
void versionErrorCb(const QString &err);
|
||||||
void versionOkCb(LoginPage::LoginMethod method);
|
void versionOkCb(bool passwordSupported, bool ssoSupported);
|
||||||
|
|
||||||
void loginOk(const mtx::responses::Login &res);
|
void loginOk(const mtx::responses::Login &res);
|
||||||
|
void showErrorMessage(QLabel *label, const QString &msg);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
@ -73,7 +74,7 @@ private slots:
|
|||||||
void onBackButtonClicked();
|
void onBackButtonClicked();
|
||||||
|
|
||||||
// Callback for the login button.
|
// Callback for the login button.
|
||||||
void onLoginButtonClicked();
|
void onLoginButtonClicked(LoginMethod loginMethod);
|
||||||
|
|
||||||
// Callback for probing the server found in the mxid
|
// Callback for probing the server found in the mxid
|
||||||
void onMatrixIdEntered();
|
void onMatrixIdEntered();
|
||||||
@ -84,7 +85,7 @@ private slots:
|
|||||||
// Callback for errors produced during server probing
|
// Callback for errors produced during server probing
|
||||||
void versionError(const QString &error_message);
|
void versionError(const QString &error_message);
|
||||||
// Callback for successful server probing
|
// Callback for successful server probing
|
||||||
void versionOk(LoginPage::LoginMethod method);
|
void versionOk(bool passwordSupported, bool ssoSupported);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void checkHomeserverVersion();
|
void checkHomeserverVersion();
|
||||||
@ -120,7 +121,7 @@ private:
|
|||||||
QString inferredServerAddress_;
|
QString inferredServerAddress_;
|
||||||
|
|
||||||
FlatButton *back_button_;
|
FlatButton *back_button_;
|
||||||
RaisedButton *login_button_;
|
RaisedButton *login_button_, *sso_login_button_;
|
||||||
|
|
||||||
QWidget *form_widget_;
|
QWidget *form_widget_;
|
||||||
QHBoxLayout *form_wrapper_;
|
QHBoxLayout *form_wrapper_;
|
||||||
@ -130,5 +131,6 @@ private:
|
|||||||
TextField *password_input_;
|
TextField *password_input_;
|
||||||
TextField *deviceName_;
|
TextField *deviceName_;
|
||||||
TextField *serverInput_;
|
TextField *serverInput_;
|
||||||
LoginMethod loginMethod = LoginMethod::Password;
|
bool passwordSupported = true;
|
||||||
|
bool ssoSupported = false;
|
||||||
};
|
};
|
||||||
|
@ -51,7 +51,6 @@
|
|||||||
#include "dialogs/Logout.h"
|
#include "dialogs/Logout.h"
|
||||||
#include "dialogs/MemberList.h"
|
#include "dialogs/MemberList.h"
|
||||||
#include "dialogs/ReadReceipts.h"
|
#include "dialogs/ReadReceipts.h"
|
||||||
#include "dialogs/RoomSettings.h"
|
|
||||||
|
|
||||||
MainWindow *MainWindow::instance_ = nullptr;
|
MainWindow *MainWindow::instance_ = nullptr;
|
||||||
|
|
||||||
@ -363,14 +362,6 @@ MainWindow::hasActiveUser()
|
|||||||
settings.contains(prefix + "auth/user_id");
|
settings.contains(prefix + "auth/user_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
MainWindow::openRoomSettings(const QString &room_id)
|
|
||||||
{
|
|
||||||
auto dialog = new dialogs::RoomSettings(room_id, this);
|
|
||||||
|
|
||||||
showDialog(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
MainWindow::openMemberListDialog(const QString &room_id)
|
MainWindow::openMemberListDialog(const QString &room_id)
|
||||||
{
|
{
|
||||||
|
@ -54,7 +54,6 @@ class LeaveRoom;
|
|||||||
class Logout;
|
class Logout;
|
||||||
class MemberList;
|
class MemberList;
|
||||||
class ReCaptcha;
|
class ReCaptcha;
|
||||||
class RoomSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
@ -78,7 +77,6 @@ public:
|
|||||||
std::function<void(const mtx::requests::CreateRoom &request)> callback);
|
std::function<void(const mtx::requests::CreateRoom &request)> callback);
|
||||||
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
|
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
|
||||||
void openLogoutDialog();
|
void openLogoutDialog();
|
||||||
void openRoomSettings(const QString &room_id);
|
|
||||||
void openMemberListDialog(const QString &room_id);
|
void openMemberListDialog(const QString &room_id);
|
||||||
void openReadReceiptsDialog(const QString &event_id);
|
void openReadReceiptsDialog(const QString &event_id);
|
||||||
|
|
||||||
|
@ -277,6 +277,7 @@ RegisterPage::RegisterPage(QWidget *parent)
|
|||||||
if (!err) {
|
if (!err) {
|
||||||
http::client()->set_user(res.user_id);
|
http::client()->set_user(res.user_id);
|
||||||
http::client()->set_access_token(res.access_token);
|
http::client()->set_access_token(res.access_token);
|
||||||
|
http::client()->set_device_id(res.device_id);
|
||||||
|
|
||||||
emit registerOk();
|
emit registerOk();
|
||||||
return;
|
return;
|
||||||
|
@ -217,4 +217,6 @@ private:
|
|||||||
|
|
||||||
QColor bubbleBgColor_;
|
QColor bubbleBgColor_;
|
||||||
QColor bubbleFgColor_;
|
QColor bubbleFgColor_;
|
||||||
|
|
||||||
|
friend struct room_sort;
|
||||||
};
|
};
|
||||||
|
@ -353,8 +353,8 @@ RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
|
|||||||
|
|
||||||
struct room_sort
|
struct room_sort
|
||||||
{
|
{
|
||||||
bool operator()(const QSharedPointer<RoomInfoListItem> a,
|
bool operator()(const QSharedPointer<RoomInfoListItem> &a,
|
||||||
const QSharedPointer<RoomInfoListItem> b) const
|
const QSharedPointer<RoomInfoListItem> &b) const
|
||||||
{
|
{
|
||||||
// Sort by "importance" (i.e. invites before mentions before
|
// Sort by "importance" (i.e. invites before mentions before
|
||||||
// notifs before new events before old events), then secondly
|
// notifs before new events before old events), then secondly
|
||||||
@ -370,9 +370,9 @@ struct room_sort
|
|||||||
// Now sort by recency
|
// Now sort by recency
|
||||||
// Zero if empty, otherwise the time that the event occured
|
// Zero if empty, otherwise the time that the event occured
|
||||||
const uint64_t a_recency =
|
const uint64_t a_recency =
|
||||||
a->lastMessageInfo().userid.isEmpty() ? 0 : a->lastMessageInfo().timestamp;
|
a->lastMsgInfo_.userid.isEmpty() ? 0 : a->lastMsgInfo_.timestamp;
|
||||||
const uint64_t b_recency =
|
const uint64_t b_recency =
|
||||||
b->lastMessageInfo().userid.isEmpty() ? 0 : b->lastMessageInfo().timestamp;
|
b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp;
|
||||||
return a_recency > b_recency;
|
return a_recency > b_recency;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
69
src/RoomsModel.cpp
Normal file
69
src/RoomsModel.cpp
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#include "RoomsModel.h"
|
||||||
|
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "Cache_p.h"
|
||||||
|
#include "CompletionModelRoles.h"
|
||||||
|
|
||||||
|
RoomsModel::RoomsModel(bool showOnlyRoomWithAliases, QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
, showOnlyRoomWithAliases_(showOnlyRoomWithAliases)
|
||||||
|
{
|
||||||
|
std::vector<std::string> rooms_ = cache::joinedRooms();
|
||||||
|
roomInfos = cache::getRoomInfo(rooms_);
|
||||||
|
|
||||||
|
for (const auto &r : rooms_) {
|
||||||
|
auto roomAliasesList = cache::client()->getRoomAliases(r);
|
||||||
|
|
||||||
|
if (showOnlyRoomWithAliases_) {
|
||||||
|
if (roomAliasesList && !roomAliasesList->alias.empty()) {
|
||||||
|
roomids.push_back(QString::fromStdString(r));
|
||||||
|
roomAliases.push_back(
|
||||||
|
QString::fromStdString(roomAliasesList->alias));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
roomids.push_back(QString::fromStdString(r));
|
||||||
|
roomAliases.push_back(
|
||||||
|
roomAliasesList ? QString::fromStdString(roomAliasesList->alias) : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray>
|
||||||
|
RoomsModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {{CompletionModel::CompletionRole, "completionRole"},
|
||||||
|
{CompletionModel::SearchRole, "searchRole"},
|
||||||
|
{CompletionModel::SearchRole2, "searchRole2"},
|
||||||
|
{Roles::RoomAlias, "roomAlias"},
|
||||||
|
{Roles::AvatarUrl, "avatarUrl"},
|
||||||
|
{Roles::RoomID, "roomid"},
|
||||||
|
{Roles::RoomName, "roomName"}};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant
|
||||||
|
RoomsModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (hasIndex(index.row(), index.column(), index.parent())) {
|
||||||
|
switch (role) {
|
||||||
|
case CompletionModel::CompletionRole: {
|
||||||
|
QString percentEncoding = QUrl::toPercentEncoding(roomAliases[index.row()]);
|
||||||
|
return QString("[%1](https://matrix.to/#/%2)")
|
||||||
|
.arg(roomAliases[index.row()], percentEncoding);
|
||||||
|
}
|
||||||
|
case CompletionModel::SearchRole:
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
case Roles::RoomAlias:
|
||||||
|
return roomAliases[index.row()];
|
||||||
|
case CompletionModel::SearchRole2:
|
||||||
|
case Roles::RoomName:
|
||||||
|
return QString::fromStdString(roomInfos.at(roomids[index.row()]).name);
|
||||||
|
case Roles::AvatarUrl:
|
||||||
|
return QString::fromStdString(
|
||||||
|
roomInfos.at(roomids[index.row()]).avatar_url);
|
||||||
|
case Roles::RoomID:
|
||||||
|
return roomids[index.row()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
33
src/RoomsModel.h
Normal file
33
src/RoomsModel.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Cache.h"
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class RoomsModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Roles
|
||||||
|
{
|
||||||
|
AvatarUrl = Qt::UserRole,
|
||||||
|
RoomAlias,
|
||||||
|
RoomID,
|
||||||
|
RoomName,
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr);
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
||||||
|
{
|
||||||
|
(void)parent;
|
||||||
|
return (int)roomids.size();
|
||||||
|
}
|
||||||
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<QString> roomids;
|
||||||
|
std::vector<QString> roomAliases;
|
||||||
|
std::map<QString, RoomInfo> roomInfos;
|
||||||
|
bool showOnlyRoomWithAliases_;
|
||||||
|
};
|
@ -1,865 +0,0 @@
|
|||||||
#include "dialogs/RoomSettings.h"
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFontDatabase>
|
|
||||||
#include <QImageReader>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QMimeDatabase>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QShortcut>
|
|
||||||
#include <QShowEvent>
|
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QStyleOption>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <mtx/responses/common.hpp>
|
|
||||||
#include <mtx/responses/media.hpp>
|
|
||||||
|
|
||||||
#include "Cache.h"
|
|
||||||
#include "ChatPage.h"
|
|
||||||
#include "Config.h"
|
|
||||||
#include "Logging.h"
|
|
||||||
#include "MatrixClient.h"
|
|
||||||
#include "Utils.h"
|
|
||||||
#include "ui/Avatar.h"
|
|
||||||
#include "ui/FlatButton.h"
|
|
||||||
#include "ui/LoadingIndicator.h"
|
|
||||||
#include "ui/Painter.h"
|
|
||||||
#include "ui/TextField.h"
|
|
||||||
#include "ui/ToggleButton.h"
|
|
||||||
|
|
||||||
using namespace dialogs;
|
|
||||||
using namespace mtx::events;
|
|
||||||
|
|
||||||
constexpr int BUTTON_SIZE = 36;
|
|
||||||
constexpr int BUTTON_RADIUS = BUTTON_SIZE / 2;
|
|
||||||
constexpr int WIDGET_MARGIN = 20;
|
|
||||||
constexpr int TOP_WIDGET_MARGIN = 2 * WIDGET_MARGIN;
|
|
||||||
constexpr int WIDGET_SPACING = 15;
|
|
||||||
constexpr int TEXT_SPACING = 4;
|
|
||||||
constexpr int BUTTON_SPACING = 2 * TEXT_SPACING;
|
|
||||||
|
|
||||||
bool
|
|
||||||
ClickableFilter::eventFilter(QObject *obj, QEvent *event)
|
|
||||||
{
|
|
||||||
if (event->type() == QEvent::MouseButtonRelease) {
|
|
||||||
emit clicked();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QObject::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditModal::EditModal(const QString &roomId, QWidget *parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
, roomId_{roomId}
|
|
||||||
{
|
|
||||||
setAutoFillBackground(true);
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
||||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
|
||||||
setWindowModality(Qt::WindowModal);
|
|
||||||
|
|
||||||
QFont largeFont;
|
|
||||||
largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4);
|
|
||||||
setMinimumWidth(conf::window::minModalWidth);
|
|
||||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
|
||||||
|
|
||||||
auto layout = new QVBoxLayout(this);
|
|
||||||
|
|
||||||
applyBtn_ = new QPushButton(tr("Apply"), this);
|
|
||||||
cancelBtn_ = new QPushButton(tr("Cancel"), this);
|
|
||||||
cancelBtn_->setDefault(true);
|
|
||||||
|
|
||||||
auto btnLayout = new QHBoxLayout;
|
|
||||||
btnLayout->addStretch(1);
|
|
||||||
btnLayout->setSpacing(15);
|
|
||||||
btnLayout->addWidget(cancelBtn_);
|
|
||||||
btnLayout->addWidget(applyBtn_);
|
|
||||||
|
|
||||||
nameInput_ = new TextField(this);
|
|
||||||
nameInput_->setLabel(tr("Name").toUpper());
|
|
||||||
topicInput_ = new TextField(this);
|
|
||||||
topicInput_->setLabel(tr("Topic").toUpper());
|
|
||||||
|
|
||||||
errorField_ = new QLabel(this);
|
|
||||||
errorField_->setWordWrap(true);
|
|
||||||
errorField_->hide();
|
|
||||||
|
|
||||||
layout->addWidget(nameInput_);
|
|
||||||
layout->addWidget(topicInput_);
|
|
||||||
layout->addLayout(btnLayout, 1);
|
|
||||||
|
|
||||||
auto labelLayout = new QHBoxLayout;
|
|
||||||
labelLayout->setAlignment(Qt::AlignHCenter);
|
|
||||||
labelLayout->addWidget(errorField_);
|
|
||||||
layout->addLayout(labelLayout);
|
|
||||||
|
|
||||||
connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked);
|
|
||||||
connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
|
|
||||||
|
|
||||||
auto window = QApplication::activeWindow();
|
|
||||||
auto center = window->frameGeometry().center();
|
|
||||||
move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
EditModal::topicEventSent()
|
|
||||||
{
|
|
||||||
errorField_->hide();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
EditModal::nameEventSent(const QString &name)
|
|
||||||
{
|
|
||||||
errorField_->hide();
|
|
||||||
emit nameChanged(name);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
EditModal::error(const QString &msg)
|
|
||||||
{
|
|
||||||
errorField_->setText(msg);
|
|
||||||
errorField_->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
EditModal::applyClicked()
|
|
||||||
{
|
|
||||||
// Check if the values are changed from the originals.
|
|
||||||
auto newName = nameInput_->text().trimmed();
|
|
||||||
auto newTopic = topicInput_->text().trimmed();
|
|
||||||
|
|
||||||
errorField_->hide();
|
|
||||||
|
|
||||||
if (newName == initialName_ && newTopic == initialTopic_) {
|
|
||||||
close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using namespace mtx::events;
|
|
||||||
auto proxy = std::make_shared<ThreadProxy>();
|
|
||||||
connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent);
|
|
||||||
connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent);
|
|
||||||
connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error);
|
|
||||||
|
|
||||||
if (newName != initialName_ && !newName.isEmpty()) {
|
|
||||||
state::Name body;
|
|
||||||
body.name = newName.toStdString();
|
|
||||||
|
|
||||||
http::client()->send_state_event(
|
|
||||||
roomId_.toStdString(),
|
|
||||||
body,
|
|
||||||
[proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
emit proxy->error(
|
|
||||||
QString::fromStdString(err->matrix_error.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit proxy->nameEventSent(newName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
|
|
||||||
state::Topic body;
|
|
||||||
body.topic = newTopic.toStdString();
|
|
||||||
|
|
||||||
http::client()->send_state_event(
|
|
||||||
roomId_.toStdString(),
|
|
||||||
body,
|
|
||||||
[proxy](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
emit proxy->error(
|
|
||||||
QString::fromStdString(err->matrix_error.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit proxy->topicEventSent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
EditModal::setFields(const QString &roomName, const QString &roomTopic)
|
|
||||||
{
|
|
||||||
initialName_ = roomName;
|
|
||||||
initialTopic_ = roomTopic;
|
|
||||||
|
|
||||||
nameInput_->setText(roomName);
|
|
||||||
topicInput_->setText(roomTopic);
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
|
|
||||||
: QFrame(parent)
|
|
||||||
, room_id_{std::move(room_id)}
|
|
||||||
{
|
|
||||||
retrieveRoomInfo();
|
|
||||||
|
|
||||||
setAutoFillBackground(true);
|
|
||||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
|
||||||
setWindowModality(Qt::WindowModal);
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
||||||
|
|
||||||
QFont largeFont;
|
|
||||||
largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
|
|
||||||
|
|
||||||
setMinimumWidth(conf::window::minModalWidth);
|
|
||||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
|
||||||
|
|
||||||
auto layout = new QVBoxLayout(this);
|
|
||||||
layout->setSpacing(WIDGET_SPACING);
|
|
||||||
layout->setContentsMargins(WIDGET_MARGIN, TOP_WIDGET_MARGIN, WIDGET_MARGIN, WIDGET_MARGIN);
|
|
||||||
|
|
||||||
QFont font;
|
|
||||||
font.setWeight(QFont::Medium);
|
|
||||||
auto settingsLabel = new QLabel(tr("Settings").toUpper(), this);
|
|
||||||
settingsLabel->setFont(font);
|
|
||||||
|
|
||||||
auto infoLabel = new QLabel(tr("Info").toUpper(), this);
|
|
||||||
infoLabel->setFont(font);
|
|
||||||
|
|
||||||
QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
|
||||||
|
|
||||||
auto roomIdLabel = new QLabel(room_id, this);
|
|
||||||
roomIdLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
||||||
roomIdLabel->setFont(monospaceFont);
|
|
||||||
|
|
||||||
auto roomIdLayout = new QHBoxLayout;
|
|
||||||
roomIdLayout->setMargin(0);
|
|
||||||
roomIdLayout->addWidget(new QLabel(tr("Internal ID"), this),
|
|
||||||
Qt::AlignBottom | Qt::AlignLeft);
|
|
||||||
roomIdLayout->addWidget(roomIdLabel, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
||||||
|
|
||||||
auto roomVersionLabel = new QLabel(QString::fromStdString(info_.version), this);
|
|
||||||
roomVersionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
||||||
roomVersionLabel->setFont(monospaceFont);
|
|
||||||
|
|
||||||
auto roomVersionLayout = new QHBoxLayout;
|
|
||||||
roomVersionLayout->setMargin(0);
|
|
||||||
roomVersionLayout->addWidget(new QLabel(tr("Room Version"), this),
|
|
||||||
Qt::AlignBottom | Qt::AlignLeft);
|
|
||||||
roomVersionLayout->addWidget(roomVersionLabel, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
||||||
|
|
||||||
auto notifLabel = new QLabel(tr("Notifications"), this);
|
|
||||||
notifCombo = new QComboBox(this);
|
|
||||||
notifCombo->addItem(tr(
|
|
||||||
"Muted")); //{"conditions":[{"kind":"event_match","key":"room_id","pattern":"!jxlRxnrZCsjpjDubDX:matrix.org"}],"actions":["dont_notify"]}
|
|
||||||
notifCombo->addItem(tr("Mentions only")); // {"actions":["dont_notify"]}
|
|
||||||
notifCombo->addItem(tr("All messages")); // delete rule
|
|
||||||
|
|
||||||
connect(this, &RoomSettings::notifChanged, notifCombo, &QComboBox::setCurrentIndex);
|
|
||||||
http::client()->get_pushrules(
|
|
||||||
"global",
|
|
||||||
"override",
|
|
||||||
room_id_.toStdString(),
|
|
||||||
[this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) {
|
|
||||||
if (err) {
|
|
||||||
if (err->status_code == boost::beast::http::status::not_found)
|
|
||||||
http::client()->get_pushrules(
|
|
||||||
"global",
|
|
||||||
"room",
|
|
||||||
room_id_.toStdString(),
|
|
||||||
[this](const mtx::pushrules::PushRule &rule,
|
|
||||||
mtx::http::RequestErr &err) {
|
|
||||||
if (err) {
|
|
||||||
emit notifChanged(2); // all messages
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.enabled)
|
|
||||||
emit notifChanged(1); // mentions only
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.enabled)
|
|
||||||
emit notifChanged(0); // muted
|
|
||||||
else
|
|
||||||
emit notifChanged(2); // all messages
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(notifCombo, QOverload<int>::of(&QComboBox::activated), [this](int index) {
|
|
||||||
std::string room_id = room_id_.toStdString();
|
|
||||||
if (index == 0) {
|
|
||||||
// mute room
|
|
||||||
// delete old rule first, then add new rule
|
|
||||||
mtx::pushrules::PushRule rule;
|
|
||||||
rule.actions = {mtx::pushrules::actions::dont_notify{}};
|
|
||||||
mtx::pushrules::PushCondition condition;
|
|
||||||
condition.kind = "event_match";
|
|
||||||
condition.key = "room_id";
|
|
||||||
condition.pattern = room_id;
|
|
||||||
rule.conditions = {condition};
|
|
||||||
|
|
||||||
http::client()->put_pushrules(
|
|
||||||
"global",
|
|
||||||
"override",
|
|
||||||
room_id,
|
|
||||||
rule,
|
|
||||||
[room_id](mtx::http::RequestErr &err) {
|
|
||||||
if (err)
|
|
||||||
nhlog::net()->error(
|
|
||||||
"failed to set pushrule for room {}: {} {}",
|
|
||||||
room_id,
|
|
||||||
static_cast<int>(err->status_code),
|
|
||||||
err->matrix_error.error);
|
|
||||||
http::client()->delete_pushrules(
|
|
||||||
"global", "room", room_id, [room_id](mtx::http::RequestErr &) {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (index == 1) {
|
|
||||||
// mentions only
|
|
||||||
// delete old rule first, then add new rule
|
|
||||||
mtx::pushrules::PushRule rule;
|
|
||||||
rule.actions = {mtx::pushrules::actions::dont_notify{}};
|
|
||||||
http::client()->put_pushrules(
|
|
||||||
"global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) {
|
|
||||||
if (err)
|
|
||||||
nhlog::net()->error(
|
|
||||||
"failed to set pushrule for room {}: {} {}",
|
|
||||||
room_id,
|
|
||||||
static_cast<int>(err->status_code),
|
|
||||||
err->matrix_error.error);
|
|
||||||
http::client()->delete_pushrules(
|
|
||||||
"global",
|
|
||||||
"override",
|
|
||||||
room_id,
|
|
||||||
[room_id](mtx::http::RequestErr &) {});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// all messages
|
|
||||||
http::client()->delete_pushrules(
|
|
||||||
"global", "override", room_id, [room_id](mtx::http::RequestErr &) {
|
|
||||||
http::client()->delete_pushrules(
|
|
||||||
"global", "room", room_id, [room_id](mtx::http::RequestErr &) {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
auto notifOptionLayout_ = new QHBoxLayout;
|
|
||||||
notifOptionLayout_->setMargin(0);
|
|
||||||
notifOptionLayout_->addWidget(notifLabel, Qt::AlignBottom | Qt::AlignLeft);
|
|
||||||
notifOptionLayout_->addWidget(notifCombo, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
||||||
|
|
||||||
auto accessLabel = new QLabel(tr("Room access"), this);
|
|
||||||
accessCombo = new QComboBox(this);
|
|
||||||
accessCombo->addItem(tr("Anyone and guests"));
|
|
||||||
accessCombo->addItem(tr("Anyone"));
|
|
||||||
accessCombo->addItem(tr("Invited users"));
|
|
||||||
accessCombo->setDisabled(
|
|
||||||
!canChangeJoinRules(room_id_.toStdString(), utils::localUser().toStdString()));
|
|
||||||
connect(accessCombo, QOverload<int>::of(&QComboBox::activated), [this](int index) {
|
|
||||||
using namespace mtx::events::state;
|
|
||||||
|
|
||||||
auto guest_access = [](int index) -> state::GuestAccess {
|
|
||||||
state::GuestAccess event;
|
|
||||||
|
|
||||||
if (index == 0)
|
|
||||||
event.guest_access = state::AccessState::CanJoin;
|
|
||||||
else
|
|
||||||
event.guest_access = state::AccessState::Forbidden;
|
|
||||||
|
|
||||||
return event;
|
|
||||||
}(index);
|
|
||||||
|
|
||||||
auto join_rule = [](int index) -> state::JoinRules {
|
|
||||||
state::JoinRules event;
|
|
||||||
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
event.join_rule = state::JoinRule::Public;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
event.join_rule = state::JoinRule::Invite;
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
|
||||||
}(index);
|
|
||||||
|
|
||||||
updateAccessRules(room_id_.toStdString(), join_rule, guest_access);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (info_.join_rule == state::JoinRule::Public) {
|
|
||||||
if (info_.guest_access) {
|
|
||||||
accessCombo->setCurrentIndex(0);
|
|
||||||
} else {
|
|
||||||
accessCombo->setCurrentIndex(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accessCombo->setCurrentIndex(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto accessOptionLayout = new QHBoxLayout();
|
|
||||||
accessOptionLayout->setMargin(0);
|
|
||||||
accessOptionLayout->addWidget(accessLabel, Qt::AlignBottom | Qt::AlignLeft);
|
|
||||||
accessOptionLayout->addWidget(accessCombo, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
||||||
|
|
||||||
auto encryptionLabel = new QLabel(tr("Encryption"), this);
|
|
||||||
encryptionToggle_ = new Toggle(this);
|
|
||||||
|
|
||||||
auto encryptionOptionLayout = new QHBoxLayout;
|
|
||||||
encryptionOptionLayout->setMargin(0);
|
|
||||||
encryptionOptionLayout->addWidget(encryptionLabel, Qt::AlignBottom | Qt::AlignLeft);
|
|
||||||
encryptionOptionLayout->addWidget(encryptionToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
||||||
|
|
||||||
auto keyRequestsLabel = new QLabel(tr("Respond to key requests"), this);
|
|
||||||
keyRequestsLabel->setToolTipDuration(6000);
|
|
||||||
keyRequestsLabel->setToolTip(
|
|
||||||
tr("Whether or not the client should respond automatically with the session keys\n"
|
|
||||||
" upon request. Use with caution, this is a temporary measure to test the\n"
|
|
||||||
" E2E implementation until device verification is completed."));
|
|
||||||
keyRequestsToggle_ = new Toggle(this);
|
|
||||||
connect(keyRequestsToggle_, &Toggle::toggled, this, [this](bool isOn) {
|
|
||||||
utils::setKeyRequestsPreference(room_id_, isOn);
|
|
||||||
});
|
|
||||||
|
|
||||||
auto keyRequestsLayout = new QHBoxLayout;
|
|
||||||
keyRequestsLayout->setMargin(0);
|
|
||||||
keyRequestsLayout->setSpacing(0);
|
|
||||||
keyRequestsLayout->addWidget(keyRequestsLabel, Qt::AlignBottom | Qt::AlignLeft);
|
|
||||||
keyRequestsLayout->addWidget(keyRequestsToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
||||||
|
|
||||||
connect(encryptionToggle_, &Toggle::toggled, this, [this, keyRequestsLabel](bool isOn) {
|
|
||||||
if (!isOn || usesEncryption_)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QMessageBox msgBox;
|
|
||||||
msgBox.setIcon(QMessageBox::Question);
|
|
||||||
msgBox.setWindowTitle(tr("End-to-End Encryption"));
|
|
||||||
msgBox.setText(tr(
|
|
||||||
"Encryption is currently experimental and things might break unexpectedly. <br>"
|
|
||||||
"Please take note that it can't be disabled afterwards."));
|
|
||||||
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
|
|
||||||
msgBox.setDefaultButton(QMessageBox::Save);
|
|
||||||
int ret = msgBox.exec();
|
|
||||||
|
|
||||||
switch (ret) {
|
|
||||||
case QMessageBox::Ok: {
|
|
||||||
encryptionToggle_->setState(true);
|
|
||||||
encryptionToggle_->setEnabled(false);
|
|
||||||
enableEncryption();
|
|
||||||
keyRequestsToggle_->show();
|
|
||||||
keyRequestsLabel->show();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable encryption button.
|
|
||||||
if (usesEncryption_) {
|
|
||||||
encryptionToggle_->setState(true);
|
|
||||||
encryptionToggle_->setEnabled(false);
|
|
||||||
|
|
||||||
keyRequestsToggle_->setState(utils::respondsToKeyRequests(room_id_));
|
|
||||||
} else {
|
|
||||||
encryptionToggle_->setState(false);
|
|
||||||
|
|
||||||
keyRequestsLabel->hide();
|
|
||||||
keyRequestsToggle_->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide encryption option for public rooms.
|
|
||||||
if (!usesEncryption_ && (info_.join_rule == state::JoinRule::Public)) {
|
|
||||||
encryptionToggle_->hide();
|
|
||||||
encryptionLabel->hide();
|
|
||||||
|
|
||||||
keyRequestsLabel->hide();
|
|
||||||
keyRequestsToggle_->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
avatar_ = new Avatar(this, 128);
|
|
||||||
avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name)));
|
|
||||||
if (!info_.avatar_url.empty())
|
|
||||||
avatar_->setImage(QString::fromStdString(info_.avatar_url));
|
|
||||||
|
|
||||||
if (canChangeAvatar(room_id_.toStdString(), utils::localUser().toStdString())) {
|
|
||||||
auto filter = new ClickableFilter(this);
|
|
||||||
avatar_->installEventFilter(filter);
|
|
||||||
avatar_->setCursor(Qt::PointingHandCursor);
|
|
||||||
connect(filter, &ClickableFilter::clicked, this, &RoomSettings::updateAvatar);
|
|
||||||
}
|
|
||||||
|
|
||||||
roomNameLabel_ = new QLabel(QString::fromStdString(info_.name), this);
|
|
||||||
roomNameLabel_->setFont(largeFont);
|
|
||||||
|
|
||||||
auto membersLabel = new QLabel(tr("%n member(s)", "", (int)info_.member_count), this);
|
|
||||||
|
|
||||||
auto textLayout = new QVBoxLayout;
|
|
||||||
textLayout->addWidget(roomNameLabel_);
|
|
||||||
textLayout->addWidget(membersLabel);
|
|
||||||
textLayout->setAlignment(roomNameLabel_, Qt::AlignCenter | Qt::AlignTop);
|
|
||||||
textLayout->setAlignment(membersLabel, Qt::AlignCenter | Qt::AlignTop);
|
|
||||||
textLayout->setSpacing(TEXT_SPACING);
|
|
||||||
textLayout->setMargin(0);
|
|
||||||
|
|
||||||
setupEditButton();
|
|
||||||
|
|
||||||
errorLabel_ = new QLabel(this);
|
|
||||||
errorLabel_->setAlignment(Qt::AlignCenter);
|
|
||||||
errorLabel_->hide();
|
|
||||||
|
|
||||||
spinner_ = new LoadingIndicator(this);
|
|
||||||
spinner_->setFixedHeight(30);
|
|
||||||
spinner_->setFixedWidth(30);
|
|
||||||
spinner_->hide();
|
|
||||||
auto spinnerLayout = new QVBoxLayout;
|
|
||||||
spinnerLayout->addWidget(spinner_);
|
|
||||||
spinnerLayout->setAlignment(Qt::AlignCenter);
|
|
||||||
spinnerLayout->setMargin(0);
|
|
||||||
spinnerLayout->setSpacing(0);
|
|
||||||
|
|
||||||
auto okBtn = new QPushButton("OK", this);
|
|
||||||
|
|
||||||
auto buttonLayout = new QHBoxLayout();
|
|
||||||
buttonLayout->setSpacing(15);
|
|
||||||
buttonLayout->addStretch(1);
|
|
||||||
buttonLayout->addWidget(okBtn);
|
|
||||||
|
|
||||||
layout->addWidget(avatar_, Qt::AlignCenter | Qt::AlignTop);
|
|
||||||
layout->addLayout(textLayout);
|
|
||||||
layout->addLayout(btnLayout_);
|
|
||||||
layout->addWidget(settingsLabel, Qt::AlignLeft);
|
|
||||||
layout->addLayout(notifOptionLayout_);
|
|
||||||
layout->addLayout(accessOptionLayout);
|
|
||||||
layout->addLayout(encryptionOptionLayout);
|
|
||||||
layout->addLayout(keyRequestsLayout);
|
|
||||||
layout->addWidget(infoLabel, Qt::AlignLeft);
|
|
||||||
layout->addLayout(roomIdLayout);
|
|
||||||
layout->addLayout(roomVersionLayout);
|
|
||||||
layout->addWidget(errorLabel_);
|
|
||||||
layout->addLayout(buttonLayout);
|
|
||||||
layout->addLayout(spinnerLayout);
|
|
||||||
layout->addStretch(1);
|
|
||||||
|
|
||||||
connect(this, &RoomSettings::enableEncryptionError, this, [this](const QString &msg) {
|
|
||||||
encryptionToggle_->setState(false);
|
|
||||||
keyRequestsToggle_->setState(false);
|
|
||||||
keyRequestsToggle_->setEnabled(false);
|
|
||||||
keyRequestsToggle_->hide();
|
|
||||||
|
|
||||||
emit ChatPage::instance()->showNotification(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(this, &RoomSettings::showErrorMessage, this, [this](const QString &msg) {
|
|
||||||
if (!errorLabel_)
|
|
||||||
return;
|
|
||||||
|
|
||||||
stopLoadingSpinner();
|
|
||||||
|
|
||||||
errorLabel_->show();
|
|
||||||
errorLabel_->setText(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(this, &RoomSettings::accessRulesUpdated, this, [this]() {
|
|
||||||
stopLoadingSpinner();
|
|
||||||
resetErrorLabel();
|
|
||||||
});
|
|
||||||
|
|
||||||
auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
|
|
||||||
connect(closeShortcut, &QShortcut::activated, this, &RoomSettings::close);
|
|
||||||
connect(okBtn, &QPushButton::clicked, this, &RoomSettings::close);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::setupEditButton()
|
|
||||||
{
|
|
||||||
btnLayout_ = new QHBoxLayout;
|
|
||||||
btnLayout_->setSpacing(BUTTON_SPACING);
|
|
||||||
btnLayout_->setMargin(0);
|
|
||||||
|
|
||||||
if (!canChangeNameAndTopic(room_id_.toStdString(), utils::localUser().toStdString()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
QIcon editIcon;
|
|
||||||
editIcon.addFile(":/icons/icons/ui/edit.png");
|
|
||||||
editFieldsBtn_ = new FlatButton(this);
|
|
||||||
editFieldsBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
|
|
||||||
editFieldsBtn_->setCornerRadius(BUTTON_RADIUS);
|
|
||||||
editFieldsBtn_->setIcon(editIcon);
|
|
||||||
editFieldsBtn_->setIcon(editIcon);
|
|
||||||
editFieldsBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS));
|
|
||||||
|
|
||||||
connect(editFieldsBtn_, &QPushButton::clicked, this, [this]() {
|
|
||||||
retrieveRoomInfo();
|
|
||||||
|
|
||||||
auto modal = new EditModal(room_id_, this);
|
|
||||||
modal->setFields(QString::fromStdString(info_.name),
|
|
||||||
QString::fromStdString(info_.topic));
|
|
||||||
modal->raise();
|
|
||||||
modal->show();
|
|
||||||
connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) {
|
|
||||||
if (roomNameLabel_)
|
|
||||||
roomNameLabel_->setText(newName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
btnLayout_->addStretch(1);
|
|
||||||
btnLayout_->addWidget(editFieldsBtn_);
|
|
||||||
btnLayout_->addStretch(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::retrieveRoomInfo()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
usesEncryption_ = cache::isRoomEncrypted(room_id_.toStdString());
|
|
||||||
info_ = cache::singleRoomInfo(room_id_.toStdString());
|
|
||||||
setAvatar();
|
|
||||||
} catch (const lmdb::error &) {
|
|
||||||
nhlog::db()->warn("failed to retrieve room info from cache: {}",
|
|
||||||
room_id_.toStdString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::enableEncryption()
|
|
||||||
{
|
|
||||||
const auto room_id = room_id_.toStdString();
|
|
||||||
http::client()->enable_encryption(
|
|
||||||
room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
int status_code = static_cast<int>(err->status_code);
|
|
||||||
nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
|
|
||||||
room_id,
|
|
||||||
err->matrix_error.error,
|
|
||||||
status_code);
|
|
||||||
emit enableEncryptionError(
|
|
||||||
tr("Failed to enable encryption: %1")
|
|
||||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nhlog::net()->info("enabled encryption on room ({})", room_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::showEvent(QShowEvent *event)
|
|
||||||
{
|
|
||||||
resetErrorLabel();
|
|
||||||
stopLoadingSpinner();
|
|
||||||
|
|
||||||
QWidget::showEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
RoomSettings::canChangeJoinRules(const std::string &room_id, const std::string &user_id) const
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return cache::hasEnoughPowerLevel({EventType::RoomJoinRules}, room_id, user_id);
|
|
||||||
} catch (const lmdb::error &e) {
|
|
||||||
nhlog::db()->warn("lmdb error: {}", e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
RoomSettings::canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return cache::hasEnoughPowerLevel(
|
|
||||||
{EventType::RoomName, EventType::RoomTopic}, room_id, user_id);
|
|
||||||
} catch (const lmdb::error &e) {
|
|
||||||
nhlog::db()->warn("lmdb error: {}", e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
RoomSettings::canChangeAvatar(const std::string &room_id, const std::string &user_id) const
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return cache::hasEnoughPowerLevel({EventType::RoomAvatar}, room_id, user_id);
|
|
||||||
} catch (const lmdb::error &e) {
|
|
||||||
nhlog::db()->warn("lmdb error: {}", e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::updateAccessRules(const std::string &room_id,
|
|
||||||
const mtx::events::state::JoinRules &join_rule,
|
|
||||||
const mtx::events::state::GuestAccess &guest_access)
|
|
||||||
{
|
|
||||||
startLoadingSpinner();
|
|
||||||
resetErrorLabel();
|
|
||||||
|
|
||||||
http::client()->send_state_event(
|
|
||||||
room_id,
|
|
||||||
join_rule,
|
|
||||||
[this, room_id, guest_access](const mtx::responses::EventId &,
|
|
||||||
mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
nhlog::net()->warn("failed to send m.room.join_rule: {} {}",
|
|
||||||
static_cast<int>(err->status_code),
|
|
||||||
err->matrix_error.error);
|
|
||||||
emit showErrorMessage(QString::fromStdString(err->matrix_error.error));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
http::client()->send_state_event(
|
|
||||||
room_id,
|
|
||||||
guest_access,
|
|
||||||
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
nhlog::net()->warn("failed to send m.room.guest_access: {} {}",
|
|
||||||
static_cast<int>(err->status_code),
|
|
||||||
err->matrix_error.error);
|
|
||||||
emit showErrorMessage(
|
|
||||||
QString::fromStdString(err->matrix_error.error));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit accessRulesUpdated();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::stopLoadingSpinner()
|
|
||||||
{
|
|
||||||
if (spinner_) {
|
|
||||||
spinner_->stop();
|
|
||||||
spinner_->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::startLoadingSpinner()
|
|
||||||
{
|
|
||||||
if (spinner_) {
|
|
||||||
spinner_->start();
|
|
||||||
spinner_->show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::displayErrorMessage(const QString &msg)
|
|
||||||
{
|
|
||||||
stopLoadingSpinner();
|
|
||||||
|
|
||||||
errorLabel_->show();
|
|
||||||
errorLabel_->setText(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::setAvatar()
|
|
||||||
{
|
|
||||||
stopLoadingSpinner();
|
|
||||||
|
|
||||||
if (avatar_)
|
|
||||||
avatar_->setImage(QString::fromStdString(info_.avatar_url));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::resetErrorLabel()
|
|
||||||
{
|
|
||||||
if (errorLabel_) {
|
|
||||||
errorLabel_->hide();
|
|
||||||
errorLabel_->clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
RoomSettings::updateAvatar()
|
|
||||||
{
|
|
||||||
const QString picturesFolder =
|
|
||||||
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
|
||||||
const QString fileName = QFileDialog::getOpenFileName(
|
|
||||||
this, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
|
|
||||||
|
|
||||||
if (fileName.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
QMimeDatabase db;
|
|
||||||
QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
|
|
||||||
|
|
||||||
const auto format = mime.name().split("/")[0];
|
|
||||||
|
|
||||||
QFile file{fileName, this};
|
|
||||||
if (format != "image") {
|
|
||||||
displayErrorMessage(tr("The selected file is not an image"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
|
||||||
displayErrorMessage(tr("Error while reading file: %1").arg(file.errorString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spinner_) {
|
|
||||||
startLoadingSpinner();
|
|
||||||
resetErrorLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events emitted from the http callbacks (different threads) will
|
|
||||||
// be queued back into the UI thread through this proxy object.
|
|
||||||
auto proxy = std::make_shared<ThreadProxy>();
|
|
||||||
connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayErrorMessage);
|
|
||||||
connect(proxy.get(), &ThreadProxy::avatarChanged, this, &RoomSettings::setAvatar);
|
|
||||||
|
|
||||||
const auto bin = file.peek(file.size());
|
|
||||||
const auto payload = std::string(bin.data(), bin.size());
|
|
||||||
const auto dimensions = QImageReader(&file).size();
|
|
||||||
|
|
||||||
// First we need to create a new mxc URI
|
|
||||||
// (i.e upload media to the Matrix content repository) for the new avatar.
|
|
||||||
http::client()->upload(
|
|
||||||
payload,
|
|
||||||
mime.name().toStdString(),
|
|
||||||
QFileInfo(fileName).fileName().toStdString(),
|
|
||||||
[proxy = std::move(proxy),
|
|
||||||
dimensions,
|
|
||||||
payload,
|
|
||||||
mimetype = mime.name().toStdString(),
|
|
||||||
size = payload.size(),
|
|
||||||
room_id = room_id_.toStdString(),
|
|
||||||
content = std::move(bin)](const mtx::responses::ContentURI &res,
|
|
||||||
mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
emit proxy->error(
|
|
||||||
tr("Failed to upload image: %s")
|
|
||||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using namespace mtx::events;
|
|
||||||
state::Avatar avatar_event;
|
|
||||||
avatar_event.image_info.w = dimensions.width();
|
|
||||||
avatar_event.image_info.h = dimensions.height();
|
|
||||||
avatar_event.image_info.mimetype = mimetype;
|
|
||||||
avatar_event.image_info.size = size;
|
|
||||||
avatar_event.url = res.content_uri;
|
|
||||||
|
|
||||||
http::client()->send_state_event(
|
|
||||||
room_id,
|
|
||||||
avatar_event,
|
|
||||||
[content = std::move(content), proxy = std::move(proxy)](
|
|
||||||
const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
emit proxy->error(
|
|
||||||
tr("Failed to upload image: %s")
|
|
||||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit proxy->avatarChanged();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QImage>
|
|
||||||
|
|
||||||
#include <mtx/events/guest_access.hpp>
|
|
||||||
|
|
||||||
#include "CacheStructs.h"
|
|
||||||
|
|
||||||
class Avatar;
|
|
||||||
class FlatButton;
|
|
||||||
class QPushButton;
|
|
||||||
class QComboBox;
|
|
||||||
class QHBoxLayout;
|
|
||||||
class QShowEvent;
|
|
||||||
class LoadingIndicator;
|
|
||||||
class QLayout;
|
|
||||||
class QPixmap;
|
|
||||||
class TextField;
|
|
||||||
class TextField;
|
|
||||||
class Toggle;
|
|
||||||
class QLabel;
|
|
||||||
class QEvent;
|
|
||||||
|
|
||||||
class ClickableFilter : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ClickableFilter(QWidget *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{}
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void clicked();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Convenience class which connects events emmited from threads
|
|
||||||
/// outside of main with the UI code.
|
|
||||||
class ThreadProxy : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void error(const QString &msg);
|
|
||||||
void avatarChanged();
|
|
||||||
void nameEventSent(const QString &);
|
|
||||||
void topicEventSent();
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditModal : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
EditModal(const QString &roomId, QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setFields(const QString &roomName, const QString &roomTopic);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void nameChanged(const QString &roomName);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void topicEventSent();
|
|
||||||
void nameEventSent(const QString &name);
|
|
||||||
void error(const QString &msg);
|
|
||||||
|
|
||||||
void applyClicked();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString roomId_;
|
|
||||||
QString initialName_;
|
|
||||||
QString initialTopic_;
|
|
||||||
|
|
||||||
QLabel *errorField_;
|
|
||||||
|
|
||||||
TextField *nameInput_;
|
|
||||||
TextField *topicInput_;
|
|
||||||
|
|
||||||
QPushButton *applyBtn_;
|
|
||||||
QPushButton *cancelBtn_;
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace dialogs {
|
|
||||||
|
|
||||||
class RoomSettings : public QFrame
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
RoomSettings(const QString &room_id, QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void enableEncryptionError(const QString &msg);
|
|
||||||
void showErrorMessage(const QString &msg);
|
|
||||||
void accessRulesUpdated();
|
|
||||||
void notifChanged(int index);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void showEvent(QShowEvent *event) override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
//! The file dialog opens so the user can select and upload a new room avatar.
|
|
||||||
void updateAvatar();
|
|
||||||
|
|
||||||
private:
|
|
||||||
//! Whether the user has enough power level to send m.room.join_rules events.
|
|
||||||
bool canChangeJoinRules(const std::string &room_id, const std::string &user_id) const;
|
|
||||||
//! Whether the user has enough power level to send m.room.name & m.room.topic events.
|
|
||||||
bool canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const;
|
|
||||||
//! Whether the user has enough power level to send m.room.avatar event.
|
|
||||||
bool canChangeAvatar(const std::string &room_id, const std::string &user_id) const;
|
|
||||||
void updateAccessRules(const std::string &room_id,
|
|
||||||
const mtx::events::state::JoinRules &,
|
|
||||||
const mtx::events::state::GuestAccess &);
|
|
||||||
void stopLoadingSpinner();
|
|
||||||
void startLoadingSpinner();
|
|
||||||
void resetErrorLabel();
|
|
||||||
void displayErrorMessage(const QString &msg);
|
|
||||||
|
|
||||||
void setAvatar();
|
|
||||||
void setupEditButton();
|
|
||||||
//! Retrieve the current room information from cache.
|
|
||||||
void retrieveRoomInfo();
|
|
||||||
void enableEncryption();
|
|
||||||
|
|
||||||
Avatar *avatar_ = nullptr;
|
|
||||||
|
|
||||||
bool usesEncryption_ = false;
|
|
||||||
QHBoxLayout *btnLayout_;
|
|
||||||
|
|
||||||
FlatButton *editFieldsBtn_ = nullptr;
|
|
||||||
|
|
||||||
RoomInfo info_;
|
|
||||||
QString room_id_;
|
|
||||||
QImage avatarImg_;
|
|
||||||
|
|
||||||
QLabel *roomNameLabel_ = nullptr;
|
|
||||||
QLabel *errorLabel_ = nullptr;
|
|
||||||
LoadingIndicator *spinner_ = nullptr;
|
|
||||||
|
|
||||||
QComboBox *notifCombo = nullptr;
|
|
||||||
QComboBox *accessCombo = nullptr;
|
|
||||||
Toggle *encryptionToggle_ = nullptr;
|
|
||||||
Toggle *keyRequestsToggle_ = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // dialogs
|
|
@ -109,7 +109,7 @@ NotificationsManager::closeNotification(uint id)
|
|||||||
"org.freedesktop.Notifications");
|
"org.freedesktop.Notifications");
|
||||||
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
|
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
|
||||||
auto watcher = new QDBusPendingCallWatcher{call, this};
|
auto watcher = new QDBusPendingCallWatcher{call, this};
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this]() {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
|
||||||
if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
|
if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
|
||||||
qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
|
qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Olm.h"
|
#include "Olm.h"
|
||||||
|
#include "RoomsModel.h"
|
||||||
#include "TimelineModel.h"
|
#include "TimelineModel.h"
|
||||||
#include "TimelineViewManager.h"
|
#include "TimelineViewManager.h"
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
@ -121,6 +122,20 @@ InputBar::insertMimeData(const QMimeData *md)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::setText(QString newText)
|
||||||
|
{
|
||||||
|
if (history_.empty())
|
||||||
|
history_.push_front(newText);
|
||||||
|
else
|
||||||
|
history_.front() = newText;
|
||||||
|
history_index_ = 0;
|
||||||
|
|
||||||
|
if (history_.size() == INPUT_HISTORY_SIZE)
|
||||||
|
history_.pop_back();
|
||||||
|
|
||||||
|
emit textChanged(newText);
|
||||||
|
}
|
||||||
void
|
void
|
||||||
InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_)
|
InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_)
|
||||||
{
|
{
|
||||||
@ -186,6 +201,11 @@ InputBar::completerFor(QString completerName)
|
|||||||
auto proxy = new CompletionProxyModel(emojiModel);
|
auto proxy = new CompletionProxyModel(emojiModel);
|
||||||
emojiModel->setParent(proxy);
|
emojiModel->setParent(proxy);
|
||||||
return proxy;
|
return proxy;
|
||||||
|
} else if (completerName == "room") {
|
||||||
|
auto roomModel = new RoomsModel(true);
|
||||||
|
auto proxy = new CompletionProxyModel(roomModel);
|
||||||
|
roomModel->setParent(proxy);
|
||||||
|
return proxy;
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -196,6 +216,10 @@ InputBar::send()
|
|||||||
if (text().trimmed().isEmpty())
|
if (text().trimmed().isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
nhlog::ui()->debug("Send: {}", text().toStdString());
|
||||||
|
|
||||||
|
auto wasEdit = !room->edit().isEmpty();
|
||||||
|
|
||||||
if (text().startsWith('/')) {
|
if (text().startsWith('/')) {
|
||||||
int command_end = text().indexOf(' ');
|
int command_end = text().indexOf(' ');
|
||||||
if (command_end == -1)
|
if (command_end == -1)
|
||||||
@ -211,12 +235,10 @@ InputBar::send()
|
|||||||
message(text());
|
message(text());
|
||||||
}
|
}
|
||||||
|
|
||||||
nhlog::ui()->debug("Send: {}", text().toStdString());
|
if (!wasEdit) {
|
||||||
|
history_.push_front("");
|
||||||
if (history_.size() == INPUT_HISTORY_SIZE)
|
setText("");
|
||||||
history_.pop_back();
|
}
|
||||||
history_.push_front("");
|
|
||||||
history_index_ = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -272,12 +294,10 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
|||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty()) {
|
||||||
text.relations.relations.push_back(
|
text.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
text.relations.relations.push_back(
|
text.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||||
room->resetEdit();
|
|
||||||
|
|
||||||
} else if (!room->reply().isEmpty()) {
|
} else if (!room->reply().isEmpty()) {
|
||||||
auto related = room->relatedInfo(room->reply());
|
auto related = room->relatedInfo(room->reply());
|
||||||
@ -307,7 +327,6 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
|||||||
|
|
||||||
text.relations.relations.push_back(
|
text.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, related.related_event});
|
{mtx::common::RelationType::InReplyTo, related.related_event});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
|
||||||
@ -330,12 +349,10 @@ InputBar::emote(QString msg)
|
|||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty()) {
|
||||||
emote.relations.relations.push_back(
|
emote.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
if (!room->edit().isEmpty()) {
|
if (!room->edit().isEmpty()) {
|
||||||
emote.relations.relations.push_back(
|
emote.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||||
room->resetEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
||||||
@ -366,12 +383,10 @@ InputBar::image(const QString &filename,
|
|||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty()) {
|
||||||
image.relations.relations.push_back(
|
image.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
if (!room->edit().isEmpty()) {
|
if (!room->edit().isEmpty()) {
|
||||||
image.relations.relations.push_back(
|
image.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||||
room->resetEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
||||||
@ -397,12 +412,10 @@ InputBar::file(const QString &filename,
|
|||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty()) {
|
||||||
file.relations.relations.push_back(
|
file.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
if (!room->edit().isEmpty()) {
|
if (!room->edit().isEmpty()) {
|
||||||
file.relations.relations.push_back(
|
file.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||||
room->resetEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
||||||
@ -429,12 +442,10 @@ InputBar::audio(const QString &filename,
|
|||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty()) {
|
||||||
audio.relations.relations.push_back(
|
audio.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
if (!room->edit().isEmpty()) {
|
if (!room->edit().isEmpty()) {
|
||||||
audio.relations.relations.push_back(
|
audio.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||||
room->resetEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
||||||
@ -460,12 +471,10 @@ InputBar::video(const QString &filename,
|
|||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty()) {
|
||||||
video.relations.relations.push_back(
|
video.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||||
room->resetReply();
|
|
||||||
}
|
}
|
||||||
if (!room->edit().isEmpty()) {
|
if (!room->edit().isEmpty()) {
|
||||||
video.relations.relations.push_back(
|
video.relations.relations.push_back(
|
||||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||||
room->resetEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
|
||||||
|
@ -41,7 +41,7 @@ public slots:
|
|||||||
QString text() const;
|
QString text() const;
|
||||||
QString previousText();
|
QString previousText();
|
||||||
QString nextText();
|
QString nextText();
|
||||||
void setText(QString newText) { emit textChanged(newText); }
|
void setText(QString newText);
|
||||||
|
|
||||||
void send();
|
void send();
|
||||||
void paste(bool fromMouse);
|
void paste(bool fromMouse);
|
||||||
|
@ -362,6 +362,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||||||
const static QRegularExpression replyFallback(
|
const static QRegularExpression replyFallback(
|
||||||
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
|
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
|
||||||
|
|
||||||
|
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
|
||||||
|
|
||||||
bool isReply = relations(event).reply_to().has_value();
|
bool isReply = relations(event).reply_to().has_value();
|
||||||
|
|
||||||
auto formattedBody_ = QString::fromStdString(formatted_body(event));
|
auto formattedBody_ = QString::fromStdString(formatted_body(event));
|
||||||
@ -380,8 +382,14 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||||||
formattedBody_ = formattedBody_.remove(replyFallback);
|
formattedBody_ = formattedBody_.remove(replyFallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
|
// TODO(Nico): Don't parse html with a regex
|
||||||
formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
|
const static QRegularExpression matchImgUri(
|
||||||
|
"(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)");
|
||||||
|
formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3");
|
||||||
|
const static QRegularExpression matchEmoticonHeight(
|
||||||
|
"(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)");
|
||||||
|
formattedBody_.replace(matchEmoticonHeight,
|
||||||
|
QString("\\1 height=\"%1\"\\3").arg(ascent));
|
||||||
|
|
||||||
return QVariant(utils::replaceEmoji(
|
return QVariant(utils::replaceEmoji(
|
||||||
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
|
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
|
||||||
@ -491,6 +499,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||||||
data(event, static_cast<int>(ProportionalHeight)));
|
data(event, static_cast<int>(ProportionalHeight)));
|
||||||
m.insert(names[Id], data(event, static_cast<int>(Id)));
|
m.insert(names[Id], data(event, static_cast<int>(Id)));
|
||||||
m.insert(names[State], data(event, static_cast<int>(State)));
|
m.insert(names[State], data(event, static_cast<int>(State)));
|
||||||
|
m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited)));
|
||||||
|
m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable)));
|
||||||
m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
|
m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
|
||||||
m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
|
m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
|
||||||
m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
|
m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
|
||||||
@ -753,11 +763,6 @@ TimelineModel::setCurrentIndex(int index)
|
|||||||
(!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
|
(!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
|
||||||
readEvent(nextEventIndexAndId->second);
|
readEvent(nextEventIndexAndId->second);
|
||||||
currentReadId = QString::fromStdString(nextEventIndexAndId->second);
|
currentReadId = QString::fromStdString(nextEventIndexAndId->second);
|
||||||
|
|
||||||
nhlog::net()->info("Marked as read {}, index {}, oldReadIndex {}",
|
|
||||||
nextEventIndexAndId->second,
|
|
||||||
nextEventIndexAndId->first,
|
|
||||||
*oldReadIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -833,6 +838,14 @@ TimelineModel::openUserProfile(QString userid, bool global)
|
|||||||
emit openProfile(userProfile);
|
emit openProfile(userProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TimelineModel::openRoomSettings()
|
||||||
|
{
|
||||||
|
RoomSettings *settings = new RoomSettings(roomId(), this);
|
||||||
|
connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged);
|
||||||
|
openRoomSettingsDialog(settings);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::replyAction(QString id)
|
TimelineModel::replyAction(QString id)
|
||||||
{
|
{
|
||||||
@ -1539,6 +1552,17 @@ TimelineModel::setEdit(QString newEdit)
|
|||||||
if (edit_.startsWith('m'))
|
if (edit_.startsWith('m'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (newEdit.isEmpty()) {
|
||||||
|
resetEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edit_.isEmpty()) {
|
||||||
|
this->textBeforeEdit = input()->text();
|
||||||
|
this->replyBeforeEdit = reply_;
|
||||||
|
nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
if (edit_ != newEdit) {
|
if (edit_ != newEdit) {
|
||||||
auto ev = events.get(newEdit.toStdString(), "");
|
auto ev = events.get(newEdit.toStdString(), "");
|
||||||
if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
|
if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
|
||||||
@ -1573,8 +1597,14 @@ TimelineModel::resetEdit()
|
|||||||
if (!edit_.isEmpty()) {
|
if (!edit_.isEmpty()) {
|
||||||
edit_ = "";
|
edit_ = "";
|
||||||
emit editChanged(edit_);
|
emit editChanged(edit_);
|
||||||
input()->setText("");
|
nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString());
|
||||||
resetReply();
|
input()->setText(textBeforeEdit);
|
||||||
|
textBeforeEdit.clear();
|
||||||
|
if (replyBeforeEdit.isEmpty())
|
||||||
|
resetReply();
|
||||||
|
else
|
||||||
|
setReply(replyBeforeEdit);
|
||||||
|
replyBeforeEdit.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
#include "CacheCryptoStructs.h"
|
#include "CacheCryptoStructs.h"
|
||||||
#include "EventStore.h"
|
#include "EventStore.h"
|
||||||
#include "InputBar.h"
|
#include "InputBar.h"
|
||||||
|
#include "ui/RoomSettings.h"
|
||||||
#include "ui/UserProfile.h"
|
#include "ui/UserProfile.h"
|
||||||
|
|
||||||
namespace mtx::http {
|
namespace mtx::http {
|
||||||
@ -216,6 +217,7 @@ public:
|
|||||||
Q_INVOKABLE void viewRawMessage(QString id) const;
|
Q_INVOKABLE void viewRawMessage(QString id) const;
|
||||||
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
||||||
Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
|
Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
|
||||||
|
Q_INVOKABLE void openRoomSettings();
|
||||||
Q_INVOKABLE void editAction(QString id);
|
Q_INVOKABLE void editAction(QString id);
|
||||||
Q_INVOKABLE void replyAction(QString id);
|
Q_INVOKABLE void replyAction(QString id);
|
||||||
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
||||||
@ -307,6 +309,7 @@ signals:
|
|||||||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||||
|
|
||||||
void openProfile(UserProfile *profile);
|
void openProfile(UserProfile *profile);
|
||||||
|
void openRoomSettingsDialog(RoomSettings *settings);
|
||||||
|
|
||||||
void newMessageToSend(mtx::events::collections::TimelineEvents event);
|
void newMessageToSend(mtx::events::collections::TimelineEvents event);
|
||||||
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
|
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
|
||||||
@ -334,6 +337,7 @@ private:
|
|||||||
|
|
||||||
QString currentId, currentReadId;
|
QString currentId, currentReadId;
|
||||||
QString reply_, edit_;
|
QString reply_, edit_;
|
||||||
|
QString textBeforeEdit, replyBeforeEdit;
|
||||||
std::vector<QString> typingUsers_;
|
std::vector<QString> typingUsers_;
|
||||||
|
|
||||||
TimelineViewManager *manager_;
|
TimelineViewManager *manager_;
|
||||||
@ -351,4 +355,6 @@ TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventTy
|
|||||||
msgCopy.content = content;
|
msgCopy.content = content;
|
||||||
msgCopy.type = eventType;
|
msgCopy.type = eventType;
|
||||||
emit newMessageToSend(msgCopy);
|
emit newMessageToSend(msgCopy);
|
||||||
|
resetReply();
|
||||||
|
resetEdit();
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
|
|||||||
0,
|
0,
|
||||||
"UserProfileModel",
|
"UserProfileModel",
|
||||||
"UserProfile needs to be instantiated on the C++ side");
|
"UserProfile needs to be instantiated on the C++ side");
|
||||||
|
qmlRegisterUncreatableType<RoomSettings>(
|
||||||
|
"im.nheko",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"RoomSettingsModel",
|
||||||
|
"Room Settings needs to be instantiated on the C++ side");
|
||||||
|
|
||||||
static auto self = this;
|
static auto self = this;
|
||||||
qmlRegisterSingletonType<MainWindow>(
|
qmlRegisterSingletonType<MainWindow>(
|
||||||
@ -387,11 +393,6 @@ TimelineViewManager::openLeaveRoomDialog() const
|
|||||||
{
|
{
|
||||||
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
|
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
|
||||||
}
|
}
|
||||||
void
|
|
||||||
TimelineViewManager::openRoomSettings() const
|
|
||||||
{
|
|
||||||
MainWindow::instance()->openRoomSettings(timeline_->roomId());
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineViewManager::verifyUser(QString userid)
|
TimelineViewManager::verifyUser(QString userid)
|
||||||
|
@ -70,7 +70,6 @@ public:
|
|||||||
Q_INVOKABLE void openInviteUsersDialog();
|
Q_INVOKABLE void openInviteUsersDialog();
|
||||||
Q_INVOKABLE void openMemberListDialog() const;
|
Q_INVOKABLE void openMemberListDialog() const;
|
||||||
Q_INVOKABLE void openLeaveRoomDialog() const;
|
Q_INVOKABLE void openLeaveRoomDialog() const;
|
||||||
Q_INVOKABLE void openRoomSettings() const;
|
|
||||||
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
|
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
|
||||||
|
|
||||||
void verifyUser(QString userid);
|
void verifyUser(QString userid);
|
||||||
|
625
src/ui/RoomSettings.cpp
Normal file
625
src/ui/RoomSettings.cpp
Normal file
@ -0,0 +1,625 @@
|
|||||||
|
#include "RoomSettings.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QImageReader>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <mtx/responses/common.hpp>
|
||||||
|
#include <mtx/responses/media.hpp>
|
||||||
|
|
||||||
|
#include "Cache.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include "Logging.h"
|
||||||
|
#include "MatrixClient.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
#include "ui/TextField.h"
|
||||||
|
|
||||||
|
using namespace mtx::events;
|
||||||
|
|
||||||
|
EditModal::EditModal(const QString &roomId, QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, roomId_{roomId}
|
||||||
|
{
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||||
|
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||||
|
setWindowModality(Qt::WindowModal);
|
||||||
|
|
||||||
|
QFont largeFont;
|
||||||
|
largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4);
|
||||||
|
setMinimumWidth(conf::window::minModalWidth);
|
||||||
|
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
||||||
|
|
||||||
|
auto layout = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
applyBtn_ = new QPushButton(tr("Apply"), this);
|
||||||
|
cancelBtn_ = new QPushButton(tr("Cancel"), this);
|
||||||
|
cancelBtn_->setDefault(true);
|
||||||
|
|
||||||
|
auto btnLayout = new QHBoxLayout;
|
||||||
|
btnLayout->addStretch(1);
|
||||||
|
btnLayout->setSpacing(15);
|
||||||
|
btnLayout->addWidget(cancelBtn_);
|
||||||
|
btnLayout->addWidget(applyBtn_);
|
||||||
|
|
||||||
|
nameInput_ = new TextField(this);
|
||||||
|
nameInput_->setLabel(tr("Name").toUpper());
|
||||||
|
topicInput_ = new TextField(this);
|
||||||
|
topicInput_->setLabel(tr("Topic").toUpper());
|
||||||
|
|
||||||
|
errorField_ = new QLabel(this);
|
||||||
|
errorField_->setWordWrap(true);
|
||||||
|
errorField_->hide();
|
||||||
|
|
||||||
|
layout->addWidget(nameInput_);
|
||||||
|
layout->addWidget(topicInput_);
|
||||||
|
layout->addLayout(btnLayout, 1);
|
||||||
|
|
||||||
|
auto labelLayout = new QHBoxLayout;
|
||||||
|
labelLayout->setAlignment(Qt::AlignHCenter);
|
||||||
|
labelLayout->addWidget(errorField_);
|
||||||
|
layout->addLayout(labelLayout);
|
||||||
|
|
||||||
|
connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked);
|
||||||
|
connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
|
||||||
|
|
||||||
|
auto window = QApplication::activeWindow();
|
||||||
|
|
||||||
|
if (window != nullptr) {
|
||||||
|
auto center = window->frameGeometry().center();
|
||||||
|
move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
EditModal::topicEventSent(const QString &topic)
|
||||||
|
{
|
||||||
|
errorField_->hide();
|
||||||
|
emit topicChanged(topic);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
EditModal::nameEventSent(const QString &name)
|
||||||
|
{
|
||||||
|
errorField_->hide();
|
||||||
|
emit nameChanged(name);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
EditModal::error(const QString &msg)
|
||||||
|
{
|
||||||
|
errorField_->setText(msg);
|
||||||
|
errorField_->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
EditModal::applyClicked()
|
||||||
|
{
|
||||||
|
// Check if the values are changed from the originals.
|
||||||
|
auto newName = nameInput_->text().trimmed();
|
||||||
|
auto newTopic = topicInput_->text().trimmed();
|
||||||
|
|
||||||
|
errorField_->hide();
|
||||||
|
|
||||||
|
if (newName == initialName_ && newTopic == initialTopic_) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace mtx::events;
|
||||||
|
auto proxy = std::make_shared<ThreadProxy>();
|
||||||
|
connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent);
|
||||||
|
connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent);
|
||||||
|
connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error);
|
||||||
|
|
||||||
|
if (newName != initialName_ && !newName.isEmpty()) {
|
||||||
|
state::Name body;
|
||||||
|
body.name = newName.toStdString();
|
||||||
|
|
||||||
|
http::client()->send_state_event(
|
||||||
|
roomId_.toStdString(),
|
||||||
|
body,
|
||||||
|
[proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
emit proxy->error(
|
||||||
|
QString::fromStdString(err->matrix_error.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit proxy->nameEventSent(newName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
|
||||||
|
state::Topic body;
|
||||||
|
body.topic = newTopic.toStdString();
|
||||||
|
|
||||||
|
http::client()->send_state_event(
|
||||||
|
roomId_.toStdString(),
|
||||||
|
body,
|
||||||
|
[proxy, newTopic](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
emit proxy->error(
|
||||||
|
QString::fromStdString(err->matrix_error.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit proxy->topicEventSent(newTopic);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
EditModal::setFields(const QString &roomName, const QString &roomTopic)
|
||||||
|
{
|
||||||
|
initialName_ = roomName;
|
||||||
|
initialTopic_ = roomTopic;
|
||||||
|
|
||||||
|
nameInput_->setText(roomName);
|
||||||
|
topicInput_->setText(roomTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomSettings::RoomSettings(QString roomid, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, roomid_{std::move(roomid)}
|
||||||
|
{
|
||||||
|
retrieveRoomInfo();
|
||||||
|
|
||||||
|
// get room setting notifications
|
||||||
|
http::client()->get_pushrules(
|
||||||
|
"global",
|
||||||
|
"override",
|
||||||
|
roomid_.toStdString(),
|
||||||
|
[this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) {
|
||||||
|
if (err) {
|
||||||
|
if (err->status_code == boost::beast::http::status::not_found)
|
||||||
|
http::client()->get_pushrules(
|
||||||
|
"global",
|
||||||
|
"room",
|
||||||
|
roomid_.toStdString(),
|
||||||
|
[this](const mtx::pushrules::PushRule &rule,
|
||||||
|
mtx::http::RequestErr &err) {
|
||||||
|
if (err) {
|
||||||
|
notifications_ = 2; // all messages
|
||||||
|
emit notificationsChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.enabled) {
|
||||||
|
notifications_ = 1; // mentions only
|
||||||
|
emit notificationsChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.enabled) {
|
||||||
|
notifications_ = 0; // muted
|
||||||
|
emit notificationsChanged();
|
||||||
|
} else {
|
||||||
|
notifications_ = 2; // all messages
|
||||||
|
emit notificationsChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// access rules
|
||||||
|
if (info_.join_rule == state::JoinRule::Public) {
|
||||||
|
if (info_.guest_access) {
|
||||||
|
accessRules_ = 0;
|
||||||
|
} else {
|
||||||
|
accessRules_ = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
accessRules_ = 2;
|
||||||
|
}
|
||||||
|
emit accessJoinRulesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
RoomSettings::roomName() const
|
||||||
|
{
|
||||||
|
return QString::fromStdString(info_.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
RoomSettings::roomTopic() const
|
||||||
|
{
|
||||||
|
return utils::linkifyMessage(QString::fromStdString(info_.topic).toHtmlEscaped());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
RoomSettings::roomId() const
|
||||||
|
{
|
||||||
|
return roomid_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
RoomSettings::roomVersion() const
|
||||||
|
{
|
||||||
|
return QString::fromStdString(info_.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
RoomSettings::isLoading() const
|
||||||
|
{
|
||||||
|
return isLoading_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
RoomSettings::roomAvatarUrl()
|
||||||
|
{
|
||||||
|
return QString::fromStdString(info_.avatar_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
RoomSettings::memberCount() const
|
||||||
|
{
|
||||||
|
return info_.member_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::retrieveRoomInfo()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString());
|
||||||
|
info_ = cache::singleRoomInfo(roomid_.toStdString());
|
||||||
|
} catch (const lmdb::error &) {
|
||||||
|
nhlog::db()->warn("failed to retrieve room info from cache: {}",
|
||||||
|
roomid_.toStdString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
RoomSettings::notifications()
|
||||||
|
{
|
||||||
|
return notifications_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
RoomSettings::accessJoinRules()
|
||||||
|
{
|
||||||
|
return accessRules_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
RoomSettings::respondsToKeyRequests()
|
||||||
|
{
|
||||||
|
return usesEncryption_ && utils::respondsToKeyRequests(roomid_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::changeKeyRequestsPreference(bool isOn)
|
||||||
|
{
|
||||||
|
utils::setKeyRequestsPreference(roomid_, isOn);
|
||||||
|
emit keyRequestsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::enableEncryption()
|
||||||
|
{
|
||||||
|
if (usesEncryption_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto room_id = roomid_.toStdString();
|
||||||
|
http::client()->enable_encryption(
|
||||||
|
room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
int status_code = static_cast<int>(err->status_code);
|
||||||
|
nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
|
||||||
|
room_id,
|
||||||
|
err->matrix_error.error,
|
||||||
|
status_code);
|
||||||
|
emit displayError(
|
||||||
|
tr("Failed to enable encryption: %1")
|
||||||
|
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||||
|
usesEncryption_ = false;
|
||||||
|
emit encryptionChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nhlog::net()->info("enabled encryption on room ({})", room_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
usesEncryption_ = true;
|
||||||
|
emit encryptionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
RoomSettings::canChangeJoinRules() const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return cache::hasEnoughPowerLevel({EventType::RoomJoinRules},
|
||||||
|
roomid_.toStdString(),
|
||||||
|
utils::localUser().toStdString());
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::db()->warn("lmdb error: {}", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
RoomSettings::canChangeNameAndTopic() const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic},
|
||||||
|
roomid_.toStdString(),
|
||||||
|
utils::localUser().toStdString());
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::db()->warn("lmdb error: {}", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
RoomSettings::canChangeAvatar() const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return cache::hasEnoughPowerLevel(
|
||||||
|
{EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString());
|
||||||
|
} catch (const lmdb::error &e) {
|
||||||
|
nhlog::db()->warn("lmdb error: {}", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
RoomSettings::isEncryptionEnabled() const
|
||||||
|
{
|
||||||
|
return usesEncryption_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::openEditModal()
|
||||||
|
{
|
||||||
|
retrieveRoomInfo();
|
||||||
|
|
||||||
|
auto modal = new EditModal(roomid_);
|
||||||
|
modal->setFields(QString::fromStdString(info_.name), QString::fromStdString(info_.topic));
|
||||||
|
modal->raise();
|
||||||
|
modal->show();
|
||||||
|
connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) {
|
||||||
|
info_.name = newName.toStdString();
|
||||||
|
emit roomNameChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(modal, &EditModal::topicChanged, this, [this](const QString &newTopic) {
|
||||||
|
info_.topic = newTopic.toStdString();
|
||||||
|
emit roomTopicChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::changeNotifications(int currentIndex)
|
||||||
|
{
|
||||||
|
notifications_ = currentIndex;
|
||||||
|
|
||||||
|
std::string room_id = roomid_.toStdString();
|
||||||
|
if (notifications_ == 0) {
|
||||||
|
// mute room
|
||||||
|
// delete old rule first, then add new rule
|
||||||
|
mtx::pushrules::PushRule rule;
|
||||||
|
rule.actions = {mtx::pushrules::actions::dont_notify{}};
|
||||||
|
mtx::pushrules::PushCondition condition;
|
||||||
|
condition.kind = "event_match";
|
||||||
|
condition.key = "room_id";
|
||||||
|
condition.pattern = room_id;
|
||||||
|
rule.conditions = {condition};
|
||||||
|
|
||||||
|
http::client()->put_pushrules(
|
||||||
|
"global", "override", room_id, rule, [room_id](mtx::http::RequestErr &err) {
|
||||||
|
if (err)
|
||||||
|
nhlog::net()->error("failed to set pushrule for room {}: {} {}",
|
||||||
|
room_id,
|
||||||
|
static_cast<int>(err->status_code),
|
||||||
|
err->matrix_error.error);
|
||||||
|
http::client()->delete_pushrules(
|
||||||
|
"global", "room", room_id, [room_id](mtx::http::RequestErr &) {});
|
||||||
|
});
|
||||||
|
} else if (notifications_ == 1) {
|
||||||
|
// mentions only
|
||||||
|
// delete old rule first, then add new rule
|
||||||
|
mtx::pushrules::PushRule rule;
|
||||||
|
rule.actions = {mtx::pushrules::actions::dont_notify{}};
|
||||||
|
http::client()->put_pushrules(
|
||||||
|
"global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) {
|
||||||
|
if (err)
|
||||||
|
nhlog::net()->error("failed to set pushrule for room {}: {} {}",
|
||||||
|
room_id,
|
||||||
|
static_cast<int>(err->status_code),
|
||||||
|
err->matrix_error.error);
|
||||||
|
http::client()->delete_pushrules(
|
||||||
|
"global", "override", room_id, [room_id](mtx::http::RequestErr &) {});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// all messages
|
||||||
|
http::client()->delete_pushrules(
|
||||||
|
"global", "override", room_id, [room_id](mtx::http::RequestErr &) {
|
||||||
|
http::client()->delete_pushrules(
|
||||||
|
"global", "room", room_id, [room_id](mtx::http::RequestErr &) {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::changeAccessRules(int index)
|
||||||
|
{
|
||||||
|
using namespace mtx::events::state;
|
||||||
|
|
||||||
|
auto guest_access = [](int index) -> state::GuestAccess {
|
||||||
|
state::GuestAccess event;
|
||||||
|
|
||||||
|
if (index == 0)
|
||||||
|
event.guest_access = state::AccessState::CanJoin;
|
||||||
|
else
|
||||||
|
event.guest_access = state::AccessState::Forbidden;
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}(index);
|
||||||
|
|
||||||
|
auto join_rule = [](int index) -> state::JoinRules {
|
||||||
|
state::JoinRules event;
|
||||||
|
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
event.join_rule = state::JoinRule::Public;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.join_rule = state::JoinRule::Invite;
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}(index);
|
||||||
|
|
||||||
|
updateAccessRules(roomid_.toStdString(), join_rule, guest_access);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::updateAccessRules(const std::string &room_id,
|
||||||
|
const mtx::events::state::JoinRules &join_rule,
|
||||||
|
const mtx::events::state::GuestAccess &guest_access)
|
||||||
|
{
|
||||||
|
isLoading_ = true;
|
||||||
|
emit loadingChanged();
|
||||||
|
|
||||||
|
http::client()->send_state_event(
|
||||||
|
room_id,
|
||||||
|
join_rule,
|
||||||
|
[this, room_id, guest_access](const mtx::responses::EventId &,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to send m.room.join_rule: {} {}",
|
||||||
|
static_cast<int>(err->status_code),
|
||||||
|
err->matrix_error.error);
|
||||||
|
emit displayError(QString::fromStdString(err->matrix_error.error));
|
||||||
|
isLoading_ = false;
|
||||||
|
emit loadingChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
http::client()->send_state_event(
|
||||||
|
room_id,
|
||||||
|
guest_access,
|
||||||
|
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
nhlog::net()->warn("failed to send m.room.guest_access: {} {}",
|
||||||
|
static_cast<int>(err->status_code),
|
||||||
|
err->matrix_error.error);
|
||||||
|
emit displayError(
|
||||||
|
QString::fromStdString(err->matrix_error.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading_ = false;
|
||||||
|
emit loadingChanged();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::stopLoading()
|
||||||
|
{
|
||||||
|
isLoading_ = false;
|
||||||
|
emit loadingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::avatarChanged()
|
||||||
|
{
|
||||||
|
retrieveRoomInfo();
|
||||||
|
emit avatarUrlChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RoomSettings::updateAvatar()
|
||||||
|
{
|
||||||
|
const QString picturesFolder =
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
||||||
|
const QString fileName = QFileDialog::getOpenFileName(
|
||||||
|
nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
|
||||||
|
|
||||||
|
if (fileName.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QMimeDatabase db;
|
||||||
|
QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
|
||||||
|
|
||||||
|
const auto format = mime.name().split("/")[0];
|
||||||
|
|
||||||
|
QFile file{fileName, this};
|
||||||
|
if (format != "image") {
|
||||||
|
emit displayError(tr("The selected file is not an image"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading_ = true;
|
||||||
|
emit loadingChanged();
|
||||||
|
|
||||||
|
// Events emitted from the http callbacks (different threads) will
|
||||||
|
// be queued back into the UI thread through this proxy object.
|
||||||
|
auto proxy = std::make_shared<ThreadProxy>();
|
||||||
|
connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError);
|
||||||
|
connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading);
|
||||||
|
|
||||||
|
const auto bin = file.peek(file.size());
|
||||||
|
const auto payload = std::string(bin.data(), bin.size());
|
||||||
|
const auto dimensions = QImageReader(&file).size();
|
||||||
|
|
||||||
|
// First we need to create a new mxc URI
|
||||||
|
// (i.e upload media to the Matrix content repository) for the new avatar.
|
||||||
|
http::client()->upload(
|
||||||
|
payload,
|
||||||
|
mime.name().toStdString(),
|
||||||
|
QFileInfo(fileName).fileName().toStdString(),
|
||||||
|
[proxy = std::move(proxy),
|
||||||
|
dimensions,
|
||||||
|
payload,
|
||||||
|
mimetype = mime.name().toStdString(),
|
||||||
|
size = payload.size(),
|
||||||
|
room_id = roomid_.toStdString(),
|
||||||
|
content = std::move(bin)](const mtx::responses::ContentURI &res,
|
||||||
|
mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
emit proxy->stopLoading();
|
||||||
|
emit proxy->error(
|
||||||
|
tr("Failed to upload image: %s")
|
||||||
|
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace mtx::events;
|
||||||
|
state::Avatar avatar_event;
|
||||||
|
avatar_event.image_info.w = dimensions.width();
|
||||||
|
avatar_event.image_info.h = dimensions.height();
|
||||||
|
avatar_event.image_info.mimetype = mimetype;
|
||||||
|
avatar_event.image_info.size = size;
|
||||||
|
avatar_event.url = res.content_uri;
|
||||||
|
|
||||||
|
http::client()->send_state_event(
|
||||||
|
room_id,
|
||||||
|
avatar_event,
|
||||||
|
[content = std::move(content), proxy = std::move(proxy)](
|
||||||
|
const mtx::responses::EventId &, mtx::http::RequestErr err) {
|
||||||
|
if (err) {
|
||||||
|
emit proxy->error(
|
||||||
|
tr("Failed to upload image: %s")
|
||||||
|
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit proxy->stopLoading();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
135
src/ui/RoomSettings.h
Normal file
135
src/ui/RoomSettings.h
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <mtx/events/guest_access.hpp>
|
||||||
|
|
||||||
|
#include "CacheStructs.h"
|
||||||
|
|
||||||
|
class TextField;
|
||||||
|
|
||||||
|
/// Convenience class which connects events emmited from threads
|
||||||
|
/// outside of main with the UI code.
|
||||||
|
class ThreadProxy : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void error(const QString &msg);
|
||||||
|
void nameEventSent(const QString &);
|
||||||
|
void topicEventSent(const QString &);
|
||||||
|
void stopLoading();
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditModal : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
EditModal(const QString &roomId, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setFields(const QString &roomName, const QString &roomTopic);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nameChanged(const QString &roomName);
|
||||||
|
void topicChanged(const QString &topic);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void topicEventSent(const QString &topic);
|
||||||
|
void nameEventSent(const QString &name);
|
||||||
|
void error(const QString &msg);
|
||||||
|
|
||||||
|
void applyClicked();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString roomId_;
|
||||||
|
QString initialName_;
|
||||||
|
QString initialTopic_;
|
||||||
|
|
||||||
|
QLabel *errorField_;
|
||||||
|
|
||||||
|
TextField *nameInput_;
|
||||||
|
TextField *topicInput_;
|
||||||
|
|
||||||
|
QPushButton *applyBtn_;
|
||||||
|
QPushButton *cancelBtn_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RoomSettings : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString roomId READ roomId CONSTANT)
|
||||||
|
Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT)
|
||||||
|
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
|
||||||
|
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
|
||||||
|
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged)
|
||||||
|
Q_PROPERTY(int memberCount READ memberCount CONSTANT)
|
||||||
|
Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged)
|
||||||
|
Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged)
|
||||||
|
Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
|
||||||
|
Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT)
|
||||||
|
Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT)
|
||||||
|
Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT)
|
||||||
|
Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged)
|
||||||
|
Q_PROPERTY(bool respondsToKeyRequests READ respondsToKeyRequests NOTIFY keyRequestsChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
RoomSettings(QString roomid, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString roomId() const;
|
||||||
|
QString roomName() const;
|
||||||
|
QString roomTopic() const;
|
||||||
|
QString roomVersion() const;
|
||||||
|
QString roomAvatarUrl();
|
||||||
|
int memberCount() const;
|
||||||
|
int notifications();
|
||||||
|
int accessJoinRules();
|
||||||
|
bool respondsToKeyRequests();
|
||||||
|
bool isLoading() const;
|
||||||
|
//! Whether the user has enough power level to send m.room.join_rules events.
|
||||||
|
bool canChangeJoinRules() const;
|
||||||
|
//! Whether the user has enough power level to send m.room.name & m.room.topic events.
|
||||||
|
bool canChangeNameAndTopic() const;
|
||||||
|
//! Whether the user has enough power level to send m.room.avatar event.
|
||||||
|
bool canChangeAvatar() const;
|
||||||
|
bool isEncryptionEnabled() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void enableEncryption();
|
||||||
|
Q_INVOKABLE void updateAvatar();
|
||||||
|
Q_INVOKABLE void openEditModal();
|
||||||
|
Q_INVOKABLE void changeAccessRules(int index);
|
||||||
|
Q_INVOKABLE void changeNotifications(int currentIndex);
|
||||||
|
Q_INVOKABLE void changeKeyRequestsPreference(bool isOn);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void loadingChanged();
|
||||||
|
void roomNameChanged();
|
||||||
|
void roomTopicChanged();
|
||||||
|
void avatarUrlChanged();
|
||||||
|
void encryptionChanged();
|
||||||
|
void keyRequestsChanged();
|
||||||
|
void notificationsChanged();
|
||||||
|
void accessJoinRulesChanged();
|
||||||
|
void displayError(const QString &errorMessage);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void stopLoading();
|
||||||
|
void avatarChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void retrieveRoomInfo();
|
||||||
|
void updateAccessRules(const std::string &room_id,
|
||||||
|
const mtx::events::state::JoinRules &,
|
||||||
|
const mtx::events::state::GuestAccess &);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString roomid_;
|
||||||
|
bool usesEncryption_ = false;
|
||||||
|
bool isLoading_ = false;
|
||||||
|
RoomInfo info_;
|
||||||
|
int notifications_ = 0;
|
||||||
|
int accessRules_ = 0;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user