mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
Merge remote-tracking branch 'upstream/master' into screenshare-x11
This commit is contained in:
commit
55fb00c67b
40 changed files with 2313 additions and 1175 deletions
|
@ -88,7 +88,7 @@ build-flatpak-amd64:
|
|||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||
tags: [docker]
|
||||
before_script:
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl
|
||||
- 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 --noninteractive install --user flathub org.kde.Platform//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 build-bundle repo nheko-amd64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
|
||||
after_script:
|
||||
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
|
||||
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-amd64.flatpak
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
|
@ -115,7 +116,7 @@ build-flatpak-arm64:
|
|||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||
tags: [docker-arm64]
|
||||
before_script:
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl
|
||||
- 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 --noninteractive install --user flathub org.kde.Platform//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 build-bundle repo nheko-arm64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
|
||||
after_script:
|
||||
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
|
||||
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-arm64.flatpak
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
|
|
|
@ -79,6 +79,7 @@ AppDir:
|
|||
- libxv1
|
||||
- libxxf86vm1
|
||||
- libzstd1
|
||||
- qml-module-qt-labs-platform
|
||||
- qml-module-qtgraphicaleffects
|
||||
- qml-module-qtmultimedia
|
||||
- qml-module-qtquick-controls2
|
||||
|
|
|
@ -257,7 +257,6 @@ set(SRC_FILES
|
|||
src/dialogs/PreviewUploadOverlay.cpp
|
||||
src/dialogs/ReCaptcha.cpp
|
||||
src/dialogs/ReadReceipts.cpp
|
||||
src/dialogs/RoomSettings.cpp
|
||||
|
||||
# Emoji
|
||||
src/emoji/EmojiModel.cpp
|
||||
|
@ -295,6 +294,7 @@ set(SRC_FILES
|
|||
src/ui/ThemeManager.cpp
|
||||
src/ui/ToggleButton.cpp
|
||||
src/ui/UserProfile.cpp
|
||||
src/ui/RoomSettings.cpp
|
||||
|
||||
src/AvatarProvider.cpp
|
||||
src/BlurhashProvider.cpp
|
||||
|
@ -326,6 +326,7 @@ set(SRC_FILES
|
|||
src/UserInfoWidget.cpp
|
||||
src/UserSettingsPage.cpp
|
||||
src/UsersModel.cpp
|
||||
src/RoomsModel.cpp
|
||||
src/Utils.cpp
|
||||
src/WebRTCSession.cpp
|
||||
src/WelcomePage.cpp
|
||||
|
@ -357,7 +358,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
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_TESTS OFF CACHE INTERNAL "")
|
||||
|
@ -474,7 +475,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/dialogs/RawMessage.h
|
||||
src/dialogs/ReCaptcha.h
|
||||
src/dialogs/ReadReceipts.h
|
||||
src/dialogs/RoomSettings.h
|
||||
|
||||
# Emoji
|
||||
src/emoji/EmojiModel.h
|
||||
|
@ -510,6 +510,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/ui/Theme.h
|
||||
src/ui/ThemeManager.h
|
||||
src/ui/UserProfile.h
|
||||
src/ui/RoomSettings.h
|
||||
|
||||
src/notifications/Manager.h
|
||||
|
||||
|
@ -538,6 +539,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/UserInfoWidget.h
|
||||
src/UserSettingsPage.h
|
||||
src/UsersModel.h
|
||||
src/RoomsModel.h
|
||||
src/WebRTCSession.h
|
||||
src/WelcomePage.h
|
||||
src/popups/PopupItem.h
|
||||
|
|
|
@ -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)
|
||||
[![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/)
|
||||
<a href='https://flatpak.neko.dev/repo/nightly/appstream/io.github.NhekoReborn.Nheko.flatpakref' download='nheko-nightly.flatpakref'><img alt='Download Nightly Flatpak' src='https://img.shields.io/badge/download-flatpak--nightly-green'/></a>
|
||||
[![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org)
|
||||
[![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko)
|
||||
<a href='https://flathub.org/apps/details/io.github.NhekoReborn.Nheko'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
|
||||
|
@ -49,7 +50,7 @@ Specifically there is support for:
|
|||
### Releases
|
||||
|
||||
Releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer)
|
||||
can be found in the [Github releases](https://github.com/Nheko-Reborn/nheko/releases).
|
||||
can be found in the [GitHub releases](https://github.com/Nheko-Reborn/nheko/releases).
|
||||
|
||||
### Repositories
|
||||
|
||||
|
@ -191,7 +192,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
|
|||
|
||||
```bash
|
||||
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
|
||||
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,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)
|
||||
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 \
|
||||
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
|
||||
qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev \
|
||||
qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts \
|
||||
qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform\
|
||||
qt5keychain-dev
|
||||
```
|
||||
|
||||
|
|
|
@ -220,7 +220,7 @@
|
|||
"name": "mtxclient",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "fee5298f068394958c2de935836a2c145f273906",
|
||||
"commit": "004d4203ceb441239aafb17e1340cd063139d029",
|
||||
"type": "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 QtQuick 2.9
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import im.nheko 1.0
|
||||
|
||||
Rectangle {
|
||||
id: inputBar
|
||||
|
||||
color: colors.window
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: textInput.height + 16
|
||||
Layout.preferredHeight: row.implicitHeight
|
||||
Layout.minimumHeight: 40
|
||||
|
||||
Component {
|
||||
|
@ -20,11 +22,9 @@ Rectangle {
|
|||
}
|
||||
|
||||
RowLayout {
|
||||
id: inputBar
|
||||
id: row
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 16
|
||||
|
||||
ImageButton {
|
||||
visible: CallManager.callsSupported
|
||||
|
@ -36,7 +36,7 @@ Rectangle {
|
|||
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
|
||||
Layout.leftMargin: 8
|
||||
Layout.margins: 8
|
||||
onClicked: {
|
||||
if (TimelineManager.timeline) {
|
||||
if (CallManager.haveCallInvite) {
|
||||
|
@ -57,7 +57,7 @@ Rectangle {
|
|||
width: 22
|
||||
height: 22
|
||||
image: ":/icons/icons/ui/paper-clip-outline.png"
|
||||
Layout.leftMargin: CallManager.callsSupported ? 0 : 8
|
||||
Layout.margins: 8
|
||||
onClicked: TimelineManager.timeline.input.openFileSelection()
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Send a file")
|
||||
|
@ -76,31 +76,13 @@ Rectangle {
|
|||
|
||||
}
|
||||
|
||||
Flickable {
|
||||
ScrollView {
|
||||
id: textInput
|
||||
|
||||
function ensureVisible(r) {
|
||||
if (contentX >= r.x)
|
||||
contentX = r.x;
|
||||
else if (contentX + width <= r.x + r.width)
|
||||
contentX = r.x + r.width - width;
|
||||
if (contentY >= r.y)
|
||||
contentY = r.y;
|
||||
else if (contentY + height <= r.y + r.height)
|
||||
contentY = r.y + r.height - height;
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
|
||||
Layout.maximumHeight: Window.height / 4
|
||||
Layout.minimumHeight: Settings.fontSize
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
implicitWidth: messageInput.width
|
||||
implicitHeight: messageInput.height
|
||||
contentWidth: messageInput.width
|
||||
contentHeight: messageInput.height
|
||||
implicitWidth: inputBar.width - 4 * (22 + 16) - 24
|
||||
|
||||
TextArea {
|
||||
id: messageInput
|
||||
|
@ -121,18 +103,11 @@ Rectangle {
|
|||
|
||||
selectByMouse: true
|
||||
placeholderText: qsTr("Write a message...")
|
||||
//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;
|
||||
|
||||
}
|
||||
placeholderTextColor: colors.buttonText
|
||||
color: colors.text
|
||||
width: textInput.width
|
||||
wrapMode: TextEdit.Wrap
|
||||
padding: 0
|
||||
padding: 8
|
||||
focus: true
|
||||
onTextChanged: {
|
||||
if (TimelineManager.timeline)
|
||||
|
@ -140,7 +115,6 @@ Rectangle {
|
|||
|
||||
forceActiveFocus();
|
||||
}
|
||||
onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle)
|
||||
onCursorPositionChanged: {
|
||||
if (!TimelineManager.timeline)
|
||||
return ;
|
||||
|
@ -182,6 +156,9 @@ Rectangle {
|
|||
} else if (event.key == Qt.Key_Colon) {
|
||||
messageInput.openCompleter(cursorPosition, "emoji");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_NumberSign) {
|
||||
messageInput.openCompleter(cursorPosition, "room");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_Escape && popup.opened) {
|
||||
completerTriggeredAt = -1;
|
||||
popup.completerName = "";
|
||||
|
@ -199,7 +176,6 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
TimelineManager.timeline.input.send();
|
||||
messageInput.clear();
|
||||
event.accepted = true;
|
||||
} else if (event.key == Qt.Key_Tab) {
|
||||
event.accepted = true;
|
||||
|
@ -231,6 +207,39 @@ Rectangle {
|
|||
} else if (event.key == Qt.Key_Down && popup.opened) {
|
||||
event.accepted = true;
|
||||
popup.down();
|
||||
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
|
||||
if (cursorPosition == 0) {
|
||||
event.accepted = true;
|
||||
var idx = TimelineManager.timeline.edit ? TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) + 1 : 0;
|
||||
while (true) {
|
||||
var id = TimelineManager.timeline.indexToId(idx);
|
||||
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) {
|
||||
TimelineManager.timeline.edit = id;
|
||||
cursorPosition = 0;
|
||||
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
|
||||
|
@ -292,15 +301,13 @@ Rectangle {
|
|||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
id: emojiButton
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.margins: 8
|
||||
hoverEnabled: true
|
||||
width: 22
|
||||
height: 22
|
||||
|
@ -315,6 +322,7 @@ Rectangle {
|
|||
|
||||
ImageButton {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.margins: 8
|
||||
hoverEnabled: true
|
||||
width: 22
|
||||
height: 22
|
||||
|
@ -324,7 +332,6 @@ Rectangle {
|
|||
ToolTip.text: qsTr("Send")
|
||||
onClicked: {
|
||||
TimelineManager.timeline.input.send();
|
||||
messageInput.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ ScrollView {
|
|||
ListView {
|
||||
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
|
||||
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 {
|
||||
id: mobileCallInviteDialog
|
||||
|
||||
|
@ -175,6 +183,16 @@ Page {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager.timeline
|
||||
onOpenRoomSettingsDialog: {
|
||||
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
|
||||
"roomSettings": settings
|
||||
});
|
||||
roomSettings.show();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CallManager
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
onClicked: TimelineManager.openRoomSettings()
|
||||
onClicked: TimelineManager.timeline.openRoomSettings()
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
|
@ -68,7 +68,7 @@ Rectangle {
|
|||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: TimelineManager.openRoomSettings()
|
||||
onClicked: TimelineManager.timeline.openRoomSettings()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ Rectangle {
|
|||
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onTriggered: TimelineManager.openRoomSettings()
|
||||
onTriggered: TimelineManager.timeline.openRoomSettings()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -129,6 +129,7 @@
|
|||
<file>qml/EncryptionIndicator.qml</file>
|
||||
<file>qml/ImageButton.qml</file>
|
||||
<file>qml/MatrixText.qml</file>
|
||||
<file>qml/ToggleButton.qml</file>
|
||||
<file>qml/MessageInput.qml</file>
|
||||
<file>qml/MessageView.qml</file>
|
||||
<file>qml/NhekoBusyIndicator.qml</file>
|
||||
|
@ -140,6 +141,7 @@
|
|||
<file>qml/TimelineRow.qml</file>
|
||||
<file>qml/TopBar.qml</file>
|
||||
<file>qml/TypingIndicator.qml</file>
|
||||
<file>qml/RoomSettings.qml</file>
|
||||
<file>qml/emoji/EmojiButton.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
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
|
||||
Cache::isHiddenEvent(lmdb::txn &txn,
|
||||
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);
|
||||
if (index == std::numeric_limits<uint64_t>::max()) {
|
||||
if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
|
||||
index = *indexVal.data<uint64_t>();
|
||||
index = to<uint64_t>(indexVal);
|
||||
} else {
|
||||
messages.end_of_cache = true;
|
||||
return messages;
|
||||
}
|
||||
} else {
|
||||
if (cursor.get(indexVal, event_id, MDB_SET)) {
|
||||
index = *indexVal.data<uint64_t>();
|
||||
index = to<uint64_t>(indexVal);
|
||||
} else {
|
||||
messages.end_of_cache = true;
|
||||
return messages;
|
||||
|
@ -1708,7 +1722,7 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
|
|||
cursor.close();
|
||||
|
||||
// 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;
|
||||
|
||||
return messages;
|
||||
|
@ -1861,12 +1875,12 @@ Cache::getTimelineRange(const std::string &room_id)
|
|||
}
|
||||
|
||||
TimelineRange range{};
|
||||
range.last = *indexVal.data<uint64_t>();
|
||||
range.last = to<uint64_t>(indexVal);
|
||||
|
||||
if (!cursor.get(indexVal, val, MDB_FIRST)) {
|
||||
return {};
|
||||
}
|
||||
range.first = *indexVal.data<uint64_t>();
|
||||
range.first = to<uint64_t>(indexVal);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
@ -1892,7 +1906,7 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
|
|||
return {};
|
||||
}
|
||||
|
||||
return *val.data<uint64_t>();
|
||||
return to<uint64_t>(val);
|
||||
}
|
||||
|
||||
std::optional<uint64_t>
|
||||
|
@ -1920,7 +1934,7 @@ Cache::getEventIndex(const std::string &room_id, std::string_view event_id)
|
|||
return {};
|
||||
}
|
||||
|
||||
return *val.data<uint64_t>();
|
||||
return to<uint64_t>(val);
|
||||
}
|
||||
|
||||
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) {
|
||||
return {};
|
||||
}
|
||||
uint64_t prevIdx = *indexVal.data<uint64_t>();
|
||||
uint64_t prevIdx = to<uint64_t>(indexVal);
|
||||
std::string prevId{eventIdVal.data(), eventIdVal.size()};
|
||||
|
||||
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)) {
|
||||
return std::pair{prevIdx, std::string(prevId)};
|
||||
} else {
|
||||
prevIdx = *indexVal.data<uint64_t>();
|
||||
prevIdx = to<uint64_t>(indexVal);
|
||||
prevId = std::move(evId);
|
||||
}
|
||||
}
|
||||
|
@ -1994,7 +2008,7 @@ Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id)
|
|||
return {};
|
||||
}
|
||||
|
||||
return *val.data<uint64_t>();
|
||||
return to<uint64_t>(val);
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
|
@ -2775,13 +2789,13 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
|
|||
uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
|
||||
auto cursor = lmdb::cursor::open(txn, orderDb);
|
||||
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;
|
||||
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
|
||||
if (msgCursor.get(indexVal, val, MDB_LAST)) {
|
||||
msgIndex = *indexVal.data<uint64_t>();
|
||||
msgIndex = to<uint64_t>(indexVal);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
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;
|
||||
if (cursor.get(indexVal, val, MDB_LAST)) {
|
||||
last = *indexVal.data<uint64_t>();
|
||||
last = to<uint64_t>(indexVal);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (cursor.get(indexVal, val, MDB_FIRST)) {
|
||||
first = *indexVal.data<uint64_t>();
|
||||
first = to<uint64_t>(indexVal);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -253,6 +253,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
|
||||
|
||||
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::highlightedNotifsRetrieved,
|
||||
|
@ -920,6 +921,13 @@ ChatPage::joinRoom(const QString &room)
|
|||
void
|
||||
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(
|
||||
room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
|
@ -960,8 +968,9 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
|
|||
return;
|
||||
}
|
||||
|
||||
emit showNotification(
|
||||
tr("Room %1 created.").arg(QString::fromStdString(res.room_id.to_string())));
|
||||
QString newRoomId = 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
|
||||
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;
|
||||
req.preset = mtx::requests::Preset::PrivateChat;
|
||||
req.visibility = mtx::common::RoomVisibility::Private;
|
||||
|
@ -1326,14 +1349,14 @@ mxidFromSegments(QStringRef sigil, QStringRef mxid)
|
|||
|
||||
auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
|
||||
|
||||
if (sigil == "user") {
|
||||
if (sigil == "u") {
|
||||
return "@" + mxid_;
|
||||
} else if (sigil == "roomid") {
|
||||
return "!" + mxid_;
|
||||
} else if (sigil == "room") {
|
||||
} else if (sigil == "r") {
|
||||
return "#" + mxid_;
|
||||
} else if (sigil == "group") {
|
||||
return "+" + mxid_;
|
||||
//} else if (sigil == "group") {
|
||||
// return "+" + mxid_;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
@ -1383,7 +1406,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||
}
|
||||
}
|
||||
|
||||
if (sigil1 == "user") {
|
||||
if (sigil1 == "u") {
|
||||
if (action.isEmpty()) {
|
||||
view_manager_->activeTimeline()->openUserProfile(mxid1);
|
||||
} else if (action == "chat") {
|
||||
|
@ -1400,10 +1423,10 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||
}
|
||||
}
|
||||
|
||||
if (action == "join") {
|
||||
if (action == "join" || action.isEmpty()) {
|
||||
joinRoomVia(targetRoomId, vias);
|
||||
}
|
||||
} else if (sigil1 == "room") {
|
||||
} else if (sigil1 == "r") {
|
||||
auto joined_rooms = cache::joinedRooms();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,6 +154,7 @@ signals:
|
|||
void tryInitialSyncCb();
|
||||
void newSyncResponse(const mtx::responses::Sync &res);
|
||||
void leftRoom(const QString &room_id);
|
||||
void newRoom(const QString &room_id);
|
||||
|
||||
void initializeRoomList(QMap<QString, RoomInfo>);
|
||||
void initializeViews(const mtx::responses::Rooms &rooms);
|
||||
|
@ -201,6 +202,7 @@ signals:
|
|||
private slots:
|
||||
void logout();
|
||||
void removeRoom(const QString &room_id);
|
||||
void changeRoom(const QString &room_id);
|
||||
void dropToLoginPage(const QString &msg);
|
||||
|
||||
void handleSyncResponse(const mtx::responses::Sync &res);
|
||||
|
|
|
@ -147,16 +147,23 @@ LoginPage::LoginPage(QWidget *parent)
|
|||
error_matrixid_label_->hide();
|
||||
|
||||
button_layout_ = new QHBoxLayout();
|
||||
button_layout_->setSpacing(0);
|
||||
button_layout_->setSpacing(20);
|
||||
button_layout_->setContentsMargins(0, 0, 0, 30);
|
||||
|
||||
login_button_ = new RaisedButton(tr("LOGIN"), this);
|
||||
login_button_->setMinimumSize(350, 65);
|
||||
login_button_->setMinimumSize(150, 65);
|
||||
login_button_->setFontSize(20);
|
||||
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_->addWidget(login_button_);
|
||||
button_layout_->addWidget(sso_login_button_);
|
||||
button_layout_->addStretch(1);
|
||||
|
||||
error_label_ = new QLabel(this);
|
||||
|
@ -179,7 +186,17 @@ LoginPage::LoginPage(QWidget *parent)
|
|||
this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
|
||||
|
||||
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(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
||||
connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
|
||||
|
@ -314,16 +331,19 @@ LoginPage::checkHomeserverVersion()
|
|||
http::client()->get_login(
|
||||
[this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
|
||||
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) {
|
||||
if (flow.type == mtx::user_interactive::auth_types::sso) {
|
||||
loginMethod_ = LoginMethod::SSO;
|
||||
break;
|
||||
ssoSupported_ = true;
|
||||
} 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
|
||||
LoginPage::versionOk(LoginMethod loginMethod_)
|
||||
LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_)
|
||||
{
|
||||
this->loginMethod = loginMethod_;
|
||||
passwordSupported = passwordSupported_;
|
||||
ssoSupported = ssoSupported_;
|
||||
|
||||
serverLayout_->removeWidget(spinner_);
|
||||
matrixidLayout_->removeWidget(spinner_);
|
||||
spinner_->stop();
|
||||
|
||||
if (loginMethod == LoginMethod::SSO) {
|
||||
password_input_->hide();
|
||||
login_button_->setText(tr("SSO LOGIN"));
|
||||
} else {
|
||||
password_input_->show();
|
||||
login_button_->setText(tr("LOGIN"));
|
||||
}
|
||||
sso_login_button_->setVisible(ssoSupported);
|
||||
login_button_->setVisible(passwordSupported);
|
||||
|
||||
if (serverInput_->isVisible())
|
||||
serverInput_->hide();
|
||||
}
|
||||
|
||||
void
|
||||
LoginPage::onLoginButtonClicked()
|
||||
LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
|
||||
{
|
||||
error_label_->setText("");
|
||||
|
||||
|
@ -411,8 +427,8 @@ LoginPage::onLoginButtonClicked()
|
|||
: deviceName_->text().toStdString(),
|
||||
[this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
showError(error_label_,
|
||||
QString::fromStdString(err->matrix_error.error));
|
||||
showErrorMessage(error_label_,
|
||||
QString::fromStdString(err->matrix_error.error));
|
||||
emit errorOccurred();
|
||||
return;
|
||||
}
|
||||
|
@ -437,7 +453,7 @@ LoginPage::onLoginButtonClicked()
|
|||
http::client()->login(
|
||||
req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
showError(
|
||||
showErrorMessage(
|
||||
error_label_,
|
||||
QString::fromStdString(err->matrix_error.error));
|
||||
emit errorOccurred();
|
||||
|
@ -456,7 +472,7 @@ LoginPage::onLoginButtonClicked()
|
|||
sso->deleteLater();
|
||||
});
|
||||
connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
|
||||
showError(error_label_, tr("SSO login failed"));
|
||||
showErrorMessage(error_label_, tr("SSO login failed"));
|
||||
emit errorOccurred();
|
||||
sso->deleteLater();
|
||||
});
|
||||
|
|
|
@ -56,9 +56,10 @@ signals:
|
|||
|
||||
//! Used to trigger the corresponding slot outside of the main thread.
|
||||
void versionErrorCb(const QString &err);
|
||||
void versionOkCb(LoginPage::LoginMethod method);
|
||||
void versionOkCb(bool passwordSupported, bool ssoSupported);
|
||||
|
||||
void loginOk(const mtx::responses::Login &res);
|
||||
void showErrorMessage(QLabel *label, const QString &msg);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
@ -73,7 +74,7 @@ private slots:
|
|||
void onBackButtonClicked();
|
||||
|
||||
// Callback for the login button.
|
||||
void onLoginButtonClicked();
|
||||
void onLoginButtonClicked(LoginMethod loginMethod);
|
||||
|
||||
// Callback for probing the server found in the mxid
|
||||
void onMatrixIdEntered();
|
||||
|
@ -84,7 +85,7 @@ private slots:
|
|||
// Callback for errors produced during server probing
|
||||
void versionError(const QString &error_message);
|
||||
// Callback for successful server probing
|
||||
void versionOk(LoginPage::LoginMethod method);
|
||||
void versionOk(bool passwordSupported, bool ssoSupported);
|
||||
|
||||
private:
|
||||
void checkHomeserverVersion();
|
||||
|
@ -120,7 +121,7 @@ private:
|
|||
QString inferredServerAddress_;
|
||||
|
||||
FlatButton *back_button_;
|
||||
RaisedButton *login_button_;
|
||||
RaisedButton *login_button_, *sso_login_button_;
|
||||
|
||||
QWidget *form_widget_;
|
||||
QHBoxLayout *form_wrapper_;
|
||||
|
@ -130,5 +131,6 @@ private:
|
|||
TextField *password_input_;
|
||||
TextField *deviceName_;
|
||||
TextField *serverInput_;
|
||||
LoginMethod loginMethod = LoginMethod::Password;
|
||||
bool passwordSupported = true;
|
||||
bool ssoSupported = false;
|
||||
};
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
#include "dialogs/Logout.h"
|
||||
#include "dialogs/MemberList.h"
|
||||
#include "dialogs/ReadReceipts.h"
|
||||
#include "dialogs/RoomSettings.h"
|
||||
|
||||
MainWindow *MainWindow::instance_ = nullptr;
|
||||
|
||||
|
@ -363,14 +362,6 @@ MainWindow::hasActiveUser()
|
|||
settings.contains(prefix + "auth/user_id");
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::openRoomSettings(const QString &room_id)
|
||||
{
|
||||
auto dialog = new dialogs::RoomSettings(room_id, this);
|
||||
|
||||
showDialog(dialog);
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::openMemberListDialog(const QString &room_id)
|
||||
{
|
||||
|
|
|
@ -54,7 +54,6 @@ class LeaveRoom;
|
|||
class Logout;
|
||||
class MemberList;
|
||||
class ReCaptcha;
|
||||
class RoomSettings;
|
||||
}
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
|
@ -78,7 +77,6 @@ public:
|
|||
std::function<void(const mtx::requests::CreateRoom &request)> callback);
|
||||
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
|
||||
void openLogoutDialog();
|
||||
void openRoomSettings(const QString &room_id);
|
||||
void openMemberListDialog(const QString &room_id);
|
||||
void openReadReceiptsDialog(const QString &event_id);
|
||||
|
||||
|
|
|
@ -277,6 +277,7 @@ RegisterPage::RegisterPage(QWidget *parent)
|
|||
if (!err) {
|
||||
http::client()->set_user(res.user_id);
|
||||
http::client()->set_access_token(res.access_token);
|
||||
http::client()->set_device_id(res.device_id);
|
||||
|
||||
emit registerOk();
|
||||
return;
|
||||
|
|
|
@ -217,4 +217,6 @@ private:
|
|||
|
||||
QColor bubbleBgColor_;
|
||||
QColor bubbleFgColor_;
|
||||
|
||||
friend struct room_sort;
|
||||
};
|
||||
|
|
|
@ -353,8 +353,8 @@ RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
|
|||
|
||||
struct room_sort
|
||||
{
|
||||
bool operator()(const QSharedPointer<RoomInfoListItem> a,
|
||||
const QSharedPointer<RoomInfoListItem> b) const
|
||||
bool operator()(const QSharedPointer<RoomInfoListItem> &a,
|
||||
const QSharedPointer<RoomInfoListItem> &b) const
|
||||
{
|
||||
// Sort by "importance" (i.e. invites before mentions before
|
||||
// notifs before new events before old events), then secondly
|
||||
|
@ -370,9 +370,9 @@ struct room_sort
|
|||
// Now sort by recency
|
||||
// Zero if empty, otherwise the time that the event occured
|
||||
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 =
|
||||
b->lastMessageInfo().userid.isEmpty() ? 0 : b->lastMessageInfo().timestamp;
|
||||
b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp;
|
||||
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");
|
||||
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
|
||||
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) {
|
||||
qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "MainWindow.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Olm.h"
|
||||
#include "RoomsModel.h"
|
||||
#include "TimelineModel.h"
|
||||
#include "TimelineViewManager.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
|
||||
InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_)
|
||||
{
|
||||
|
@ -186,6 +201,11 @@ InputBar::completerFor(QString completerName)
|
|||
auto proxy = new CompletionProxyModel(emojiModel);
|
||||
emojiModel->setParent(proxy);
|
||||
return proxy;
|
||||
} else if (completerName == "room") {
|
||||
auto roomModel = new RoomsModel(true);
|
||||
auto proxy = new CompletionProxyModel(roomModel);
|
||||
roomModel->setParent(proxy);
|
||||
return proxy;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -196,6 +216,10 @@ InputBar::send()
|
|||
if (text().trimmed().isEmpty())
|
||||
return;
|
||||
|
||||
nhlog::ui()->debug("Send: {}", text().toStdString());
|
||||
|
||||
auto wasEdit = !room->edit().isEmpty();
|
||||
|
||||
if (text().startsWith('/')) {
|
||||
int command_end = text().indexOf(' ');
|
||||
if (command_end == -1)
|
||||
|
@ -211,12 +235,10 @@ InputBar::send()
|
|||
message(text());
|
||||
}
|
||||
|
||||
nhlog::ui()->debug("Send: {}", text().toStdString());
|
||||
|
||||
if (history_.size() == INPUT_HISTORY_SIZE)
|
||||
history_.pop_back();
|
||||
history_.push_front("");
|
||||
history_index_ = 0;
|
||||
if (!wasEdit) {
|
||||
history_.push_front("");
|
||||
setText("");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -272,12 +294,10 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
|||
if (!room->reply().isEmpty()) {
|
||||
text.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||
room->resetReply();
|
||||
}
|
||||
|
||||
text.relations.relations.push_back(
|
||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||
room->resetEdit();
|
||||
|
||||
} else if (!room->reply().isEmpty()) {
|
||||
auto related = room->relatedInfo(room->reply());
|
||||
|
@ -307,7 +327,6 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
|||
|
||||
text.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, related.related_event});
|
||||
room->resetReply();
|
||||
}
|
||||
|
||||
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
|
||||
|
@ -330,12 +349,10 @@ InputBar::emote(QString msg)
|
|||
if (!room->reply().isEmpty()) {
|
||||
emote.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||
room->resetReply();
|
||||
}
|
||||
if (!room->edit().isEmpty()) {
|
||||
emote.relations.relations.push_back(
|
||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||
room->resetEdit();
|
||||
}
|
||||
|
||||
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
||||
|
@ -366,12 +383,10 @@ InputBar::image(const QString &filename,
|
|||
if (!room->reply().isEmpty()) {
|
||||
image.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||
room->resetReply();
|
||||
}
|
||||
if (!room->edit().isEmpty()) {
|
||||
image.relations.relations.push_back(
|
||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||
room->resetEdit();
|
||||
}
|
||||
|
||||
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
||||
|
@ -397,12 +412,10 @@ InputBar::file(const QString &filename,
|
|||
if (!room->reply().isEmpty()) {
|
||||
file.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||
room->resetReply();
|
||||
}
|
||||
if (!room->edit().isEmpty()) {
|
||||
file.relations.relations.push_back(
|
||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||
room->resetEdit();
|
||||
}
|
||||
|
||||
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
||||
|
@ -429,12 +442,10 @@ InputBar::audio(const QString &filename,
|
|||
if (!room->reply().isEmpty()) {
|
||||
audio.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||
room->resetReply();
|
||||
}
|
||||
if (!room->edit().isEmpty()) {
|
||||
audio.relations.relations.push_back(
|
||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||
room->resetEdit();
|
||||
}
|
||||
|
||||
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
||||
|
@ -460,12 +471,10 @@ InputBar::video(const QString &filename,
|
|||
if (!room->reply().isEmpty()) {
|
||||
video.relations.relations.push_back(
|
||||
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
||||
room->resetReply();
|
||||
}
|
||||
if (!room->edit().isEmpty()) {
|
||||
video.relations.relations.push_back(
|
||||
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
||||
room->resetEdit();
|
||||
}
|
||||
|
||||
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
|
||||
|
|
|
@ -41,7 +41,7 @@ public slots:
|
|||
QString text() const;
|
||||
QString previousText();
|
||||
QString nextText();
|
||||
void setText(QString newText) { emit textChanged(newText); }
|
||||
void setText(QString newText);
|
||||
|
||||
void send();
|
||||
void paste(bool fromMouse);
|
||||
|
|
|
@ -362,6 +362,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
|||
const static QRegularExpression replyFallback(
|
||||
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
|
||||
|
||||
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
|
||||
|
||||
bool isReply = relations(event).reply_to().has_value();
|
||||
|
||||
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_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
|
||||
formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
|
||||
// TODO(Nico): Don't parse html with a regex
|
||||
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(
|
||||
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)));
|
||||
m.insert(names[Id], data(event, static_cast<int>(Id)));
|
||||
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[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
|
||||
m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
|
||||
|
@ -753,11 +763,6 @@ TimelineModel::setCurrentIndex(int index)
|
|||
(!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
|
||||
readEvent(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);
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::openRoomSettings()
|
||||
{
|
||||
RoomSettings *settings = new RoomSettings(roomId(), this);
|
||||
connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged);
|
||||
openRoomSettingsDialog(settings);
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::replyAction(QString id)
|
||||
{
|
||||
|
@ -1539,6 +1552,17 @@ TimelineModel::setEdit(QString newEdit)
|
|||
if (edit_.startsWith('m'))
|
||||
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) {
|
||||
auto ev = events.get(newEdit.toStdString(), "");
|
||||
if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
|
||||
|
@ -1573,8 +1597,14 @@ TimelineModel::resetEdit()
|
|||
if (!edit_.isEmpty()) {
|
||||
edit_ = "";
|
||||
emit editChanged(edit_);
|
||||
input()->setText("");
|
||||
resetReply();
|
||||
nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString());
|
||||
input()->setText(textBeforeEdit);
|
||||
textBeforeEdit.clear();
|
||||
if (replyBeforeEdit.isEmpty())
|
||||
resetReply();
|
||||
else
|
||||
setReply(replyBeforeEdit);
|
||||
replyBeforeEdit.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "CacheCryptoStructs.h"
|
||||
#include "EventStore.h"
|
||||
#include "InputBar.h"
|
||||
#include "ui/RoomSettings.h"
|
||||
#include "ui/UserProfile.h"
|
||||
|
||||
namespace mtx::http {
|
||||
|
@ -216,6 +217,7 @@ public:
|
|||
Q_INVOKABLE void viewRawMessage(QString id) const;
|
||||
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
|
||||
Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
|
||||
Q_INVOKABLE void openRoomSettings();
|
||||
Q_INVOKABLE void editAction(QString id);
|
||||
Q_INVOKABLE void replyAction(QString id);
|
||||
Q_INVOKABLE void readReceiptsAction(QString id) const;
|
||||
|
@ -307,6 +309,7 @@ signals:
|
|||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||
|
||||
void openProfile(UserProfile *profile);
|
||||
void openRoomSettingsDialog(RoomSettings *settings);
|
||||
|
||||
void newMessageToSend(mtx::events::collections::TimelineEvents event);
|
||||
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
|
||||
|
@ -334,6 +337,7 @@ private:
|
|||
|
||||
QString currentId, currentReadId;
|
||||
QString reply_, edit_;
|
||||
QString textBeforeEdit, replyBeforeEdit;
|
||||
std::vector<QString> typingUsers_;
|
||||
|
||||
TimelineViewManager *manager_;
|
||||
|
@ -351,4 +355,6 @@ TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventTy
|
|||
msgCopy.content = content;
|
||||
msgCopy.type = eventType;
|
||||
emit newMessageToSend(msgCopy);
|
||||
resetReply();
|
||||
resetEdit();
|
||||
}
|
||||
|
|
|
@ -128,6 +128,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
|
|||
0,
|
||||
"UserProfileModel",
|
||||
"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;
|
||||
qmlRegisterSingletonType<MainWindow>(
|
||||
|
@ -387,11 +393,6 @@ TimelineViewManager::openLeaveRoomDialog() const
|
|||
{
|
||||
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
|
||||
}
|
||||
void
|
||||
TimelineViewManager::openRoomSettings() const
|
||||
{
|
||||
MainWindow::instance()->openRoomSettings(timeline_->roomId());
|
||||
}
|
||||
|
||||
void
|
||||
TimelineViewManager::verifyUser(QString userid)
|
||||
|
|
|
@ -70,7 +70,6 @@ public:
|
|||
Q_INVOKABLE void openInviteUsersDialog();
|
||||
Q_INVOKABLE void openMemberListDialog() const;
|
||||
Q_INVOKABLE void openLeaveRoomDialog() const;
|
||||
Q_INVOKABLE void openRoomSettings() const;
|
||||
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
|
||||
|
||||
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 a new issue