Compare commits
No commits in common. "main" and "10.rc" have entirely different histories.
@ -17,9 +17,9 @@ include:
|
||||
- local: .gitlab-ci/run_checks.yml
|
||||
- local: .gitlab-ci/build.yml
|
||||
- local: .gitlab-ci/test.yml
|
||||
- component: "gitlab.gnome.org/GNOME/citemplates/basic-deploy-docs@master"
|
||||
inputs:
|
||||
docs-job-name: "build-docs"
|
||||
- local: .gitlab-ci/publish_docs.yml
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
- local: .gitlab-ci/publish_nightly.yml
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
@ -1,8 +1,6 @@
|
||||
# Build the Flatpak
|
||||
|
||||
include:
|
||||
- project: "GNOME/citemplates"
|
||||
file: "flatpak/flatpak_ci_initiative.yml"
|
||||
include: 'https://gitlab.gnome.org/GNOME/citemplates/-/raw/master/flatpak/flatpak_ci_initiative.yml'
|
||||
|
||||
variables:
|
||||
RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo"
|
||||
@ -30,13 +28,10 @@ build@aarch64:
|
||||
|
||||
# Test builds with the stable runtime to make sure that the Flatpak will build on Flathub.
|
||||
# Should be run manually before tagging a new release.
|
||||
#
|
||||
# To get a list of available GNOME and LLVM versions, see:
|
||||
# https://gitlab.gnome.org/GNOME/gnome-runtime-images/-/blob/master/.gitlab-ci.yml
|
||||
.build-stable:
|
||||
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-${GNOME_STABLE_VERSION}'
|
||||
variables:
|
||||
GNOME_STABLE_VERSION: "48"
|
||||
GNOME_STABLE_VERSION: "47"
|
||||
LLVM_NIGHTLY_VERSION: "18"
|
||||
LLVM_STABLE_VERSION: "18"
|
||||
RUNTIME_REPO: "https://flathub.org/repo/flathub.flatpakrepo"
|
||||
|
17
.gitlab-ci/docs.yml
Normal file
@ -0,0 +1,17 @@
|
||||
.docs:
|
||||
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-master'
|
||||
tags:
|
||||
- flatpak
|
||||
script:
|
||||
- flatpak install --user --noninteractive org.freedesktop.Sdk.Extension.rust-nightly//24.08
|
||||
# We want to use rust-nightly to build the app.
|
||||
- sed -i 's|"org.freedesktop.Sdk.Extension.rust-stable"|"org.freedesktop.Sdk.Extension.rust-nightly"|g' ${MANIFEST_PATH}
|
||||
- sed -i 's|/rust-stable/bin|/rust-nightly/extra/sdk/rust-nightly/bin|g' ${MANIFEST_PATH}
|
||||
- flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse --stop-at=${FLATPAK_MODULE} flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH}
|
||||
- echo "ninja src/doc" | flatpak-builder --disable-rofiles-fuse --build-shell=${FLATPAK_MODULE} flatpak_app ${MANIFEST_PATH}
|
||||
- mv .flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/src/doc ${DOCS_FOLDER}
|
||||
- chmod -R a=rwx ${DOCS_FOLDER}
|
||||
dependencies: []
|
||||
artifacts:
|
||||
paths:
|
||||
- $DOCS_FOLDER
|
18
.gitlab-ci/publish_docs.yml
Normal file
@ -0,0 +1,18 @@
|
||||
# Build and publish the docs
|
||||
|
||||
include: '.gitlab-ci/docs.yml'
|
||||
|
||||
pages:
|
||||
extends:
|
||||
- .docs
|
||||
stage: deploy
|
||||
variables:
|
||||
DOCS_FOLDER: "public"
|
||||
rules:
|
||||
- changes:
|
||||
- src/**/*
|
||||
- Cargo.lock
|
||||
- Cargo.toml
|
||||
when: always
|
||||
- when: manual
|
||||
allow_failure: true
|
@ -1,8 +1,6 @@
|
||||
# Publish the nightly (Devel) version
|
||||
|
||||
include:
|
||||
- project: "GNOME/citemplates"
|
||||
file: "flatpak/flatpak_ci_initiative.yml"
|
||||
include: 'https://gitlab.gnome.org/GNOME/citemplates/-/raw/master/flatpak/flatpak_ci_initiative.yml'
|
||||
|
||||
publish_nightly@x86_64:
|
||||
extends: .publish_nightly
|
||||
|
@ -1,24 +1,18 @@
|
||||
# Configure and run code checks
|
||||
|
||||
include: '.gitlab-ci/utils.yml'
|
||||
|
||||
# Custom checks and lints
|
||||
checks:
|
||||
stage: check
|
||||
image: "rustlang/rust:nightly-slim"
|
||||
interruptible: true
|
||||
script:
|
||||
- hooks/checks.sh --verbose --force-install
|
||||
|
||||
# Lint the code
|
||||
cargo-clippy:
|
||||
extends:
|
||||
- .remove_build_only_modules
|
||||
stage: check
|
||||
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-master'
|
||||
tags:
|
||||
- flatpak
|
||||
interruptible: true
|
||||
script:
|
||||
- flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse --stop-at=${FLATPAK_MODULE} flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH}
|
||||
- echo "cargo clippy -- -D warnings" | flatpak-builder --disable-rofiles-fuse --build-shell=${FLATPAK_MODULE} flatpak_app ${MANIFEST_PATH}
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Tests after the app is built.
|
||||
|
||||
include: '.gitlab-ci/utils.yml'
|
||||
include: '.gitlab-ci/docs.yml'
|
||||
|
||||
# Validate the metainfo with Flathub's tool.
|
||||
lint-metainfo:
|
||||
@ -10,7 +10,6 @@ lint-metainfo:
|
||||
entrypoint: [""]
|
||||
variables:
|
||||
METAINFO: "${APP_ID}.metainfo.xml"
|
||||
interruptible: true
|
||||
script:
|
||||
# This tool has extra tests on top of appstreamcli and is required to pass for Flathub.
|
||||
- flatpak-builder-lint appstream ${METAINFO}
|
||||
@ -20,24 +19,17 @@ lint-metainfo:
|
||||
|
||||
# Run the Rust tests.
|
||||
rust-tests:
|
||||
extends:
|
||||
- .remove_build_only_modules
|
||||
stage: test
|
||||
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-master'
|
||||
tags:
|
||||
- flatpak
|
||||
interruptible: true
|
||||
variables:
|
||||
TMP_MANIFEST_PATH: "build-aux/org.gnome.Fractal.CiRust.json"
|
||||
script:
|
||||
# Create a temporary file.
|
||||
- TMP_FILE=$(mktemp)
|
||||
# Add a module for nextest to the Flatpak manifest and write it to the temporary file.
|
||||
- jq --slurpfile nextest .gitlab-ci/nextest.module.json '.modules = [$nextest[], .modules[]]' ${MANIFEST_PATH} > ${TMP_FILE}
|
||||
# Replace the manifest with the temporary file.
|
||||
- mv $TMP_FILE ${MANIFEST_PATH}
|
||||
# Initialize the Flatpak sandbox.
|
||||
- flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse --stop-at=${FLATPAK_MODULE} flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH}
|
||||
# Run the tests.
|
||||
- echo "cargo-nextest nextest run --config-file ../.gitlab-ci/nextest.toml" | flatpak-builder --disable-rofiles-fuse --build-shell=${FLATPAK_MODULE} flatpak_app ${MANIFEST_PATH}
|
||||
# Add a module for nextest to the Flatpak manifest
|
||||
- jq --slurpfile nextest .gitlab-ci/nextest.module.json '.modules = [$nextest[], .modules[]]' ${MANIFEST_PATH} > ${TMP_MANIFEST_PATH}
|
||||
- flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse --stop-at=${FLATPAK_MODULE} flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${TMP_MANIFEST_PATH}
|
||||
- echo "cargo-nextest nextest run --config-file ../.gitlab-ci/nextest.toml" | flatpak-builder --disable-rofiles-fuse --build-shell=${FLATPAK_MODULE} flatpak_app ${TMP_MANIFEST_PATH}
|
||||
dependencies: []
|
||||
artifacts:
|
||||
reports:
|
||||
@ -46,21 +38,9 @@ rust-tests:
|
||||
# Test that there are no errors in the docs.
|
||||
build-docs:
|
||||
extends:
|
||||
- .remove_build_only_modules
|
||||
- .docs
|
||||
stage: test
|
||||
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-master'
|
||||
tags:
|
||||
- flatpak
|
||||
interruptible: true
|
||||
script:
|
||||
- flatpak install --user --noninteractive org.freedesktop.Sdk.Extension.rust-nightly//24.08
|
||||
# We want to use the nightly toolchain inside the build terminal.
|
||||
- sed -i 's|"org.freedesktop.Sdk.Extension.rust-stable"|"org.freedesktop.Sdk.Extension.rust-nightly"|g' ${MANIFEST_PATH}
|
||||
- sed -i 's|/rust-stable/bin|/rust-nightly/extra/sdk/rust-nightly/bin|g' ${MANIFEST_PATH}
|
||||
- flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse --stop-at=${FLATPAK_MODULE} flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH}
|
||||
- echo "ninja src/doc" | flatpak-builder --disable-rofiles-fuse --build-shell=${FLATPAK_MODULE} flatpak_app ${MANIFEST_PATH}
|
||||
- tar --auto-compress --create --file "${CI_PROJECT_DIR}/${CI_PROJECT_NAME}-docs.tar.gz" --directory ".flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/src/doc" .
|
||||
dependencies: []
|
||||
artifacts:
|
||||
paths:
|
||||
- ${CI_PROJECT_NAME}-docs.tar.gz
|
||||
variables:
|
||||
DOCS_FOLDER: "doc"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH != "main"
|
||||
|
@ -1,16 +0,0 @@
|
||||
# Utilities to include in other jobs.
|
||||
|
||||
# Remove the Flatpak modules that are only necessary when building the app with meson.
|
||||
.remove_build_only_modules:
|
||||
variables:
|
||||
# JSON array of the names of the Flatpak modules to remove.
|
||||
MODULES_TO_REMOVE: '["grass", "glycin-loaders"]'
|
||||
before_script:
|
||||
# Create a temporary file.
|
||||
- TMP_FILE=$(mktemp)
|
||||
# Remove the modules in the manifest and write the output to the temporary file.
|
||||
- jq --argjson modules_to_remove "${MODULES_TO_REMOVE}" 'del(.modules[] | select(IN(.name; $modules_to_remove | .[])))' ${MANIFEST_PATH} > $TMP_FILE
|
||||
# Replace the manifest with the temporary file.
|
||||
- mv $TMP_FILE ${MANIFEST_PATH}
|
||||
# Use meson's build-env profile.
|
||||
- sed -i "s|-Dprofile=development|-Dprofile=build-env|g" ${MANIFEST_PATH}
|
@ -12,10 +12,9 @@ or videos showing the issue.
|
||||
## Information
|
||||
|
||||
* [ ] This bug is reproducible from the latest nightly build <!-- Check this box if the bug happens on Fractal's development version -->
|
||||
* [ ] This bug is reproducible with an [officially supported flatpak](https://gitlab.gnome.org/World/fractal#installation-instructions)
|
||||
<!-- ⚠️ Issue with third party packages (distribution repository, AUR, snap, Fedora flatpak…) should be reported to your distributor -->
|
||||
* **Fractal Version**: <!-- The version of Fractal you were using when the bug occurred. Check the "About Fractal" dialog for this information -->
|
||||
* **OS Version**: <!-- Operating system version, e.g. Fedora 36 -->
|
||||
* **Installation Source**: <!-- Where you installed Fractal from, e.g. Flathub, GNOME Apps Nightly, AUR, or distro repositories -->
|
||||
* **Homeserver**: <!-- The homeserver for your matrix account, e.g. matrix.org, gnome.org, … You can mention several of them if this is reproducible on multiple ones. -->
|
||||
|
||||
<!-- If you have error logs or a crash report, use the "Attach A File" button in the issue editor to attach it, or paste it in a code block below.
|
||||
|
@ -1,10 +1,7 @@
|
||||
<!-- Please note that some features missing in the stable release are already available in the
|
||||
development version. To avoid duplicates and unnecessary issues, please check that your request is
|
||||
for something that is not yet implemented, and doesn’t have an existing issue that is open or that
|
||||
was closed as out of scope.
|
||||
We also recommend talking to us in the #fractal:gnome.org Matrix room first. We do not intend to
|
||||
implement everything and don’t want our issue tracker to become a giant wishlist, but rather a
|
||||
curated list of known problems and planned features. -->
|
||||
was closed as out of scope. -->
|
||||
|
||||
Detailed description of the feature. Provide as much information as you can.
|
||||
|
||||
|
2345
Cargo.lock
generated
62
Cargo.toml
@ -1,14 +1,11 @@
|
||||
[package]
|
||||
name = "fractal"
|
||||
version = "11.2.0"
|
||||
version = "10.0.0-rc"
|
||||
authors = ["Julian Sparber <julian@sparber.net>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.82"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["serde_bytes"] # Used by the SecretFile API.
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
lto = "thin"
|
||||
@ -24,8 +21,7 @@ codegen-units = 16
|
||||
|
||||
# Please keep dependencies sorted.
|
||||
[dependencies]
|
||||
blurhash = "0.2"
|
||||
cfg-if = "1"
|
||||
async-once-cell = "0.5"
|
||||
diff = "0.1"
|
||||
djb_hash = "0.1"
|
||||
futures-channel = "0.3"
|
||||
@ -36,16 +32,16 @@ indexmap = "2"
|
||||
linkify = "0.10.0"
|
||||
mime = "0.3"
|
||||
mime_guess = "2"
|
||||
pulldown-cmark = "0.13"
|
||||
pulldown-cmark = "0.12"
|
||||
qrcode = { version = "0.14", default-features = false }
|
||||
rand = "0.9"
|
||||
rand = "0.8"
|
||||
regex = "1"
|
||||
rmp-serde = "1"
|
||||
rqrr = { version = "0.8", default-features = false }
|
||||
secular = { version = "1", features = ["bmp", "normalization"] }
|
||||
serde = "1"
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1"
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tld = "2"
|
||||
@ -55,14 +51,13 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
url = "2"
|
||||
webp = { version = "0.3", default-features = false }
|
||||
wtinylfu = "0.2"
|
||||
zeroize = "1"
|
||||
|
||||
# gtk-rs project and dependents. These usually need to be updated together.
|
||||
adw = { package = "libadwaita", version = "0.7", features = ["v1_7"] }
|
||||
glycin = { version = "2", default-features = false, features = ["tokio", "gdk4"] }
|
||||
adw = { package = "libadwaita", version = "0.7", features = ["v1_6"] }
|
||||
glycin = { version = "2.0.1", default-features = false, features = ["tokio", "gdk4"] }
|
||||
gst = { version = "0.23", package = "gstreamer" }
|
||||
gst_app = { version = "0.23", package = "gstreamer-app" }
|
||||
gst_base = { version = "0.23", package = "gstreamer-base" }
|
||||
gst_pbutils = { version = "0.23", package = "gstreamer-pbutils" }
|
||||
gst_play = { version = "0.23", package = "gstreamer-play" }
|
||||
gst_video = { version = "0.23", package = "gstreamer-video" }
|
||||
@ -71,25 +66,25 @@ shumate = { package = "libshumate", version = "0.6" }
|
||||
sourceview = { package = "sourceview5", version = "0.9" }
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
version = "0.12"
|
||||
# git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
# rev = "1348525447e99cb27ddca2e23885d9bab3837297"
|
||||
features = ["socks", "sso-login", "markdown", "qrcode"]
|
||||
|
||||
[dependencies.matrix-sdk-store-encryption]
|
||||
version = "0.12"
|
||||
# git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
# rev = "1348525447e99cb27ddca2e23885d9bab3837297"
|
||||
# version = "0.9"
|
||||
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
rev = "9514388108c7007cbdc822c582d26ad9e89af5d9"
|
||||
features = [
|
||||
"socks",
|
||||
"sso-login",
|
||||
"markdown",
|
||||
"qrcode",
|
||||
]
|
||||
|
||||
[dependencies.matrix-sdk-ui]
|
||||
version = "0.12"
|
||||
# git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
# rev = "1348525447e99cb27ddca2e23885d9bab3837297"
|
||||
# version = "0.9"
|
||||
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
rev = "9514388108c7007cbdc822c582d26ad9e89af5d9"
|
||||
|
||||
[dependencies.ruma]
|
||||
version = "0.12.3"
|
||||
# git = "https://github.com/ruma/ruma.git"
|
||||
# rev = "a8fd1b0322649bf59e2a5cfc73ab4fe46b21edd7"
|
||||
# version = "0.12"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "b266343136e8470a7d040efc207e16af0c20d374"
|
||||
features = [
|
||||
"unstable-unspecified",
|
||||
"client-api-c",
|
||||
@ -99,18 +94,19 @@ features = [
|
||||
"compat-null",
|
||||
"compat-optional",
|
||||
"compat-unset-avatar",
|
||||
"compat-get-3pids",
|
||||
"html-matrix",
|
||||
"unstable-msc3824",
|
||||
]
|
||||
|
||||
# Linux-only dependencies.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
aperture = "0.9"
|
||||
ashpd = { version = "0.11", default-features = false, features = [
|
||||
ashpd = { version = "0.10", default-features = false, features = [
|
||||
"pipewire",
|
||||
"tracing",
|
||||
"tokio",
|
||||
] }
|
||||
oo7 = { version = "0.4", default-features = false, features = [
|
||||
oo7 = { version = "0.3", default-features = false, features = [
|
||||
"openssl_crypto",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
@ -38,7 +38,7 @@ development version while keeping the stable release around for daily use.
|
||||
|
||||
### Stable version
|
||||
|
||||
The current stable version is 11.2 (released June 10th 2025).
|
||||
The current stable version is 9 (released October 30th 2024).
|
||||
|
||||
You can get the official Fractal Flatpak from Flathub.
|
||||
|
||||
@ -53,7 +53,7 @@ You can get the official Fractal Flatpak from Flathub.
|
||||
|
||||
### Beta version
|
||||
|
||||
The current beta version is 11.2 (same as stable).
|
||||
The current beta version is 10.rc (released January 14th 2025).
|
||||
|
||||
It is available as a Flatpak on Flathub Beta.
|
||||
|
||||
@ -154,10 +154,6 @@ following dependencies at runtime:
|
||||
* Camera: scan QR codes during verification.
|
||||
* Location: send the user’s location in a conversation.
|
||||
* Settings: get the 12h/24h time format system preference.
|
||||
* GStreamer plugins:
|
||||
* gst-plugin-gtk4 (gstgtk4): required to preview videos in the timeline and to present the output
|
||||
of the camera.
|
||||
* libgstpipewire with the `pipewiredeviceprovider`: used to list and access the cameras.
|
||||
* glycin: all images are loaded with this library so loaders for the different image formats need to
|
||||
be installed.
|
||||
|
||||
|
63
RELEASING.md
@ -16,7 +16,6 @@
|
||||
4. Create a release on GitLab for that tag.
|
||||
5. Make a fast-forward merge of the major version branch to `main`.
|
||||
6. [Publish the new version on Flathub and Flathub beta](#publishing-a-version-on-flathub).
|
||||
7. [Get the stable branch added to Damned Lies](#getting-a-branch-added-to-damned-lies).
|
||||
|
||||
## Making a new beta release
|
||||
|
||||
@ -49,11 +48,6 @@ Make a single release commit containing the following changes:
|
||||
- **stable.** remove all the `development` entries.
|
||||
- **stable.** update the paths of the screenshots to point to the major version branch.
|
||||
- **stable.** If there were visible changes in the UI, update the screenshots in `/screenshots`.
|
||||
They should follow [Flathub's quality guidelines](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots),
|
||||
with the following window sizes:
|
||||
- `main.png`: 760×550.
|
||||
- `adaptive.png`: 360×600.
|
||||
- `media-history.png`: 500×540.
|
||||
|
||||
A good practice in this merge request is to launch the `build-stable` CI jobs to make sure that
|
||||
Fractal builds with the stable Flatpak runtime.
|
||||
@ -75,42 +69,39 @@ You will be prompted for a tag message. This message doesn't really matter so so
|
||||
|
||||
Publishing a version of Fractal on Flathub is done via its [Flathub repository on GitHub](https://github.com/flathub/org.gnome.Fractal/).
|
||||
A permission from the Flathub team granted to your GitHub account is necessary to merge PRs on this
|
||||
repository, but anyone can open a PR.
|
||||
repository and interact with the buildbot, but anyone can open a PR.
|
||||
|
||||
1. Open a PR against the correct branch. For a stable build, work against the `master` branch, for a
|
||||
beta build, work against the `beta` branch.
|
||||
|
||||
It must contain a commit that updates the manifest to:
|
||||
|
||||
beta build, work against the `beta` branch. It must contain a commit that updates the manifest to:
|
||||
- Use the latest GNOME runtime.
|
||||
- Make sure that the Flatpak dependencies are the same as in the nightly manifest, and using the
|
||||
same version.
|
||||
- Build the latest version of Fractal, identified by its tag _and_ commit hash.
|
||||
2. When the PR is opened, a CI job will update the `cargo-sources.json` file with the latest Rust
|
||||
dependencies for Fractal and add a commit to the PR.
|
||||
3. The Flathub buildbot will likely launch 2 builds, one for when the PR was opened, and another one
|
||||
after the `cargo-sources.json` commit. You can stop any of those 2 builds, as long as the
|
||||
remaining one uses the latest commit as the `flathub_revision` in the `Build Properties` tab.
|
||||
4. Once the build succeeds, test the generated Flatpak as instructed and watch for obvious errors.
|
||||
If there are no issues, merge the PR.
|
||||
5. To watch the next steps, go to https://buildbot.flathub.org/. You should see a job for Fractal
|
||||
start soon, unless there is a queue in `Builds > Pending Buildrequests`. Open that job and wait
|
||||
for it to complete. When it is complete, there will be instructions again to test that build.
|
||||
6. If everything is fine with the build, click the `Publish` button to publish the build right away.
|
||||
If this is not done, the build will be published anyway after 3 to 4 hours. There can be some time
|
||||
before the publish job is complete and `flatpak update` offers to update the app.
|
||||
|
||||
If the list of Rust modules to build changes, the `MODULES` variable in the
|
||||
`update-cargo-sources.sh` script must also be updated.
|
||||
2. When the PR is opened, a CI job will update the `*-cargo-sources.json` files with the latest
|
||||
dependencies for the Rust modules and add a commit to the PR if necessary.
|
||||
3. The CI will trigger a test build automatically.
|
||||
## Launching a build manually for Flathub
|
||||
|
||||
If the build fails in CI and you want to trigger it again, post a comment saying `bot, build`.
|
||||
In most cases, this should not be necessary. Flathub launches builds automatically for PRs and after
|
||||
a PR is merged. If those builds fail, there is a `Rebuild` button to trigger a new build. However in
|
||||
some cases, if the previous builds are not available anymore, if is necessary to trigger a build
|
||||
manually:
|
||||
|
||||
If the build succeeds, test the generated Flatpak as instructed and watch for obvious errors. If
|
||||
there are no issues, merge the PR.
|
||||
4. Merging the PR will trigger an "official" build that will then be published on Flathub or Flathub
|
||||
beta within 1 to 2 hours. If this build fails, contact the Flathub admins to launch it again.
|
||||
|
||||
More details about these steps can be found in the Flathub docs about [maintenance](https://docs.flathub.org/docs/for-app-authors/maintenance)
|
||||
and [updates](https://docs.flathub.org/docs/for-app-authors/updates).
|
||||
|
||||
## Getting a branch added to Damned Lies
|
||||
|
||||
Damned Lies is the GNOME translation management platform. It provides translation workflows, but
|
||||
also statistics. Even though we don’t publish any release from stable branches after the initial
|
||||
one, we add them there so we can keep track of the evolution of translation coverage.
|
||||
|
||||
1. Go to https://l10n.gnome.org/module/fractal/ and log in.
|
||||
2. Click on the pencil icon next to the branch list.
|
||||
3. In the entry at the bottom, type in the name of the new branch, then click on the Save button.
|
||||
4. Assign the newly added branch to the “Other Apps (stable)” Release, unassign the previous one.
|
||||
5. Hit Save again for the assignments to take effect.
|
||||
1. Go to https://buildbot.flathub.org/ and log in.
|
||||
2. Click on `Start build`.
|
||||
3. Enter only the App ID. It is `org.gnome.Fractal` for the stable branch, and
|
||||
`org.gnome.Fractal/beta` for the beta branch.
|
||||
4. If you only want to trigger a test build, i.e. one that will not be published in the end, check
|
||||
the corresponding setting.
|
||||
5. Click on `Start build`.
|
||||
|
@ -23,7 +23,6 @@
|
||||
"append-ld-library-path": "/usr/lib/sdk/llvm18/lib",
|
||||
"append-path": "/usr/lib/sdk/llvm18/bin:/usr/lib/sdk/rust-stable/bin",
|
||||
"env": {
|
||||
"RUSTFLAGS": "-C force-frame-pointers=yes",
|
||||
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER": "clang",
|
||||
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold --cfg=ruma_identifiers_storage=\"Arc\"",
|
||||
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "clang",
|
||||
@ -49,6 +48,9 @@
|
||||
"cleanup-commands": [
|
||||
"mkdir -p ${FLATPAK_DEST}/lib/ffmpeg"
|
||||
],
|
||||
"cleanup": [
|
||||
"bin/grass"
|
||||
],
|
||||
"modules": [
|
||||
{
|
||||
"name": "grass",
|
||||
@ -63,14 +65,12 @@
|
||||
"mkdir -p /app/bin",
|
||||
"install -D ./target/release/grass /app/bin/"
|
||||
],
|
||||
"cleanup": ["*"],
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/connorskees/grass",
|
||||
"tag": "0.13.4",
|
||||
"commit": "e0bb9e2eabfc3a58e42b03089cd7b22c68d09d0b",
|
||||
"disable-submodules": true
|
||||
"commit": "e0bb9e2eabfc3a58e42b03089cd7b22c68d09d0b"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -84,8 +84,8 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/protobuf-c/protobuf-c.git",
|
||||
"tag": "v1.5.2",
|
||||
"commit": "4719fdd7760624388c2c5b9d6759eb6a47490626"
|
||||
"tag": "v1.5.0",
|
||||
"commit": "8c201f6e47a53feaab773922a743091eb6c8972a"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -102,8 +102,8 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.gnome.org/GNOME/libshumate.git",
|
||||
"tag": "1.4.0",
|
||||
"commit": "06021e35f0d479612fb1a3af91a73ba562175e03"
|
||||
"tag": "1.3.0",
|
||||
"commit": "e08d1442b80d0a352026505564e2cbe164b03997"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -122,9 +122,8 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.gnome.org/sophie-h/glycin.git",
|
||||
"tag": "1.2.1",
|
||||
"commit": "7d10c2f3e59e1d4dbc2a33241eb54aa4d9f84b3f",
|
||||
"disable-submodules": true
|
||||
"tag": "1.1.2",
|
||||
"commit": "ec00d78b3e4f48030011601fcf991d0749fa3dab"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -35,15 +35,15 @@
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-11/screenshots/main.png</image>
|
||||
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-9/screenshots/main.png</image>
|
||||
<caption>Fractal’s main window</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-11/screenshots/media-history.png</image>
|
||||
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-9/screenshots/media-history.png</image>
|
||||
<caption>View the media history of a Matrix room</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-11/screenshots/adaptive.png</image>
|
||||
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-9/screenshots/adaptive.png</image>
|
||||
<caption>Fractal’s interface adapts to small screens</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
@ -71,107 +71,69 @@
|
||||
</content_rating>
|
||||
|
||||
<releases>@development-release@
|
||||
<release version="11.2" type="stable" date="2025-06-10">
|
||||
<release version="10~rc" type="development" date="2025-01-14">
|
||||
<description>
|
||||
<p>
|
||||
This version updates the matrix-sdk-crypto dependency to include a fix for a high severity
|
||||
security issue.
|
||||
</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="11.1" type="stable" date="2025-05-15">
|
||||
<description>
|
||||
<p>
|
||||
Due to a pesky bug that makes Fractal crash when our users attempt to start a
|
||||
verification, we are releasing Fractal 11.1 only 2 weeks after Fractal 11. And while we’re
|
||||
at it we also backported a few fixes for smaller paper cuts!
|
||||
</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="11" type="stable" date="2025-05-01">
|
||||
<description>
|
||||
<p>
|
||||
A new version of Fractal numbered Eleven? Stranger things have happened… Features come
|
||||
running up that hill:
|
||||
In this cold weather, we hope this new release candidate will warm your hearts. Let’s
|
||||
celebrate this with our own awards ceremony:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Support for login using the OAuth 2.0 API (as used by matrix.org, which recently made
|
||||
the switch to Matrix Authentication Service)
|
||||
The most next-gen addition goes to… making Fractal OIDC aware. This ensures
|
||||
compatibility with the upcoming authentication changes for matrix.org.
|
||||
</li>
|
||||
<li>
|
||||
Overhaul of the page that lists user sessions, with details moved to subpages, for a
|
||||
less cluttered feel, and allowing to rename sessions!
|
||||
The most valuable fix goes to… showing consistently pills for users and rooms mentions
|
||||
in the right place instead of seemingly random places, getting rid of one of our oldest
|
||||
and most annoying bug.
|
||||
</li>
|
||||
<li>
|
||||
Rearranged account settings, with a new Safety tab that includes a setting to toggle
|
||||
media preview visibility
|
||||
The most sensible improvement goes to… using the send queue for attachments, ensuring
|
||||
correct order of all messages and improving the visual feedback.
|
||||
</li>
|
||||
<li>
|
||||
BlurHashes for images and videos, that are used as placeholders while the media is
|
||||
loading or if the preview is disabled
|
||||
The most underrated feature goes to… allowing to react to stickers, fixing a crash in
|
||||
the process.
|
||||
</li>
|
||||
<li>
|
||||
Contiguous state events are grouped behind a single item
|
||||
The most obvious tweak goes to… removing the “Open Direct Chat” menu entry from avatar
|
||||
menu and member profile in direct chats.
|
||||
</li>
|
||||
<li>
|
||||
The clearest enhancement goes to… labelling experimental versions in the room upgrade
|
||||
menu as such.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
As usual, this release includes other improvements and fixes thanks to all our
|
||||
contributors, and our upstream projects.
|
||||
As usual, this release includes other improvements, fixes and new translations thanks to
|
||||
all our contributors, and our upstream projects.
|
||||
</p>
|
||||
<p>
|
||||
We want to address special thanks to the translators who worked on this version. We know
|
||||
this is a huge undertaking and have a deep appreciation for what you’ve done. If you want
|
||||
to help with this effort, head over to l10n.gnome.org.
|
||||
As the version implies, it should be mostly stable and we expect to only include minor
|
||||
improvements until the release of Fractal 10.
|
||||
</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="10.1" type="stable" date="2025-02-10">
|
||||
<release version="10~beta" type="development" date="2024-12-26">
|
||||
<description>
|
||||
<p>
|
||||
Due to a couple of unfortunate but important regressions in Fractal 10, we are releasing
|
||||
Fractal 10.1 so our users don’t have to wait too long for them to be addressed. This minor
|
||||
version fixes the following issues:
|
||||
Vive le vent ! Vive le vent ! Vive le vent d’hiver ! 🎶🌲 And vive Fractal 10.beta!
|
||||
While everyone is resting for the holidays, we thought you could use a new release. It
|
||||
focuses on improvements and bug fixes, including:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Some rooms were stuck in an unread state, even after reading them or marking them as
|
||||
read.
|
||||
</li>
|
||||
<li>
|
||||
Joining or creating a room would crash the app.
|
||||
</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="10" type="stable" date="2025-01-30">
|
||||
<description>
|
||||
<p>
|
||||
How are you going to find your friends and coordinate end of day drinks when you’re lost
|
||||
in the middle of a large crowd in a big city? With the new version of your favorite Matrix
|
||||
client, of course! Here is Fractal 10.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The QR code scanning code has been ported to libaperture, the library behind GNOME
|
||||
Camera. This should result in better performance and more reliability.
|
||||
</li>
|
||||
<li>
|
||||
OAuth 2.0 compatibility was added, to make sure that we are ready for the upcoming
|
||||
authentication changes for matrix.org.
|
||||
</li>
|
||||
<li>
|
||||
Pills for users and rooms mentions show consistently in the right place instead of
|
||||
seemingly random places, getting rid of one of our oldest and most annoying bug.
|
||||
</li>
|
||||
<li>
|
||||
Attachments go through the send queue, ensuring correct order of all messages and
|
||||
improving the visual feedback.
|
||||
</li>
|
||||
<li>
|
||||
Videos were often not playing after loading in the room history. This was fixed, and we
|
||||
Videos were often not playing after loading in the room history, this was fixed, and we
|
||||
also show properly when an error occurred.
|
||||
</li>
|
||||
<li>
|
||||
Computing the size of media messages was slightly wrong, which meant that sometimes a
|
||||
grey line would appear below them. We rebooted the code and that problem is gone!
|
||||
</li>
|
||||
<li>
|
||||
Our CSS file was a bit too big for our taste, so we decided to make use of SASS and
|
||||
split it.
|
||||
</li>
|
||||
<li>
|
||||
We were downloading too many different sizes for avatar images, which would fill the
|
||||
media cache needlessly. We now only download a couple of sizes. This has the extra
|
||||
@ -183,9 +145,8 @@
|
||||
contributors, and our upstream projects.
|
||||
</p>
|
||||
<p>
|
||||
We want to address special thanks to the translators who worked on this version. We know
|
||||
this is a huge undertaking and have a deep appreciation for what you’ve done. If you want
|
||||
to help with this effort, head over to l10n.gnome.org.
|
||||
As the version implies, there might be a slight risk of regressions, but it should be
|
||||
mostly stable. If all goes well the next step is the release candidate!
|
||||
</p>
|
||||
</description>
|
||||
</release>
|
||||
|
@ -5,15 +5,47 @@
|
||||
viewBox="0 0 89.958331 52.916668"
|
||||
version="1.1"
|
||||
id="svg8662"
|
||||
sodipodi:docname="welcome-export.svg"
|
||||
inkscape:version="1.1-rc (52f87abb86, 2021-05-02)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview47"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
objecttolerance="10.0"
|
||||
gridtolerance="10.0"
|
||||
guidetolerance="10.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.99583586"
|
||||
inkscape:cx="303.76492"
|
||||
inkscape:cy="15.062723"
|
||||
inkscape:current-layer="svg8662"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-text-baseline="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid1470" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs8656">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39832">
|
||||
<stop
|
||||
style="stop-color:#3584e4;stop-opacity:1"
|
||||
@ -25,6 +57,7 @@
|
||||
id="stop39830" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient24559">
|
||||
<stop
|
||||
style="stop-color:#ed333b;stop-opacity:1;"
|
||||
@ -36,6 +69,7 @@
|
||||
id="stop24557" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient12858">
|
||||
<stop
|
||||
style="stop-color:#f66151;stop-opacity:1"
|
||||
@ -69,6 +103,7 @@
|
||||
id="stop8143" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient976"
|
||||
id="radialGradient1221"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -78,6 +113,7 @@
|
||||
fy="212"
|
||||
r="60" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient976">
|
||||
<stop
|
||||
style="stop-color:#f8e45c;stop-opacity:1"
|
||||
@ -89,6 +125,7 @@
|
||||
id="stop974" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1117"
|
||||
id="radialGradient1223"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -99,6 +136,7 @@
|
||||
fy="224"
|
||||
r="16" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1117">
|
||||
<stop
|
||||
style="stop-color:#5e5c64;stop-opacity:1"
|
||||
@ -110,6 +148,7 @@
|
||||
id="stop1115" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1117"
|
||||
id="radialGradient1225"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -120,6 +159,7 @@
|
||||
fy="224"
|
||||
r="16" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient1325">
|
||||
<stop
|
||||
style="stop-color:#f66151;stop-opacity:1"
|
||||
@ -135,6 +175,7 @@
|
||||
id="stop1323" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient976"
|
||||
id="radialGradient1393-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -145,6 +186,7 @@
|
||||
r="60"
|
||||
gradientTransform="matrix(0.26458333,0,0,0.26458333,10.972649,-67.464743)" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1325"
|
||||
id="radialGradient1395-6"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -155,6 +197,7 @@
|
||||
fy="29.856375"
|
||||
r="16.084499" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1325"
|
||||
id="radialGradient1397-9-9"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -165,6 +208,7 @@
|
||||
fy="29.856375"
|
||||
r="16.084499" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient12858"
|
||||
id="linearGradient12860"
|
||||
x1="3467.3748"
|
||||
@ -173,6 +217,7 @@
|
||||
y2="-383.00339"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient8145"
|
||||
id="linearGradient15076"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -182,6 +227,7 @@
|
||||
x2="-3272.5"
|
||||
y2="-438.48035" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39832"
|
||||
id="radialGradient39804"
|
||||
cx="3496.6987"
|
||||
@ -192,6 +238,7 @@
|
||||
gradientTransform="matrix(0.61314223,0,0,0.49927324,-2207.2336,232.19657)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient8161"
|
||||
id="linearGradient40220"
|
||||
x1="428.48035"
|
||||
@ -201,6 +248,7 @@
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.85000059,0,0,0.85000059,64.271801,-492.37307)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient24559"
|
||||
id="linearGradient63749"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
@ -289,10 +337,14 @@
|
||||
id="g137264"
|
||||
style="stroke-width:0.666667">
|
||||
<path
|
||||
sodipodi:nodetypes="sssscccsssss"
|
||||
style="display:inline;fill:#62a0ea;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
|
||||
d="m -3912.0003,-233.37879 c -4.4319,0 -8,3.56799 -8,8 v 27.89844 c 0,4.43202 3.5681,8 8,8 h 36.3333 l 12.6667,12.66667 v -12.66667 h 8.3333 c 4.432,0 8,-3.56798 8,-8 v -27.89844 c 0,-4.43201 -3.568,-8 -8,-8 z"
|
||||
id="path137260" />
|
||||
id="path137260"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="cssccccsccsccssc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path137262"
|
||||
d="m -3920.0003,-199.48035 v 2 c 0,4.43202 3.5681,8 8,8 h 36.3333 l 12.6667,12.66667 v -2 l -12.6667,-12.66667 h -36.3333 c -4.4319,0 -8,-3.56798 -8,-8 z m 73.3333,0 c 0,4.43202 -3.568,8 -8,8 h -8.3333 v 2 h 8.3333 c 4.432,0 8,-3.56798 8,-8 z"
|
||||
style="display:inline;fill:#3584e4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
|
||||
@ -322,13 +374,17 @@
|
||||
id="g137282"
|
||||
style="stroke-width:0.666667">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path137272"
|
||||
d="m -3913.6666,-217.48035 c -4.4319,0 -8,3.56798 -8,8 v 24 c 0,4.43202 3.5681,8 8,8 h 15.3333 v 12.66667 l 12.6667,-12.66667 h 45.9999 c 4.432,0 8,-3.56798 8,-8 v -24 c 0,-4.43202 -3.568,-8 -8,-8 z"
|
||||
style="display:inline;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
|
||||
style="display:inline;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
|
||||
sodipodi:nodetypes="sssscccsssss" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path137274"
|
||||
d="m -3921.6666,-187.48035 v 2 c 0,4.43202 3.5681,8 8,8 h 15.3333 v -2 h -15.3333 c -4.4319,0 -8,-3.56798 -8,-8 z m 89.9999,0 c 0,4.43202 -3.568,8 -8,8 h -45.9999 l -12.6667,12.66667 v 2 l 12.6667,-12.66667 h 45.9999 c 4.432,0 8,-3.56798 8,-8 z"
|
||||
style="display:inline;fill:#241f31;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
|
||||
style="display:inline;fill:#241f31;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
|
||||
sodipodi:nodetypes="cssccsccsccccssc" />
|
||||
<rect
|
||||
style="opacity:1;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:9.33333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect137276"
|
||||
@ -413,12 +469,13 @@
|
||||
ry="3.175" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:4.23333px;line-height:1.25;font-family:'Adwaita Sans';-inkscape-font-specification:'Adwaita Sans Ultra-Bold';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;stroke-width:0.264583"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.23333px;line-height:1.25;font-family:Cantarell;-inkscape-font-specification:'Cantarell Bold';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;stroke-width:0.264583"
|
||||
x="25.548162"
|
||||
y="7.6729164"
|
||||
id="text63745"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan63743"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:4.23333px;font-family:'Adwaita Sans';-inkscape-font-specification:'Adwaita Sans Ultra-Bold';fill:#ffffff;stroke-width:0.264583"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:4.23333px;font-family:Cantarell;-inkscape-font-specification:'Cantarell Ultra-Bold';fill:#ffffff;stroke-width:0.264583"
|
||||
x="25.548162"
|
||||
y="7.6729164">999+</tspan></text>
|
||||
</g>
|
||||
@ -494,12 +551,22 @@
|
||||
rx="2"
|
||||
ry="2" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="opacity:1;vector-effect:none;fill:#e5a50a;fill-opacity:1;stroke:none;stroke-width:12.8571;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 123.82422,231.53906 A 60,60 0 0 1 64,288 60,60 0 0 1 4.17578,232.46094 60,60 0 0 0 4,236 a 60,60 0 0 0 60,60 60,60 0 0 0 60,-60 60,60 0 0 0 -0.17578,-4.46094 z"
|
||||
id="path1193" />
|
||||
<path
|
||||
sodipodi:open="true"
|
||||
sodipodi:end="3.1415927"
|
||||
sodipodi:start="0"
|
||||
sodipodi:ry="7.0068064"
|
||||
sodipodi:rx="7.6309938"
|
||||
sodipodi:cy="236.99103"
|
||||
sodipodi:cx="64"
|
||||
sodipodi:type="arc"
|
||||
id="path1195"
|
||||
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#3d3846;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
sodipodi:arc-type="arc"
|
||||
d="m 71.630994,236.99103 a 7.6309938,7.0068064 0 0 1 -3.815497,6.06807 7.6309938,7.0068064 0 0 1 -7.630994,0 7.6309938,7.0068064 0 0 1 -3.815497,-6.06807" />
|
||||
<rect
|
||||
ry="8"
|
||||
@ -513,7 +580,16 @@
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#241f31;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path1199"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="64"
|
||||
sodipodi:cy="-216"
|
||||
sodipodi:rx="8"
|
||||
sodipodi:ry="8"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="3.1415927"
|
||||
sodipodi:open="true"
|
||||
transform="scale(1,-1)"
|
||||
sodipodi:arc-type="arc"
|
||||
d="m 72,-216 a 8,8 0 0 1 -4,6.9282 8,8 0 0 1 -8,0 A 8,8 0 0 1 56,-216" />
|
||||
<rect
|
||||
style="opacity:1;vector-effect:none;fill:#241f31;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
@ -564,13 +640,17 @@
|
||||
transform="matrix(1.5,0,0,1.5,2465.0001,-75.912199)"
|
||||
id="g137294">
|
||||
<path
|
||||
sodipodi:nodetypes="sssccssss"
|
||||
style="display:inline;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
|
||||
d="m -3905.3334,-234.37879 c -4.4319,0 -8,3.56799 -8,8 v 27.33333 c 0,4.43202 3.5681,8 8,8 H -3848 c 4.432,0 8,-3.56798 8,-8 v -27.33333 c 0,-4.43201 -3.568,-8 -8,-8 z"
|
||||
id="path137290" />
|
||||
id="path137290"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path137292"
|
||||
d="m -3913.3334,-201.04546 v 2 c 0,4.43202 3.5681,8 8,8 h 19 l 12.6667,12.66667 v -2 l -12.6667,-12.66667 h -19 c -4.4319,0 -8,-3.56798 -8,-8 z m 73.3334,0 c 0,4.43202 -3.568,8 -8,8 h -25.6667 v 2 H -3848 c 4.432,0 8,-3.56798 8,-8 z"
|
||||
style="display:inline;fill:#deddda;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
|
||||
style="display:inline;fill:#deddda;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
|
||||
sodipodi:nodetypes="cssccccsccsccssc" />
|
||||
</g>
|
||||
<rect
|
||||
y="-412.48035"
|
||||
@ -609,28 +689,42 @@
|
||||
cy="-5.023077"
|
||||
r="15.875" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="display:inline;fill:#e5a50a;fill-opacity:1;stroke:none;stroke-width:3.40177;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
|
||||
d="M 43.734473,-6.2033675 A 15.875,15.875 0 0 1 27.905982,8.735256 15.875,15.875 0 0 1 12.077491,-5.9594534 a 15.875,15.875 0 0 0 -0.04651,0.9363763 15.875,15.875 0 0 0 15.875,15.8750001 15.875,15.875 0 0 0 15.875,-15.8750001 15.875,15.875 0 0 0 -0.04651,-1.1802904 z"
|
||||
id="path1369-2" />
|
||||
<path
|
||||
sodipodi:open="true"
|
||||
sodipodi:end="3.1415927"
|
||||
sodipodi:start="0"
|
||||
sodipodi:ry="1.8538842"
|
||||
sodipodi:rx="2.0190337"
|
||||
sodipodi:cy="-4.7608676"
|
||||
sodipodi:cx="27.905983"
|
||||
sodipodi:type="arc"
|
||||
id="path1371-3"
|
||||
style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke:#3d3846;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
|
||||
sodipodi:arc-type="arc"
|
||||
d="m 29.925017,-4.7608676 a 2.0190337,1.8538842 0 0 1 -1.009517,1.6055108 2.0190337,1.8538842 0 0 1 -2.019034,0 2.0190337,1.8538842 0 0 1 -1.009517,-1.6055108" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1373-7"
|
||||
d="m 24.707728,-13.490777 a 2.1168783,2.1168783 0 0 0 -1.459859,0.642337 l -0.620118,0.620117 -0.620117,-0.620117 a 2.1168783,2.1168783 0 0 0 -1.51877,-0.641302 2.1168783,2.1168783 0 0 0 -1.474329,3.6349196 l 2.865975,2.8659745 v -0.00212 a 1.0583333,1.0583333 0 0 0 1.49655,0 l 2.863907,-2.8639082 A 2.1168783,2.1168783 0 0 0 24.707728,-13.49083 Z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient1395-6);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient1397-9-9);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 35.291109,-13.490777 a 2.1168783,2.1168783 0 0 0 -1.459859,0.642337 l -0.620118,0.620117 -0.620117,-0.620117 a 2.1168783,2.1168783 0 0 0 -1.51877,-0.641302 2.1168783,2.1168783 0 0 0 -1.474329,3.6349196 l 2.865975,2.8659745 v -0.00212 a 1.0583333,1.0583333 0 0 0 1.49655,0 l 2.863908,-2.8639082 a 2.1168783,2.1168783 0 0 0 -1.53324,-3.6359539 z"
|
||||
id="path1375-3-5" />
|
||||
<path
|
||||
id="path1377-9"
|
||||
d="m 18.412505,-11.77202 a 2.1168783,2.1168783 0 0 0 0.60203,1.9171976 l 2.865975,2.8659745 v -0.00212 a 1.0583333,1.0583333 0 0 0 1.49655,0 l 2.863907,-2.8639109 a 2.1168783,2.1168783 0 0 0 0.604615,-1.9166782 2.1168783,2.1168783 0 0 1 -0.604615,1.122929 l -2.863907,2.8639074 a 1.0583333,1.0583333 0 0 1 -1.49655,0 v 0.00212 l -2.865975,-2.8659714 a 2.1168783,2.1168783 0 0 1 -0.60203,-1.123448 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c01c28;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c01c28;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path1379-6-2"
|
||||
d="m 28.995838,-11.77202 a 2.1168783,2.1168783 0 0 0 0.60203,1.9171976 l 2.865975,2.8659745 v -0.00212 a 1.0583333,1.0583333 0 0 0 1.49655,0 l 2.863908,-2.8639109 a 2.1168783,2.1168783 0 0 0 0.604614,-1.9166782 2.1168783,2.1168783 0 0 1 -0.604614,1.122929 l -2.863908,2.8639074 a 1.0583333,1.0583333 0 0 1 -1.49655,0 v 0.00212 l -2.865975,-2.8659714 a 2.1168783,2.1168783 0 0 1 -0.60203,-1.123448 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c01c28;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c01c28;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 36 KiB |
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 13.980469 1.988281 c -0.261719 0.007813 -0.507813 0.117188 -0.6875 0.304688 l -0.984375 0.984375 c -1.285156 -0.828125 -2.78125 -1.273438 -4.308594 -1.277344 c -3.648438 0.003906 -6.832031 2.476562 -7.738281 6.011719 c 0.460937 1.746093 1.496093 3.285156 2.941406 4.371093 l -0.910156 0.910157 c -0.261719 0.25 -0.367188 0.625 -0.273438 0.972656 c 0.089844 0.351563 0.363281 0.625 0.714844 0.714844 c 0.347656 0.09375 0.722656 -0.011719 0.972656 -0.273438 l 11 -11 c 0.296875 -0.289062 0.382813 -0.726562 0.222657 -1.105469 c -0.160157 -0.382812 -0.539063 -0.625 -0.949219 -0.613281 z m -5.980469 2.011719 c 0.957031 0 1.886719 0.347656 2.609375 0.976562 l -1.417969 1.417969 c -0.34375 -0.257812 -0.761718 -0.394531 -1.191406 -0.394531 c -1.105469 0 -2 0.894531 -2 2 c 0 0.429688 0.140625 0.847656 0.394531 1.1875 l -1.417969 1.421875 c -0.628906 -0.726563 -0.972656 -1.652344 -0.976562 -2.609375 c 0 -2.210938 1.789062 -4 4 -4 z m 7.027344 2.207031 l -3.34375 3.34375 c -0.402344 0.960938 -1.167969 1.722657 -2.125 2.128907 l -2.28125 2.277343 c 0.242187 0.027344 0.480468 0.039063 0.722656 0.042969 c 3.648438 -0.003906 6.832031 -2.476562 7.738281 -6.011719 c -0.164062 -0.617187 -0.402343 -1.214843 -0.710937 -1.78125 z m -7.527344 0.792969 c 0.277344 0 0.5 0.222656 0.5 0.5 s -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 s 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#222222"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="8" x2="58" y1="69.999985" y2="69.999985">
|
||||
<stop offset="0" stop-color="#4aaac9"/>
|
||||
<stop offset="0.16" stop-color="#8bddf7"/>
|
||||
<stop offset="0.32" stop-color="#4aaac9"/>
|
||||
<stop offset="1" stop-color="#4aaac9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" gradientUnits="userSpaceOnUse" x1="31.462524" x2="39" y1="113.997253" y2="113.997253">
|
||||
<stop offset="0" stop-color="#4aaac9"/>
|
||||
<stop offset="0.469318" stop-color="#74d7f7"/>
|
||||
<stop offset="1" stop-color="#4aaac9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" gradientUnits="userSpaceOnUse" x1="104" x2="120" y1="84" y2="84">
|
||||
<stop offset="0" stop-color="#1a5fb4"/>
|
||||
<stop offset="0.5" stop-color="#4296ff"/>
|
||||
<stop offset="1" stop-color="#1a5fb4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="d">
|
||||
<path d="m 8 24 h 97 v 84 h -97 z m 0 0"/>
|
||||
</clipPath>
|
||||
<clipPath id="e">
|
||||
<path d="m 24 24 h 80 c 8.835938 0 16 7.164062 16 16 v 52 c 0 8.835938 -7.164062 16 -16 16 h -80 c -8.835938 0 -16 -7.164062 -16 -16 v -52 c 0 -8.835938 7.164062 -16 16 -16 z m 0 0"/>
|
||||
</clipPath>
|
||||
<linearGradient id="f" gradientUnits="userSpaceOnUse" x1="55.608135" x2="71.783539" y1="100" y2="48.532928">
|
||||
<stop offset="0" stop-color="#81dffe"/>
|
||||
<stop offset="1" stop-color="#9bf8fe"/>
|
||||
</linearGradient>
|
||||
<filter id="g" height="100%" width="100%" x="0%" y="0%">
|
||||
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="h">
|
||||
<g filter="url(#g)">
|
||||
<rect fill-opacity="0.35" height="128" width="128"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="i">
|
||||
<rect height="152" width="192"/>
|
||||
</clipPath>
|
||||
<path d="m 24 28 h 72 c 8.835938 0 16 7.164062 16 16 v 52 c 0 8.835938 -7.164062 16 -16 16 h -72 c -8.835938 0 -16 -7.164062 -16 -16 v -52 c 0 -8.835938 7.164062 -16 16 -16 z m 0 0" fill="url(#a)"/>
|
||||
<path d="m 24 28 h 80 c 8.835938 0 16 7.164062 16 16 v 48 c 0 8.835938 -7.164062 16 -16 16 h -80 c -8.835938 0 -16 -7.164062 -16 -16 v -48 c 0 -8.835938 7.164062 -16 16 -16 z m 0 0" fill="#53bde0"/>
|
||||
<path d="m 24 100 v 12 h 4 c 2.210938 0 4 1.789062 4 4 v 7 c 0 1.992188 1.183594 3.792969 3.011719 4.585938 c 1.828125 0.789062 3.953125 0.417968 5.40625 -0.945313 l 13.523437 -12.707031 c 1.324219 -1.242188 3.070313 -1.933594 4.882813 -1.933594 h 9.175781 v -12 z m 0 0" fill="url(#b)" fill-rule="evenodd"/>
|
||||
<path d="m 102 58.566406 h 2 c 8.835938 0 16 7.164063 16 16 v 21.433594 c 0 8.835938 -7.164062 16 -16 16 h -2 c -8.835938 0 -16 -7.164062 -16 -16 v -21.433594 c 0 -8.835937 7.164062 -16 16 -16 z m 0 0" fill="url(#c)"/>
|
||||
<path d="m 86 87 h 18 v 25 h -18 z m 0 0" fill="#1a5fb4"/>
|
||||
<path d="m 48 24 h 56 c 8.835938 0 16 7.164062 16 16 v 52 c 0 8.835938 -7.164062 16 -16 16 h -56 c -8.835938 0 -16 -7.164062 -16 -16 v -52 c 0 -8.835938 7.164062 -16 16 -16 z m 0 0" fill="#3584e4"/>
|
||||
<g clip-path="url(#d)">
|
||||
<g clip-path="url(#e)">
|
||||
<path d="m 78.804688 16.023438 l 0.527343 2.460937 c -1.207031 -0.082031 -2.417969 4.964844 -3.621093 4.988281 c -19.335938 0.371094 -38.003907 14.230469 -39.148438 34.546875 c -0.835938 14.761719 9.570312 29.839844 25.15625 30.488281 c 10.371094 0.433594 20.96875 -6.957031 21.242188 -17.925781 c 0.179687 -7.078125 -4.953126 -14.3125 -12.488282 -14.355469 c -4.683594 -0.027343 -9.484375 3.425782 -9.398437 8.429688 c 0.074219 2.980469 2.300781 6.042969 5.511719 5.902344 c 1.8125 -0.082032 3.691406 -1.488282 3.539062 -3.453125 c -0.078125 -1.042969 -0.921875 -2.128907 -2.0625 -1.996094 c -0.5625 0.066406 -1.148438 0.539063 -1.046875 1.15625 c 0.070313 0.273437 0.285156 0.570313 0.597656 0.5 c 0.121094 -0.03125 0.25 -0.144531 0.214844 -0.28125 c 0 -0.042969 -0.070313 -0.09375 -0.113281 -0.074219 c 0 0.003906 -0.070313 0.023438 0 0.035156 v 0.007813 v -0.003906 v 0.035156 c 0 0.050781 -0.09375 0.050781 -0.136719 0.03125 c -0.121094 -0.066406 -0.113281 -0.242187 -0.070313 -0.347656 c 0.164063 -0.265625 0.542969 -0.230469 0.777344 -0.074219 c 0.519532 0.367188 0.429688 1.117188 0.070313 1.558594 c -0.710938 0.898437 -2.074219 0.726562 -2.867188 0.042968 c -1.5 -1.28125 -1.167969 -3.601562 0.070313 -4.941406 c 2.167968 -2.367187 5.929687 -1.792968 8.074218 0.28125 c 3.601563 3.476563 2.652344 9.308594 -0.675781 12.597656 c -5.359375 5.292969 -14.109375 3.800782 -18.992187 -1.324218 c -7.570313 -7.953125 -5.304688 -20.664063 2.335937 -27.6875 c 11.480469 -10.550782 29.507813 -7.242188 39.363281 3.785156 c 14.414063 16.121094 9.6875 41.066406 -5.855468 54.582031 c -11.121094 9.226563 -22.246094 15.429688 -32.949219 19.4375 c -13.058594 75.445313 -75.230469 6.835938 -81.039063 -4.195312 l 0.285157 -105.054688 z m 0 0" fill="url(#f)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="m 24 106 v 2 h 4 c 2.210938 0 4 1.789062 4 4 v 7 c 0 1.992188 1.183594 3.792969 3.011719 4.585938 c 1.828125 0.789062 3.953125 0.417968 5.40625 -0.945313 l 13.523437 -12.707031 c 1.324219 -1.242188 3.070313 -1.933594 4.882813 -1.933594 h 9.175781 v -2 z m 0 0" fill="#81dffe" fill-rule="evenodd"/>
|
||||
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -8 -16)">
|
||||
<path d="m 173 17 h 8 c 1.65625 0 3 1.34375 3 3 v 7 c 0 1.65625 -1.34375 3 -3 3 h -8 c -1.65625 0 -3 -1.34375 -3 -3 v -7 c 0 -1.65625 1.34375 -3 3 -3 z m 0 0" fill="#241f31"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.5 KiB |
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 11.019531 7.996094 c 0 1.65625 -1.34375 3 -3 3 s -3 -1.34375 -3 -3 s 1.34375 -3 3 -3 s 3 1.34375 3 3 z m 0 0" fill="#222222"/></svg>
|
Before Width: | Height: | Size: 270 B |
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 8 1 c -1.65625 0 -3 1.34375 -3 3 s 1.34375 3 3 3 s 3 -1.34375 3 -3 s -1.34375 -3 -3 -3 z m -1.5 7 c -2.492188 0 -4.5 2.007812 -4.5 4.5 v 0.5 c 0 1.109375 0.890625 2 2 2 h 8 c 1.109375 0 2 -0.890625 2 -2 v -0.5 c 0 -2.492188 -2.007812 -4.5 -4.5 -4.5 z m 0 0" fill="#2e3436"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 424 B |
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 8 0 c 0.554688 0 1 0.445312 1 1 v 6.5 s 0 0.5 0.5 0.5 s 0.5 -0.5 0.5 -0.5 v -4.5 c 0 -0.554688 0.445312 -1 1 -1 s 1 0.445312 1 1 v 8.5 c 0 0.5 0.5 0.5 0.5 0.5 l 1.792969 -1.707031 c 0.1875 -0.195313 0.445312 -0.300781 0.71875 -0.304688 c 1.082031 0.085938 1.144531 1.269531 0.695312 1.71875 l -3 3 c -0.707031 0.792969 -1.757812 1.289063 -2.707031 1.292969 h -6 c -3 0 -3 -3 -3 -3 v -8 c 0 -0.554688 0.445312 -1 1 -1 s 1 0.445312 1 1 v 3.5 s 0 0.5 0.5 0.5 s 0.5 -0.5 0.5 -0.5 v -6.5 c 0 -0.554688 0.445312 -1 1 -1 s 1 0.445312 1 1 v 5.5 s 0 0.5 0.5 0.5 s 0.5 -0.5 0.5 -0.5 v -6.5 c 0 -0.554688 0.445312 -1 1 -1 z m 0 0" fill="#2e3436"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 786 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#bebebe" fill="#2e3436"><path d="M6 0a3 3 0 100 6 3 3 0 000-6zM4.5 7A4.49 4.49 0 000 11.5v.5c0 1 1 1 1 1h6V8.875c0-.83.587-1.554 1.355-1.79A4.532 4.532 0 007.5 7zM9 9v4h1V9z" style="marker:none" overflow="visible"/><path d="M8.875 8A.863.863 0 008 8.875v6.25c0 .492.383.875.875.875h6.25a.863.863 0 00.875-.875v-6.25A.863.863 0 0015.125 8zM11 9h2v1h-2zm0 2h2v4h-2z" style="marker:none" overflow="visible"/></g></svg>
|
Before Width: | Height: | Size: 488 B |
@ -18,7 +18,6 @@
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/go-bottom-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/go-next-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/go-previous-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/hide-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/idp-apple-dark.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/idp-apple.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/idp-facebook.svg</file>
|
||||
@ -43,16 +42,13 @@
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/settings-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/system-search-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/user-add-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/apps/org.gnome.Fractal.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/audio-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/blocked-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/checkmark-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/devices-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/document-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/done-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/dot-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/empty-page-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/encryption-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/error-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/explore-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/home-symbolic.svg</file>
|
||||
@ -60,12 +56,10 @@
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/key-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/no-camera-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/notifications-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/person-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/safety-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/security-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/sync-off-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/sync-on-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/sync-partial-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/user-info-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/users-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/verified-danger-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/status/verified-symbolic.svg</file>
|
||||
|
@ -115,7 +115,3 @@ scrolledwindow.card {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.padded-top-bar {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
@use 'vendor';
|
||||
|
||||
// Visual media history viewer
|
||||
visual-media-history-viewer {
|
||||
background: black;
|
||||
color: white;
|
||||
@ -18,7 +19,7 @@ visual-media-history-viewer {
|
||||
}
|
||||
|
||||
visual-media-history-viewer-item {
|
||||
background-color: var(--border-color);
|
||||
background: none;
|
||||
transition: vendor.$ease-out-quad;
|
||||
|
||||
&:hover, &:focus {
|
||||
@ -47,6 +48,7 @@ file-history-viewer, audio-history-viewer {
|
||||
}
|
||||
}
|
||||
|
||||
// Room details
|
||||
.room-details listview {
|
||||
background: transparent;
|
||||
}
|
||||
@ -107,11 +109,3 @@ permissions-member-row {
|
||||
background-color: vendor.$active_color;
|
||||
}
|
||||
}
|
||||
|
||||
.user-search-results {
|
||||
padding: 12px 0px;
|
||||
|
||||
> row {
|
||||
border-radius: vendor.$menu_radius;
|
||||
}
|
||||
}
|
||||
|
@ -3,40 +3,6 @@
|
||||
@use 'config';
|
||||
@use 'vendor';
|
||||
|
||||
%nested-effect {
|
||||
border-left: 2px solid var(--accent-bg-color);
|
||||
padding-left: 6px;
|
||||
opacity: if(config.$contrast == 'high', 90%, 70%);
|
||||
}
|
||||
|
||||
room-title {
|
||||
margin-top: -6px;
|
||||
margin-bottom: -6px;
|
||||
min-height: 12px;
|
||||
padding: 3px 0;
|
||||
|
||||
.title {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&.with-subtitle {
|
||||
button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.title, .subtitle {
|
||||
margin-top: -0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-history .room-history-list {
|
||||
padding-bottom: 0;
|
||||
|
||||
@ -47,7 +13,7 @@ room-title {
|
||||
}
|
||||
}
|
||||
|
||||
.room-history-row {
|
||||
room-history-row {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 8px;
|
||||
@ -56,12 +22,12 @@ room-title {
|
||||
|
||||
@include vendor.focus-ring();
|
||||
|
||||
&.has-avatar {
|
||||
&.has-header {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
&:not(.has-avatar) {
|
||||
.event-content {
|
||||
&:not(.has-header) {
|
||||
.event-content, message-reactions {
|
||||
&:dir(ltr) {
|
||||
margin-left: 54px;
|
||||
}
|
||||
@ -99,241 +65,30 @@ room-title {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender-avatar {
|
||||
padding: 5px;
|
||||
border-radius: 100%;
|
||||
|
||||
@include vendor.focus-ring();
|
||||
|
||||
&:hover {
|
||||
background-color: vendor.$hover_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.07) ;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: vendor.$active_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.16) ;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: vendor.$selected_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.1) ;
|
||||
}
|
||||
}
|
||||
|
||||
popover button.text-button {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.event-content {
|
||||
.h1 {
|
||||
font-weight: 800;
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-weight: 800;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
font-weight: 700;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.h4 {
|
||||
font-weight: 700;
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
.h5 {
|
||||
font-weight: 700;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.h6 {
|
||||
font-weight: 700;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
.emoji-message {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.emote {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.quote {
|
||||
@extend %nested-effect;
|
||||
}
|
||||
|
||||
expander-widget > box > {
|
||||
title {
|
||||
border-spacing: 6px;
|
||||
.event-content {
|
||||
.emoji {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
:not(title) {
|
||||
padding: 12px;
|
||||
.emote {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.codeview {
|
||||
border-radius: vendor.$menu_radius;
|
||||
padding: 6px;
|
||||
font-family: monospace;
|
||||
background-color: var(--text-view-bg);
|
||||
color: var(--view-fg-color);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
min-width: 36px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
state-group-row.room-history-row {
|
||||
&:not(.has-avatar) {
|
||||
.event-content {
|
||||
&:dir(ltr) {
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
margin-right: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expander-title {
|
||||
padding: 6px 12px;
|
||||
border-radius: vendor.$menu_radius;
|
||||
|
||||
&:hover {
|
||||
background-color: vendor.$button_hover_color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: vendor.$button_active_color;
|
||||
}
|
||||
}
|
||||
|
||||
image.arrow {
|
||||
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
&:not(:checked) image.arrow {
|
||||
&:dir(ltr) {
|
||||
transform: rotate(-0.5turn);
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
transform: rotate(0.5turn);
|
||||
}
|
||||
}
|
||||
|
||||
.expander-content {
|
||||
padding: 3px 6px;
|
||||
background-color: color-mix(in srgb, var(--view-fg-color) 4%, transparent);
|
||||
border-radius: vendor.$menu_radius;
|
||||
}
|
||||
|
||||
state-group-item-row {
|
||||
padding: 6px 12px;
|
||||
margin: 2px 0;
|
||||
border-radius: vendor.$menu_radius;
|
||||
|
||||
@include vendor.focus-ring();
|
||||
|
||||
&.has-open-popup {
|
||||
background-color: vendor.$hover_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message-visual-media {
|
||||
border-radius: vendor.$menu_radius;
|
||||
|
||||
@include vendor.focus-ring();
|
||||
}
|
||||
|
||||
.visual-content {
|
||||
&.opaque-bg {
|
||||
border-radius: vendor.$menu_radius;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
> .overlaid {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
> .instructions {
|
||||
padding: 12px;
|
||||
border-radius: vendor.$menu_radius;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
> .instructions {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from .osd button style in https://gitlab.gnome.org/GNOME/libadwaita/-/blob/main/src/stylesheet/widgets/_buttons.scss
|
||||
&.has-placeholder {
|
||||
> .instructions {
|
||||
color: vendor.$osd_fg_color;
|
||||
background-color: rgb(0 0 0 / 65%);
|
||||
|
||||
@if config.$contrast == 'high' {
|
||||
box-shadow: 0 0 0 1px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.compact) {
|
||||
&:hover {
|
||||
> .instructions {
|
||||
color: white;
|
||||
background-color: color-mix(in srgb, black calc(0.85 * 65%), currentColor calc(0.15 * 65%));
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
> .instructions {
|
||||
color: white;
|
||||
background-color: color-mix(in srgb, black calc(0.75 * 65%), currentColor calc(0.25 * 65%));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .spinner {
|
||||
.spinner {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
> button {
|
||||
// Leave enough space at the start to click to be able to view small images.
|
||||
&:dir(ltr) {
|
||||
margin-left: 64px;
|
||||
}
|
||||
&:dir(rtl) {
|
||||
margin-right: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
> image.osd.circular {
|
||||
image.osd.circular {
|
||||
min-width: 64px;
|
||||
min-height: 64px;
|
||||
border-radius: 32px;
|
||||
@ -346,12 +101,12 @@ message-visual-media {
|
||||
}
|
||||
|
||||
&.compact {
|
||||
> .spinner {
|
||||
.spinner {
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
> image.osd.circular {
|
||||
image.osd.circular {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
-gtk-icon-size: 16px;
|
||||
@ -359,14 +114,21 @@ message-visual-media {
|
||||
}
|
||||
}
|
||||
|
||||
message-reactions {
|
||||
flowboxchild {
|
||||
&:hover, &:active {
|
||||
// Cancel effect under .navigation-sidebar from libadwaita
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
room-history-row .event-content .quote,
|
||||
.related-event-content {
|
||||
border-left: 2px solid var(--accent-bg-color);
|
||||
padding-left: 6px;
|
||||
opacity: if(config.$contrast == 'high', 90%, 70%);;
|
||||
}
|
||||
|
||||
message-reactions flowboxchild {
|
||||
&:hover, &:active {
|
||||
// Cancel effect under .navigation-sidebar from libadwaita
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
message-reactions {
|
||||
&:dir(ltr) .toggle {
|
||||
padding: 1px 0 1px 6px;
|
||||
}
|
||||
@ -375,11 +137,11 @@ message-reactions {
|
||||
padding: 1px 6px 1px 0;
|
||||
}
|
||||
|
||||
.reaction-key-text {
|
||||
.reaction-key {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.reaction-key-emoji {
|
||||
.reaction-key.emoji {
|
||||
font-size: 1.1em;
|
||||
padding-right: 2px;
|
||||
padding-left: 2px;
|
||||
@ -443,9 +205,17 @@ divider-row {
|
||||
}
|
||||
}
|
||||
|
||||
typing-row {
|
||||
padding: 0 6px;
|
||||
min-height: 30px;
|
||||
.timestamp {
|
||||
min-width: 36px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.codeview {
|
||||
border-radius: vendor.$menu_radius;
|
||||
padding: 6px;
|
||||
font-family: monospace;
|
||||
background-color: var(--text-view-bg);
|
||||
color: var(--view-fg-color);
|
||||
}
|
||||
|
||||
.related-event-toolbar {
|
||||
@ -456,18 +226,125 @@ typing-row {
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.event-content {
|
||||
@extend %nested-effect;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
.related-event-content {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
typing-row {
|
||||
padding: 0 6px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
room-history-row, .related-event-content {
|
||||
.h1 {
|
||||
font-weight: 800;
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-weight: 800;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
font-weight: 700;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.h4 {
|
||||
font-weight: 700;
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
.h5 {
|
||||
font-weight: 700;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.h6 {
|
||||
font-weight: 700;
|
||||
font-size: 11pt;
|
||||
}
|
||||
}
|
||||
|
||||
room-history-row expander-widget > box > {
|
||||
title {
|
||||
border-spacing: 6px;
|
||||
}
|
||||
|
||||
:not(title) {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
room-title {
|
||||
margin-top: -6px;
|
||||
margin-bottom: -6px;
|
||||
min-height: 12px;
|
||||
padding: 3px 0;
|
||||
|
||||
.title {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&.with-subtitle {
|
||||
button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.title, .subtitle {
|
||||
margin-top: -0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender-avatar {
|
||||
padding: 3px;
|
||||
border-radius: 100%;
|
||||
|
||||
@include vendor.focus-ring();
|
||||
|
||||
&:hover {
|
||||
background-color: vendor.$hover_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.07) ;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: vendor.$active_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.16) ;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: vendor.$selected_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.1) ;
|
||||
}
|
||||
}
|
||||
|
||||
popover button.text-button {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
button.send-text-message-button image {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.composer-replacement {
|
||||
margin: 12px;
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ sidebar {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.dimmed, &.drop-empty .dimmed {
|
||||
.dim-label, &.drop-empty .dim-label {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -199,6 +199,14 @@ sidebar {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.invite-search-results {
|
||||
padding: 12px 0px;
|
||||
|
||||
> row {
|
||||
border-radius: vendor.$menu_radius;
|
||||
}
|
||||
}
|
||||
|
||||
// Event details dialog
|
||||
.event-details-dialog .sourceview {
|
||||
font-family: monospace;
|
||||
|
@ -10,7 +10,6 @@ $active_color: color-mix(in srgb, currentColor 16%, transparent);
|
||||
$selected_color: color-mix(in srgb, currentColor 10%, transparent);
|
||||
$selected_hover_color: color-mix(in srgb, currentColor 13%, transparent);
|
||||
$selected_active_color: color-mix(in srgb, currentColor 19%, transparent);
|
||||
$osd_fg_color: RGB(255 255 255 / 90%);
|
||||
|
||||
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.6.1/src/stylesheet/widgets/_buttons.scss
|
||||
$button_color: color-mix(in srgb, currentColor 10%, transparent);
|
||||
@ -22,8 +21,9 @@ $ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
$focus_transition: outline-color 200ms $ease-out-quad,
|
||||
outline-width 200ms $ease-out-quad,
|
||||
outline-offset 200ms $ease-out-quad;
|
||||
$card_radius: 12px;
|
||||
$menu_radius: 9px;
|
||||
$button_radius: 6px;
|
||||
$card_radius: $button_radius + 6;
|
||||
$menu_radius: 6px;
|
||||
|
||||
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.6.1/src/stylesheet/_drawing.scss
|
||||
@mixin focus-ring($target: null, $width: 2px, $offset: -$width, $outer: false, $focus-state: ':focus:focus-visible', $transition: null) {
|
||||
|
34
deny.toml
@ -1,34 +0,0 @@
|
||||
[advisories]
|
||||
yanked = "deny"
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste is unmaintained but used by various dependencies" },
|
||||
]
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
# To check if a license if compatible with our GPL v3.0 license, see: https://www.gnu.org/licenses/license-list.html
|
||||
# Keep list sorted alphabetically.
|
||||
[licenses]
|
||||
unused-allowed-license = "deny"
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"GPL-3.0",
|
||||
"LGPL-2.1",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[sources]
|
||||
required-git-spec="rev"
|
||||
allow-git = [
|
||||
"https://github.com/ruma/ruma",
|
||||
"https://github.com/matrix-org/matrix-rust-sdk",
|
||||
]
|
@ -121,12 +121,12 @@ check_cargo() {
|
||||
elif [ ! -t 1 ]; then
|
||||
exit 2
|
||||
elif check_rustup; then
|
||||
echo -e "$error rustup is installed but the cargo command isn’t available"
|
||||
echo -e "$error rustup is installed but the cargo command isn't available"
|
||||
exit 2
|
||||
else
|
||||
echo ""
|
||||
echo "y: Install cargo via rustup"
|
||||
echo "N: Don’t install cargo and abort checks"
|
||||
echo "N: Don't install cargo and abort checks"
|
||||
echo ""
|
||||
while true; do
|
||||
echo -n "Install cargo? [y/N]: "; read yn < /dev/tty
|
||||
@ -176,7 +176,7 @@ run_rustfmt() {
|
||||
echo "Rustfmt is needed to check Fractal’s code style, but it isn’t available"
|
||||
echo ""
|
||||
echo "y: Install rustfmt via rustup"
|
||||
echo "N: Don’t install rustfmt and abort checks"
|
||||
echo "N: Don't install rustfmt and abort checks"
|
||||
echo ""
|
||||
while true; do
|
||||
echo -n "Install rustfmt? [y/N]: "; read yn < /dev/tty
|
||||
@ -255,7 +255,7 @@ run_typos() {
|
||||
echo "Typos is needed to check spelling mistakes, but it isn’t available"
|
||||
echo ""
|
||||
echo "y: Install typos via cargo"
|
||||
echo "N: Don’t install typos and abort checks"
|
||||
echo "N: Don't install typos and abort checks"
|
||||
echo ""
|
||||
while true; do
|
||||
echo -n "Install typos? [y/N]: "; read yn < /dev/tty
|
||||
@ -316,7 +316,7 @@ run_machete() {
|
||||
echo "cargo-machete is needed to check for unused dependencies, but it isn’t available"
|
||||
echo ""
|
||||
echo "y: Install cargo-machete via cargo"
|
||||
echo "N: Don’t install cargo-machete and abort checks"
|
||||
echo "N: Don't install cargo-machete and abort checks"
|
||||
echo ""
|
||||
while true; do
|
||||
echo -n "Install cargo-machete? [y/N]: "; read yn < /dev/tty
|
||||
@ -353,65 +353,6 @@ run_machete() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Install cargo-deny with cargo.
|
||||
install_cargo_deny() {
|
||||
echo -e "$Installing cargo-deny…"
|
||||
cargo install cargo-deny
|
||||
if ! cargo deny --version>/dev/null 2>&1; then
|
||||
echo -e "$Could not install cargo-deny"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
# Run cargo-deny to check Rust dependencies.
|
||||
run_cargo_deny() {
|
||||
if ! cargo deny --version >/dev/null 2>&1; then
|
||||
if [[ $force_install -eq 1 ]]; then
|
||||
install_cargo_deny
|
||||
elif [ ! -t 1 ]; then
|
||||
echo "Could not check Rust dependencies, because cargo-deny could not be run"
|
||||
exit 2
|
||||
else
|
||||
echo "cargo-deny is needed to check the Rust dependencies, but it isn’t available"
|
||||
echo ""
|
||||
echo "y: Install cargo-deny via cargo"
|
||||
echo "N: Don’t install cargo-deny and abort checks"
|
||||
echo ""
|
||||
while true; do
|
||||
echo -n "Install cargo-deny? [y/N]: "; read yn < /dev/tty
|
||||
case $yn in
|
||||
[Yy]* )
|
||||
install_cargo_deny
|
||||
break
|
||||
;;
|
||||
[Nn]* | "" )
|
||||
exit 2
|
||||
;;
|
||||
* )
|
||||
echo $invalid
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "$Checking Rust dependencies…"
|
||||
|
||||
if [[ $verbose -eq 1 ]]; then
|
||||
echo ""
|
||||
cargo deny --version
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if ! cargo deny check; then
|
||||
echo -e " Checking Rust dependencies result: $fail"
|
||||
echo "Please fix the above issues, either by removing the dependencies, or by adding the necessary configuration option in deny.toml (see cargo-deny documentation)"
|
||||
exit 1
|
||||
else
|
||||
echo -e " Checking Rust dependencies result: $ok"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if files in POTFILES.in are correct.
|
||||
#
|
||||
# This checks, in that order:
|
||||
@ -638,7 +579,7 @@ run_cargo_sort() {
|
||||
echo "Cargo-sort is needed to check the sorting in Cargo.toml, but it isn’t available"
|
||||
echo ""
|
||||
echo "y: Install cargo-sort via cargo"
|
||||
echo "N: Don’t install cargo-sort and abort checks"
|
||||
echo "N: Don't install cargo-sort and abort checks"
|
||||
echo ""
|
||||
while true; do
|
||||
echo -n "Install cargo-sort? [y/N]: "; read yn < /dev/tty
|
||||
@ -715,8 +656,6 @@ run_typos
|
||||
echo ""
|
||||
run_machete
|
||||
echo ""
|
||||
run_cargo_deny
|
||||
echo ""
|
||||
check_potfiles
|
||||
echo ""
|
||||
if [[ $git_staged -eq 1 ]]; then
|
||||
|
29
meson.build
@ -1,6 +1,6 @@
|
||||
project('fractal',
|
||||
'rust',
|
||||
version: '11.2',
|
||||
version: '10.rc',
|
||||
license: 'GPL-3.0-or-later',
|
||||
meson_version: '>= 1.1')
|
||||
|
||||
@ -10,8 +10,8 @@ gnome = import('gnome')
|
||||
base_id = 'org.gnome.Fractal'
|
||||
application_id = base_id
|
||||
|
||||
major_version = '11.2'
|
||||
pre_release_version = ''
|
||||
major_version = '10'
|
||||
pre_release_version = 'rc'
|
||||
|
||||
version = major_version
|
||||
if pre_release_version != ''
|
||||
@ -22,9 +22,10 @@ full_version = version
|
||||
dependency('glib-2.0', version: '>= 2.76') # update when changing gtk version
|
||||
dependency('gio-2.0', version: '>= 2.76') # always same version as glib
|
||||
dependency('gtk4', version: '>= 4.16')
|
||||
dependency('libadwaita-1', version: '>= 1.7')
|
||||
dependency('libadwaita-1', version: '>= 1.6')
|
||||
|
||||
# Please keep these dependencies sorted.
|
||||
dependency('gstgtk4', version: '>= 0.13.0')
|
||||
dependency('gstreamer-1.0', version: '>= 1.20')
|
||||
dependency('gstreamer-app-1.0', version: '>= 1.20')
|
||||
dependency('gstreamer-base-1.0', version: '>= 1.20')
|
||||
@ -33,17 +34,17 @@ dependency('gstreamer-play-1.0', version: '>= 1.20')
|
||||
dependency('gstreamer-video-1.0', version: '>= 1.20')
|
||||
dependency('gtksourceview-5', version: '>= 5.0.0')
|
||||
dependency('libwebp', version: '>= 1.0.0')
|
||||
dependency('openssl', version: '>= 3.0.0')
|
||||
dependency('openssl', version: '>= 1.0.1')
|
||||
dependency('shumate-1.0', version: '>= 1.0.0')
|
||||
dependency('sqlite3', version: '>= 3.24.0')
|
||||
|
||||
# Required by glycin crate
|
||||
dependency('lcms2', version: '>=2.12.0')
|
||||
dependency('libseccomp', version: '>= 2.5.0')
|
||||
|
||||
# Linux-only dependencies
|
||||
if build_machine.system() == 'linux'
|
||||
# Required by glycin crate
|
||||
dependency('libseccomp', version: '>= 2.5.0')
|
||||
dependency('libpipewire-0.3', version: '>= 0.3.0')
|
||||
endif
|
||||
|
||||
glib_compile_resources = find_program('glib-compile-resources', required: true)
|
||||
@ -66,12 +67,7 @@ iconsdir = datadir / 'icons'
|
||||
podir = meson.project_source_root() / 'po'
|
||||
gettext_package = meson.project_name()
|
||||
|
||||
# When the `build-env` profile is used, we only want to set up the build
|
||||
# environment for the sandbox, we will not try to compile the app, so we can
|
||||
# remove some build steps.
|
||||
build_env_only = get_option('profile') == 'build-env'
|
||||
|
||||
if get_option('profile') == 'development' or build_env_only
|
||||
if get_option('profile') == 'development'
|
||||
profile = 'Devel'
|
||||
application_id += '.Devel'
|
||||
elif get_option('profile') == 'hack'
|
||||
@ -99,11 +95,8 @@ if profile == 'Devel'
|
||||
run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit')
|
||||
endif
|
||||
|
||||
if not build_env_only
|
||||
subdir('data')
|
||||
subdir('po')
|
||||
endif
|
||||
|
||||
subdir('data')
|
||||
subdir('po')
|
||||
subdir('src')
|
||||
|
||||
gnome.post_install(
|
||||
|
@ -6,7 +6,6 @@ option(
|
||||
'beta',
|
||||
'development',
|
||||
'hack',
|
||||
'build-env',
|
||||
],
|
||||
value: 'default',
|
||||
description: 'The build profile for Fractal. One of "default", "beta", "development" or "hack".'
|
||||
|
@ -41,5 +41,4 @@ sv
|
||||
th
|
||||
tr
|
||||
uk
|
||||
uz
|
||||
zh_CN
|
||||
|
@ -12,20 +12,15 @@ src/application.rs
|
||||
src/components/action_button.ui
|
||||
src/components/avatar/editable.rs
|
||||
src/components/avatar/editable.ui
|
||||
src/components/camera/qrcode_scanner.rs
|
||||
src/components/camera/qrcode_scanner.ui
|
||||
src/components/camera/viewfinder.rs
|
||||
src/components/crypto/identity_setup_view.rs
|
||||
src/components/crypto/identity_setup_view.ui
|
||||
src/components/crypto/recovery_setup_view.rs
|
||||
src/components/crypto/recovery_setup_view.ui
|
||||
src/components/dialogs/auth/in_browser_page.ui
|
||||
src/components/dialogs/auth/mod.rs
|
||||
src/components/dialogs/auth/mod.ui
|
||||
src/components/dialogs/auth/password_page.ui
|
||||
src/components/dialogs/auth.rs
|
||||
src/components/dialogs/auth.ui
|
||||
src/components/dialogs/join_room.rs
|
||||
src/components/dialogs/join_room.ui
|
||||
src/components/dialogs/message_dialogs.rs
|
||||
src/components/dialogs/room_preview.rs
|
||||
src/components/dialogs/room_preview.ui
|
||||
src/components/dialogs/user_profile.ui
|
||||
src/components/offline_banner.rs
|
||||
src/components/media/content_viewer.rs
|
||||
@ -35,6 +30,7 @@ src/components/power_level_selection/popover.ui
|
||||
src/components/rows/loading_row.ui
|
||||
src/components/user_page.rs
|
||||
src/components/user_page.ui
|
||||
src/contrib/qr_code_scanner/mod.ui
|
||||
src/contrib/qr_code.rs
|
||||
src/error_page.rs
|
||||
src/error_page.ui
|
||||
@ -48,7 +44,6 @@ src/identity_verification_view/completed_page.rs
|
||||
src/identity_verification_view/completed_page.ui
|
||||
src/identity_verification_view/confirm_qr_code_page.rs
|
||||
src/identity_verification_view/confirm_qr_code_page.ui
|
||||
src/identity_verification_view/mod.rs
|
||||
src/identity_verification_view/mod.ui
|
||||
src/identity_verification_view/no_supported_methods_page.rs
|
||||
src/identity_verification_view/no_supported_methods_page.ui
|
||||
@ -65,7 +60,6 @@ src/login/advanced_dialog.ui
|
||||
src/login/greeter.ui
|
||||
src/login/homeserver_page.rs
|
||||
src/login/homeserver_page.ui
|
||||
src/login/in_browser_page.rs
|
||||
src/login/in_browser_page.ui
|
||||
src/login/method_page.rs
|
||||
src/login/method_page.ui
|
||||
@ -82,11 +76,6 @@ src/session/model/room/permissions.rs
|
||||
src/session/model/room_list/mod.rs
|
||||
src/session/model/sidebar_data/section/name.rs
|
||||
src/session/model/sidebar_data/icon_item.rs
|
||||
src/session/model/user_sessions_list/user_session.rs
|
||||
src/session/view/account_settings/encryption_page/import_export_keys_subpage.rs
|
||||
src/session/view/account_settings/encryption_page/import_export_keys_subpage.ui
|
||||
src/session/view/account_settings/encryption_page/mod.rs
|
||||
src/session/view/account_settings/encryption_page/mod.ui
|
||||
src/session/view/account_settings/general_page/change_password_subpage.rs
|
||||
src/session/view/account_settings/general_page/change_password_subpage.ui
|
||||
src/session/view/account_settings/general_page/deactivate_account_subpage.rs
|
||||
@ -98,15 +87,16 @@ src/session/view/account_settings/general_page/mod.ui
|
||||
src/session/view/account_settings/mod.ui
|
||||
src/session/view/account_settings/notifications_page.rs
|
||||
src/session/view/account_settings/notifications_page.ui
|
||||
src/session/view/account_settings/safety_page/ignored_users_subpage/ignored_user_row.rs
|
||||
src/session/view/account_settings/safety_page/ignored_users_subpage/ignored_user_row.ui
|
||||
src/session/view/account_settings/safety_page/ignored_users_subpage/mod.ui
|
||||
src/session/view/account_settings/safety_page/mod.rs
|
||||
src/session/view/account_settings/safety_page/mod.ui
|
||||
src/session/view/account_settings/user_session/user_session_list_subpage.ui
|
||||
src/session/view/account_settings/user_session/user_session_row.ui
|
||||
src/session/view/account_settings/user_session/user_session_subpage.rs
|
||||
src/session/view/account_settings/user_session/user_session_subpage.ui
|
||||
src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs
|
||||
src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.ui
|
||||
src/session/view/account_settings/security_page/ignored_users_subpage/mod.ui
|
||||
src/session/view/account_settings/security_page/import_export_keys_subpage.rs
|
||||
src/session/view/account_settings/security_page/import_export_keys_subpage.ui
|
||||
src/session/view/account_settings/security_page/mod.rs
|
||||
src/session/view/account_settings/security_page/mod.ui
|
||||
src/session/view/account_settings/user_sessions_page/mod.ui
|
||||
src/session/view/account_settings/user_sessions_page/user_session_row.rs
|
||||
src/session/view/account_settings/user_sessions_page/user_session_row.ui
|
||||
src/session/view/content/explore/mod.ui
|
||||
src/session/view/content/explore/public_room_row.rs
|
||||
src/session/view/content/explore/servers_popover.ui
|
||||
@ -144,10 +134,9 @@ src/session/view/content/room_details/permissions/permissions_subpage.rs
|
||||
src/session/view/content/room_details/permissions/permissions_subpage.ui
|
||||
src/session/view/content/room_details/room_upgrade_dialog.rs
|
||||
src/session/view/content/room_history/divider_row.rs
|
||||
src/session/view/content/room_history/event_actions/context_menu.rs
|
||||
src/session/view/content/room_history/event_actions/context_menu.ui
|
||||
src/session/view/content/room_history/event_actions/group.rs
|
||||
src/session/view/content/room_history/event_actions/quick_reaction_chooser.ui
|
||||
src/session/view/content/room_history/event_context_menu.ui
|
||||
src/session/view/content/room_history/item_row.rs
|
||||
src/session/view/content/room_history/item_row_context_menu.rs
|
||||
src/session/view/content/room_history/message_row/audio.rs
|
||||
src/session/view/content/room_history/message_row/content.rs
|
||||
src/session/view/content/room_history/message_row/file.rs
|
||||
@ -161,7 +150,6 @@ src/session/view/content/room_history/message_row/reaction_list.ui
|
||||
src/session/view/content/room_history/message_row/reply.ui
|
||||
src/session/view/content/room_history/message_row/text/widgets.rs
|
||||
src/session/view/content/room_history/message_row/visual_media.rs
|
||||
src/session/view/content/room_history/message_row/visual_media.ui
|
||||
src/session/view/content/room_history/message_toolbar/attachment_dialog.ui
|
||||
src/session/view/content/room_history/message_toolbar/completion/completion_popover.rs
|
||||
src/session/view/content/room_history/message_toolbar/mod.rs
|
||||
@ -169,24 +157,26 @@ src/session/view/content/room_history/message_toolbar/mod.ui
|
||||
src/session/view/content/room_history/member_timestamp/row.rs
|
||||
src/session/view/content/room_history/mod.rs
|
||||
src/session/view/content/room_history/mod.ui
|
||||
src/session/view/content/room_history/quick_reaction_chooser.ui
|
||||
src/session/view/content/room_history/read_receipts_list/mod.rs
|
||||
src/session/view/content/room_history/sender_avatar/mod.rs
|
||||
src/session/view/content/room_history/sender_avatar/mod.ui
|
||||
src/session/view/content/room_history/state/content.rs
|
||||
src/session/view/content/room_history/state/creation.rs
|
||||
src/session/view/content/room_history/state/creation.ui
|
||||
src/session/view/content/room_history/state/group_row.rs
|
||||
src/session/view/content/room_history/state_row/creation.rs
|
||||
src/session/view/content/room_history/state_row/creation.ui
|
||||
src/session/view/content/room_history/state_row/mod.rs
|
||||
src/session/view/content/room_history/state_row/tombstone.rs
|
||||
src/session/view/content/room_history/state_row/tombstone.ui
|
||||
src/session/view/content/room_history/title.ui
|
||||
src/session/view/content/room_history/typing_row.rs
|
||||
src/session/view/content/room_history/verification_info_bar.rs
|
||||
src/session/view/create_direct_chat_dialog/mod.rs
|
||||
src/session/view/create_direct_chat_dialog/mod.ui
|
||||
src/session/view/create_room_dialog.rs
|
||||
src/session/view/create_room_dialog.ui
|
||||
src/session/view/create_dm_dialog/mod.rs
|
||||
src/session/view/create_dm_dialog/mod.ui
|
||||
src/session/view/event_details_dialog.rs
|
||||
src/session/view/event_details_dialog.ui
|
||||
src/session/view/media_viewer.rs
|
||||
src/session/view/media_viewer.ui
|
||||
src/session/view/room_creation.rs
|
||||
src/session/view/room_creation.ui
|
||||
src/session/view/sidebar/mod.rs
|
||||
src/session/view/sidebar/mod.ui
|
||||
src/session/view/sidebar/room_row.rs
|
||||
|
@ -1,4 +1,4 @@
|
||||
# These are files that we don't want to translate
|
||||
# Please keep this file sorted alphabetically.
|
||||
src/i18n.rs
|
||||
src/utils/toast.rs
|
||||
src/utils/macros.rs
|
||||
|
2695
po/pt_BR.po
2623
po/zh_CN.po
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 113 KiB |
@ -25,7 +25,7 @@
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="hexpand">True</property>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="dim-label"/>
|
||||
<class name="caption"/>
|
||||
</style>
|
||||
</object>
|
||||
|
@ -1,25 +1,30 @@
|
||||
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
use gtk::{
|
||||
glib::{self, clone, closure},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
CompositeTemplate,
|
||||
};
|
||||
|
||||
use super::AccountSwitcherPopover;
|
||||
use crate::{
|
||||
components::Avatar,
|
||||
session_list::SessionInfo,
|
||||
utils::{BoundObjectWeakRef, TemplateCallbacks},
|
||||
utils::{template_callbacks::TemplateCallbacks, BoundObjectWeakRef},
|
||||
Window,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/account_switcher/account_switcher_button.ui")]
|
||||
#[properties(wrapper_type = super::AccountSwitcherButton)]
|
||||
pub struct AccountSwitcherButton {
|
||||
/// The popover of this button.
|
||||
#[property(get, set = Self::set_popover, explicit_notify, nullable)]
|
||||
popover: BoundObjectWeakRef<AccountSwitcherPopover>,
|
||||
pub popover: BoundObjectWeakRef<AccountSwitcherPopover>,
|
||||
pub watch: RefCell<Option<gtk::ExpressionWatch>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -33,7 +38,6 @@ mod imp {
|
||||
SessionInfo::ensure_type();
|
||||
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
TemplateCallbacks::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
@ -42,85 +46,40 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for AccountSwitcherButton {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
obj.connect_toggled(|obj| {
|
||||
obj.handle_toggled();
|
||||
});
|
||||
|
||||
let watch = obj
|
||||
.property_expression("root")
|
||||
.chain_property::<Window>("session-selection")
|
||||
.chain_property::<gtk::SingleSelection>("n-items")
|
||||
.chain_closure::<bool>(closure!(|_: Option<glib::Object>, n_items: u32| {
|
||||
n_items > 0
|
||||
}))
|
||||
.bind(&*obj, "visible", glib::Object::NONE);
|
||||
self.watch.replace(Some(watch));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.reset();
|
||||
if let Some(watch) = self.watch.take() {
|
||||
watch.unwatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for AccountSwitcherButton {}
|
||||
impl ButtonImpl for AccountSwitcherButton {}
|
||||
impl ToggleButtonImpl for AccountSwitcherButton {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl AccountSwitcherButton {
|
||||
/// Set the popover of this button.
|
||||
fn set_popover(&self, popover: Option<&AccountSwitcherPopover>) {
|
||||
if self.popover.obj().as_ref() == popover {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the state.
|
||||
self.reset();
|
||||
let obj = self.obj();
|
||||
|
||||
if let Some(popover) = popover {
|
||||
// We need to remove the popover from the previous button, if any.
|
||||
if let Some(parent) = popover
|
||||
.parent()
|
||||
.and_downcast::<super::AccountSwitcherButton>()
|
||||
{
|
||||
parent.set_popover(None::<AccountSwitcherPopover>);
|
||||
}
|
||||
|
||||
let closed_handler = popover.connect_closed(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_| {
|
||||
obj.set_active(false);
|
||||
}
|
||||
));
|
||||
|
||||
popover.set_parent(&*obj);
|
||||
self.popover.set(popover, vec![closed_handler]);
|
||||
}
|
||||
|
||||
obj.notify_popover();
|
||||
}
|
||||
|
||||
/// Toggle the popover of this button.
|
||||
#[template_callback]
|
||||
fn toggle_popover(&self) {
|
||||
let obj = self.obj();
|
||||
|
||||
if obj.is_active() {
|
||||
let Some(window) = obj.root().and_downcast::<Window>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let popover = window.account_switcher();
|
||||
self.set_popover(Some(popover));
|
||||
|
||||
popover.popup();
|
||||
} else if let Some(popover) = self.popover.obj() {
|
||||
popover.popdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the state of this button.
|
||||
fn reset(&self) {
|
||||
if let Some(popover) = self.popover.obj() {
|
||||
popover.unparent();
|
||||
}
|
||||
self.popover.disconnect_signals();
|
||||
self.obj().set_active(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A button showing the currently selected session and opening the account switcher popover.
|
||||
/// A button showing the currently selected account and opening the account switcher popover.
|
||||
pub struct AccountSwitcherButton(ObjectSubclass<imp::AccountSwitcherButton>)
|
||||
@extends gtk::Widget, gtk::Button, gtk::ToggleButton, @implements gtk::Accessible;
|
||||
}
|
||||
@ -130,6 +89,60 @@ impl AccountSwitcherButton {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
pub fn popover(&self) -> Option<AccountSwitcherPopover> {
|
||||
self.imp().popover.obj()
|
||||
}
|
||||
|
||||
pub fn set_popover(&self, popover: Option<&AccountSwitcherPopover>) {
|
||||
let old_popover = self.popover();
|
||||
|
||||
if old_popover.as_ref() == popover {
|
||||
return;
|
||||
}
|
||||
|
||||
let imp = self.imp();
|
||||
|
||||
// Reset the state.
|
||||
if let Some(popover) = old_popover {
|
||||
popover.unparent();
|
||||
}
|
||||
imp.popover.disconnect_signals();
|
||||
self.set_active(false);
|
||||
|
||||
if let Some(popover) = popover {
|
||||
// We need to remove the popover from the previous button, if any.
|
||||
if let Some(parent) = popover.parent().and_downcast::<AccountSwitcherButton>() {
|
||||
parent.set_popover(None);
|
||||
}
|
||||
|
||||
let closed_handler = popover.connect_closed(clone!(
|
||||
#[weak(rename_to = obj)]
|
||||
self,
|
||||
move |_| {
|
||||
obj.set_active(false);
|
||||
}
|
||||
));
|
||||
|
||||
popover.set_parent(self);
|
||||
imp.popover.set(popover, vec![closed_handler]);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_toggled(&self) {
|
||||
if self.is_active() {
|
||||
let Some(window) = self.root().and_downcast::<Window>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let popover = window.account_switcher();
|
||||
self.set_popover(Some(popover));
|
||||
|
||||
popover.popup();
|
||||
} else if let Some(popover) = self.popover() {
|
||||
popover.popdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AccountSwitcherButton {
|
||||
|
@ -1,18 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="AccountSwitcherButton" parent="GtkToggleButton">
|
||||
<signal name="toggled" handler="toggle_popover" swapped="yes"/>
|
||||
<binding name="visible">
|
||||
<closure type="gboolean" function="invert_boolean">
|
||||
<closure type="gboolean" function="guint_is_zero">
|
||||
<lookup name="n-items" type="GtkSingleSelection">
|
||||
<lookup name="session-selection" type="Window">
|
||||
<lookup name="root">AccountSwitcherButton</lookup>
|
||||
</lookup>
|
||||
</lookup>
|
||||
</closure>
|
||||
</closure>
|
||||
</binding>
|
||||
<binding name="tooltip-text">
|
||||
<lookup name="user-id-string" type="SessionInfo">
|
||||
<lookup name="selected-item" type="GtkSingleSelection">
|
||||
@ -28,7 +16,6 @@
|
||||
</accessibility>
|
||||
<style>
|
||||
<class name="image-button"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="Avatar">
|
||||
|
@ -18,12 +18,12 @@ mod imp {
|
||||
#[properties(wrapper_type = super::AccountSwitcherPopover)]
|
||||
pub struct AccountSwitcherPopover {
|
||||
#[template_child]
|
||||
sessions: TemplateChild<gtk::ListBox>,
|
||||
pub sessions: TemplateChild<gtk::ListBox>,
|
||||
/// The model containing the logged-in sessions selection.
|
||||
#[property(get, set = Self::set_session_selection, explicit_notify, nullable)]
|
||||
session_selection: BoundObjectWeakRef<gtk::SingleSelection>,
|
||||
pub session_selection: BoundObjectWeakRef<gtk::SingleSelection>,
|
||||
/// The selected row.
|
||||
selected_row: glib::WeakRef<SessionItemRow>,
|
||||
pub selected_row: glib::WeakRef<SessionItemRow>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -34,7 +34,7 @@ mod imp {
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
Self::Type::bind_template_callbacks(klass);
|
||||
|
||||
klass.install_action("account-switcher.close", None, |obj, _, _| {
|
||||
obj.popdown();
|
||||
@ -52,94 +52,97 @@ mod imp {
|
||||
impl WidgetImpl for AccountSwitcherPopover {}
|
||||
impl PopoverImpl for AccountSwitcherPopover {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl AccountSwitcherPopover {
|
||||
/// Set the model containing the logged-in sessions selection.
|
||||
fn set_session_selection(&self, selection: Option<>k::SingleSelection>) {
|
||||
if selection == self.session_selection.obj().as_ref() {
|
||||
return;
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
self.session_selection.disconnect_signals();
|
||||
|
||||
self.sessions.bind_model(selection, |session| {
|
||||
let row = SessionItemRow::new(
|
||||
session
|
||||
.downcast_ref()
|
||||
.expect("sessions list box item should be a Session"),
|
||||
);
|
||||
let row = SessionItemRow::new(session.downcast_ref().unwrap());
|
||||
row.upcast()
|
||||
});
|
||||
|
||||
if let Some(selection) = selection {
|
||||
let selected_handler = selection.connect_selected_item_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
#[weak]
|
||||
obj,
|
||||
move |selection| {
|
||||
imp.update_selected_item(selection.selected());
|
||||
obj.update_selected_item(selection.selected());
|
||||
}
|
||||
));
|
||||
self.update_selected_item(selection.selected());
|
||||
obj.update_selected_item(selection.selected());
|
||||
|
||||
self.session_selection
|
||||
.set(selection, vec![selected_handler]);
|
||||
}
|
||||
|
||||
self.obj().notify_session_selection();
|
||||
}
|
||||
|
||||
/// Select the given row in the session list.
|
||||
#[template_callback]
|
||||
fn select_row(&self, row: >k::ListBoxRow) {
|
||||
self.obj().popdown();
|
||||
|
||||
let Some(selection) = self.session_selection.obj() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let index = row.index().try_into().expect("selected row has an index");
|
||||
selection.set_selected(index);
|
||||
}
|
||||
|
||||
/// Update the selected item in the session list.
|
||||
fn update_selected_item(&self, selected: u32) {
|
||||
let old_selected = self.selected_row.upgrade();
|
||||
let new_selected = if selected == gtk::INVALID_LIST_POSITION {
|
||||
None
|
||||
} else {
|
||||
let index = selected.try_into().expect("item index should fit into i32");
|
||||
self.sessions
|
||||
.row_at_index(index)
|
||||
.and_downcast::<SessionItemRow>()
|
||||
};
|
||||
|
||||
if old_selected == new_selected {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(row) = &old_selected {
|
||||
row.set_selected(false);
|
||||
}
|
||||
if let Some(row) = &new_selected {
|
||||
row.set_selected(true);
|
||||
}
|
||||
|
||||
self.selected_row.set(new_selected.as_ref());
|
||||
obj.notify_session_selection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A popover allowing to switch between the available sessions, to open their
|
||||
/// account settings, or to log into a new account.
|
||||
pub struct AccountSwitcherPopover(ObjectSubclass<imp::AccountSwitcherPopover>)
|
||||
@extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl AccountSwitcherPopover {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
fn selected_row(&self) -> Option<SessionItemRow> {
|
||||
self.imp().selected_row.upgrade()
|
||||
}
|
||||
|
||||
/// Select the given row in the session list.
|
||||
#[template_callback]
|
||||
fn select_row(&self, row: >k::ListBoxRow) {
|
||||
self.popdown();
|
||||
|
||||
let Some(selection) = self.session_selection() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let index = row.index().try_into().expect("selected row has an index");
|
||||
selection.set_selected(index);
|
||||
}
|
||||
|
||||
/// Update the selected item in the session list.
|
||||
fn update_selected_item(&self, selected: u32) {
|
||||
let imp = self.imp();
|
||||
|
||||
let old_selected = self.selected_row();
|
||||
let new_selected = if selected == gtk::INVALID_LIST_POSITION {
|
||||
None
|
||||
} else {
|
||||
let index = selected
|
||||
.try_into()
|
||||
.expect("item index always fits into i32");
|
||||
imp.sessions
|
||||
.row_at_index(index)
|
||||
.and_downcast::<SessionItemRow>()
|
||||
};
|
||||
|
||||
if old_selected == new_selected {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(row) = &old_selected {
|
||||
row.set_selected(false);
|
||||
}
|
||||
if let Some(row) = &new_selected {
|
||||
row.set_selected(true);
|
||||
}
|
||||
|
||||
imp.selected_row.set(new_selected.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AccountSwitcherPopover {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, CompositeTemplate};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, prelude::*, CompositeTemplate};
|
||||
|
||||
use crate::components::{Avatar, AvatarData};
|
||||
|
||||
@ -15,18 +15,18 @@ mod imp {
|
||||
#[properties(wrapper_type = super::AvatarWithSelection)]
|
||||
pub struct AvatarWithSelection {
|
||||
#[template_child]
|
||||
child_avatar: TemplateChild<Avatar>,
|
||||
pub child_avatar: TemplateChild<Avatar>,
|
||||
#[template_child]
|
||||
checkmark: TemplateChild<gtk::Image>,
|
||||
pub checkmark: TemplateChild<gtk::Image>,
|
||||
/// The [`AvatarData`] displayed by this widget.
|
||||
#[property(get = Self::data, set = Self::set_data, explicit_notify, nullable)]
|
||||
data: PhantomData<Option<AvatarData>>,
|
||||
pub data: PhantomData<Option<AvatarData>>,
|
||||
/// The size of the Avatar.
|
||||
#[property(get = Self::size, set = Self::set_size, minimum = -1, default = -1)]
|
||||
size: PhantomData<i32>,
|
||||
pub size: PhantomData<i32>,
|
||||
/// Whether this avatar is selected.
|
||||
#[property(get = Self::is_selected, set = Self::set_selected, explicit_notify)]
|
||||
selected: PhantomData<bool>,
|
||||
pub selected: PhantomData<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -103,7 +103,7 @@ mod imp {
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A widget displaying an [`Avatar`] and an optional selected effect.
|
||||
/// A widget displaying an `Avatar` for a `Room` or `User` and an optional selected effect.
|
||||
pub struct AvatarWithSelection(ObjectSubclass<imp::AvatarWithSelection>)
|
||||
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
||||
}
|
||||
@ -112,4 +112,8 @@ impl AvatarWithSelection {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
pub fn avatar(&self) -> &Avatar {
|
||||
&self.imp().child_avatar
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ mod account_switcher_popover;
|
||||
mod avatar_with_selection;
|
||||
mod session_item;
|
||||
|
||||
pub(crate) use self::{
|
||||
pub use self::{
|
||||
account_switcher_button::AccountSwitcherButton,
|
||||
account_switcher_popover::AccountSwitcherPopover,
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
use gtk::{self, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
|
||||
use super::avatar_with_selection::AvatarWithSelection;
|
||||
use crate::{
|
||||
@ -20,22 +20,22 @@ mod imp {
|
||||
#[properties(wrapper_type = super::SessionItemRow)]
|
||||
pub struct SessionItemRow {
|
||||
#[template_child]
|
||||
avatar: TemplateChild<AvatarWithSelection>,
|
||||
pub avatar: TemplateChild<AvatarWithSelection>,
|
||||
#[template_child]
|
||||
display_name: TemplateChild<gtk::Label>,
|
||||
pub display_name: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
user_id: TemplateChild<gtk::Label>,
|
||||
pub user_id: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
state_stack: TemplateChild<gtk::Stack>,
|
||||
pub state_stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
error_image: TemplateChild<gtk::Image>,
|
||||
pub error_image: TemplateChild<gtk::Image>,
|
||||
/// The session this item represents.
|
||||
#[property(get, set = Self::set_session, explicit_notify)]
|
||||
session: glib::WeakRef<SessionInfo>,
|
||||
user_bindings: RefCell<Vec<glib::Binding>>,
|
||||
pub session: glib::WeakRef<SessionInfo>,
|
||||
pub user_bindings: RefCell<Vec<glib::Binding>>,
|
||||
/// Whether this session is selected.
|
||||
#[property(get = Self::is_selected, set = Self::set_selected, explicit_notify)]
|
||||
selected: PhantomData<bool>,
|
||||
pub selected: PhantomData<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -46,7 +46,7 @@ mod imp {
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
Self::Type::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
@ -66,7 +66,6 @@ mod imp {
|
||||
impl WidgetImpl for SessionItemRow {}
|
||||
impl ListBoxRowImpl for SessionItemRow {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl SessionItemRow {
|
||||
/// Whether this session is selected.
|
||||
fn is_selected(&self) -> bool {
|
||||
@ -146,23 +145,6 @@ mod imp {
|
||||
self.session.set(session);
|
||||
self.obj().notify_session();
|
||||
}
|
||||
|
||||
/// Show the account settings for the session of this row.
|
||||
#[template_callback]
|
||||
fn show_account_settings(&self) {
|
||||
let Some(session) = self.session.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let obj = self.obj();
|
||||
obj.activate_action("account-switcher.close", None)
|
||||
.expect("`account-switcher.close` action should exist");
|
||||
obj.activate_action(
|
||||
"win.open-account-settings",
|
||||
Some(&session.session_id().to_variant()),
|
||||
)
|
||||
.expect("`win.open-account-settings` action should exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,8 +154,24 @@ glib::wrapper! {
|
||||
@extends gtk::Widget, gtk::ListBoxRow, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl SessionItemRow {
|
||||
pub fn new(session: &SessionInfo) -> Self {
|
||||
glib::Object::builder().property("session", session).build()
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
pub fn show_account_settings(&self) {
|
||||
let Some(session) = self.session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.activate_action("account-switcher.close", None)
|
||||
.unwrap();
|
||||
self.activate_action(
|
||||
"win.open-account-settings",
|
||||
Some(&session.session_id().to_variant()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -21,16 +21,14 @@
|
||||
<object class="GtkLabel" id="display_name">
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="user_id">
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="dim-label"/>
|
||||
<class name="caption"/>
|
||||
</style>
|
||||
</object>
|
||||
@ -60,6 +58,9 @@
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="tooltip-text" translatable="yes">Account Settings</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Account Settings</property>
|
||||
</accessibility>
|
||||
<signal name="clicked" handler="show_account_settings" swapped="true"/>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
|
1042
src/application.rs
@ -66,12 +66,15 @@ impl AvatarData {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
/// Constructs an `AvatarData` with the given image data.
|
||||
pub(crate) fn with_image(image: AvatarImage) -> Self {
|
||||
glib::Object::builder().property("image", image).build()
|
||||
}
|
||||
|
||||
/// Get this avatar as a notification icon.
|
||||
///
|
||||
/// If `inhibit_image` is set, the image of the avatar will not be used.
|
||||
///
|
||||
/// Returns `None` if an error occurred while generating the icon.
|
||||
pub(crate) async fn as_notification_icon(&self, inhibit_image: bool) -> Option<gdk::Texture> {
|
||||
pub(crate) async fn as_notification_icon(&self) -> Option<gdk::Texture> {
|
||||
let Some(window) = Application::default().active_window() else {
|
||||
warn!("Could not generate icon for notification: no active window");
|
||||
return None;
|
||||
@ -82,23 +85,21 @@ impl AvatarData {
|
||||
};
|
||||
let scale_factor = window.scale_factor();
|
||||
|
||||
if !inhibit_image {
|
||||
if let Some(image) = self.image() {
|
||||
match image.load_small_paintable().await {
|
||||
Ok(Some(paintable)) => {
|
||||
let texture = paintable_as_notification_icon(
|
||||
paintable.upcast_ref(),
|
||||
scale_factor,
|
||||
&renderer,
|
||||
);
|
||||
return Some(texture);
|
||||
}
|
||||
// No paintable, we will try to generate the fallback.
|
||||
Ok(None) => {}
|
||||
// Could not get the paintable, we will try to generate the fallback.
|
||||
Err(error) => {
|
||||
warn!("Could not generate icon for notification: {error}");
|
||||
}
|
||||
if let Some(image) = self.image() {
|
||||
match image.load_small_paintable().await {
|
||||
Ok(Some(paintable)) => {
|
||||
let texture = paintable_as_notification_icon(
|
||||
paintable.upcast_ref(),
|
||||
scale_factor,
|
||||
&renderer,
|
||||
);
|
||||
return Some(texture);
|
||||
}
|
||||
// No paintable, we will try to generate the fallback.
|
||||
Ok(None) => {}
|
||||
// Could not get the paintable, we will try to generate the fallback.
|
||||
Err(error) => {
|
||||
warn!("Could not generate icon for notification: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ use crate::{
|
||||
image::{ImageError, IMAGE_QUEUE},
|
||||
FrameDimensions,
|
||||
},
|
||||
BoundObject, BoundObjectWeakRef, CountedRef, SingleItemListModel,
|
||||
BoundObject, BoundObjectWeakRef, CountedRef,
|
||||
},
|
||||
};
|
||||
|
||||
@ -505,11 +505,12 @@ impl EditableAvatar {
|
||||
|
||||
/// Choose a new avatar.
|
||||
pub(super) async fn choose_avatar(&self) {
|
||||
let filters = gio::ListStore::new::<gtk::FileFilter>();
|
||||
|
||||
let image_filter = gtk::FileFilter::new();
|
||||
image_filter.set_name(Some(&gettext("Images")));
|
||||
image_filter.add_mime_type("image/*");
|
||||
|
||||
let filters = SingleItemListModel::new(&image_filter);
|
||||
filters.append(&image_filter);
|
||||
|
||||
let dialog = gtk::FileDialog::builder()
|
||||
.title(gettext("Choose Avatar"))
|
||||
|
@ -1,5 +1,5 @@
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
|
||||
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
|
||||
|
||||
mod crop_circle;
|
||||
mod data;
|
||||
@ -16,38 +16,11 @@ pub use self::{
|
||||
};
|
||||
use crate::{
|
||||
components::AnimatedImagePaintable,
|
||||
session::model::Room,
|
||||
utils::{BoundObject, BoundObjectWeakRef, CountedRef},
|
||||
};
|
||||
|
||||
/// The safety setting to watch to decide whether the image of the avatar should
|
||||
/// be displayed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum, Default)]
|
||||
#[enum_type(name = "AvatarImageSafetySetting")]
|
||||
pub enum AvatarImageSafetySetting {
|
||||
/// No setting needs to be watched, the image is always shown when
|
||||
/// available.
|
||||
#[default]
|
||||
None,
|
||||
|
||||
/// The media previews safety setting should be watched, with the image only
|
||||
/// shown when allowed.
|
||||
///
|
||||
/// This setting also requires the [`Room`] where the avatar is presented.
|
||||
MediaPreviews,
|
||||
|
||||
/// The invite avatars safety setting should be watched, with the image only
|
||||
/// shown when allowed.
|
||||
///
|
||||
/// This setting also requires the [`Room`] where the avatar is presented.
|
||||
InviteAvatars,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
marker::PhantomData,
|
||||
};
|
||||
use std::{cell::RefCell, marker::PhantomData};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
@ -68,19 +41,8 @@ mod imp {
|
||||
/// The size of the Avatar.
|
||||
#[property(get = Self::size, set = Self::set_size, explicit_notify, builder().default_value(-1).minimum(-1))]
|
||||
size: PhantomData<i32>,
|
||||
/// The safety setting to watch to decide whether the image of the
|
||||
/// avatar should be displayed.
|
||||
#[property(get, set = Self::set_watched_safety_setting, explicit_notify, builder(AvatarImageSafetySetting::default()))]
|
||||
watched_safety_setting: Cell<AvatarImageSafetySetting>,
|
||||
/// The room to watch to apply the current safety settings.
|
||||
///
|
||||
/// This is required if `watched_safety_setting` is not `None`.
|
||||
#[property(get, set = Self::set_watched_room, explicit_notify, nullable)]
|
||||
watched_room: RefCell<Option<Room>>,
|
||||
paintable_ref: RefCell<Option<CountedRef>>,
|
||||
paintable_animation_ref: RefCell<Option<CountedRef>>,
|
||||
watched_room_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
watched_global_account_data_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -104,11 +66,7 @@ mod imp {
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Avatar {
|
||||
fn dispose(&self) {
|
||||
self.disconnect_safety_setting_signals();
|
||||
}
|
||||
}
|
||||
impl ObjectImpl for Avatar {}
|
||||
|
||||
impl WidgetImpl for Avatar {
|
||||
fn map(&self) {
|
||||
@ -150,136 +108,6 @@ mod imp {
|
||||
self.obj().notify_size();
|
||||
}
|
||||
|
||||
/// Set the safety setting to watch to decide whether the image of the
|
||||
/// avatar should be displayed.
|
||||
fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
|
||||
if self.watched_safety_setting.get() == setting {
|
||||
return;
|
||||
}
|
||||
|
||||
self.disconnect_safety_setting_signals();
|
||||
|
||||
self.watched_safety_setting.set(setting);
|
||||
|
||||
self.connect_safety_setting_signals();
|
||||
self.obj().notify_watched_safety_setting();
|
||||
}
|
||||
|
||||
/// Set the room to watch to apply the current safety settings.
|
||||
fn set_watched_room(&self, room: Option<Room>) {
|
||||
if *self.watched_room.borrow() == room {
|
||||
return;
|
||||
}
|
||||
|
||||
self.disconnect_safety_setting_signals();
|
||||
|
||||
self.watched_room.replace(room);
|
||||
|
||||
self.connect_safety_setting_signals();
|
||||
self.obj().notify_watched_room();
|
||||
}
|
||||
|
||||
/// Connect to the proper signals for the current safety setting.
|
||||
fn connect_safety_setting_signals(&self) {
|
||||
let Some(room) = self.watched_room.borrow().clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(session) = room.session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self.watched_safety_setting.get() {
|
||||
AvatarImageSafetySetting::None => {}
|
||||
AvatarImageSafetySetting::MediaPreviews => {
|
||||
let room_handler = room.connect_join_rule_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_paintable();
|
||||
}
|
||||
));
|
||||
self.watched_room_handler.replace(Some(room_handler));
|
||||
|
||||
let global_account_data_handler = session
|
||||
.global_account_data()
|
||||
.connect_media_previews_enabled_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_paintable();
|
||||
}
|
||||
));
|
||||
self.watched_global_account_data_handler
|
||||
.replace(Some(global_account_data_handler));
|
||||
}
|
||||
AvatarImageSafetySetting::InviteAvatars => {
|
||||
let room_handler = room.connect_is_invite_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_paintable();
|
||||
}
|
||||
));
|
||||
self.watched_room_handler.replace(Some(room_handler));
|
||||
|
||||
let global_account_data_handler = session
|
||||
.global_account_data()
|
||||
.connect_invite_avatars_enabled_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_paintable();
|
||||
}
|
||||
));
|
||||
self.watched_global_account_data_handler
|
||||
.replace(Some(global_account_data_handler));
|
||||
}
|
||||
}
|
||||
|
||||
self.update_paintable();
|
||||
}
|
||||
|
||||
/// Disconnect the handlers for the signals of the safety setting.
|
||||
fn disconnect_safety_setting_signals(&self) {
|
||||
if let Some(room) = self.watched_room.borrow().as_ref() {
|
||||
if let Some(handler) = self.watched_room_handler.take() {
|
||||
room.disconnect(handler);
|
||||
}
|
||||
|
||||
if let Some(handler) = self.watched_global_account_data_handler.take() {
|
||||
room.session()
|
||||
.inspect(|session| session.global_account_data().disconnect(handler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether we can display the image of the avatar with the current
|
||||
/// state.
|
||||
fn can_show_image(&self) -> bool {
|
||||
let watched_safety_setting = self.watched_safety_setting.get();
|
||||
|
||||
if watched_safety_setting == AvatarImageSafetySetting::None {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(room) = self.watched_room.borrow().clone() else {
|
||||
return false;
|
||||
};
|
||||
let Some(session) = room.session() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match watched_safety_setting {
|
||||
AvatarImageSafetySetting::None => unreachable!(),
|
||||
AvatarImageSafetySetting::MediaPreviews => session
|
||||
.global_account_data()
|
||||
.should_room_show_media_previews(&room),
|
||||
AvatarImageSafetySetting::InviteAvatars => {
|
||||
!room.is_invite() || session.global_account_data().invite_avatars_enabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`AvatarData`] displayed by this widget.
|
||||
fn set_data(&self, data: Option<AvatarData>) {
|
||||
if self.data.obj() == data {
|
||||
@ -360,13 +188,6 @@ mod imp {
|
||||
fn update_paintable(&self) {
|
||||
let _old_paintable_ref = self.paintable_ref.take();
|
||||
|
||||
if !self.can_show_image() {
|
||||
// We need to unset the paintable.
|
||||
self.avatar.set_custom_image(None::<&gdk::Paintable>);
|
||||
self.update_animated_paintable_state();
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.obj().is_mapped() {
|
||||
// We do not need a paintable.
|
||||
self.update_animated_paintable_state();
|
||||
@ -397,7 +218,7 @@ mod imp {
|
||||
fn update_animated_paintable_state(&self) {
|
||||
let _old_paintable_animation_ref = self.paintable_animation_ref.take();
|
||||
|
||||
if !self.can_show_image() || !self.obj().is_mapped() {
|
||||
if !self.obj().is_mapped() {
|
||||
// We do not need to animate the paintable.
|
||||
return;
|
||||
}
|
||||
|
@ -1,17 +1,14 @@
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{gdk, gio, glib, glib::clone};
|
||||
use tracing::error;
|
||||
use adw::prelude::*;
|
||||
use gtk::{gdk, gio, glib, glib::clone, subclass::prelude::*};
|
||||
|
||||
use super::{crop_circle::CropCircle, Avatar, AvatarData};
|
||||
use crate::utils::BoundObject;
|
||||
|
||||
/// Function to extract the avatar data from a supported `GObject`.
|
||||
type ExtractAvatarDataFn = dyn Fn(&glib::Object) -> AvatarData + 'static;
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
marker::PhantomData,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -27,9 +24,12 @@ mod imp {
|
||||
#[property(get, set = Self::set_spacing, explicit_notify)]
|
||||
spacing: Cell<u32>,
|
||||
/// The maximum number of avatars to display.
|
||||
#[property(get = Self::max_avatars, set = Self::set_max_avatars)]
|
||||
max_avatars: PhantomData<u32>,
|
||||
slice_model: gtk::SliceListModel,
|
||||
///
|
||||
/// `0` means that all avatars are displayed.
|
||||
#[property(get, set = Self::set_max_avatars, explicit_notify)]
|
||||
max_avatars: Cell<u32>,
|
||||
/// The list model that is bound, if any.
|
||||
bound_model: BoundObject<gio::ListModel>,
|
||||
/// The method used to extract `AvatarData` from the items of the list
|
||||
/// model, if any.
|
||||
extract_avatar_data_fn: RefCell<Option<Box<ExtractAvatarDataFn>>>,
|
||||
@ -48,18 +48,6 @@ mod imp {
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for OverlappingAvatars {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.slice_model.connect_items_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_, position, removed, added| {
|
||||
imp.handle_items_changed(position, removed, added);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
for child in self.children.take() {
|
||||
child.unparent();
|
||||
@ -70,7 +58,7 @@ mod imp {
|
||||
impl WidgetImpl for OverlappingAvatars {
|
||||
fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
|
||||
if self.children.borrow().is_empty() {
|
||||
return (0, 0, -1, -1);
|
||||
return (0, 0, -1, 1);
|
||||
}
|
||||
|
||||
let avatar_size = self.avatar_size.get();
|
||||
@ -167,47 +155,113 @@ mod imp {
|
||||
obj.notify_avatar_size();
|
||||
}
|
||||
|
||||
/// The maximum number of avatars to display.
|
||||
fn max_avatars(&self) -> u32 {
|
||||
self.slice_model.size()
|
||||
}
|
||||
|
||||
/// Set the maximum number of avatars to display.
|
||||
fn set_max_avatars(&self, max_avatars: u32) {
|
||||
self.slice_model.set_size(max_avatars);
|
||||
let old_max_avatars = self.max_avatars.get();
|
||||
|
||||
if old_max_avatars == max_avatars {
|
||||
return;
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
self.max_avatars.set(max_avatars);
|
||||
if max_avatars != 0 && self.children.borrow().len() > max_avatars as usize {
|
||||
// We have more children than we should, remove them.
|
||||
let children = self.children.borrow_mut().split_off(max_avatars as usize);
|
||||
|
||||
for child in children {
|
||||
child.unparent();
|
||||
}
|
||||
|
||||
if let Some(child) = self.children.borrow().last() {
|
||||
child.set_is_cropped(false);
|
||||
}
|
||||
|
||||
obj.queue_resize();
|
||||
} else if max_avatars == 0 || (old_max_avatars != 0 && max_avatars > old_max_avatars) {
|
||||
let Some(model) = self.bound_model.obj() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let diff = model.n_items() - old_max_avatars;
|
||||
if diff > 0 {
|
||||
// We could have more children, create them.
|
||||
self.handle_items_changed(&model, old_max_avatars, 0, diff);
|
||||
}
|
||||
}
|
||||
|
||||
obj.notify_max_avatars();
|
||||
}
|
||||
|
||||
/// Bind a `GListModel` to this list.
|
||||
/// Bind a `ListModel` to this list.
|
||||
pub(super) fn bind_model<P: Fn(&glib::Object) -> AvatarData + 'static>(
|
||||
&self,
|
||||
model: Option<&gio::ListModel>,
|
||||
model: Option<gio::ListModel>,
|
||||
extract_avatar_data_fn: P,
|
||||
) {
|
||||
self.bound_model.disconnect_signals();
|
||||
for child in self.children.take() {
|
||||
child.unparent();
|
||||
}
|
||||
self.extract_avatar_data_fn.take();
|
||||
|
||||
let Some(model) = model else {
|
||||
return;
|
||||
};
|
||||
|
||||
let signal_handler_id = model.connect_items_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |model, position, removed, added| {
|
||||
imp.handle_items_changed(model, position, removed, added);
|
||||
}
|
||||
));
|
||||
|
||||
self.bound_model.set(model.clone(), vec![signal_handler_id]);
|
||||
self.extract_avatar_data_fn
|
||||
.replace(Some(Box::new(extract_avatar_data_fn)));
|
||||
self.slice_model.set_model(model);
|
||||
|
||||
self.handle_items_changed(&model, 0, 0, model.n_items());
|
||||
}
|
||||
|
||||
/// Handle when the items of the model changed.
|
||||
fn handle_items_changed(&self, position: u32, removed: u32, added: u32) {
|
||||
let mut children = self.children.borrow_mut();
|
||||
let prev_count = children.len();
|
||||
fn handle_items_changed(
|
||||
&self,
|
||||
model: &impl IsA<gio::ListModel>,
|
||||
position: u32,
|
||||
mut removed: u32,
|
||||
added: u32,
|
||||
) {
|
||||
let max_avatars = self.max_avatars.get();
|
||||
if max_avatars != 0 && position >= max_avatars {
|
||||
// No changes here.
|
||||
return;
|
||||
}
|
||||
|
||||
let mut children = self.children.borrow_mut();
|
||||
let extract_avatar_data_fn_borrow = self.extract_avatar_data_fn.borrow();
|
||||
let extract_avatar_data_fn = extract_avatar_data_fn_borrow
|
||||
.as_ref()
|
||||
.expect("extract avatar data fn should be set if model is set");
|
||||
let extract_avatar_data_fn = extract_avatar_data_fn_borrow.as_ref().unwrap();
|
||||
|
||||
let avatar_size = i32::try_from(self.avatar_size.get()).unwrap_or(i32::MAX);
|
||||
let cropped_width = self.overlap();
|
||||
|
||||
while removed > 0 {
|
||||
if position as usize >= children.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let child = children.remove(position as usize);
|
||||
child.unparent();
|
||||
removed -= 1;
|
||||
}
|
||||
|
||||
let obj = self.obj();
|
||||
|
||||
let added = (position..(position + added)).filter_map(|position| {
|
||||
let Some(item) = self.slice_model.item(position) else {
|
||||
error!("Could not get item in slice model at position {position}");
|
||||
return None;
|
||||
};
|
||||
for i in position..(position + added) {
|
||||
if max_avatars != 0 && i >= max_avatars {
|
||||
break;
|
||||
}
|
||||
|
||||
let item = model.item(i).unwrap();
|
||||
let avatar_data = extract_avatar_data_fn(&item);
|
||||
|
||||
let avatar = Avatar::new();
|
||||
@ -219,22 +273,16 @@ mod imp {
|
||||
child.set_cropped_width(cropped_width);
|
||||
child.set_parent(&*obj);
|
||||
|
||||
Some(child)
|
||||
});
|
||||
|
||||
for child in children.splice(position as usize..(position + removed) as usize, added) {
|
||||
child.unparent();
|
||||
children.insert(i as usize, child);
|
||||
}
|
||||
|
||||
// Make sure that only the last avatar is not cropped.
|
||||
let mut peekable_children = children.iter().peekable();
|
||||
while let Some(child) = peekable_children.next() {
|
||||
child.set_is_cropped(peekable_children.peek().is_some());
|
||||
let last_pos = children.len().saturating_sub(1);
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
child.set_is_cropped(i != last_pos);
|
||||
}
|
||||
|
||||
if prev_count != children.len() {
|
||||
obj.queue_resize();
|
||||
}
|
||||
obj.queue_resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -251,13 +299,13 @@ impl OverlappingAvatars {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
/// Bind a `GListModel` to this list.
|
||||
/// Bind a `ListModel` to this list.
|
||||
pub(crate) fn bind_model<P: Fn(&glib::Object) -> AvatarData + 'static>(
|
||||
&self,
|
||||
model: Option<&impl IsA<gio::ListModel>>,
|
||||
model: Option<impl IsA<gio::ListModel>>,
|
||||
extract_avatar_data_fn: P,
|
||||
) {
|
||||
self.imp()
|
||||
.bind_model(model.map(Cast::upcast_ref), extract_avatar_data_fn);
|
||||
.bind_model(model.and_upcast(), extract_avatar_data_fn);
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ashpd::desktop::camera;
|
||||
use gtk::prelude::*;
|
||||
use tokio::time::timeout;
|
||||
use tracing::error;
|
||||
|
||||
mod viewfinder;
|
||||
|
||||
use self::viewfinder::LinuxCameraViewfinder;
|
||||
use super::{CameraExt, CameraViewfinder};
|
||||
use crate::spawn_tokio;
|
||||
|
||||
/// Camera API under Linux.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LinuxCamera;
|
||||
|
||||
impl CameraExt for LinuxCamera {
|
||||
async fn has_cameras() -> bool {
|
||||
let fut = async move {
|
||||
let camera = match camera::Camera::new().await {
|
||||
Ok(camera) => camera,
|
||||
Err(error) => {
|
||||
error!("Could not create instance of camera proxy: {error}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match camera.is_present().await {
|
||||
Ok(is_present) => is_present,
|
||||
Err(error) => {
|
||||
error!("Could not check whether system has cameras: {error}");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
let handle = spawn_tokio!(async move { timeout(Duration::from_secs(1), fut).await });
|
||||
|
||||
if let Ok(is_present) = handle.await.expect("task was not aborted") {
|
||||
is_present
|
||||
} else {
|
||||
error!("Could not check whether system has cameras: request timed out");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn viewfinder() -> Option<CameraViewfinder> {
|
||||
LinuxCameraViewfinder::new().await.and_upcast()
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
use ashpd::desktop::camera;
|
||||
use gtk::{
|
||||
glib,
|
||||
glib::{clone, subclass::prelude::*},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
};
|
||||
use matrix_sdk::encryption::verification::QrVerificationData;
|
||||
use tokio::task::AbortHandle;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
components::camera::{
|
||||
CameraViewfinder, CameraViewfinderExt, CameraViewfinderImpl, CameraViewfinderState,
|
||||
},
|
||||
spawn_tokio,
|
||||
};
|
||||
|
||||
impl From<aperture::ViewfinderState> for CameraViewfinderState {
|
||||
fn from(value: aperture::ViewfinderState) -> Self {
|
||||
match value {
|
||||
aperture::ViewfinderState::Loading => Self::Loading,
|
||||
aperture::ViewfinderState::Ready => Self::Ready,
|
||||
aperture::ViewfinderState::NoCameras => Self::NoCameras,
|
||||
aperture::ViewfinderState::Error => Self::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use matrix_sdk::encryption::verification::DecodingError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LinuxCameraViewfinder {
|
||||
/// The child viewfinder.
|
||||
child: aperture::Viewfinder,
|
||||
/// The device provider for the viewfinder.
|
||||
provider: aperture::DeviceProvider,
|
||||
abort_handle: RefCell<Option<AbortHandle>>,
|
||||
}
|
||||
|
||||
impl Default for LinuxCameraViewfinder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
child: Default::default(),
|
||||
provider: aperture::DeviceProvider::instance().clone(),
|
||||
abort_handle: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LinuxCameraViewfinder {
|
||||
const NAME: &'static str = "LinuxCameraViewfinder";
|
||||
type Type = super::LinuxCameraViewfinder;
|
||||
type ParentType = CameraViewfinder;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_layout_manager_type::<gtk::BinLayout>();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for LinuxCameraViewfinder {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
self.child.set_parent(&*obj);
|
||||
self.child.set_detect_codes(true);
|
||||
|
||||
self.child.connect_state_notify(glib::clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_state();
|
||||
}
|
||||
));
|
||||
self.update_state();
|
||||
|
||||
self.child.connect_code_detected(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, code| {
|
||||
match QrVerificationData::from_bytes(&code) {
|
||||
Ok(data) => obj.emit_qrcode_detected(data),
|
||||
Err(error) => {
|
||||
let code = String::from_utf8_lossy(&code);
|
||||
|
||||
if matches!(error, DecodingError::Header) {
|
||||
debug!("Detected non-Matrix QR Code: {code}");
|
||||
} else {
|
||||
error!(
|
||||
"Could not decode Matrix verification QR code {code}: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.child.stop_stream();
|
||||
self.child.unparent();
|
||||
|
||||
if let Some(abort_handle) = self.abort_handle.take() {
|
||||
abort_handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for LinuxCameraViewfinder {}
|
||||
impl CameraViewfinderImpl for LinuxCameraViewfinder {}
|
||||
|
||||
impl LinuxCameraViewfinder {
|
||||
/// Initialize the viewfinder.
|
||||
pub(super) async fn init(&self) -> Result<(), ()> {
|
||||
if self.provider.started() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let handle = spawn_tokio!(camera::request());
|
||||
self.set_abort_handle(Some(handle.abort_handle()));
|
||||
|
||||
let Ok(request_result) = handle.await else {
|
||||
debug!("Camera request was aborted");
|
||||
self.set_abort_handle(None);
|
||||
return Err(());
|
||||
};
|
||||
|
||||
self.set_abort_handle(None);
|
||||
|
||||
let fd = match request_result {
|
||||
Ok(Some(fd)) => fd,
|
||||
Ok(None) => {
|
||||
error!("Could not access camera: no camera present");
|
||||
return Err(());
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Could not access camera: {error}");
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = self.provider.set_fd(fd) {
|
||||
error!("Could not access camera: {error}");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if let Err(error) = self.provider.start_with_default(|camera| {
|
||||
matches!(camera.location(), aperture::CameraLocation::Back)
|
||||
}) {
|
||||
error!("Could not access camera: {error}");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the current state.
|
||||
fn update_state(&self) {
|
||||
self.obj().set_state(self.child.state().into());
|
||||
}
|
||||
|
||||
/// Set the current abort handle.
|
||||
fn set_abort_handle(&self, abort_handle: Option<AbortHandle>) {
|
||||
self.abort_handle.replace(abort_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A camera viewfinder widget for Linux.
|
||||
pub struct LinuxCameraViewfinder(ObjectSubclass<imp::LinuxCameraViewfinder>)
|
||||
@extends gtk::Widget, CameraViewfinder;
|
||||
}
|
||||
|
||||
impl LinuxCameraViewfinder {
|
||||
pub(super) async fn new() -> Option<Self> {
|
||||
let obj = glib::Object::new::<Self>();
|
||||
|
||||
obj.imp().init().await.ok()?;
|
||||
|
||||
Some(obj)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
//! Camera API.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
mod qrcode_scanner;
|
||||
mod viewfinder;
|
||||
|
||||
pub(crate) use self::qrcode_scanner::QrCodeScanner;
|
||||
use self::{
|
||||
qrcode_scanner::QrVerificationDataBoxed,
|
||||
viewfinder::{
|
||||
CameraViewfinder, CameraViewfinderExt, CameraViewfinderImpl, CameraViewfinderState,
|
||||
},
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
/// The camera API.
|
||||
pub(crate) type Camera = linux::LinuxCamera;
|
||||
} else {
|
||||
/// The camera API.
|
||||
pub(crate) type Camera = unimplemented::UnimplementedCamera;
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implemented by camera backends.
|
||||
pub trait CameraExt {
|
||||
/// Whether any cameras are available.
|
||||
async fn has_cameras() -> bool;
|
||||
|
||||
/// Get a viewfinder displaying the output of the camera.
|
||||
///
|
||||
/// This method should try to get the permission to access cameras, and
|
||||
/// return `None` when it fails.
|
||||
async fn viewfinder() -> Option<CameraViewfinder>;
|
||||
}
|
||||
|
||||
/// The fallback `Camera` API, to use on platforms where it is unimplemented.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
mod unimplemented {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnimplementedCamera;
|
||||
|
||||
impl CameraExt for UnimplementedCamera {
|
||||
async fn has_cameras() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn viewfinder() -> Option<CameraViewfinder> {
|
||||
tracing::error!("The camera API is not supported on this platform");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
glib,
|
||||
glib::{clone, closure_local},
|
||||
};
|
||||
use matrix_sdk::encryption::verification::QrVerificationData;
|
||||
|
||||
use super::{Camera, CameraExt, CameraViewfinder, CameraViewfinderExt, CameraViewfinderState};
|
||||
use crate::utils::BoundConstructOnlyObject;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, glib::Boxed)]
|
||||
#[boxed_type(name = "QrVerificationDataBoxed")]
|
||||
pub(super) struct QrVerificationDataBoxed(pub(super) QrVerificationData);
|
||||
|
||||
mod imp {
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use glib::subclass::{InitializingObject, Signal};
|
||||
use gtk::CompositeTemplate;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, CompositeTemplate, Default, glib::Properties)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/camera/qrcode_scanner.ui")]
|
||||
#[properties(wrapper_type = super::QrCodeScanner)]
|
||||
pub struct QrCodeScanner {
|
||||
#[template_child]
|
||||
stack: TemplateChild<gtk::Stack>,
|
||||
/// The viewfinder to use to scan the QR code.
|
||||
#[property(get, set = Self::set_viewfinder, construct_only)]
|
||||
viewfinder: BoundConstructOnlyObject<CameraViewfinder>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for QrCodeScanner {
|
||||
const NAME: &'static str = "QrCodeScanner";
|
||||
type Type = super::QrCodeScanner;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for QrCodeScanner {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
|
||||
vec![Signal::builder("qrcode-detected")
|
||||
.param_types([QrVerificationDataBoxed::static_type()])
|
||||
.run_first()
|
||||
.build()]
|
||||
});
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for QrCodeScanner {}
|
||||
impl BinImpl for QrCodeScanner {}
|
||||
|
||||
impl QrCodeScanner {
|
||||
/// Set the viewfinder to use to scan the QR code.
|
||||
fn set_viewfinder(&self, viewfinder: CameraViewfinder) {
|
||||
let obj = self.obj();
|
||||
|
||||
let state_handler = viewfinder.connect_state_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_visible_page();
|
||||
}
|
||||
));
|
||||
let qrcode_detected_handler = viewfinder.connect_qrcode_detected(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, data| {
|
||||
obj.emit_by_name::<()>("qrcode-detected", &[&QrVerificationDataBoxed(data)]);
|
||||
}
|
||||
));
|
||||
|
||||
viewfinder.set_overflow(gtk::Overflow::Hidden);
|
||||
viewfinder.add_css_class("card");
|
||||
|
||||
self.stack
|
||||
.add_titled(&viewfinder, Some("camera"), &gettext("Camera"));
|
||||
|
||||
self.viewfinder
|
||||
.set(viewfinder, vec![state_handler, qrcode_detected_handler]);
|
||||
|
||||
self.update_visible_page();
|
||||
}
|
||||
|
||||
/// Update the visible page according to the current state.
|
||||
fn update_visible_page(&self) {
|
||||
let name = match self.viewfinder.obj().state() {
|
||||
CameraViewfinderState::Loading => "loading",
|
||||
CameraViewfinderState::Ready => "camera",
|
||||
CameraViewfinderState::NoCameras | CameraViewfinderState::Error => "no-camera",
|
||||
};
|
||||
|
||||
self.stack.set_visible_child_name(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A widget to show the output of the camera and detect QR codes with it.
|
||||
pub struct QrCodeScanner(ObjectSubclass<imp::QrCodeScanner>)
|
||||
@extends gtk::Widget, adw::Bin;
|
||||
}
|
||||
|
||||
impl QrCodeScanner {
|
||||
/// Try to construct a new `QrCodeScanner`.
|
||||
///
|
||||
/// Returns `None` if we could not get a [`CameraViewfinder`].
|
||||
pub async fn new() -> Option<Self> {
|
||||
let viewfinder = Camera::viewfinder().await?;
|
||||
|
||||
let obj = glib::Object::builder()
|
||||
.property("viewfinder", viewfinder)
|
||||
.build();
|
||||
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
/// Connect to the signal emitted when a QR code is detected.
|
||||
pub fn connect_qrcode_detected<F: Fn(&Self, QrVerificationData) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
self.connect_closure(
|
||||
"qrcode-detected",
|
||||
true,
|
||||
closure_local!(move |obj: Self, data: QrVerificationDataBoxed| {
|
||||
f(&obj, data.0);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
//! Camera viewfinder API.
|
||||
|
||||
use gettextrs::gettext;
|
||||
use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*};
|
||||
use matrix_sdk::encryption::verification::QrVerificationData;
|
||||
|
||||
use super::QrVerificationDataBoxed;
|
||||
|
||||
/// The possible states of a [`CameraViewfinder`].
|
||||
#[derive(Default, Debug, Copy, Clone, glib::Enum, PartialEq)]
|
||||
#[enum_type(name = "CameraViewfinderState")]
|
||||
pub enum CameraViewfinderState {
|
||||
/// The viewfinder is still loading.
|
||||
#[default]
|
||||
Loading,
|
||||
/// The viewfinder is ready for use.
|
||||
Ready,
|
||||
/// The viewfinder could not find any cameras to use.
|
||||
NoCameras,
|
||||
/// The viewfinder had an error and is not usable.
|
||||
Error,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::{cell::Cell, sync::LazyLock};
|
||||
|
||||
use glib::subclass::Signal;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CameraViewfinderClass {
|
||||
parent_class: glib::object::Class<gtk::Widget>,
|
||||
}
|
||||
|
||||
unsafe impl ClassStruct for CameraViewfinderClass {
|
||||
type Type = CameraViewfinder;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, glib::Properties)]
|
||||
#[properties(wrapper_type = super::CameraViewfinder)]
|
||||
pub struct CameraViewfinder {
|
||||
/// The state of this viewfinder.
|
||||
#[property(get, set = Self::set_state, explicit_notify, builder(CameraViewfinderState::default()))]
|
||||
state: Cell<CameraViewfinderState>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for CameraViewfinder {
|
||||
const NAME: &'static str = "CameraViewfinder";
|
||||
type Type = super::CameraViewfinder;
|
||||
type ParentType = gtk::Widget;
|
||||
type Class = CameraViewfinderClass;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for CameraViewfinder {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
|
||||
vec![Signal::builder("qrcode-detected")
|
||||
.param_types([QrVerificationDataBoxed::static_type()])
|
||||
.run_first()
|
||||
.build()]
|
||||
});
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj()
|
||||
.update_property(&[gtk::accessible::Property::Label(&gettext("Viewfinder"))]);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for CameraViewfinder {}
|
||||
|
||||
impl CameraViewfinder {
|
||||
/// Set the state of this viewfinder.
|
||||
fn set_state(&self, state: CameraViewfinderState) {
|
||||
if self.state.get() == state {
|
||||
return;
|
||||
}
|
||||
|
||||
self.state.set(state);
|
||||
self.obj().notify_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Subclassable camera viewfinder widget.
|
||||
///
|
||||
/// The widget presents the output of the camera and detects QR codes.
|
||||
///
|
||||
/// To construct this, use `Camera::viewfinder()`.
|
||||
pub struct CameraViewfinder(ObjectSubclass<imp::CameraViewfinder>)
|
||||
@extends gtk::Widget, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
/// Trait implemented by types that subclass [`CameraViewfinder`].
|
||||
#[allow(dead_code)]
|
||||
pub(super) trait CameraViewfinderExt: 'static {
|
||||
/// The state of this viewfinder.
|
||||
fn state(&self) -> CameraViewfinderState;
|
||||
|
||||
/// Set the state of this viewfinder.
|
||||
fn set_state(&self, state: CameraViewfinderState);
|
||||
|
||||
/// Connect to the signal emitted when a QR code is detected.
|
||||
fn connect_qrcode_detected<F: Fn(&Self, QrVerificationData) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId;
|
||||
|
||||
/// Emit the signal that a QR code was detected.
|
||||
fn emit_qrcode_detected(&self, data: QrVerificationData);
|
||||
}
|
||||
|
||||
impl<O: IsA<CameraViewfinder>> CameraViewfinderExt for O {
|
||||
fn state(&self) -> CameraViewfinderState {
|
||||
self.upcast_ref().state()
|
||||
}
|
||||
|
||||
/// Set the state of this viewfinder.
|
||||
fn set_state(&self, state: CameraViewfinderState) {
|
||||
self.upcast_ref().set_state(state);
|
||||
}
|
||||
|
||||
fn connect_qrcode_detected<F: Fn(&Self, QrVerificationData) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
self.connect_closure(
|
||||
"qrcode-detected",
|
||||
true,
|
||||
closure_local!(|obj: Self, data: QrVerificationDataBoxed| {
|
||||
f(&obj, data.0);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn emit_qrcode_detected(&self, data: QrVerificationData) {
|
||||
self.emit_by_name::<()>("qrcode-detected", &[&QrVerificationDataBoxed(data)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait that must be implemented for types that subclass `CameraViewfinder`.
|
||||
///
|
||||
/// Overriding a method from this Trait overrides also its behavior in
|
||||
/// [`CameraViewfinderExt`].
|
||||
pub(super) trait CameraViewfinderImpl: ObjectImpl {}
|
||||
|
||||
unsafe impl<T> IsSubclassable<T> for CameraViewfinder
|
||||
where
|
||||
T: CameraViewfinderImpl + WidgetImpl,
|
||||
T::Type: IsA<CameraViewfinder>,
|
||||
{
|
||||
fn class_init(class: &mut glib::Class<Self>) {
|
||||
Self::parent_class_init::<T>(class.upcast_ref_mut());
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
|
||||
|
||||
use crate::utils::{key_bindings, BoundObject};
|
||||
use crate::utils::BoundObject;
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, RefCell};
|
||||
@ -62,7 +62,16 @@ mod imp {
|
||||
klass.install_action("context-menu.activate", None, |obj, _, _| {
|
||||
obj.open_menu_at(0, 0);
|
||||
});
|
||||
key_bindings::add_context_menu_bindings(klass, "context-menu.activate");
|
||||
klass.add_binding_action(
|
||||
gdk::Key::F10,
|
||||
gdk::ModifierType::SHIFT_MASK,
|
||||
"context-menu.activate",
|
||||
);
|
||||
klass.add_binding_action(
|
||||
gdk::Key::Menu,
|
||||
gdk::ModifierType::empty(),
|
||||
"context-menu.activate",
|
||||
);
|
||||
|
||||
klass.install_action("context-menu.close", None, |obj, _, _| {
|
||||
if let Some(popover) = obj.popover() {
|
||||
|
@ -380,10 +380,8 @@ mod imp {
|
||||
self.send_request_btn.set_is_loading(true);
|
||||
|
||||
if let Err(()) = session.verification_list().create(None).await {
|
||||
toast!(
|
||||
self.obj(),
|
||||
gettext("Could not send a new verification request")
|
||||
);
|
||||
let obj = self.obj();
|
||||
toast!(obj, gettext("Could not send a new verification request"));
|
||||
}
|
||||
|
||||
// On success, the verification should be shown automatically.
|
||||
@ -434,7 +432,7 @@ mod imp {
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Could not bootstrap cross-signing: {error:?}");
|
||||
toast!(obj, gettext("Could not create the crypto identity"));
|
||||
toast!(obj, gettext("Could not create the crypto identity",));
|
||||
}
|
||||
}
|
||||
|
||||
|
446
src/components/dialogs/auth.rs
Normal file
@ -0,0 +1,446 @@
|
||||
use std::{fmt::Debug, future::Future};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use futures_channel::oneshot;
|
||||
use gettextrs::gettext;
|
||||
use gtk::{glib, glib::clone, CompositeTemplate};
|
||||
use matrix_sdk::{encryption::CrossSigningResetAuthType, Error};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::StandardErrorBody,
|
||||
uiaa::{
|
||||
AuthData, AuthType, Dummy, FallbackAcknowledgement, Password, UiaaInfo, UserIdentifier,
|
||||
},
|
||||
},
|
||||
assign,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{prelude::*, session::model::Session, spawn, spawn_tokio};
|
||||
|
||||
/// An error during UIAA interaction.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
/// The server returned a non-UIAA error.
|
||||
#[error(transparent)]
|
||||
ServerResponse(#[from] Error),
|
||||
|
||||
/// The ID of the UIAA session is missing for a stage that requires it.
|
||||
#[error("The ID of the session is missing")]
|
||||
MissingSessionId,
|
||||
|
||||
/// The available flows are empty or done but the endpoint still requires
|
||||
/// UIAA.
|
||||
#[error("There is no stage to choose from")]
|
||||
NoStageToChoose,
|
||||
|
||||
/// The user cancelled the authentication.
|
||||
#[error("The user cancelled the authentication")]
|
||||
UserCancelled,
|
||||
|
||||
/// The parent `Session` could not be upgraded.
|
||||
#[error("The session could not be upgraded")]
|
||||
NoSession,
|
||||
|
||||
/// The parent `gtk::Widget` could not be upgraded.
|
||||
#[error("The parent widget could not be upgraded")]
|
||||
NoParentWidget,
|
||||
|
||||
/// An unexpected error occurred.
|
||||
#[error("An unexpected error occurred")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/dialogs/auth.ui")]
|
||||
#[properties(wrapper_type = super::AuthDialog)]
|
||||
pub struct AuthDialog {
|
||||
#[template_child]
|
||||
password: TemplateChild<gtk::PasswordEntry>,
|
||||
#[template_child]
|
||||
open_browser_btn: TemplateChild<gtk::Button>,
|
||||
open_browser_btn_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
#[template_child]
|
||||
error: TemplateChild<gtk::Label>,
|
||||
/// The parent session.
|
||||
#[property(get, set, construct_only)]
|
||||
session: glib::WeakRef<Session>,
|
||||
/// The parent widget.
|
||||
#[property(get)]
|
||||
parent: glib::WeakRef<gtk::Widget>,
|
||||
/// The sender for the response.
|
||||
sender: RefCell<Option<oneshot::Sender<String>>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for AuthDialog {
|
||||
const NAME: &'static str = "AuthDialog";
|
||||
type Type = super::AuthDialog;
|
||||
type ParentType = adw::AlertDialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for AuthDialog {}
|
||||
|
||||
impl WidgetImpl for AuthDialog {}
|
||||
impl AdwDialogImpl for AuthDialog {}
|
||||
|
||||
impl AdwAlertDialogImpl for AuthDialog {
|
||||
fn response(&self, response: &str) {
|
||||
if let Some(sender) = self.sender.take() {
|
||||
if sender.send(response.to_owned()).is_err() {
|
||||
error!("Could not send response");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl AuthDialog {
|
||||
/// Authenticate the user to the server via an interactive
|
||||
/// authentication flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(super) async fn authenticate<
|
||||
Response: Send + 'static,
|
||||
F1: Future<Output = Result<Response, Error>> + Send + 'static,
|
||||
FN: Fn(matrix_sdk::Client, Option<AuthData>) -> F1 + Send + 'static + Sync + Clone,
|
||||
>(
|
||||
&self,
|
||||
parent: >k::Widget,
|
||||
callback: FN,
|
||||
) -> Result<Response, AuthError> {
|
||||
let Some(client) = self.session.upgrade().map(|s| s.client()) else {
|
||||
return Err(AuthError::NoSession);
|
||||
};
|
||||
|
||||
self.parent.set(Some(parent));
|
||||
|
||||
let mut auth_data = None;
|
||||
|
||||
loop {
|
||||
let callback_clone = callback.clone();
|
||||
let client_clone = client.clone();
|
||||
|
||||
// Get the current state of the authentication.
|
||||
let handle =
|
||||
spawn_tokio!(async move { callback_clone(client_clone, auth_data).await });
|
||||
let response = handle.await.expect("task was not aborted");
|
||||
|
||||
let error = match response {
|
||||
// Authentication is over.
|
||||
Ok(result) => return Ok(result),
|
||||
Err(error) => error,
|
||||
};
|
||||
// If this is a UIAA error, authentication continues.
|
||||
let Some(uiaa_info) = error.as_uiaa_response() else {
|
||||
return Err(error.into());
|
||||
};
|
||||
|
||||
let next_auth_data = self.perform_next_stage(uiaa_info).await?;
|
||||
auth_data = Some(next_auth_data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the cross-signing keys while handling the interactive
|
||||
/// authentication flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(super) async fn reset_cross_signing(
|
||||
&self,
|
||||
parent: >k::Widget,
|
||||
) -> Result<(), AuthError> {
|
||||
let Some(encryption) = self.session.upgrade().map(|s| s.client().encryption()) else {
|
||||
return Err(AuthError::NoSession);
|
||||
};
|
||||
|
||||
self.parent.set(Some(parent));
|
||||
|
||||
let handle = spawn_tokio!(async move { encryption.reset_cross_signing().await })
|
||||
.await
|
||||
.expect("task was not aborted")?;
|
||||
|
||||
if let Some(handle) = handle {
|
||||
match handle.auth_type() {
|
||||
CrossSigningResetAuthType::Uiaa(uiaa_info) => {
|
||||
let auth_data = self.perform_next_stage(uiaa_info).await?;
|
||||
|
||||
spawn_tokio!(async move { handle.auth(Some(auth_data)).await })
|
||||
.await
|
||||
.expect("task was not aborted")?;
|
||||
}
|
||||
CrossSigningResetAuthType::Oidc(_) => {
|
||||
// According to the code, this is only used with the `experimental-oidc`
|
||||
// feature. Return an error in case this changes.
|
||||
error!(
|
||||
"Could not perform cross-signing reset: received unexpected OIDC stage"
|
||||
);
|
||||
return Err(AuthError::Unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs the preferred next stage in the given UIAA info.
|
||||
///
|
||||
/// Stages that are actually supported are preferred. If no stages are
|
||||
/// supported, we use the web-based fallback.
|
||||
async fn perform_next_stage(&self, uiaa_info: &UiaaInfo) -> Result<AuthData, AuthError> {
|
||||
// Show the authentication error, if there is one.
|
||||
self.show_auth_error(uiaa_info.auth_error.as_ref());
|
||||
|
||||
// Find and perform the next stage.
|
||||
let stages = uiaa_info
|
||||
.flows
|
||||
.iter()
|
||||
.filter_map(|flow| flow.stages.strip_prefix(uiaa_info.completed.as_slice()))
|
||||
.filter_map(|stages_left| stages_left.first());
|
||||
|
||||
let mut first_stage = None;
|
||||
for stage in stages {
|
||||
if let Some(auth_result) = self
|
||||
.try_perform_stage(uiaa_info.session.as_ref(), stage)
|
||||
.await
|
||||
{
|
||||
return auth_result;
|
||||
}
|
||||
|
||||
if first_stage.is_none() {
|
||||
first_stage = Some(stage);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to first stage if no stages are supported.
|
||||
let first_stage = first_stage.ok_or(AuthError::NoStageToChoose)?;
|
||||
self.perform_fallback(uiaa_info.session.clone(), first_stage)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Tries to perform the given stage.
|
||||
///
|
||||
/// Returns `None` if the stage is not implemented.
|
||||
async fn try_perform_stage(
|
||||
&self,
|
||||
uiaa_session: Option<&String>,
|
||||
stage: &AuthType,
|
||||
) -> Option<Result<AuthData, AuthError>> {
|
||||
match stage {
|
||||
AuthType::Password => {
|
||||
Some(self.perform_password_stage(uiaa_session.cloned()).await)
|
||||
}
|
||||
AuthType::Sso => Some(self.perform_fallback(uiaa_session.cloned(), stage).await),
|
||||
AuthType::Dummy => Some(Ok(Self::perform_dummy_stage(uiaa_session.cloned()))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs the password stage.
|
||||
async fn perform_password_stage(
|
||||
&self,
|
||||
uiaa_session: Option<String>,
|
||||
) -> Result<AuthData, AuthError> {
|
||||
let Some(session) = self.session.upgrade() else {
|
||||
return Err(AuthError::NoSession);
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
self.password.set_visible(true);
|
||||
self.open_browser_btn.set_visible(false);
|
||||
obj.set_body(&gettext(
|
||||
"Please authenticate the operation with your password",
|
||||
));
|
||||
obj.set_response_enabled("confirm", false);
|
||||
|
||||
self.show_and_wait_for_response().await?;
|
||||
|
||||
let user_id = session.user_id().to_string();
|
||||
let password = self.password.text().into();
|
||||
|
||||
let data = assign!(
|
||||
Password::new(UserIdentifier::UserIdOrLocalpart(user_id), password),
|
||||
{ session: uiaa_session }
|
||||
);
|
||||
|
||||
Ok(AuthData::Password(data))
|
||||
}
|
||||
|
||||
/// Performs the dummy stage.
|
||||
fn perform_dummy_stage(uiaa_session: Option<String>) -> AuthData {
|
||||
AuthData::Dummy(assign!(Dummy::new(), { session: uiaa_session }))
|
||||
}
|
||||
|
||||
/// Performs a web-based fallback for the given stage.
|
||||
async fn perform_fallback(
|
||||
&self,
|
||||
uiaa_session: Option<String>,
|
||||
stage: &AuthType,
|
||||
) -> Result<AuthData, AuthError> {
|
||||
let Some(client) = self.session.upgrade().map(|s| s.client()) else {
|
||||
return Err(AuthError::NoSession);
|
||||
};
|
||||
let uiaa_session = uiaa_session.ok_or(AuthError::MissingSessionId)?;
|
||||
let obj = self.obj();
|
||||
|
||||
self.password.set_visible(false);
|
||||
self.open_browser_btn.set_visible(true);
|
||||
obj.set_body(&gettext(
|
||||
"Please authenticate the operation via the browser and, once completed, press confirm",
|
||||
));
|
||||
obj.set_response_enabled("confirm", false);
|
||||
|
||||
let homeserver = client.homeserver();
|
||||
self.set_up_fallback(homeserver.as_str(), stage.as_ref(), &uiaa_session);
|
||||
|
||||
self.show_and_wait_for_response().await?;
|
||||
|
||||
Ok(AuthData::FallbackAcknowledgement(
|
||||
FallbackAcknowledgement::new(uiaa_session),
|
||||
))
|
||||
}
|
||||
|
||||
/// Let the user complete the current stage.
|
||||
async fn show_and_wait_for_response(&self) -> Result<(), AuthError> {
|
||||
let Some(parent) = self.parent.upgrade() else {
|
||||
return Err(AuthError::NoParentWidget);
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
let (sender, receiver) = futures_channel::oneshot::channel();
|
||||
self.sender.replace(Some(sender));
|
||||
|
||||
// Show this dialog.
|
||||
obj.present(Some(&parent));
|
||||
|
||||
// Wait for the response.
|
||||
let result = receiver.await;
|
||||
|
||||
// Close this dialog.
|
||||
obj.close();
|
||||
|
||||
match result.as_deref() {
|
||||
Ok("confirm") => Ok(()),
|
||||
Ok(_) => Err(AuthError::UserCancelled),
|
||||
Err(_) => {
|
||||
error!("Could not get the response, the channel was closed");
|
||||
Err(AuthError::Unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the given error.
|
||||
fn show_auth_error(&self, auth_error: Option<&StandardErrorBody>) {
|
||||
if let Some(auth_error) = auth_error {
|
||||
self.error.set_label(&auth_error.message);
|
||||
}
|
||||
|
||||
self.error.set_visible(auth_error.is_some());
|
||||
}
|
||||
|
||||
/// Prepare the button to open the web-based fallback with the given
|
||||
/// settings.
|
||||
fn set_up_fallback(&self, homeserver: &str, auth_type: &str, uiaa_session: &str) {
|
||||
if let Some(handler) = self.open_browser_btn_handler.take() {
|
||||
self.open_browser_btn.disconnect(handler);
|
||||
}
|
||||
|
||||
let uri = format!(
|
||||
"{homeserver}_matrix/client/r0/auth/{auth_type}/fallback/web?session={uiaa_session}"
|
||||
);
|
||||
|
||||
let handler = self.open_browser_btn.connect_clicked(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
let uri = uri.clone();
|
||||
spawn!(async move {
|
||||
let Some(parent) = imp.parent.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(error) = gtk::UriLauncher::new(&uri)
|
||||
.launch_future(parent.root().and_downcast_ref::<gtk::Window>())
|
||||
.await
|
||||
{
|
||||
error!("Could not launch URI: {error}");
|
||||
}
|
||||
|
||||
imp.obj().set_response_enabled("confirm", true);
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
self.open_browser_btn_handler.replace(Some(handler));
|
||||
}
|
||||
|
||||
/// Update the confirm response for the current state.
|
||||
#[template_callback]
|
||||
fn update_confirm(&self) {
|
||||
self.obj()
|
||||
.set_response_enabled("confirm", !self.password.text().is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Dialog to guide the user through the [User-Interactive Authentication API] (UIAA).
|
||||
///
|
||||
/// [User-Interactive Authentication API]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api
|
||||
pub struct AuthDialog(ObjectSubclass<imp::AuthDialog>)
|
||||
@extends gtk::Widget, adw::Dialog, adw::AlertDialog, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl AuthDialog {
|
||||
pub fn new(session: &Session) -> Self {
|
||||
glib::Object::builder().property("session", session).build()
|
||||
}
|
||||
|
||||
/// Authenticate the user to the server via an interactive authentication
|
||||
/// flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(crate) async fn authenticate<
|
||||
Response: Send + 'static,
|
||||
F1: Future<Output = Result<Response, Error>> + Send + 'static,
|
||||
FN: Fn(matrix_sdk::Client, Option<AuthData>) -> F1 + Send + 'static + Sync + Clone,
|
||||
>(
|
||||
&self,
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
callback: FN,
|
||||
) -> Result<Response, AuthError> {
|
||||
self.imp().authenticate(parent.upcast_ref(), callback).await
|
||||
}
|
||||
|
||||
/// Reset the cross-signing keys while handling the interactive
|
||||
/// authentication flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(crate) async fn reset_cross_signing(
|
||||
&self,
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
) -> Result<(), AuthError> {
|
||||
self.imp().reset_cross_signing(parent.upcast_ref()).await
|
||||
}
|
||||
}
|
68
src/components/dialogs/auth.ui
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="AuthDialog" parent="AdwAlertDialog">
|
||||
<property name="heading" translatable="yes">Authentication</property>
|
||||
<property name="default-response">confirm</property>
|
||||
<property name="close-response">cancel</property>
|
||||
<property name="extra_child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkPasswordEntry" id="password">
|
||||
<property name="activates-default">True</property>
|
||||
<property name="show-peek-icon">True</property>
|
||||
<signal name="changed" handler="update_confirm" swapped="yes" />
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="open_browser_btn">
|
||||
<property name="can-shrink">true</property>
|
||||
<property name="halign">center</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="pill"/>
|
||||
<class name="large"/>
|
||||
<class name="image-text-button"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Contin_ue</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="mnemonic-widget">open_browser_btn</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">external-link-symbolic</property>
|
||||
<property name="accessible-role">presentation</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="error">
|
||||
<property name="visible">False</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="max-width-chars">60</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<responses>
|
||||
<response id="cancel" translatable="yes">_Cancel</response>
|
||||
<response id="confirm" translatable="yes" appearance="suggested" enabled="false">C_onfirm</response>
|
||||
</responses>
|
||||
</template>
|
||||
</interface>
|
@ -1,104 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, CompositeTemplate};
|
||||
use tracing::error;
|
||||
|
||||
use crate::components::LoadingButton;
|
||||
|
||||
mod imp {
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/dialogs/auth/in_browser_page.ui")]
|
||||
#[properties(wrapper_type = super::AuthDialogInBrowserPage)]
|
||||
pub struct AuthDialogInBrowserPage {
|
||||
#[template_child]
|
||||
pub(super) confirm_button: TemplateChild<LoadingButton>,
|
||||
/// The URL to launch.
|
||||
#[property(get, construct_only)]
|
||||
url: OnceCell<String>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for AuthDialogInBrowserPage {
|
||||
const NAME: &'static str = "AuthDialogInBrowserPage";
|
||||
type Type = super::AuthDialogInBrowserPage;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for AuthDialogInBrowserPage {}
|
||||
|
||||
impl WidgetImpl for AuthDialogInBrowserPage {}
|
||||
impl BinImpl for AuthDialogInBrowserPage {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl AuthDialogInBrowserPage {
|
||||
/// Open the URL in the browser.
|
||||
#[template_callback]
|
||||
async fn launch_url(&self) {
|
||||
let url = self
|
||||
.url
|
||||
.get()
|
||||
.expect("URL should be set during construction");
|
||||
|
||||
if let Err(error) = gtk::UriLauncher::new(url)
|
||||
.launch_future(self.obj().root().and_downcast_ref::<gtk::Window>())
|
||||
.await
|
||||
{
|
||||
error!("Could not launch authentication URI: {error}");
|
||||
}
|
||||
|
||||
self.confirm_button.set_sensitive(true);
|
||||
}
|
||||
|
||||
/// Proceed to authentication with the current password.
|
||||
#[template_callback]
|
||||
fn proceed(&self) {
|
||||
self.confirm_button.set_is_loading(true);
|
||||
let _ = self.obj().activate_action("auth-dialog.continue", None);
|
||||
}
|
||||
|
||||
/// Retry this stage.
|
||||
pub(super) fn retry(&self) {
|
||||
self.confirm_button.set_is_loading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Page to pass a stage in the browser for the [`AuthDialog`].
|
||||
pub struct AuthDialogInBrowserPage(ObjectSubclass<imp::AuthDialogInBrowserPage>)
|
||||
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl AuthDialogInBrowserPage {
|
||||
/// Construct an `AuthDialogInBrowserPage` that will launch the given URL.
|
||||
pub fn new(url: String) -> Self {
|
||||
glib::Object::builder().property("url", url).build()
|
||||
}
|
||||
|
||||
/// Get the default widget of this page.
|
||||
pub fn default_widget(&self) -> >k::Widget {
|
||||
self.imp().confirm_button.upcast_ref()
|
||||
}
|
||||
|
||||
/// Retry this stage.
|
||||
pub fn retry(&self) {
|
||||
self.imp().retry();
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="AuthDialogInBrowserPage" parent="AdwBin">
|
||||
<property name="child">
|
||||
<object class="GtkWindowHandle">
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="propagate-natural-width">True</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<style>
|
||||
<class name="body-scrolled-window"/>
|
||||
<class name="undershoot-bottom"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="message-area"/>
|
||||
<class name="has-heading"/>
|
||||
<class name="has-body"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<style>
|
||||
<class name="heading-bin"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="justify">center</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="label" translatable="yes">Authentication</property>
|
||||
<style>
|
||||
<class name="title-2"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="label" translatable="yes">Please authenticate the operation via the browser and, once completed, press confirm</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="launch_url_btn">
|
||||
<property name="margin-top">6</property>
|
||||
<property name="can-shrink">true</property>
|
||||
<property name="halign">center</property>
|
||||
<signal name="clicked" handler="launch_url" swapped="yes"/>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="pill"/>
|
||||
<class name="large"/>
|
||||
<class name="image-text-button"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Contin_ue</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="mnemonic-widget">launch_url_btn</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">external-link-symbolic</property>
|
||||
<property name="accessible-role">presentation</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwWrapBox">
|
||||
<property name="child-spacing">12</property>
|
||||
<property name="line-spacing">12</property>
|
||||
<property name="justify">fill</property>
|
||||
<property name="justify-last-line">True</property>
|
||||
<style>
|
||||
<class name="response-area"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="can-shrink">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="label" translatable="yes">_Cancel</property>
|
||||
<property name="action-name">auth-dialog.close</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="LoadingButton" id="confirm_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-shrink">True</property>
|
||||
<property name="content-label" translatable="yes">C_onfirm</property>
|
||||
<signal name="clicked" handler="proceed" swapped="yes"/>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
@ -1,635 +0,0 @@
|
||||
use std::{fmt::Debug, future::Future};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use futures_channel::oneshot;
|
||||
use gettextrs::gettext;
|
||||
use gtk::{glib, glib::clone, CompositeTemplate};
|
||||
use matrix_sdk::{encryption::CrossSigningResetAuthType, Error};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::uiaa::{
|
||||
get_uiaa_fallback_page, AuthData, AuthType, Dummy, FallbackAcknowledgement, Password,
|
||||
UiaaInfo, UserIdentifier,
|
||||
},
|
||||
MatrixVersion, OutgoingRequest, SendAccessToken,
|
||||
},
|
||||
assign,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
mod in_browser_page;
|
||||
mod password_page;
|
||||
|
||||
use self::{in_browser_page::AuthDialogInBrowserPage, password_page::AuthDialogPasswordPage};
|
||||
use crate::{components::ToastableDialog, prelude::*, session::model::Session, spawn_tokio, toast};
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/dialogs/auth/mod.ui")]
|
||||
#[properties(wrapper_type = super::AuthDialog)]
|
||||
pub struct AuthDialog {
|
||||
#[template_child]
|
||||
stack: TemplateChild<gtk::Stack>,
|
||||
/// The parent session.
|
||||
#[property(get, set, construct_only)]
|
||||
session: glib::WeakRef<Session>,
|
||||
/// Whether this dialog is presented.
|
||||
is_presented: Cell<bool>,
|
||||
/// The current state of the authentication.
|
||||
///
|
||||
/// `None` means that the authentication has not started yet.
|
||||
state: RefCell<Option<AuthState>>,
|
||||
/// The page for the current stage.
|
||||
current_page: RefCell<Option<gtk::Widget>>,
|
||||
/// The sender to get the signal to perform the current stage.
|
||||
sender: RefCell<Option<oneshot::Sender<()>>>,
|
||||
/// The handle to abort the current future.
|
||||
abort_handle: RefCell<Option<tokio::task::AbortHandle>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for AuthDialog {
|
||||
const NAME: &'static str = "AuthDialog";
|
||||
type Type = super::AuthDialog;
|
||||
type ParentType = ToastableDialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
|
||||
klass.install_action("auth-dialog.continue", None, |obj, _, _| {
|
||||
if let Some(sender) = obj.imp().sender.take() {
|
||||
let _ = sender.send(());
|
||||
}
|
||||
});
|
||||
|
||||
klass.install_action("auth-dialog.close", None, |obj, _, _| {
|
||||
obj.imp().close();
|
||||
});
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for AuthDialog {
|
||||
fn dispose(&self) {
|
||||
if let Some(abort_handle) = self.abort_handle.take() {
|
||||
abort_handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for AuthDialog {}
|
||||
impl AdwDialogImpl for AuthDialog {}
|
||||
impl ToastableDialogImpl for AuthDialog {}
|
||||
|
||||
impl AuthDialog {
|
||||
/// Authenticate the user to the server via an interactive
|
||||
/// authentication flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(super) async fn authenticate<Response, Fut, FN>(
|
||||
&self,
|
||||
parent: >k::Widget,
|
||||
callback: FN,
|
||||
) -> Result<Response, AuthError>
|
||||
where
|
||||
Response: Send + 'static,
|
||||
Fut: Future<Output = Result<Response, Error>> + Send + 'static,
|
||||
FN: Fn(matrix_sdk::Client, Option<AuthData>) -> Fut + Send + Sync + 'static + Clone,
|
||||
{
|
||||
let Some(client) = self.session.upgrade().map(|s| s.client()) else {
|
||||
return Err(AuthError::Unknown);
|
||||
};
|
||||
|
||||
// Perform the request once, to see if UIAA if required.
|
||||
let callback_clone = callback.clone();
|
||||
let client_clone = client.clone();
|
||||
let handle = spawn_tokio!(async move { callback_clone(client_clone, None).await });
|
||||
let result = self.await_tokio_task(handle).await;
|
||||
|
||||
// If this is a UIAA error, we need authentication.
|
||||
let Some(uiaa_info) = result.uiaa_info() else {
|
||||
return result;
|
||||
};
|
||||
|
||||
let result = self
|
||||
.perform_uiaa(uiaa_info.clone(), parent, move |auth_data| {
|
||||
let client = client.clone();
|
||||
let callback = callback.clone();
|
||||
async move { callback(client, Some(auth_data)).await }
|
||||
})
|
||||
.await;
|
||||
|
||||
self.close();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Reset the cross-signing keys while handling the interactive
|
||||
/// authentication flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication.
|
||||
///
|
||||
/// Note that due to the implementation of the underlying SDK API, this
|
||||
/// will not work if there are several stages in the flow.
|
||||
///
|
||||
/// Returns the last server response on success.
|
||||
pub(super) async fn reset_cross_signing(
|
||||
&self,
|
||||
parent: >k::Widget,
|
||||
) -> Result<(), AuthError> {
|
||||
let Some(encryption) = self.session.upgrade().map(|s| s.client().encryption()) else {
|
||||
return Err(AuthError::Unknown);
|
||||
};
|
||||
|
||||
let handle = spawn_tokio!(async move { encryption.reset_cross_signing().await });
|
||||
let result = self.await_tokio_task(handle).await?;
|
||||
|
||||
let Some(cross_signing_reset_handle) = result else {
|
||||
// No authentication is needed.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let result = match cross_signing_reset_handle.auth_type().clone() {
|
||||
CrossSigningResetAuthType::Uiaa(uiaa_info) => {
|
||||
let cross_signing_reset_handle = Arc::new(cross_signing_reset_handle);
|
||||
|
||||
self.perform_uiaa(uiaa_info, parent, move |auth_data| {
|
||||
let cross_signing_reset_handle = cross_signing_reset_handle.clone();
|
||||
async move { cross_signing_reset_handle.auth(Some(auth_data)).await }
|
||||
})
|
||||
.await
|
||||
}
|
||||
CrossSigningResetAuthType::OAuth(info) => {
|
||||
// This is a special stage, which requires opening a URL in the browser.
|
||||
let page = AuthDialogInBrowserPage::new(info.approval_url.to_string());
|
||||
let default_widget = page.default_widget().clone();
|
||||
|
||||
self.show_page(page.upcast(), &default_widget, parent);
|
||||
|
||||
// The `CrossSigningResetHandle` will poll the endpoint until it succeeds.
|
||||
let handle =
|
||||
spawn_tokio!(async move { cross_signing_reset_handle.auth(None).await });
|
||||
self.await_tokio_task(handle).await
|
||||
}
|
||||
};
|
||||
|
||||
self.close();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Await the given tokio task, handling if it is aborted.
|
||||
async fn await_tokio_task<Response>(
|
||||
&self,
|
||||
handle: JoinHandle<Result<Response, Error>>,
|
||||
) -> Result<Response, AuthError>
|
||||
where
|
||||
Response: Send + 'static,
|
||||
{
|
||||
self.abort_handle.replace(Some(handle.abort_handle()));
|
||||
|
||||
let Ok(result) = handle.await else {
|
||||
// The future was aborted, which means that the user closed the dialog.
|
||||
return Err(AuthError::UserCancelled);
|
||||
};
|
||||
|
||||
self.abort_handle.take();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Perform UIAA for the given callback, starting with the given UIAA
|
||||
/// info.
|
||||
async fn perform_uiaa<Response, Fut, FN>(
|
||||
&self,
|
||||
mut uiaa_info: UiaaInfo,
|
||||
parent: >k::Widget,
|
||||
callback: FN,
|
||||
) -> Result<Response, AuthError>
|
||||
where
|
||||
Response: Send + 'static,
|
||||
Fut: Future<Output = Result<Response, Error>> + Send + 'static,
|
||||
FN: Fn(AuthData) -> Fut + Send + Sync + 'static + Clone,
|
||||
{
|
||||
loop {
|
||||
let callback = callback.clone();
|
||||
|
||||
let auth_data = self.perform_next_stage(&uiaa_info, parent).await?;
|
||||
|
||||
// Get the current state of the authentication.
|
||||
let handle = spawn_tokio!(async move { callback(auth_data).await });
|
||||
let result = self.await_tokio_task(handle).await;
|
||||
|
||||
// If this is a UIAA error, authentication continues.
|
||||
let Some(next_uiaa_info) = result.uiaa_info() else {
|
||||
return result;
|
||||
};
|
||||
|
||||
uiaa_info = next_uiaa_info.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform the preferred next stage in the given UIAA info.
|
||||
///
|
||||
/// Stages that are actually supported are preferred. If no stages are
|
||||
/// supported, we use the web-based fallback.
|
||||
///
|
||||
/// When this function returns, the next stage is ready to be performed.
|
||||
async fn perform_next_stage(
|
||||
&self,
|
||||
uiaa_info: &UiaaInfo,
|
||||
parent: >k::Widget,
|
||||
) -> Result<AuthData, AuthError> {
|
||||
let Some(next_state) = AuthState::next(uiaa_info) else {
|
||||
// There is no stage left, this should not happen.
|
||||
error!("Cannot perform next stage when flow is complete");
|
||||
return Err(AuthError::Unknown);
|
||||
};
|
||||
|
||||
if matches!(next_state.stage, AuthType::Dummy) {
|
||||
// We can just do this stage without waiting for user input.
|
||||
self.state.replace(Some(next_state));
|
||||
return self.current_stage_auth_data();
|
||||
}
|
||||
|
||||
let (sender, receiver) = futures_channel::oneshot::channel();
|
||||
self.sender.replace(Some(sender));
|
||||
|
||||
// If the stage didn't succeed, we get the same state again.
|
||||
let is_same_state = self
|
||||
.state
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|state| *state == next_state);
|
||||
|
||||
if is_same_state {
|
||||
self.retry_current_stage(&next_state.stage, uiaa_info);
|
||||
} else {
|
||||
let (next_page, default_widget) = self.page(&next_state).await?;
|
||||
self.show_page(next_page, &default_widget, parent);
|
||||
self.state.replace(Some(next_state));
|
||||
}
|
||||
|
||||
if receiver.await.is_err() {
|
||||
// The sender was dropped, which means that the user closed the dialog.
|
||||
return Err(AuthError::UserCancelled);
|
||||
}
|
||||
|
||||
self.sender.take();
|
||||
|
||||
self.current_stage_auth_data()
|
||||
}
|
||||
|
||||
// Retry the current stage.
|
||||
fn retry_current_stage(&self, stage: &AuthType, uiaa_info: &UiaaInfo) {
|
||||
// Show the authentication error, if there is one.
|
||||
if let Some(error) = &uiaa_info.auth_error {
|
||||
warn!("Could not perform authentication stage: {}", error.message);
|
||||
|
||||
if matches!(stage, AuthType::Password) {
|
||||
toast!(self.stack, gettext("The password is invalid."));
|
||||
} else {
|
||||
toast!(self.stack, gettext("An unexpected error occurred."));
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the loading state of the page.
|
||||
if let Some(page) = self.current_page.borrow().as_ref() {
|
||||
if let Some(password_page) = page.downcast_ref::<AuthDialogPasswordPage>() {
|
||||
password_page.retry();
|
||||
} else if let Some(in_browser_page) = page.downcast_ref::<AuthDialogInBrowserPage>()
|
||||
{
|
||||
in_browser_page.retry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the given page.
|
||||
fn show_page(&self, page: gtk::Widget, default_widget: >k::Widget, parent: >k::Widget) {
|
||||
self.stack.add_child(&page);
|
||||
self.stack.set_visible_child(&page);
|
||||
self.obj().set_default_widget(Some(default_widget));
|
||||
|
||||
let prev_page = self.current_page.replace(Some(page));
|
||||
|
||||
// Remove the previous page from the stack when the transition is over.
|
||||
if let Some(page) = prev_page {
|
||||
let cell = Rc::new(RefCell::new(None));
|
||||
|
||||
let handler = self.stack.connect_transition_running_notify(clone!(
|
||||
#[strong]
|
||||
cell,
|
||||
#[strong]
|
||||
page,
|
||||
move |stack| {
|
||||
if !stack.is_transition_running()
|
||||
&& stack.visible_child().is_some_and(|child| child != page)
|
||||
{
|
||||
stack.remove(&page);
|
||||
|
||||
if let Some(handler) = cell.take() {
|
||||
stack.disconnect(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
cell.replace(Some(handler));
|
||||
}
|
||||
|
||||
// Present the dialog if it is not already the case.
|
||||
if !self.is_presented.get() {
|
||||
self.obj().present(Some(parent));
|
||||
self.is_presented.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the page for the given state.
|
||||
///
|
||||
/// Returns a `(page, default_widget)` tuple.
|
||||
async fn page(&self, state: &AuthState) -> Result<(gtk::Widget, gtk::Widget), AuthError> {
|
||||
if state.stage == AuthType::Password {
|
||||
let page = AuthDialogPasswordPage::new();
|
||||
let default_widget = page.default_widget().clone();
|
||||
Ok((page.upcast(), default_widget))
|
||||
} else {
|
||||
let fallback_url = self.fallback_url(state).await?;
|
||||
let page = AuthDialogInBrowserPage::new(fallback_url);
|
||||
let default_widget = page.default_widget().clone();
|
||||
Ok((page.upcast(), default_widget))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the fallback URL for the given state.
|
||||
async fn fallback_url(&self, state: &AuthState) -> Result<String, AuthError> {
|
||||
let Some(session) = self.session.upgrade() else {
|
||||
return Err(AuthError::Unknown);
|
||||
};
|
||||
|
||||
let uiaa_session = state.session.clone().ok_or(AuthError::MissingSessionId)?;
|
||||
|
||||
let request =
|
||||
get_uiaa_fallback_page::v3::Request::new(state.stage.to_string(), uiaa_session);
|
||||
|
||||
let client = session.client();
|
||||
let homeserver = client.homeserver();
|
||||
|
||||
let handle =
|
||||
spawn_tokio!(async move { client.server_versions().await.map_err(Into::into) });
|
||||
let result = self.await_tokio_task(handle).await;
|
||||
|
||||
let server_versions = match result {
|
||||
Ok(server_versions) => server_versions,
|
||||
Err(AuthError::ServerResponse(server_error)) => {
|
||||
warn!("Could not get Matrix versions supported by homeserver: {server_error}");
|
||||
// Default to the v3 endpoint.
|
||||
Box::new([MatrixVersion::V1_1])
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let http_request = match request.try_into_http_request::<Vec<u8>>(
|
||||
homeserver.as_ref(),
|
||||
SendAccessToken::None,
|
||||
&server_versions,
|
||||
) {
|
||||
Ok(http_request) => http_request,
|
||||
Err(error) => {
|
||||
error!("Could not construct fallback UIAA URL: {error}");
|
||||
return Err(AuthError::Unknown);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(http_request.uri().to_string())
|
||||
}
|
||||
|
||||
/// Get the authentication data for the current stage.
|
||||
fn current_stage_auth_data(&self) -> Result<AuthData, AuthError> {
|
||||
let Some(state) = self.state.borrow().clone() else {
|
||||
error!("Could not get current authentication state");
|
||||
return Err(AuthError::Unknown);
|
||||
};
|
||||
|
||||
let auth_data = match state.stage {
|
||||
AuthType::Password => {
|
||||
let password = self
|
||||
.current_page
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.and_then(|page| page.downcast_ref::<AuthDialogPasswordPage>())
|
||||
.ok_or_else(|| {
|
||||
error!(
|
||||
"Could not get password because current page is not password page"
|
||||
);
|
||||
AuthError::Unknown
|
||||
})?
|
||||
.password();
|
||||
|
||||
let user_id = self
|
||||
.session
|
||||
.upgrade()
|
||||
.ok_or(AuthError::Unknown)?
|
||||
.user_id()
|
||||
.to_string();
|
||||
|
||||
AuthData::Password(assign!(
|
||||
Password::new(UserIdentifier::UserIdOrLocalpart(user_id), password),
|
||||
{ session: state.session }
|
||||
))
|
||||
}
|
||||
AuthType::Dummy => AuthData::Dummy(assign!(Dummy::new(), {
|
||||
session: state.session
|
||||
})),
|
||||
_ => {
|
||||
let uiaa_session = state.session.ok_or(AuthError::MissingSessionId)?;
|
||||
|
||||
AuthData::FallbackAcknowledgement(FallbackAcknowledgement::new(uiaa_session))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(auth_data)
|
||||
}
|
||||
|
||||
// Close the dialog and cancel any ongoing task.
|
||||
fn close(&self) {
|
||||
if self.is_presented.get() {
|
||||
self.obj().close();
|
||||
}
|
||||
|
||||
if let Some(abort_handle) = self.abort_handle.take() {
|
||||
abort_handle.abort();
|
||||
}
|
||||
|
||||
self.sender.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Dialog to guide the user through the [User-Interactive Authentication API] (UIAA).
|
||||
///
|
||||
/// [User-Interactive Authentication API]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api
|
||||
pub struct AuthDialog(ObjectSubclass<imp::AuthDialog>)
|
||||
@extends gtk::Widget, adw::Dialog, ToastableDialog,
|
||||
@implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl AuthDialog {
|
||||
pub fn new(session: &Session) -> Self {
|
||||
glib::Object::builder().property("session", session).build()
|
||||
}
|
||||
|
||||
/// Authenticate the user to the server via an interactive authentication
|
||||
/// flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(crate) async fn authenticate<Response, Fut, FN>(
|
||||
&self,
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
callback: FN,
|
||||
) -> Result<Response, AuthError>
|
||||
where
|
||||
Response: Send + 'static,
|
||||
Fut: Future<Output = Result<Response, Error>> + Send + 'static,
|
||||
FN: Fn(matrix_sdk::Client, Option<AuthData>) -> Fut + Send + Sync + 'static + Clone,
|
||||
{
|
||||
self.imp().authenticate(parent.upcast_ref(), callback).await
|
||||
}
|
||||
|
||||
/// Reset the cross-signing keys while handling the interactive
|
||||
/// authentication flow.
|
||||
///
|
||||
/// The type of flow and the required stages are negotiated during the
|
||||
/// authentication. Returns the last server response on success.
|
||||
pub(crate) async fn reset_cross_signing(
|
||||
&self,
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
) -> Result<(), AuthError> {
|
||||
self.imp().reset_cross_signing(parent.upcast_ref()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Data about the current authentication state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AuthState {
|
||||
/// The completed stages.
|
||||
completed: Vec<AuthType>,
|
||||
|
||||
/// The current stage.
|
||||
stage: AuthType,
|
||||
|
||||
/// The ID of the authentication session.
|
||||
session: Option<String>,
|
||||
}
|
||||
|
||||
impl AuthState {
|
||||
/// Try to construct the next `AuthState` from the given UIAA info.
|
||||
///
|
||||
/// Returns `None` if the next stage could not be determined.
|
||||
fn next(uiaa_info: &UiaaInfo) -> Option<Self> {
|
||||
// Find the possible next stages.
|
||||
// These are the next stage in flows that have the same stages as the ones we
|
||||
// have completed.
|
||||
let stages = uiaa_info
|
||||
.flows
|
||||
.iter()
|
||||
.filter_map(|flow| flow.stages.strip_prefix(uiaa_info.completed.as_slice()))
|
||||
.filter_map(|stages_left| stages_left.first());
|
||||
|
||||
// Now get the first stage that we support.
|
||||
let mut next_stage = None;
|
||||
for stage in stages {
|
||||
if matches!(stage, AuthType::Password | AuthType::Sso | AuthType::Dummy) {
|
||||
// We found a supported stage.
|
||||
next_stage = Some(stage);
|
||||
break;
|
||||
} else if next_stage.is_none() {
|
||||
// We will default to the first stage if we do not find one that we support.
|
||||
next_stage = Some(stage);
|
||||
}
|
||||
}
|
||||
|
||||
let stage = next_stage?.clone();
|
||||
|
||||
Some(Self {
|
||||
completed: uiaa_info.completed.clone(),
|
||||
stage,
|
||||
session: uiaa_info.session.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An error during UIAA interaction.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
/// The server returned a non-UIAA error.
|
||||
#[error(transparent)]
|
||||
ServerResponse(#[from] Error),
|
||||
|
||||
/// The ID of the UIAA session is missing for a stage that requires it.
|
||||
#[error("The ID of the session is missing")]
|
||||
MissingSessionId,
|
||||
|
||||
/// The user cancelled the authentication.
|
||||
#[error("The user cancelled the authentication")]
|
||||
UserCancelled,
|
||||
|
||||
/// An unexpected error occurred.
|
||||
#[error("An unexpected error occurred")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Helper trait to extract [`UiaaInfo`].
|
||||
trait ExtractUiaa {
|
||||
/// Extract the [`UiaaInfo`] from this type, if it contains one.
|
||||
fn uiaa_info(&self) -> Option<&UiaaInfo>;
|
||||
}
|
||||
|
||||
impl ExtractUiaa for AuthError {
|
||||
fn uiaa_info(&self) -> Option<&UiaaInfo> {
|
||||
if let Self::ServerResponse(server_error) = self {
|
||||
server_error.as_uiaa_response()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractUiaa for Error {
|
||||
fn uiaa_info(&self) -> Option<&UiaaInfo> {
|
||||
self.as_uiaa_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Err> ExtractUiaa for Result<T, Err>
|
||||
where
|
||||
Err: ExtractUiaa,
|
||||
{
|
||||
fn uiaa_info(&self) -> Option<&UiaaInfo> {
|
||||
match self {
|
||||
Ok(_) => None,
|
||||
Err(error) => error.uiaa_info(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="AuthDialog" parent="ToastableDialog">
|
||||
<property name="title" translatable="yes">Authentication</property>
|
||||
<property name="follows-content-size">True</property>
|
||||
<property name="presentation-mode">floating</property>
|
||||
<style>
|
||||
<class name="alert"/>
|
||||
</style>
|
||||
<property name="child-content">
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="transition-type">slide-left</property>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
@ -1,99 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, CompositeTemplate};
|
||||
|
||||
use crate::components::LoadingButton;
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/dialogs/auth/password_page.ui")]
|
||||
pub struct AuthDialogPasswordPage {
|
||||
#[template_child]
|
||||
pub(super) password: TemplateChild<gtk::PasswordEntry>,
|
||||
#[template_child]
|
||||
pub(super) confirm_button: TemplateChild<LoadingButton>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for AuthDialogPasswordPage {
|
||||
const NAME: &'static str = "AuthDialogPasswordPage";
|
||||
type Type = super::AuthDialogPasswordPage;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for AuthDialogPasswordPage {}
|
||||
impl WidgetImpl for AuthDialogPasswordPage {}
|
||||
impl BinImpl for AuthDialogPasswordPage {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl AuthDialogPasswordPage {
|
||||
/// Whether the user can proceed given the current state.
|
||||
fn can_proceed(&self) -> bool {
|
||||
!self.password.text().is_empty()
|
||||
}
|
||||
|
||||
/// Update the confirm button for the current state.
|
||||
#[template_callback]
|
||||
fn update_confirm(&self) {
|
||||
self.confirm_button.set_sensitive(self.can_proceed());
|
||||
}
|
||||
|
||||
/// Proceed to authentication with the current password.
|
||||
#[template_callback]
|
||||
fn proceed(&self) {
|
||||
if !self.can_proceed() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.confirm_button.set_is_loading(true);
|
||||
let _ = self.obj().activate_action("auth-dialog.continue", None);
|
||||
}
|
||||
|
||||
/// Retry this stage.
|
||||
pub(super) fn retry(&self) {
|
||||
self.confirm_button.set_is_loading(false);
|
||||
self.update_confirm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Page to pass the password stage for the [`AuthDialog`].
|
||||
pub struct AuthDialogPasswordPage(ObjectSubclass<imp::AuthDialogPasswordPage>)
|
||||
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl AuthDialogPasswordPage {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
/// Get the default widget of this page.
|
||||
pub fn default_widget(&self) -> >k::Widget {
|
||||
self.imp().confirm_button.upcast_ref()
|
||||
}
|
||||
|
||||
/// Get the current password in the entry.
|
||||
pub fn password(&self) -> String {
|
||||
self.imp().password.text().into()
|
||||
}
|
||||
|
||||
/// Retry this stage.
|
||||
pub fn retry(&self) {
|
||||
self.imp().retry();
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="AuthDialogPasswordPage" parent="AdwBin">
|
||||
<property name="child">
|
||||
<object class="GtkWindowHandle">
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="propagate-natural-width">True</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<style>
|
||||
<class name="body-scrolled-window"/>
|
||||
<class name="undershoot-bottom"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="message-area"/>
|
||||
<class name="has-heading"/>
|
||||
<class name="has-body"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<style>
|
||||
<class name="heading-bin"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="justify">center</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="label" translatable="yes">Authentication</property>
|
||||
<style>
|
||||
<class name="title-2"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="label" translatable="yes">Please authenticate the operation with your password</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPasswordEntry" id="password">
|
||||
<property name="margin-top">6</property>
|
||||
<property name="margin-start">24</property>
|
||||
<property name="margin-end">24</property>
|
||||
<property name="activates-default">True</property>
|
||||
<property name="show-peek-icon">True</property>
|
||||
<signal name="changed" handler="update_confirm" swapped="yes" />
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwWrapBox">
|
||||
<property name="child-spacing">12</property>
|
||||
<property name="line-spacing">12</property>
|
||||
<property name="justify">fill</property>
|
||||
<property name="justify-last-line">True</property>
|
||||
<style>
|
||||
<class name="response-area"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="can-shrink">True</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="label" translatable="yes">_Cancel</property>
|
||||
<property name="action-name">auth-dialog.close</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="LoadingButton" id="confirm_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-shrink">True</property>
|
||||
<property name="content-label" translatable="yes">C_onfirm</property>
|
||||
<signal name="clicked" handler="proceed" swapped="yes"/>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
@ -24,9 +24,9 @@ mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/dialogs/room_preview.ui")]
|
||||
#[properties(wrapper_type = super::RoomPreviewDialog)]
|
||||
pub struct RoomPreviewDialog {
|
||||
#[template(resource = "/org/gnome/Fractal/ui/components/dialogs/join_room.ui")]
|
||||
#[properties(wrapper_type = super::JoinRoomDialog)]
|
||||
pub struct JoinRoomDialog {
|
||||
#[template_child]
|
||||
go_back_btn: TemplateChild<gtk::Button>,
|
||||
#[template_child]
|
||||
@ -50,7 +50,7 @@ mod imp {
|
||||
#[template_child]
|
||||
room_members_count: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
view_or_join_btn: TemplateChild<LoadingButton>,
|
||||
join_btn: TemplateChild<LoadingButton>,
|
||||
/// The current session.
|
||||
#[property(get, set = Self::set_session, construct_only)]
|
||||
session: glib::WeakRef<Session>,
|
||||
@ -61,14 +61,12 @@ mod imp {
|
||||
room: RefCell<Option<RemoteRoom>>,
|
||||
/// Whether the "Go back" button is disabled.
|
||||
disable_go_back: Cell<bool>,
|
||||
room_loading_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
room_list_info_handlers: RefCell<Vec<glib::SignalHandlerId>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for RoomPreviewDialog {
|
||||
const NAME: &'static str = "RoomPreviewDialog";
|
||||
type Type = super::RoomPreviewDialog;
|
||||
impl ObjectSubclass for JoinRoomDialog {
|
||||
const NAME: &'static str = "JoinRoomDialog";
|
||||
type Type = super::JoinRoomDialog;
|
||||
type ParentType = ToastableDialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
@ -82,7 +80,7 @@ mod imp {
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for RoomPreviewDialog {
|
||||
impl ObjectImpl for JoinRoomDialog {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
@ -107,18 +105,14 @@ mod imp {
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.disconnect_signals();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for RoomPreviewDialog {}
|
||||
impl AdwDialogImpl for RoomPreviewDialog {}
|
||||
impl ToastableDialogImpl for RoomPreviewDialog {}
|
||||
impl WidgetImpl for JoinRoomDialog {}
|
||||
impl AdwDialogImpl for JoinRoomDialog {}
|
||||
impl ToastableDialogImpl for JoinRoomDialog {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl RoomPreviewDialog {
|
||||
impl JoinRoomDialog {
|
||||
/// Set the current session.
|
||||
fn set_session(&self, session: Option<&Session>) {
|
||||
self.session.set(session);
|
||||
@ -137,64 +131,39 @@ mod imp {
|
||||
}
|
||||
|
||||
/// Set the room that is previewed.
|
||||
pub(super) fn set_room(&self, room: &RemoteRoom) {
|
||||
if self.room.borrow().as_ref().is_some_and(|r| r == room) {
|
||||
pub(super) fn set_room(&self, room: Option<RemoteRoom>) {
|
||||
if *self.room.borrow() == room {
|
||||
return;
|
||||
}
|
||||
|
||||
self.disconnect_signals();
|
||||
self.room.replace(room.clone());
|
||||
|
||||
let room_list_info = room.room_list_info();
|
||||
let is_joining_handler = room_list_info.connect_is_joining_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_view_or_join_button();
|
||||
}
|
||||
));
|
||||
let local_room_handler = room_list_info.connect_local_room_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_view_or_join_button();
|
||||
}
|
||||
));
|
||||
self.room_list_info_handlers
|
||||
.replace(vec![is_joining_handler, local_room_handler]);
|
||||
|
||||
self.room.replace(Some(room.clone()));
|
||||
|
||||
if matches!(
|
||||
room.loading_state(),
|
||||
LoadingState::Ready | LoadingState::Error
|
||||
) {
|
||||
self.fill_details();
|
||||
} else {
|
||||
let room_loading_handler = room.connect_loading_state_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |room| {
|
||||
if matches!(
|
||||
room.loading_state(),
|
||||
LoadingState::Ready | LoadingState::Error
|
||||
) {
|
||||
if let Some(handler) = imp.room_loading_handler.take() {
|
||||
room.disconnect(handler);
|
||||
if let Some(room) = room {
|
||||
if matches!(
|
||||
room.loading_state(),
|
||||
LoadingState::Ready | LoadingState::Error
|
||||
) {
|
||||
self.fill_details();
|
||||
} else {
|
||||
room.connect_loading_state_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |room| {
|
||||
if matches!(
|
||||
room.loading_state(),
|
||||
LoadingState::Ready | LoadingState::Error
|
||||
) {
|
||||
imp.fill_details();
|
||||
}
|
||||
|
||||
imp.fill_details();
|
||||
}
|
||||
}
|
||||
));
|
||||
self.room_loading_handler
|
||||
.replace(Some(room_loading_handler));
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.update_view_or_join_button();
|
||||
self.obj().notify_room();
|
||||
}
|
||||
|
||||
/// Set whether to disable the "Go back" button.
|
||||
/// Se whether to disable the "Go back" button.
|
||||
pub(super) fn disable_go_back(&self, disable: bool) {
|
||||
self.disable_go_back.set(disable);
|
||||
}
|
||||
@ -230,11 +199,7 @@ mod imp {
|
||||
let id = uri.id.clone();
|
||||
self.uri.replace(Some(uri));
|
||||
|
||||
if session
|
||||
.room_list()
|
||||
.get_by_identifier(&id)
|
||||
.is_some_and(|room| room.is_joined())
|
||||
{
|
||||
if session.room_list().joined_room(&id).is_some() {
|
||||
// Translators: This is a verb, as in 'View Room'.
|
||||
self.look_up_btn.set_content_label(gettext("View"));
|
||||
} else {
|
||||
@ -275,9 +240,10 @@ mod imp {
|
||||
|
||||
// Reset state before switching to possible pages.
|
||||
self.go_back_btn.set_sensitive(true);
|
||||
self.join_btn.set_is_loading(false);
|
||||
|
||||
let room = session.remote_cache().room(uri);
|
||||
self.set_room(&room);
|
||||
let room = RemoteRoom::new(&session, uri);
|
||||
self.set_room(Some(room));
|
||||
}
|
||||
|
||||
/// Fill the details with the given result.
|
||||
@ -288,7 +254,7 @@ mod imp {
|
||||
|
||||
self.room_name.set_label(&room.display_name());
|
||||
|
||||
let alias = room.canonical_alias();
|
||||
let alias = room.alias();
|
||||
if let Some(alias) = &alias {
|
||||
self.room_alias.set_label(alias.as_str());
|
||||
}
|
||||
@ -334,50 +300,18 @@ mod imp {
|
||||
self.set_visible_page("details");
|
||||
}
|
||||
|
||||
/// Update the button for viewing or joining the previewed room given
|
||||
/// the current state.
|
||||
fn update_view_or_join_button(&self) {
|
||||
let Some(room) = self.room.borrow().clone() else {
|
||||
return;
|
||||
};
|
||||
let room_list_info = room.room_list_info();
|
||||
|
||||
let label = if room_list_info.local_room().is_some() {
|
||||
gettext("View")
|
||||
} else {
|
||||
gettext("Join")
|
||||
};
|
||||
self.view_or_join_btn.set_content_label(label);
|
||||
self.view_or_join_btn
|
||||
.set_is_loading(room_list_info.is_joining());
|
||||
}
|
||||
|
||||
/// View or join the room that was previewed.
|
||||
/// Join the room that was entered, if it is valid.
|
||||
#[template_callback]
|
||||
async fn view_or_join_room(&self) {
|
||||
let Some(room) = self.room.borrow().clone() else {
|
||||
async fn join_room(&self) {
|
||||
let Some(session) = self.session.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(local_room) = room.room_list_info().local_room() {
|
||||
let obj = self.obj();
|
||||
|
||||
if let Some(window) = obj.root().and_downcast_ref::<Window>() {
|
||||
window.session_view().select_room(local_room);
|
||||
obj.close();
|
||||
}
|
||||
} else {
|
||||
self.join_room(&room).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Join the room that was previewed, if it is valid.
|
||||
async fn join_room(&self, room: &RemoteRoom) {
|
||||
let Some(session) = self.session.upgrade() else {
|
||||
let Some(room) = self.room.borrow().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.go_back_btn.set_sensitive(false);
|
||||
self.join_btn.set_is_loading(true);
|
||||
|
||||
// Join the room with the given identifier.
|
||||
let room_list = session.room_list();
|
||||
@ -387,17 +321,19 @@ mod imp {
|
||||
Ok(room_id) => {
|
||||
let obj = self.obj();
|
||||
|
||||
if let Some(local_room) = room_list.get_wait(&room_id, None).await {
|
||||
if let Some(room) = room_list.get_wait(&room_id).await {
|
||||
if let Some(window) = obj.root().and_downcast_ref::<Window>() {
|
||||
window.session_view().select_room(local_room);
|
||||
window.session_view().select_room(Some(room));
|
||||
}
|
||||
}
|
||||
|
||||
obj.close();
|
||||
}
|
||||
Err(error) => {
|
||||
toast!(self.obj(), error);
|
||||
let obj = self.obj();
|
||||
toast!(obj, error);
|
||||
|
||||
self.join_btn.set_is_loading(false);
|
||||
self.go_back_btn.set_sensitive(true);
|
||||
}
|
||||
}
|
||||
@ -417,32 +353,17 @@ mod imp {
|
||||
self.obj().close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect the signal handlers of this dialog.
|
||||
fn disconnect_signals(&self) {
|
||||
if let Some(room) = self.room.borrow().as_ref() {
|
||||
if let Some(handler) = self.room_loading_handler.take() {
|
||||
room.disconnect(handler);
|
||||
}
|
||||
|
||||
let room_list_info = room.room_list_info();
|
||||
for handler in self.room_list_info_handlers.take() {
|
||||
room_list_info.disconnect(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Dialog to preview a room and eventually join it.
|
||||
pub struct RoomPreviewDialog(ObjectSubclass<imp::RoomPreviewDialog>)
|
||||
@extends gtk::Widget, adw::Dialog, ToastableDialog,
|
||||
@implements gtk::Accessible;
|
||||
/// Dialog to join a room.
|
||||
pub struct JoinRoomDialog(ObjectSubclass<imp::JoinRoomDialog>)
|
||||
@extends gtk::Widget, adw::Dialog, ToastableDialog, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl RoomPreviewDialog {
|
||||
impl JoinRoomDialog {
|
||||
pub fn new(session: &Session) -> Self {
|
||||
glib::Object::builder().property("session", session).build()
|
||||
}
|
||||
@ -453,9 +374,9 @@ impl RoomPreviewDialog {
|
||||
}
|
||||
|
||||
/// Set the room to preview.
|
||||
pub(crate) fn set_room(&self, room: &RemoteRoom) {
|
||||
pub(crate) fn set_room(&self, room: RemoteRoom) {
|
||||
let imp = self.imp();
|
||||
imp.disable_go_back(true);
|
||||
imp.set_room(room);
|
||||
imp.set_room(Some(room));
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="RoomPreviewDialog" parent="ToastableDialog">
|
||||
<template class="JoinRoomDialog" parent="ToastableDialog">
|
||||
<property name="title" translatable="yes">Join a Room</property>
|
||||
<property name="content-width">480</property>
|
||||
<property name="content-height">500</property>
|
||||
@ -150,7 +150,7 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
@ -172,24 +172,25 @@
|
||||
<property name="icon-name">users-symbolic</property>
|
||||
<property name="accessible-role">presentation</property>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="room_members_count">
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="LoadingButton" id="view_or_join_btn">
|
||||
<object class="LoadingButton" id="join_btn">
|
||||
<property name="content-label" translatable="yes">Join</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="halign">center</property>
|
||||
<signal name="clicked" handler="view_or_join_room" swapped="yes"/>
|
||||
<signal name="clicked" handler="join_room" swapped="yes"/>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="standalone-button"/>
|
@ -1,13 +1,13 @@
|
||||
mod auth;
|
||||
mod join_room;
|
||||
mod message_dialogs;
|
||||
mod room_preview;
|
||||
mod toastable;
|
||||
mod user_profile;
|
||||
|
||||
pub(crate) use self::{
|
||||
auth::{AuthDialog, AuthError},
|
||||
join_room::JoinRoomDialog,
|
||||
message_dialogs::*,
|
||||
room_preview::RoomPreviewDialog,
|
||||
toastable::{ToastableDialog, ToastableDialogExt, ToastableDialogImpl},
|
||||
user_profile::UserProfileDialog,
|
||||
};
|
||||
|
@ -6,13 +6,11 @@ use super::ToastableDialog;
|
||||
use crate::{
|
||||
components::UserPage,
|
||||
prelude::*,
|
||||
session::model::{Member, Session, User},
|
||||
utils::LoadingState,
|
||||
session::model::{Member, RemoteUser, Session, User},
|
||||
spawn,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
@ -24,7 +22,6 @@ mod imp {
|
||||
stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
user_page: TemplateChild<UserPage>,
|
||||
user_loading_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -42,70 +39,31 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for UserProfileDialog {
|
||||
fn dispose(&self) {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for UserProfileDialog {}
|
||||
impl WidgetImpl for UserProfileDialog {}
|
||||
impl AdwDialogImpl for UserProfileDialog {}
|
||||
impl ToastableDialogImpl for UserProfileDialog {}
|
||||
|
||||
impl UserProfileDialog {
|
||||
/// Show the details page.
|
||||
fn show_details(&self) {
|
||||
self.stack.set_visible_child_name("details");
|
||||
}
|
||||
|
||||
/// Load the user with the given session and user ID.
|
||||
pub(super) fn load_user(&self, session: &Session, user_id: OwnedUserId) {
|
||||
self.reset();
|
||||
|
||||
let user = session.remote_cache().user(user_id);
|
||||
let user = RemoteUser::new(session, user_id);
|
||||
self.user_page.set_user(Some(user.clone()));
|
||||
|
||||
if matches!(
|
||||
user.loading_state(),
|
||||
LoadingState::Initial | LoadingState::Loading
|
||||
) {
|
||||
let user_loading_handler = user.connect_loading_state_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |user| {
|
||||
if !matches!(
|
||||
user.loading_state(),
|
||||
LoadingState::Initial | LoadingState::Loading
|
||||
) {
|
||||
if let Some(handler) = imp.user_loading_handler.take() {
|
||||
user.disconnect(handler);
|
||||
imp.show_details();
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
self.user_loading_handler
|
||||
.replace(Some(user_loading_handler));
|
||||
} else {
|
||||
self.show_details();
|
||||
}
|
||||
spawn!(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
async move {
|
||||
user.load_profile().await;
|
||||
imp.stack.set_visible_child_name("details");
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/// Set the member to present.
|
||||
pub(super) fn set_room_member(&self, member: Member) {
|
||||
self.reset();
|
||||
|
||||
self.user_page.set_user(Some(member.upcast::<User>()));
|
||||
self.show_details();
|
||||
}
|
||||
|
||||
/// Reset this dialog.
|
||||
fn reset(&self) {
|
||||
if let Some(handler) = self.user_loading_handler.take() {
|
||||
if let Some(user) = self.user_page.user() {
|
||||
user.disconnect(handler);
|
||||
}
|
||||
}
|
||||
self.stack.set_visible_child_name("details");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -338,9 +338,3 @@ impl LabelWithWidgets {
|
||||
self.imp().set_label_and_widgets(label, widgets);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LabelWithWidgets {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
use adw::prelude::*;
|
||||
use gtk::{glib, subclass::prelude::*, CompositeTemplate};
|
||||
|
||||
use crate::utils::ChildPropertyExt;
|
||||
|
||||
mod imp {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
@ -120,13 +118,3 @@ impl Default for LoadingBin {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChildPropertyExt for LoadingBin {
|
||||
fn child_property(&self) -> Option<gtk::Widget> {
|
||||
self.child()
|
||||
}
|
||||
|
||||
fn set_child_property(&self, child: Option<&impl IsA<gtk::Widget>>) {
|
||||
self.set_child(child.map(Cast::upcast_ref));
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, pango};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, pango, prelude::*};
|
||||
|
||||
use super::LoadingBin;
|
||||
use crate::prelude::*;
|
||||
|
||||
mod imp {
|
||||
use std::marker::PhantomData;
|
||||
@ -66,22 +65,26 @@ mod imp {
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
let child_label = self.loading_bin.child_or_else::<gtk::Label>(|| {
|
||||
let child_label = gtk::Label::builder()
|
||||
.ellipsize(pango::EllipsizeMode::End)
|
||||
.use_underline(true)
|
||||
.mnemonic_widget(&*obj)
|
||||
.css_classes(["text-button"])
|
||||
.build();
|
||||
let child_label =
|
||||
if let Some(child_label) = self.loading_bin.child().and_downcast::<gtk::Label>() {
|
||||
child_label
|
||||
} else {
|
||||
let child_label = gtk::Label::builder()
|
||||
.ellipsize(pango::EllipsizeMode::End)
|
||||
.use_underline(true)
|
||||
.mnemonic_widget(&*obj)
|
||||
.css_classes(["text-button"])
|
||||
.build();
|
||||
|
||||
// In case it was an image before.
|
||||
obj.remove_css_class("image-button");
|
||||
obj.update_relation(&[gtk::accessible::Relation::LabelledBy(&[
|
||||
child_label.upcast_ref()
|
||||
])]);
|
||||
self.loading_bin.set_child(Some(child_label.clone()));
|
||||
// In case it was an image before.
|
||||
obj.remove_css_class("image-button");
|
||||
obj.update_relation(&[gtk::accessible::Relation::LabelledBy(&[
|
||||
child_label.upcast_ref()
|
||||
])]);
|
||||
|
||||
child_label
|
||||
});
|
||||
child_label
|
||||
};
|
||||
|
||||
child_label.set_label(label);
|
||||
|
||||
@ -103,13 +106,19 @@ mod imp {
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
let child_image = self.loading_bin.child_or_else::<gtk::Image>(|| {
|
||||
obj.add_css_class("image-button");
|
||||
let child_image =
|
||||
if let Some(child_image) = self.loading_bin.child().and_downcast::<gtk::Image>() {
|
||||
child_image
|
||||
} else {
|
||||
let child_image = gtk::Image::builder()
|
||||
.accessible_role(gtk::AccessibleRole::Presentation)
|
||||
.build();
|
||||
|
||||
gtk::Image::builder()
|
||||
.accessible_role(gtk::AccessibleRole::Presentation)
|
||||
.build()
|
||||
});
|
||||
self.loading_bin.set_child(Some(child_image.clone()));
|
||||
obj.add_css_class("image-button");
|
||||
|
||||
child_image
|
||||
};
|
||||
|
||||
child_image.set_icon_name(Some(icon_name));
|
||||
|
||||
|
@ -230,7 +230,14 @@ mod imp {
|
||||
|
||||
/// View the given location as a geo URI.
|
||||
pub(super) fn view_location(&self, geo_uri: &GeoUri) {
|
||||
let location = self.viewer.child_or_default::<LocationViewer>();
|
||||
let location =
|
||||
if let Some(location) = self.viewer.child().and_downcast::<LocationViewer>() {
|
||||
location
|
||||
} else {
|
||||
let location = LocationViewer::new();
|
||||
self.viewer.set_child(Some(&location));
|
||||
location
|
||||
};
|
||||
|
||||
location.set_location(geo_uri);
|
||||
self.update_animated_paintable_state();
|
||||
|
@ -148,9 +148,3 @@ impl LocationViewer {
|
||||
self.imp().set_location(geo_uri);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LocationViewer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,6 @@ mod imp {
|
||||
state: Cell<LoadingState>,
|
||||
/// The current error, if any.
|
||||
pub(super) error: RefCell<Option<glib::Error>>,
|
||||
/// The duration of the video, if it is known.
|
||||
duration: Cell<Option<gst::ClockTime>>,
|
||||
bus_guard: OnceCell<gst::bus::BusWatchGuard>,
|
||||
}
|
||||
|
||||
@ -112,8 +110,6 @@ mod imp {
|
||||
}
|
||||
|
||||
self.compact.set(compact);
|
||||
|
||||
self.update_timestamp();
|
||||
self.obj().notify_compact();
|
||||
}
|
||||
|
||||
@ -132,7 +128,7 @@ mod imp {
|
||||
let uri = file.uri();
|
||||
self.file.replace(Some(file));
|
||||
|
||||
self.set_duration(None);
|
||||
self.duration_changed(None);
|
||||
self.set_state(LoadingState::Loading);
|
||||
|
||||
self.player.set_uri(Some(uri.as_ref()));
|
||||
@ -159,7 +155,7 @@ mod imp {
|
||||
}
|
||||
}
|
||||
gst_play::PlayMessage::DurationChanged { duration } => {
|
||||
self.set_duration(duration);
|
||||
self.duration_changed(duration);
|
||||
}
|
||||
gst_play::PlayMessage::Warning { error, .. } => {
|
||||
warn!("Warning playing video: {error}");
|
||||
@ -173,23 +169,9 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the duration of the video.
|
||||
fn set_duration(&self, duration: Option<gst::ClockTime>) {
|
||||
if self.duration.get() == duration {
|
||||
return;
|
||||
}
|
||||
|
||||
self.duration.set(duration);
|
||||
self.update_timestamp();
|
||||
}
|
||||
|
||||
/// Update the timestamp for the current state.
|
||||
fn update_timestamp(&self) {
|
||||
// We show the duration if we know it and if we are not in compact mode.
|
||||
let visible_duration = self.duration.get().filter(|_| !self.compact.get());
|
||||
let is_visible = visible_duration.is_some();
|
||||
|
||||
if let Some(duration) = visible_duration {
|
||||
/// Handle when the duration changed.
|
||||
fn duration_changed(&self, duration: Option<gst::ClockTime>) {
|
||||
if let Some(duration) = duration {
|
||||
let mut time = duration.seconds();
|
||||
|
||||
let sec = time % 60;
|
||||
@ -211,7 +193,7 @@ mod imp {
|
||||
self.timestamp.set_label(&label);
|
||||
}
|
||||
|
||||
self.timestamp.set_visible(is_visible);
|
||||
self.timestamp.set_visible(duration.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
<class name="osd"/>
|
||||
<class name="timestamp"/>
|
||||
</style>
|
||||
<property name="visible">false</property>
|
||||
<property name="visible" bind-source="VideoPlayer" bind-property="compact" bind-flags="sync-create | invert-boolean"/>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="margin-start">5</property>
|
||||
|
@ -1,6 +1,5 @@
|
||||
mod action_button;
|
||||
mod avatar;
|
||||
mod camera;
|
||||
mod context_menu_bin;
|
||||
pub mod crypto;
|
||||
mod custom_entry;
|
||||
@ -20,7 +19,6 @@ mod user_page;
|
||||
pub(crate) use self::{
|
||||
action_button::{ActionButton, ActionState},
|
||||
avatar::*,
|
||||
camera::{Camera, CameraExt, QrCodeScanner},
|
||||
context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl},
|
||||
custom_entry::CustomEntry,
|
||||
dialogs::*,
|
||||
|
@ -1,20 +1,18 @@
|
||||
use gettextrs::gettext;
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
use ruma::RoomId;
|
||||
use gtk::{glib, subclass::prelude::*};
|
||||
use ruma::OwnedRoomId;
|
||||
|
||||
use crate::{components::PillSource, prelude::*, session::model::Room};
|
||||
use crate::{components::PillSource, prelude::*};
|
||||
|
||||
mod imp {
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, glib::Properties)]
|
||||
#[properties(wrapper_type = super::AtRoom)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AtRoom {
|
||||
/// The room represented by this mention.
|
||||
#[property(get, set = Self::set_room, construct_only)]
|
||||
room: OnceCell<Room>,
|
||||
/// The ID of the room currently represented.
|
||||
room_id: OnceCell<OwnedRoomId>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@ -24,7 +22,6 @@ mod imp {
|
||||
type ParentType = PillSource;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for AtRoom {}
|
||||
|
||||
impl PillSourceImpl for AtRoom {
|
||||
@ -34,23 +31,14 @@ mod imp {
|
||||
}
|
||||
|
||||
impl AtRoom {
|
||||
/// Set the room represented by this mention.
|
||||
fn set_room(&self, room: Room) {
|
||||
let room = self.room.get_or_init(|| room);
|
||||
|
||||
// Bind the avatar image so it always looks the same.
|
||||
room.avatar_data()
|
||||
.bind_property("image", &self.obj().avatar_data(), "image")
|
||||
.sync_create()
|
||||
.build();
|
||||
/// Set the ID of the room currently represented.
|
||||
pub(super) fn set_room_id(&self, room_id: OwnedRoomId) {
|
||||
self.room_id.set(room_id).expect("room ID is uninitialized");
|
||||
}
|
||||
|
||||
/// The ID of the room represented by this mention.
|
||||
pub(super) fn room_id(&self) -> &RoomId {
|
||||
self.room
|
||||
.get()
|
||||
.expect("room should be initialized")
|
||||
.room_id()
|
||||
/// The ID of the room currently represented.
|
||||
pub(super) fn room_id(&self) -> &OwnedRoomId {
|
||||
self.room_id.get().expect("room ID is initialized")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,16 +49,19 @@ glib::wrapper! {
|
||||
}
|
||||
|
||||
impl AtRoom {
|
||||
/// Constructs an `@room` mention for the given room.
|
||||
pub fn new(room: &Room) -> Self {
|
||||
glib::Object::builder()
|
||||
/// Constructs an empty `@room` mention.
|
||||
pub fn new(room_id: OwnedRoomId) -> Self {
|
||||
let obj = glib::Object::builder::<Self>()
|
||||
.property("display-name", "@room")
|
||||
.property("room", room)
|
||||
.build()
|
||||
.build();
|
||||
|
||||
obj.imp().set_room_id(room_id);
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
/// The ID of the room represented by this mention.
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
/// The ID of the room currently represented.
|
||||
pub fn room_id(&self) -> &OwnedRoomId {
|
||||
self.imp().room_id()
|
||||
}
|
||||
}
|
||||
|
@ -12,21 +12,18 @@ pub use self::{
|
||||
source::{PillSource, PillSourceExt, PillSourceImpl},
|
||||
source_row::PillSourceRow,
|
||||
};
|
||||
use super::{Avatar, AvatarImageSafetySetting, RoomPreviewDialog, UserProfileDialog};
|
||||
use super::{Avatar, JoinRoomDialog, UserProfileDialog};
|
||||
use crate::{
|
||||
prelude::*,
|
||||
session::{
|
||||
model::{Member, RemoteRoom, Room},
|
||||
view::SessionView,
|
||||
},
|
||||
utils::{key_bindings, BoundObject},
|
||||
utils::{add_activate_binding_action, BoundObject},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
marker::PhantomData,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
@ -48,15 +45,6 @@ mod imp {
|
||||
/// Whether the pill can be activated.
|
||||
#[property(get, set = Self::set_activatable, explicit_notify)]
|
||||
activatable: Cell<bool>,
|
||||
/// The safety setting to watch to decide whether the image of the
|
||||
/// avatar should be displayed.
|
||||
#[property(get = Self::watched_safety_setting, set = Self::set_watched_safety_setting, builder(AvatarImageSafetySetting::default()))]
|
||||
watched_safety_setting: PhantomData<AvatarImageSafetySetting>,
|
||||
/// The room to watch to apply the current safety settings.
|
||||
///
|
||||
/// This is required if `watched_safety_setting` is not `None`.
|
||||
#[property(get = Self::watched_room, set = Self::set_watched_room, nullable)]
|
||||
watched_room: PhantomData<Option<Room>>,
|
||||
gesture_click: RefCell<Option<gtk::GestureClick>>,
|
||||
}
|
||||
|
||||
@ -76,7 +64,7 @@ mod imp {
|
||||
obj.imp().activate();
|
||||
});
|
||||
|
||||
key_bindings::add_activate_bindings(klass, "pill.activate");
|
||||
add_activate_binding_action(klass, "pill.activate");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
@ -177,28 +165,6 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
/// The safety setting to watch to decide whether the image of the
|
||||
/// avatar should be displayed.
|
||||
fn watched_safety_setting(&self) -> AvatarImageSafetySetting {
|
||||
self.avatar.watched_safety_setting()
|
||||
}
|
||||
|
||||
/// Set the safety setting to watch to decide whether the image of the
|
||||
/// avatar should be displayed.
|
||||
fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
|
||||
self.avatar.set_watched_safety_setting(setting);
|
||||
}
|
||||
|
||||
/// The room to watch to apply the current safety settings.
|
||||
fn watched_room(&self) -> Option<Room> {
|
||||
self.avatar.watched_room()
|
||||
}
|
||||
|
||||
/// Set the room to watch to apply the current safety settings.
|
||||
fn set_watched_room(&self, room: Option<Room>) {
|
||||
self.avatar.set_watched_room(room);
|
||||
}
|
||||
|
||||
/// Set the display name of this pill.
|
||||
fn set_display_name(&self, label: &str) {
|
||||
// We ellipsize the string manually because GtkTextView uses the minimum width.
|
||||
@ -235,13 +201,13 @@ mod imp {
|
||||
return;
|
||||
};
|
||||
|
||||
session_view.select_room(room.clone());
|
||||
} else if let Some(room) = source.downcast_ref::<RemoteRoom>() {
|
||||
session_view.select_room(Some(room.clone()));
|
||||
} else if let Ok(room) = source.downcast::<RemoteRoom>() {
|
||||
let Some(session) = room.session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dialog = RoomPreviewDialog::new(&session);
|
||||
let dialog = JoinRoomDialog::new(&session);
|
||||
dialog.set_room(room);
|
||||
dialog.present(Some(&*obj));
|
||||
}
|
||||
@ -256,30 +222,8 @@ glib::wrapper! {
|
||||
}
|
||||
|
||||
impl Pill {
|
||||
/// Create a pill with the given source and watching the given safety
|
||||
/// setting.
|
||||
pub fn new(
|
||||
source: &impl IsA<PillSource>,
|
||||
watched_safety_setting: AvatarImageSafetySetting,
|
||||
watched_room: Option<Room>,
|
||||
) -> Self {
|
||||
let source = source.upcast_ref();
|
||||
|
||||
let (watched_safety_setting, watched_room) = if let Some(room) = source
|
||||
.downcast_ref::<Room>()
|
||||
.cloned()
|
||||
.or_else(|| source.downcast_ref::<AtRoom>().map(AtRoom::room))
|
||||
{
|
||||
// We must always watch the invite avatars setting for local rooms.
|
||||
(AvatarImageSafetySetting::InviteAvatars, Some(room))
|
||||
} else {
|
||||
(watched_safety_setting, watched_room)
|
||||
};
|
||||
|
||||
glib::Object::builder()
|
||||
.property("source", source)
|
||||
.property("watched-safety-setting", watched_safety_setting)
|
||||
.property("watched-room", watched_room)
|
||||
.build()
|
||||
/// Create a pill with the given source.
|
||||
pub fn new(source: &impl IsA<PillSource>) -> Self {
|
||||
glib::Object::builder().property("source", source).build()
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use gtk::{
|
||||
CompositeTemplate,
|
||||
};
|
||||
|
||||
use crate::components::{AvatarImageSafetySetting, Pill, PillSource};
|
||||
use crate::components::{Pill, PillSource};
|
||||
|
||||
mod imp {
|
||||
use std::{cell::RefCell, collections::HashMap, marker::PhantomData, sync::LazyLock};
|
||||
@ -136,12 +136,7 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for PillSearchEntry {
|
||||
fn grab_focus(&self) -> bool {
|
||||
self.text_view.grab_focus()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for PillSearchEntry {}
|
||||
impl BinImpl for PillSearchEntry {}
|
||||
|
||||
impl PillSearchEntry {
|
||||
@ -175,9 +170,7 @@ mod imp {
|
||||
return;
|
||||
}
|
||||
|
||||
// We do not need to watch the safety setting as this entry should only be used
|
||||
// with search results.
|
||||
let pill = Pill::new(source, AvatarImageSafetySetting::None, None);
|
||||
let pill = Pill::new(source);
|
||||
pill.set_margin_start(3);
|
||||
pill.set_margin_end(3);
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="PillSearchEntry" parent="AdwBin">
|
||||
<property name="accessible-role">search</property>
|
||||
<property name="child">
|
||||
<object class="CustomEntry">
|
||||
<!-- FIXME: inserting a Pill makes the Entry grow, therefore we force more height so that it doesn't grow visually
|
||||
Would be nice to fix it properly. Including the vertical alignment of Pills in the textview
|
||||
-->
|
||||
<property name="height-request">74</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
@ -15,21 +18,15 @@
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vscrollbar-policy">external</property>
|
||||
<property name="max-content-height">200</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="height-request">38</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="justification">left</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
<property name="pixels-above-lines">6</property>
|
||||
<property name="pixels-below-lines">11</property>
|
||||
<property name="pixels-inside-wrap">11</property>
|
||||
<property name="pixels_above_lines">3</property>
|
||||
<property name="pixels_below_lines">3</property>
|
||||
<property name="pixels_inside_wrap">6</property>
|
||||
<property name="buffer">
|
||||
<object class="GtkTextBuffer" id="text_buffer"/>
|
||||
</property>
|
||||
|
@ -1,10 +1,7 @@
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
|
||||
use super::Pill;
|
||||
use crate::{
|
||||
components::{AvatarData, AvatarImageSafetySetting},
|
||||
session::model::Room,
|
||||
};
|
||||
use crate::components::AvatarData;
|
||||
|
||||
mod imp {
|
||||
use std::{cell::Cell, marker::PhantomData};
|
||||
@ -156,13 +153,8 @@ pub trait PillSourceExt: 'static {
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId;
|
||||
|
||||
/// Get a `Pill` representing this source, watching the given safety
|
||||
/// setting.
|
||||
fn to_pill(
|
||||
&self,
|
||||
watched_safety_setting: AvatarImageSafetySetting,
|
||||
watched_room: Option<Room>,
|
||||
) -> Pill;
|
||||
/// Get a `Pill` representing this source.
|
||||
fn to_pill(&self) -> Pill;
|
||||
}
|
||||
|
||||
impl<O: IsA<PillSource>> PillSourceExt for O {
|
||||
@ -217,12 +209,8 @@ impl<O: IsA<PillSource>> PillSourceExt for O {
|
||||
}
|
||||
|
||||
/// Get a `Pill` representing this source.
|
||||
fn to_pill(
|
||||
&self,
|
||||
watched_safety_setting: AvatarImageSafetySetting,
|
||||
watched_room: Option<Room>,
|
||||
) -> Pill {
|
||||
Pill::new(self, watched_safety_setting, watched_room)
|
||||
fn to_pill(&self) -> Pill {
|
||||
Pill::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
|