mirror of
https://gitlab.gnome.org/World/fractal.git
synced 2025-08-16 00:02:48 -04:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5a873ff96d | ||
|
d87aa8ff60 | ||
|
31c0607568 | ||
|
17533ddfe9 | ||
|
71bd5988a5 | ||
|
49a3ce16a3 | ||
|
99b0a54e01 | ||
|
7a64036bb1 | ||
|
e3c34328ee | ||
|
0e9d34dd9d | ||
|
4597519128 | ||
|
8a7c690d21 | ||
|
07cc8f9787 | ||
|
2a09a76fb0 |
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,15 +1,19 @@
|
||||
target/
|
||||
build/
|
||||
# Common build directory names
|
||||
_build/
|
||||
build/
|
||||
builddir/
|
||||
build-aux/app
|
||||
|
||||
# Cargo
|
||||
target/
|
||||
|
||||
# Flatpak
|
||||
.flatpak-builder
|
||||
src/config.rs
|
||||
.flatpak
|
||||
.fenv
|
||||
|
||||
# Temporary files
|
||||
*.ui.in~
|
||||
*.ui~
|
||||
.flatpak
|
||||
subprojects/libadwaita
|
||||
subprojects/gtksourceview
|
||||
.vscode
|
||||
.fenv
|
||||
.zed
|
||||
|
||||
# Dynamically-generated file
|
||||
src/config.rs
|
||||
|
466
Cargo.lock
generated
466
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@ -61,7 +61,7 @@ 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"] }
|
||||
glycin = { version = "3.0.0-beta.1", default-features = false, features = ["tokio", "gdk4"] }
|
||||
gst = { version = "0.23", package = "gstreamer" }
|
||||
gst_app = { version = "0.23", package = "gstreamer-app" }
|
||||
gst_pbutils = { version = "0.23", package = "gstreamer-pbutils" }
|
||||
@ -74,23 +74,23 @@ sourceview = { package = "sourceview5", version = "0.9" }
|
||||
[dependencies.matrix-sdk]
|
||||
# version = "0.13"
|
||||
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
rev = "872713c4bc024ac9246dfa72f834584ebe92a3d7"
|
||||
rev = "a9ce1c6e5822b8eb8411c5bc257049d9a9d15884"
|
||||
features = ["socks", "sso-login", "markdown", "qrcode"]
|
||||
|
||||
[dependencies.matrix-sdk-store-encryption]
|
||||
# version = "0.13"
|
||||
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
rev = "872713c4bc024ac9246dfa72f834584ebe92a3d7"
|
||||
rev = "a9ce1c6e5822b8eb8411c5bc257049d9a9d15884"
|
||||
|
||||
[dependencies.matrix-sdk-ui]
|
||||
# version = "0.13"
|
||||
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
|
||||
rev = "872713c4bc024ac9246dfa72f834584ebe92a3d7"
|
||||
rev = "a9ce1c6e5822b8eb8411c5bc257049d9a9d15884"
|
||||
|
||||
[dependencies.ruma]
|
||||
# version = "0.12.5"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "e73f302e4df7f5f0511fca1aa43853d4cf8416c8"
|
||||
rev = "a2fe858133ba932b4bda730dc7472c9c985739a0"
|
||||
features = [
|
||||
"client-api-c",
|
||||
"markdown",
|
||||
|
@ -95,27 +95,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "glycin-loaders",
|
||||
"buildsystem": "meson",
|
||||
"config-opts": [
|
||||
"-Dtests=false",
|
||||
"-Dlibglycin=false",
|
||||
"-Dintrospection=false",
|
||||
"-Dvapi=false",
|
||||
"-Dcapi_docs=false",
|
||||
"-Dpython_tests=false"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.gnome.org/sophie-h/glycin.git",
|
||||
"tag": "1.2.2",
|
||||
"commit": "c7d362287303944721cf583d4d9e9f7721bfa407",
|
||||
"disable-submodules": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fractal",
|
||||
"buildsystem": "meson",
|
||||
|
@ -99,43 +99,30 @@ room-title {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender-avatar {
|
||||
padding: 5px;
|
||||
border-radius: 100%;
|
||||
button.sender-avatar {
|
||||
padding: 5px;
|
||||
|
||||
@include vendor.focus-ring();
|
||||
/* @include vendor.focus-ring(); */
|
||||
|
||||
&:hover {
|
||||
background-color: vendor.$hover_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.07) ;
|
||||
&:hover {
|
||||
image {
|
||||
filter: brightness(1.07) ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: vendor.$active_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.16) ;
|
||||
&:active {
|
||||
image {
|
||||
filter: brightness(1.16) ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: vendor.$selected_color;
|
||||
|
||||
image {
|
||||
filter: brightness(1.1) ;
|
||||
&:checked {
|
||||
image {
|
||||
filter: brightness(1.1) ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popover button.text-button {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
message-sender {
|
||||
@ -208,7 +195,6 @@ message-sender {
|
||||
.codeview {
|
||||
border-radius: vendor.$menu_radius;
|
||||
padding: 6px;
|
||||
font-family: monospace;
|
||||
background-color: var(--text-view-bg);
|
||||
color: var(--view-fg-color);
|
||||
}
|
||||
|
@ -201,11 +201,6 @@ sidebar {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
// Event details dialog
|
||||
.event-details-dialog .sourceview {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
// Account settings
|
||||
.account-settings listview {
|
||||
background: transparent;
|
||||
|
@ -17,8 +17,8 @@ allow = [
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"GPL-3.0",
|
||||
"LGPL-2.1",
|
||||
"GPL-3.0-or-later",
|
||||
"GPL-3.0-only",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
|
@ -162,6 +162,7 @@ src/session/view/content/room_history/message_row/location.rs
|
||||
src/session/view/content/room_history/message_row/location.ui
|
||||
src/session/view/content/room_history/message_row/message_state_stack.ui
|
||||
src/session/view/content/room_history/message_row/mod.rs
|
||||
src/session/view/content/room_history/message_row/mod.ui
|
||||
src/session/view/content/room_history/message_row/reaction/mod.rs
|
||||
src/session/view/content/room_history/message_row/reaction_list.ui
|
||||
src/session/view/content/room_history/message_row/reply.ui
|
||||
@ -177,8 +178,6 @@ 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/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
|
||||
|
@ -62,7 +62,6 @@
|
||||
<property name="label" translatable="yes">Select the account you want to open the URI with</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
<class name="description"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
|
@ -47,6 +47,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -163,6 +166,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -171,6 +177,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -238,6 +247,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -246,6 +258,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -254,6 +269,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -55,6 +55,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -157,6 +160,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -182,6 +188,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -190,6 +199,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -269,6 +281,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -277,6 +292,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -285,6 +303,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -72,7 +72,6 @@
|
||||
<property name="label" translatable="yes">Enter a room ID, room alias, or link to look up a room</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
<class name="description"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
@ -161,6 +160,9 @@
|
||||
<property name="justify">center</property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -6,7 +6,7 @@ use tracing::error;
|
||||
|
||||
use crate::{
|
||||
spawn, spawn_tokio,
|
||||
utils::{CountedRef, File},
|
||||
utils::{CountedRef, File, TokioDrop},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
@ -19,9 +19,12 @@ mod imp {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AnimatedImagePaintable {
|
||||
/// The image loader.
|
||||
image_loader: OnceCell<Arc<Image<'static>>>,
|
||||
/// The image decoder.
|
||||
decoder: OnceCell<Arc<TokioDrop<Image>>>,
|
||||
/// The file of the image.
|
||||
///
|
||||
/// We need to keep a strong reference to the temporary file or it will
|
||||
/// be destroyed.
|
||||
file: OnceCell<File>,
|
||||
/// The current frame that is displayed.
|
||||
pub(super) current_frame: RefCell<Option<Arc<Frame>>>,
|
||||
@ -49,7 +52,7 @@ mod imp {
|
||||
self.current_frame
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map_or_else(|| self.image_loader().info().height, |f| f.height())
|
||||
.map_or_else(|| self.decoder().details().height(), |f| f.height())
|
||||
.try_into()
|
||||
.unwrap_or(i32::MAX)
|
||||
}
|
||||
@ -58,7 +61,7 @@ mod imp {
|
||||
self.current_frame
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map_or_else(|| self.image_loader().info().width, |f| f.width())
|
||||
.map_or_else(|| self.decoder().details().width(), |f| f.width())
|
||||
.try_into()
|
||||
.unwrap_or(i32::MAX)
|
||||
}
|
||||
@ -94,26 +97,27 @@ mod imp {
|
||||
}
|
||||
|
||||
impl AnimatedImagePaintable {
|
||||
/// The image loader.
|
||||
fn image_loader(&self) -> &Arc<Image<'static>> {
|
||||
self.image_loader
|
||||
.get()
|
||||
.expect("image loader is initialized")
|
||||
/// The image decoder.
|
||||
fn decoder(&self) -> &Arc<TokioDrop<Image>> {
|
||||
self.decoder.get().expect("decoder should be initialized")
|
||||
}
|
||||
|
||||
/// Initialize the image.
|
||||
pub(super) fn init(
|
||||
&self,
|
||||
file: File,
|
||||
image_loader: Arc<Image<'static>>,
|
||||
decoder: Arc<TokioDrop<Image>>,
|
||||
first_frame: Arc<Frame>,
|
||||
file: Option<File>,
|
||||
) {
|
||||
self.file.set(file).expect("file is uninitialized");
|
||||
self.image_loader
|
||||
.set(image_loader)
|
||||
.expect("image loader is uninitialized");
|
||||
self.decoder
|
||||
.set(decoder)
|
||||
.expect("decoder should be uninitialized");
|
||||
self.current_frame.replace(Some(first_frame));
|
||||
|
||||
if let Some(file) = file {
|
||||
self.file.set(file).expect("file should be uninitialized");
|
||||
}
|
||||
|
||||
self.update_animation();
|
||||
}
|
||||
|
||||
@ -198,9 +202,9 @@ mod imp {
|
||||
}
|
||||
|
||||
async fn load_next_frame_inner(&self) {
|
||||
let image = self.image_loader().clone();
|
||||
let decoder = self.decoder().clone();
|
||||
|
||||
let result = spawn_tokio!(async move { image.next_frame().await })
|
||||
let result = spawn_tokio!(async move { decoder.next_frame().await })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -229,16 +233,16 @@ glib::wrapper! {
|
||||
}
|
||||
|
||||
impl AnimatedImagePaintable {
|
||||
/// Construct an `AnimatedImagePaintable` with the given loader and first
|
||||
/// frame.
|
||||
/// Construct an `AnimatedImagePaintable` with the given decoder, first
|
||||
/// frame, and the file containing the image, if any.
|
||||
pub(crate) fn new(
|
||||
file: File,
|
||||
image_loader: Arc<Image<'static>>,
|
||||
decoder: Arc<TokioDrop<Image>>,
|
||||
first_frame: Arc<Frame>,
|
||||
file: Option<File>,
|
||||
) -> Self {
|
||||
let obj = glib::Object::new::<Self>();
|
||||
|
||||
obj.imp().init(file, image_loader, first_frame);
|
||||
obj.imp().init(decoder, first_frame, file);
|
||||
|
||||
obj
|
||||
}
|
||||
|
@ -59,6 +59,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="label" translatable="yes">Fractal relies on a Secret Portal to manage your sensitive session information and an error occurred while we were trying to restore your sessions.</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -71,6 +74,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="label" translatable="yes">Here are a few things that might help you fix issues with the Secret Portal:</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -88,6 +94,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="label" translatable="yes">Make sure you have a Secret Portal Backend Provider installed, like gnome-keyring.</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@ -111,6 +120,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="label" translatable="yes">If you prefer to use a Secret Service Provider instead, you need to allow Fractal to interact with it, like this:</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -168,6 +180,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="label" translatable="yes">Check that you have a default keyring and that it is unlocked.</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@ -180,6 +195,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="label" translatable="yes">Check the application logs and your distribution’s documentation for more details.</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -34,6 +34,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -33,6 +33,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -33,6 +33,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -39,6 +39,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -40,6 +40,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -34,6 +34,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -42,6 +45,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="label" translatable="yes">You can accept this verification from another session or decline it for all your sessions</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -40,6 +40,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -35,6 +35,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="label" translatable="yes">You are no longer in the room where the verification was taking place</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -33,6 +33,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -33,6 +33,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -49,6 +52,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -34,6 +34,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -42,6 +45,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -56,6 +62,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -80,6 +80,9 @@
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="label" translatable="yes">This session is ready to send and receive secure messages</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -316,7 +316,7 @@ mod imp {
|
||||
|
||||
if let Some(send_state) = item.send_state() {
|
||||
match send_state {
|
||||
EventSendState::NotSentYet => return MessageState::Sending,
|
||||
EventSendState::NotSentYet { .. } => return MessageState::Sending,
|
||||
EventSendState::SendingFailed {
|
||||
error,
|
||||
is_recoverable,
|
||||
|
@ -58,10 +58,6 @@ pub enum SessionState {
|
||||
Ready = 2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, glib::Boxed)]
|
||||
#[boxed_type(name = "BoxedClient")]
|
||||
pub struct BoxedClient(Client);
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
|
||||
@ -70,9 +66,8 @@ mod imp {
|
||||
#[derive(Debug, Default, glib::Properties)]
|
||||
#[properties(wrapper_type = super::Session)]
|
||||
pub struct Session {
|
||||
/// The Matrix client.
|
||||
#[property(construct_only)]
|
||||
client: TokioDrop<BoxedClient>,
|
||||
/// The Matrix client for this session.
|
||||
client: OnceCell<TokioDrop<Client>>,
|
||||
/// The list model of the sidebar.
|
||||
#[property(get = Self::sidebar_list_model)]
|
||||
sidebar_list_model: OnceCell<SidebarListModel>,
|
||||
@ -128,27 +123,6 @@ mod imp {
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Session {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
self.ignored_users.set_session(Some(obj.clone()));
|
||||
self.notifications.set_session(Some(obj.clone()));
|
||||
self.user_sessions.init(&obj, obj.user_id().clone());
|
||||
|
||||
let monitor = gio::NetworkMonitor::default();
|
||||
let handler_id = monitor.connect_network_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_, _| {
|
||||
spawn!(async move {
|
||||
imp.update_homeserver_reachable().await;
|
||||
});
|
||||
}
|
||||
));
|
||||
self.network_monitor_handler_id.replace(Some(handler_id));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
// Needs to be disconnected or else it may restart the sync
|
||||
if let Some(handler_id) = self.network_monitor_handler_id.take() {
|
||||
@ -176,9 +150,34 @@ mod imp {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
// The Matrix client.
|
||||
/// Set the Matrix client for this session.
|
||||
pub(super) fn set_client(&self, client: Client) {
|
||||
self.client
|
||||
.set(TokioDrop::new(client))
|
||||
.expect("client should be uninitialized");
|
||||
|
||||
let obj = self.obj();
|
||||
|
||||
self.ignored_users.set_session(Some(obj.clone()));
|
||||
self.notifications.set_session(Some(obj.clone()));
|
||||
self.user_sessions.init(&obj, obj.user_id().clone());
|
||||
|
||||
let monitor = gio::NetworkMonitor::default();
|
||||
let handler_id = monitor.connect_network_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_, _| {
|
||||
spawn!(async move {
|
||||
imp.update_homeserver_reachable().await;
|
||||
});
|
||||
}
|
||||
));
|
||||
self.network_monitor_handler_id.replace(Some(handler_id));
|
||||
}
|
||||
|
||||
/// The Matrix client for this session.
|
||||
pub(super) fn client(&self) -> &Client {
|
||||
&self.client.get().expect("session should be restored").0
|
||||
self.client.get().expect("client should be initialized")
|
||||
}
|
||||
|
||||
/// The list model of the sidebar.
|
||||
@ -752,11 +751,13 @@ impl Session {
|
||||
.await
|
||||
.expect("task was not aborted")?;
|
||||
|
||||
Ok(glib::Object::builder()
|
||||
let obj = glib::Object::builder::<Self>()
|
||||
.property("info", stored_session)
|
||||
.property("settings", settings)
|
||||
.property("client", BoxedClient(client))
|
||||
.build())
|
||||
.build();
|
||||
obj.imp().set_client(client);
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
/// Create a new session from the session of the given Matrix client.
|
||||
|
@ -33,6 +33,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -41,6 +44,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -44,6 +44,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -53,6 +56,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -43,6 +43,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -33,7 +33,6 @@
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="description">
|
||||
<property name="halign">start</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="lines">4</property>
|
||||
<property name="wrap">True</property>
|
||||
@ -41,6 +40,9 @@
|
||||
<property name="xalign">0</property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -101,6 +101,7 @@
|
||||
</binding>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
@ -108,6 +109,9 @@
|
||||
<object class="LabelWithWidgets" id="inviter">
|
||||
<property name="halign">center</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -95,6 +95,7 @@
|
||||
</binding>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
@ -105,6 +106,9 @@
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="label" translatable="yes">You requested an invite to this room</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -57,6 +57,9 @@
|
||||
</lookup>
|
||||
</closure>
|
||||
</binding>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -93,6 +96,7 @@
|
||||
<property name="label" translatable="yes">There are no members in this room</property>
|
||||
<style>
|
||||
<class name="dimmed"/>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
|
@ -1,5 +1,6 @@
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{CompositeTemplate, gdk, glib, glib::clone};
|
||||
use tracing::error;
|
||||
|
||||
mod audio;
|
||||
mod caption;
|
||||
@ -19,11 +20,13 @@ use self::{
|
||||
message_state_stack::MessageStateStack, reaction_list::MessageReactionList,
|
||||
sender_name::MessageSenderName,
|
||||
};
|
||||
use super::{ReadReceiptsList, SenderAvatar};
|
||||
use super::ReadReceiptsList;
|
||||
use crate::{
|
||||
Application, gettext_f,
|
||||
Application,
|
||||
components::UserProfileDialog,
|
||||
gettext_f,
|
||||
prelude::*,
|
||||
session::model::{Event, EventHeaderState},
|
||||
session::model::{Event, EventHeaderState, Member},
|
||||
system_settings::ClockFormat,
|
||||
utils::BoundObject,
|
||||
};
|
||||
@ -42,7 +45,7 @@ mod imp {
|
||||
#[properties(wrapper_type = super::MessageRow)]
|
||||
pub struct MessageRow {
|
||||
#[template_child]
|
||||
avatar: TemplateChild<SenderAvatar>,
|
||||
avatar_button: TemplateChild<gtk::Button>,
|
||||
#[template_child]
|
||||
header: TemplateChild<gtk::Box>,
|
||||
#[template_child]
|
||||
@ -62,6 +65,9 @@ mod imp {
|
||||
/// The event that is presented.
|
||||
#[property(get, set = Self::set_event, explicit_notify)]
|
||||
event: BoundObject<Event>,
|
||||
/// The sender of the event that is presented.
|
||||
#[property(get = Self::sender)]
|
||||
sender: PhantomData<Option<Member>>,
|
||||
/// The texture of the image preview displayed by the descendant of this
|
||||
/// widget, if any.
|
||||
#[property(get = Self::texture)]
|
||||
@ -76,6 +82,7 @@ mod imp {
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
@ -133,6 +140,7 @@ mod imp {
|
||||
impl WidgetImpl for MessageRow {}
|
||||
impl BinImpl for MessageRow {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MessageRow {
|
||||
/// Set the event that is presented.
|
||||
fn set_event(&self, event: Event) {
|
||||
@ -145,7 +153,6 @@ mod imp {
|
||||
}
|
||||
|
||||
let sender = event.sender();
|
||||
self.avatar.set_sender(Some(sender.clone()));
|
||||
self.display_name.set_sender(Some(sender));
|
||||
|
||||
let state_binding = event
|
||||
@ -193,12 +200,18 @@ mod imp {
|
||||
],
|
||||
);
|
||||
obj.notify_event();
|
||||
obj.notify_sender();
|
||||
|
||||
self.update_content();
|
||||
self.update_header();
|
||||
self.update_timestamp();
|
||||
}
|
||||
|
||||
/// The sender of the event that is presented.
|
||||
fn sender(&self) -> Option<Member> {
|
||||
self.event.obj().map(|event| event.sender())
|
||||
}
|
||||
|
||||
/// Update the header for the current event.
|
||||
fn update_header(&self) {
|
||||
let Some(event) = self.event.obj() else {
|
||||
@ -209,7 +222,7 @@ mod imp {
|
||||
let avatar_name_visible = header_state == EventHeaderState::Full;
|
||||
let header_visible = header_state != EventHeaderState::Hidden;
|
||||
|
||||
self.avatar.set_visible(avatar_name_visible);
|
||||
self.avatar_button.set_visible(avatar_name_visible);
|
||||
self.display_name.set_visible(avatar_name_visible);
|
||||
self.header.set_visible(header_visible);
|
||||
|
||||
@ -258,6 +271,19 @@ mod imp {
|
||||
pub(super) fn texture(&self) -> Option<gdk::Texture> {
|
||||
self.content.texture()
|
||||
}
|
||||
|
||||
/// View the profile of the sender.
|
||||
#[template_callback]
|
||||
fn view_sender_profile(&self) {
|
||||
let Some(sender) = self.sender() else {
|
||||
error!("Could not open profile for missing sender");
|
||||
return;
|
||||
};
|
||||
|
||||
let dialog = UserProfileDialog::new();
|
||||
dialog.set_room_member(sender);
|
||||
dialog.present(Some(&*self.obj()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,26 @@
|
||||
</style>
|
||||
<property name="column-spacing">8</property>
|
||||
<child>
|
||||
<object class="ContentSenderAvatar" id="avatar">
|
||||
<object class="GtkButton" id="avatar_button">
|
||||
<property name="tooltip-text" translatable="yes">View Sender Profile</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="child">
|
||||
<object class="Avatar" id="avatar">
|
||||
<property name="size">36</property>
|
||||
<property name="accessible-role">presentation</property>
|
||||
<binding name="data">
|
||||
<lookup name="avatar-data">
|
||||
<lookup name="sender">ContentMessageRow</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</property>
|
||||
<signal name="clicked" handler="view_sender_profile" swapped="yes"/>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
<class name="flat"/>
|
||||
<class name="sender-avatar"/>
|
||||
</style>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">0</property>
|
||||
|
@ -147,7 +147,7 @@ mod imp {
|
||||
let prev_activatable = self.gesture_click.borrow().is_some();
|
||||
|
||||
let obj = self.obj();
|
||||
obj.action_set_enabled("message-sender.mention", activatable);
|
||||
obj.action_set_enabled("message-sender.activate", activatable);
|
||||
|
||||
if activatable == prev_activatable {
|
||||
// Nothing to update.
|
||||
|
@ -283,6 +283,7 @@ mod imp {
|
||||
let obj = self.obj();
|
||||
let child = obj.child_or_default::<LabelWithWidgets>();
|
||||
|
||||
child.add_css_class("document");
|
||||
child.set_ellipsize(ellipsize);
|
||||
child.set_use_markup(true);
|
||||
child.set_label_and_widgets(result, pills);
|
||||
|
@ -32,6 +32,7 @@ pub(super) fn new_message_label() -> gtk::Label {
|
||||
.xalign(0.0)
|
||||
.valign(gtk::Align::Start)
|
||||
.use_markup(true)
|
||||
.css_classes(["document"])
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -190,6 +191,7 @@ fn label_for_inline_html(
|
||||
}
|
||||
}
|
||||
let w = LabelWithWidgets::new();
|
||||
w.add_css_class("document");
|
||||
w.set_use_markup(true);
|
||||
w.set_ellipsize(config.ellipsize);
|
||||
w.set_label_and_widgets(text, widgets);
|
||||
@ -318,7 +320,10 @@ impl ListType {
|
||||
/// Construct the widget for the bullet of the current type at the given
|
||||
/// position.
|
||||
fn bullet(&self, position: usize) -> gtk::Label {
|
||||
let bullet = gtk::Label::builder().valign(gtk::Align::Baseline).build();
|
||||
let bullet = gtk::Label::builder()
|
||||
.css_classes(["document"])
|
||||
.valign(gtk::Align::Baseline)
|
||||
.build();
|
||||
|
||||
match self {
|
||||
ListType::Unordered => bullet.set_label("•"),
|
||||
@ -404,7 +409,7 @@ fn widget_for_preformatted_text(
|
||||
let view = sourceview::View::builder()
|
||||
.buffer(&buffer)
|
||||
.editable(false)
|
||||
.css_classes(["codeview", "frame"])
|
||||
.css_classes(["codeview", "frame", "monospace"])
|
||||
.hexpand(true)
|
||||
.build();
|
||||
|
||||
|
@ -813,8 +813,7 @@ mod imp {
|
||||
}
|
||||
future::Either::Right((response, _)) => {
|
||||
// The linux location stream requires a tokio executor when dropped.
|
||||
let stream_drop = TokioDrop::new();
|
||||
let _ = stream_drop.set(location_stream);
|
||||
let _ = TokioDrop::new(location_stream);
|
||||
|
||||
if response == gtk::ResponseType::Ok {
|
||||
break;
|
||||
|
@ -19,7 +19,6 @@ mod member_timestamp;
|
||||
mod message_row;
|
||||
mod message_toolbar;
|
||||
mod read_receipts_list;
|
||||
mod sender_avatar;
|
||||
mod state;
|
||||
mod title;
|
||||
mod typing_row;
|
||||
@ -32,7 +31,6 @@ use self::{
|
||||
message_row::MessageRow,
|
||||
message_toolbar::MessageToolbar,
|
||||
read_receipts_list::ReadReceiptsList,
|
||||
sender_avatar::SenderAvatar,
|
||||
state::{StateGroupRow, StateRow},
|
||||
title::RoomHistoryTitle,
|
||||
typing_row::TypingRow,
|
||||
@ -71,8 +69,6 @@ mod imp {
|
||||
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/mod.ui")]
|
||||
#[properties(wrapper_type = super::RoomHistory)]
|
||||
pub struct RoomHistory {
|
||||
#[template_child]
|
||||
sender_menu_model: TemplateChild<gio::Menu>,
|
||||
#[template_child]
|
||||
pub(super) header_bar: TemplateChild<adw::HeaderBar>,
|
||||
#[template_child]
|
||||
@ -103,7 +99,6 @@ mod imp {
|
||||
drag_overlay: TemplateChild<DragOverlay>,
|
||||
/// The context menu for rows presenting an [`Event`].
|
||||
event_context_menu: OnceCell<EventActionsContextMenu>,
|
||||
sender_context_menu: OnceCell<gtk::PopoverMenu>,
|
||||
/// The timeline currently displayed.
|
||||
#[property(get, set = Self::set_timeline, explicit_notify, nullable)]
|
||||
timeline: BoundObject<Timeline>,
|
||||
@ -1127,21 +1122,6 @@ mod imp {
|
||||
self.event_context_menu.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The context menu for the sender avatars.
|
||||
pub(super) fn sender_context_menu(&self) -> >k::PopoverMenu {
|
||||
self.sender_context_menu.get_or_init(|| {
|
||||
let popover = gtk::PopoverMenu::builder()
|
||||
.has_arrow(false)
|
||||
.halign(gtk::Align::Start)
|
||||
.menu_model(&*self.sender_menu_model)
|
||||
.build();
|
||||
popover.update_property(&[gtk::accessible::Property::Label(&gettext(
|
||||
"Sender Context Menu",
|
||||
))]);
|
||||
popover
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens the room details with the given initial view.
|
||||
fn open_room_details(&self, initial_view: room_details::InitialView) {
|
||||
let Some(room) = self.room() else {
|
||||
@ -1205,11 +1185,6 @@ impl RoomHistory {
|
||||
fn event_context_menu(&self) -> &EventActionsContextMenu {
|
||||
self.imp().event_context_menu()
|
||||
}
|
||||
|
||||
/// The context menu for the sender avatars.
|
||||
fn sender_context_menu(&self) -> >k::PopoverMenu {
|
||||
self.imp().sender_context_menu()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the proper child of the given `GtkListItem` for the given
|
||||
|
@ -31,93 +31,6 @@
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="sender_menu_model">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="custom">user-id</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<!-- Translators: In this string, 'Mention' is a verb. -->
|
||||
<attribute name="label" translatable="yes">_Mention</attribute>
|
||||
<attribute name="action">sender-avatar.mention</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<!-- Translators: In this string, 'Open' is a verb. -->
|
||||
<attribute name="label" translatable="yes">_Open Direct Chat</attribute>
|
||||
<attribute name="action">sender-avatar.open-direct-chat</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Copy _Link</attribute>
|
||||
<attribute name="action">sender-avatar.permalink</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<!-- Translators: In this string, 'Invite' is a verb. -->
|
||||
<attribute name="label" translatable="yes">_Invite</attribute>
|
||||
<attribute name="action">sender-avatar.invite</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Revoke _Invite</attribute>
|
||||
<attribute name="action">sender-avatar.revoke-invite</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<!-- Translators: In this string, 'Mute' is a verb, as in 'Mute room member'. -->
|
||||
<attribute name="label" translatable="yes">M_ute</attribute>
|
||||
<attribute name="action">sender-avatar.mute</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Unmute</attribute>
|
||||
<attribute name="action">sender-avatar.unmute</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<!-- Translators: In this string, 'Kick' is a verb. -->
|
||||
<attribute name="label" translatable="yes">_Kick</attribute>
|
||||
<attribute name="action">sender-avatar.kick</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<!-- Translators: In this string, 'Ban' is a verb. -->
|
||||
<attribute name="label" translatable="yes">_Ban</attribute>
|
||||
<attribute name="action">sender-avatar.ban</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Un_ban</attribute>
|
||||
<attribute name="action">sender-avatar.unban</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Remove Messages</attribute>
|
||||
<attribute name="action">sender-avatar.remove-messages</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">I_gnore</attribute>
|
||||
<attribute name="action">sender-avatar.ignore</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Stop I_gnoring</attribute>
|
||||
<attribute name="action">sender-avatar.stop-ignoring</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_View Details</attribute>
|
||||
<attribute name="action">sender-avatar.view-details</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<template class="ContentRoomHistory" parent="AdwBin">
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
|
@ -1,817 +0,0 @@
|
||||
use std::slice;
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::{gettext, ngettext};
|
||||
use gtk::{CompositeTemplate, gdk, glib, glib::clone};
|
||||
use ruma::{
|
||||
Int, OwnedEventId,
|
||||
events::room::power_levels::{PowerLevelUserAction, UserPowerLevel},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Window,
|
||||
components::{
|
||||
Avatar, RoomMemberDestructiveAction, UserProfileDialog, confirm_mute_room_member_dialog,
|
||||
confirm_room_member_destructive_action_dialog,
|
||||
},
|
||||
gettext_f,
|
||||
prelude::*,
|
||||
session::{
|
||||
model::{Member, MemberRole, Membership, User},
|
||||
view::content::RoomHistory,
|
||||
},
|
||||
toast,
|
||||
utils::{BoundObject, key_bindings},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/sender_avatar/mod.ui"
|
||||
)]
|
||||
#[properties(wrapper_type = super::SenderAvatar)]
|
||||
pub struct SenderAvatar {
|
||||
#[template_child]
|
||||
avatar: TemplateChild<Avatar>,
|
||||
#[template_child]
|
||||
user_id_btn: TemplateChild<gtk::Button>,
|
||||
/// Whether this avatar is active.
|
||||
///
|
||||
/// This avatar is active when the popover is displayed.
|
||||
#[property(get)]
|
||||
active: Cell<bool>,
|
||||
direct_member_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
/// The displayed member.
|
||||
#[property(get, set = Self::set_sender, explicit_notify, nullable)]
|
||||
sender: BoundObject<Member>,
|
||||
/// The popover of this avatar.
|
||||
popover: BoundObject<gtk::PopoverMenu>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for SenderAvatar {
|
||||
const NAME: &'static str = "ContentSenderAvatar";
|
||||
type Type = super::SenderAvatar;
|
||||
type ParentType = gtk::Widget;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::bind_template_callbacks(klass);
|
||||
|
||||
klass.set_layout_manager_type::<gtk::BinLayout>();
|
||||
klass.set_css_name("sender-avatar");
|
||||
klass.set_accessible_role(gtk::AccessibleRole::ToggleButton);
|
||||
|
||||
klass.install_action("sender-avatar.copy-user-id", None, |obj, _, _| {
|
||||
if let Some(popover) = obj.imp().popover.obj() {
|
||||
popover.popdown();
|
||||
}
|
||||
|
||||
let Some(sender) = obj.sender() else {
|
||||
return;
|
||||
};
|
||||
|
||||
obj.clipboard().set_text(sender.user_id().as_str());
|
||||
toast!(obj, gettext("Matrix user ID copied to clipboard"));
|
||||
});
|
||||
|
||||
klass.install_action("sender-avatar.mention", None, |obj, _, _| {
|
||||
obj.imp().mention();
|
||||
});
|
||||
|
||||
klass.install_action_async(
|
||||
"sender-avatar.open-direct-chat",
|
||||
None,
|
||||
|obj, _, _| async move {
|
||||
obj.imp().open_direct_chat().await;
|
||||
},
|
||||
);
|
||||
|
||||
klass.install_action("sender-avatar.permalink", None, |obj, _, _| {
|
||||
let Some(sender) = obj.sender() else {
|
||||
return;
|
||||
};
|
||||
|
||||
obj.clipboard()
|
||||
.set_text(&sender.matrix_to_uri().to_string());
|
||||
toast!(obj, gettext("Link copied to clipboard"));
|
||||
});
|
||||
|
||||
klass.install_action_async("sender-avatar.invite", None, |obj, _, _| async move {
|
||||
obj.imp().invite().await;
|
||||
});
|
||||
|
||||
klass.install_action_async(
|
||||
"sender-avatar.revoke-invite",
|
||||
None,
|
||||
|obj, _, _| async move {
|
||||
obj.imp().kick().await;
|
||||
},
|
||||
);
|
||||
|
||||
klass.install_action_async("sender-avatar.mute", None, |obj, _, _| async move {
|
||||
obj.imp().toggle_muted().await;
|
||||
});
|
||||
|
||||
klass.install_action_async("sender-avatar.unmute", None, |obj, _, _| async move {
|
||||
obj.imp().toggle_muted().await;
|
||||
});
|
||||
|
||||
klass.install_action_async("sender-avatar.kick", None, |obj, _, _| async move {
|
||||
obj.imp().kick().await;
|
||||
});
|
||||
|
||||
klass.install_action_async("sender-avatar.ban", None, |obj, _, _| async move {
|
||||
obj.imp().ban().await;
|
||||
});
|
||||
|
||||
klass.install_action_async("sender-avatar.unban", None, |obj, _, _| async move {
|
||||
obj.imp().unban().await;
|
||||
});
|
||||
|
||||
klass.install_action_async(
|
||||
"sender-avatar.remove-messages",
|
||||
None,
|
||||
|obj, _, _| async move {
|
||||
obj.imp().remove_messages().await;
|
||||
},
|
||||
);
|
||||
|
||||
klass.install_action_async("sender-avatar.ignore", None, |obj, _, _| async move {
|
||||
obj.imp().toggle_ignored().await;
|
||||
});
|
||||
|
||||
klass.install_action_async(
|
||||
"sender-avatar.stop-ignoring",
|
||||
None,
|
||||
|obj, _, _| async move {
|
||||
obj.imp().toggle_ignored().await;
|
||||
},
|
||||
);
|
||||
|
||||
klass.install_action("sender-avatar.view-details", None, |obj, _, _| {
|
||||
obj.imp().view_details();
|
||||
});
|
||||
|
||||
klass.install_action("sender-avatar.activate", None, |obj, _, _| {
|
||||
obj.imp().show_popover(1, 0.0, 0.0);
|
||||
});
|
||||
|
||||
key_bindings::add_activate_bindings(klass, "sender-avatar.activate");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for SenderAvatar {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.set_pressed_state(false);
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.disconnect_signals();
|
||||
|
||||
if let Some(popover) = self.popover.obj() {
|
||||
popover.unparent();
|
||||
popover.remove_child(&*self.user_id_btn);
|
||||
}
|
||||
|
||||
self.avatar.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for SenderAvatar {}
|
||||
|
||||
impl AccessibleImpl for SenderAvatar {
|
||||
fn first_accessible_child(&self) -> Option<gtk::Accessible> {
|
||||
// Hide the children in the a11y tree.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl SenderAvatar {
|
||||
/// Set the displayed member.
|
||||
fn set_sender(&self, sender: Option<Member>) {
|
||||
let prev_sender = self.sender.obj();
|
||||
|
||||
if prev_sender == sender {
|
||||
return;
|
||||
}
|
||||
|
||||
self.disconnect_signals();
|
||||
|
||||
if let Some(sender) = sender {
|
||||
let room = sender.room();
|
||||
let direct_member_handler = room.connect_direct_member_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_actions();
|
||||
}
|
||||
));
|
||||
self.direct_member_handler
|
||||
.replace(Some(direct_member_handler));
|
||||
|
||||
let permissions_handler = room.permissions().connect_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_actions();
|
||||
}
|
||||
));
|
||||
self.permissions_handler.replace(Some(permissions_handler));
|
||||
|
||||
let display_name_handler = sender.connect_display_name_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_accessible_label();
|
||||
}
|
||||
));
|
||||
|
||||
let membership_handler = sender.connect_membership_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_actions();
|
||||
}
|
||||
));
|
||||
|
||||
let power_level_handler = sender.connect_power_level_changed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_actions();
|
||||
}
|
||||
));
|
||||
|
||||
let is_ignored_handler = sender.connect_is_ignored_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_actions();
|
||||
}
|
||||
));
|
||||
|
||||
self.sender.set(
|
||||
sender,
|
||||
vec![
|
||||
display_name_handler,
|
||||
membership_handler,
|
||||
power_level_handler,
|
||||
is_ignored_handler,
|
||||
],
|
||||
);
|
||||
self.update_accessible_label();
|
||||
self.update_actions();
|
||||
}
|
||||
|
||||
self.obj().notify_sender();
|
||||
}
|
||||
|
||||
/// Disconnect all the signals.
|
||||
fn disconnect_signals(&self) {
|
||||
if let Some(sender) = self.sender.obj() {
|
||||
let room = sender.room();
|
||||
|
||||
if let Some(handler) = self.direct_member_handler.take() {
|
||||
room.disconnect(handler);
|
||||
}
|
||||
if let Some(handler) = self.permissions_handler.take() {
|
||||
room.permissions().disconnect(handler);
|
||||
}
|
||||
}
|
||||
|
||||
self.sender.disconnect_signals();
|
||||
}
|
||||
|
||||
/// Update the accessible label for the current sender.
|
||||
fn update_accessible_label(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let label = gettext_f("{user}’s avatar", &[("user", &sender.display_name())]);
|
||||
self.obj()
|
||||
.update_property(&[gtk::accessible::Property::Label(&label)]);
|
||||
}
|
||||
|
||||
/// Update the actions for the current state.
|
||||
fn update_actions(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
let room = sender.room();
|
||||
let is_direct_chat = room.direct_member().is_some();
|
||||
let permissions = room.permissions();
|
||||
let membership = sender.membership();
|
||||
let sender_id = sender.user_id();
|
||||
let is_own_user = sender.is_own_user();
|
||||
let power_level = sender.power_level();
|
||||
let role = permissions.role(power_level);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.mention",
|
||||
!is_own_user && membership == Membership::Join && permissions.can_send_message(),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.open-direct-chat",
|
||||
!is_direct_chat && !is_own_user,
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.invite",
|
||||
!is_own_user
|
||||
&& matches!(membership, Membership::Leave | Membership::Knock)
|
||||
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.revoke-invite",
|
||||
!is_own_user
|
||||
&& membership == Membership::Invite
|
||||
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.mute",
|
||||
!is_own_user
|
||||
&& role != MemberRole::Muted
|
||||
&& permissions.default_power_level() > permissions.mute_power_level()
|
||||
&& permissions
|
||||
.can_do_to_user(sender_id, PowerLevelUserAction::ChangePowerLevel),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.unmute",
|
||||
!is_own_user
|
||||
&& role == MemberRole::Muted
|
||||
&& permissions.default_power_level() > permissions.mute_power_level()
|
||||
&& permissions
|
||||
.can_do_to_user(sender_id, PowerLevelUserAction::ChangePowerLevel),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.kick",
|
||||
!is_own_user
|
||||
&& membership == Membership::Join
|
||||
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.ban",
|
||||
!is_own_user
|
||||
&& membership != Membership::Ban
|
||||
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Ban),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.unban",
|
||||
!is_own_user
|
||||
&& membership == Membership::Ban
|
||||
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Unban),
|
||||
);
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.remove-messages",
|
||||
!is_own_user && permissions.can_redact_other(),
|
||||
);
|
||||
|
||||
obj.action_set_enabled("sender-avatar.ignore", !is_own_user && !sender.is_ignored());
|
||||
|
||||
obj.action_set_enabled(
|
||||
"sender-avatar.stop-ignoring",
|
||||
!is_own_user && sender.is_ignored(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the popover of this avatar.
|
||||
fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
|
||||
let old_popover = self.popover.obj();
|
||||
|
||||
if old_popover == popover {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the state.
|
||||
if let Some(popover) = old_popover {
|
||||
popover.unparent();
|
||||
popover.remove_child(&*self.user_id_btn);
|
||||
}
|
||||
self.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 popover.parent().is_some() {
|
||||
popover.unparent();
|
||||
}
|
||||
|
||||
let parent_handler = popover.connect_parent_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |popover| {
|
||||
if popover.parent().is_none_or(|w| w != *imp.obj()) {
|
||||
imp.popover.disconnect_signals();
|
||||
popover.remove_child(&*imp.user_id_btn);
|
||||
}
|
||||
}
|
||||
));
|
||||
let closed_handler = popover.connect_closed(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.set_active(false);
|
||||
}
|
||||
));
|
||||
|
||||
popover.add_child(&*self.user_id_btn, "user-id");
|
||||
popover.set_parent(&*self.obj());
|
||||
|
||||
self.popover
|
||||
.set(popover, vec![parent_handler, closed_handler]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether this avatar is active.
|
||||
fn set_active(&self, active: bool) {
|
||||
if self.active.get() == active {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active.set(active);
|
||||
|
||||
self.obj().notify_active();
|
||||
self.set_pressed_state(active);
|
||||
}
|
||||
|
||||
/// Set the CSS and a11 states.
|
||||
fn set_pressed_state(&self, pressed: bool) {
|
||||
let obj = self.obj();
|
||||
|
||||
if pressed {
|
||||
obj.set_state_flags(gtk::StateFlags::CHECKED, false);
|
||||
} else {
|
||||
obj.unset_state_flags(gtk::StateFlags::CHECKED);
|
||||
}
|
||||
|
||||
let tristate = if pressed {
|
||||
gtk::AccessibleTristate::True
|
||||
} else {
|
||||
gtk::AccessibleTristate::False
|
||||
};
|
||||
obj.update_state(&[gtk::accessible::State::Pressed(tristate)]);
|
||||
}
|
||||
|
||||
/// The `RoomHistory` that is an ancestor of this avatar.
|
||||
fn room_history(&self) -> Option<RoomHistory> {
|
||||
self.obj()
|
||||
.ancestor(RoomHistory::static_type())
|
||||
.and_downcast()
|
||||
}
|
||||
|
||||
/// Handle a click on the container.
|
||||
///
|
||||
/// Shows a popover with the room member menu.
|
||||
#[template_callback]
|
||||
fn show_popover(&self, _n_press: i32, x: f64, y: f64) {
|
||||
let Some(room_history) = self.room_history() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_active(true);
|
||||
|
||||
let popover = room_history.sender_context_menu();
|
||||
self.set_popover(Some(popover.clone()));
|
||||
|
||||
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 0, 0)));
|
||||
popover.popup();
|
||||
}
|
||||
|
||||
/// Add a mention of the sender to the message composer.
|
||||
fn mention(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
let Some(room_history) = self.room_history() else {
|
||||
return;
|
||||
};
|
||||
|
||||
room_history.message_toolbar().mention_member(&sender);
|
||||
}
|
||||
|
||||
/// View the sender details.
|
||||
fn view_details(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dialog = UserProfileDialog::new();
|
||||
dialog.set_room_member(sender);
|
||||
dialog.present(Some(&*self.obj()));
|
||||
}
|
||||
|
||||
/// Open a direct chat with the current sender.
|
||||
///
|
||||
/// If one doesn't exist already, it is created.
|
||||
async fn open_direct_chat(&self) {
|
||||
let Some(sender) = self.sender.obj().and_upcast::<User>() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
let room = if let Some(room) = sender.direct_chat() {
|
||||
room
|
||||
} else {
|
||||
toast!(obj, &gettext("Creating a new Direct Chat…"));
|
||||
|
||||
if let Ok(room) = sender.get_or_create_direct_chat().await {
|
||||
room
|
||||
} else {
|
||||
toast!(obj, &gettext("Could not create a new Direct Chat"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(main_window) = obj.root().and_downcast::<Window>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
main_window.session_view().select_room(room);
|
||||
}
|
||||
|
||||
/// Invite the sender to the room.
|
||||
async fn invite(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
toast!(obj, gettext("Inviting user…"));
|
||||
|
||||
let room = sender.room();
|
||||
let user_id = sender.user_id().clone();
|
||||
if room.invite(&[user_id]).await.is_err() {
|
||||
toast!(obj, gettext("Could not invite user"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Kick the user from the room.
|
||||
async fn kick(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
let Some(response) = confirm_room_member_destructive_action_dialog(
|
||||
&sender,
|
||||
RoomMemberDestructiveAction::Kick,
|
||||
&*obj,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let membership = sender.membership();
|
||||
|
||||
let label = match membership {
|
||||
Membership::Invite => gettext("Revoking invite…"),
|
||||
_ => gettext("Kicking user…"),
|
||||
};
|
||||
toast!(obj, label);
|
||||
|
||||
let room = sender.room();
|
||||
let user_id = sender.user_id().clone();
|
||||
if room.kick(&[(user_id, response.reason)]).await.is_err() {
|
||||
let error = match membership {
|
||||
Membership::Invite => gettext("Could not revoke invite of user"),
|
||||
_ => gettext("Could not kick user"),
|
||||
};
|
||||
toast!(obj, error);
|
||||
}
|
||||
}
|
||||
|
||||
/// (Un)mute the user in the room.
|
||||
async fn toggle_muted(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let UserPowerLevel::Int(old_power_level) = sender.power_level() else {
|
||||
// We cannot mute someone with an infinite power level.
|
||||
return;
|
||||
};
|
||||
|
||||
let old_power_level = i64::from(old_power_level);
|
||||
let obj = self.obj();
|
||||
let permissions = sender.room().permissions();
|
||||
|
||||
// Warn if user is muted but was not before.
|
||||
let mute_power_level = permissions.mute_power_level();
|
||||
let mute = old_power_level > mute_power_level;
|
||||
if mute && !confirm_mute_room_member_dialog(slice::from_ref(&sender), &*obj).await {
|
||||
return;
|
||||
}
|
||||
|
||||
let user_id = sender.user_id().clone();
|
||||
|
||||
let (new_power_level, text) = if mute {
|
||||
(mute_power_level, gettext("Muting member…"))
|
||||
} else {
|
||||
(
|
||||
permissions.default_power_level(),
|
||||
gettext("Unmuting member…"),
|
||||
)
|
||||
};
|
||||
toast!(obj, text);
|
||||
|
||||
let text = if permissions
|
||||
.set_user_power_level(user_id, Int::new_saturating(new_power_level))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if mute {
|
||||
gettext("Member muted")
|
||||
} else {
|
||||
gettext("Member unmuted")
|
||||
}
|
||||
} else if mute {
|
||||
gettext("Could not mute member")
|
||||
} else {
|
||||
gettext("Could not unmute member")
|
||||
};
|
||||
toast!(obj, text);
|
||||
}
|
||||
|
||||
/// Ban the room member.
|
||||
async fn ban(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
let permissions = sender.room().permissions();
|
||||
let redactable_events = if permissions.can_redact_other() {
|
||||
sender.redactable_events()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let Some(response) = confirm_room_member_destructive_action_dialog(
|
||||
&sender,
|
||||
RoomMemberDestructiveAction::Ban(redactable_events.len()),
|
||||
&*obj,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
toast!(obj, gettext("Banning user…"));
|
||||
|
||||
let room = sender.room();
|
||||
let user_id = sender.user_id().clone();
|
||||
if room
|
||||
.ban(&[(user_id, response.reason.clone())])
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
toast!(obj, gettext("Could not ban user"));
|
||||
}
|
||||
|
||||
if response.remove_events {
|
||||
self.remove_known_messages_inner(&sender, redactable_events, response.reason)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unban the room member.
|
||||
async fn unban(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
toast!(obj, gettext("Unbanning user…"));
|
||||
|
||||
let room = sender.room();
|
||||
let user_id = sender.user_id().clone();
|
||||
if room.unban(&[(user_id, None)]).await.is_err() {
|
||||
toast!(obj, gettext("Could not unban user"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the known events of the room member.
|
||||
async fn remove_messages(&self) {
|
||||
let Some(sender) = self.sender.obj() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let redactable_events = sender.redactable_events();
|
||||
|
||||
let Some(response) = confirm_room_member_destructive_action_dialog(
|
||||
&sender,
|
||||
RoomMemberDestructiveAction::RemoveMessages(redactable_events.len()),
|
||||
&*self.obj(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.remove_known_messages_inner(&sender, redactable_events, response.reason)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn remove_known_messages_inner(
|
||||
&self,
|
||||
sender: &Member,
|
||||
events: Vec<OwnedEventId>,
|
||||
reason: Option<String>,
|
||||
) {
|
||||
let obj = self.obj();
|
||||
let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
|
||||
toast!(
|
||||
obj,
|
||||
ngettext(
|
||||
// Translators: Do NOT translate the content between '{' and '}',
|
||||
// this is a variable name.
|
||||
"Removing 1 message sent by the user…",
|
||||
"Removing {n} messages sent by the user…",
|
||||
n,
|
||||
),
|
||||
n,
|
||||
);
|
||||
|
||||
let room = sender.room();
|
||||
|
||||
if let Err(failed_events) = room.redact(&events, reason).await {
|
||||
let n = u32::try_from(failed_events.len()).unwrap_or(u32::MAX);
|
||||
toast!(
|
||||
obj,
|
||||
ngettext(
|
||||
// Translators: Do NOT translate the content between '{' and '}',
|
||||
// this is a variable name.
|
||||
"Could not remove 1 message sent by the user",
|
||||
"Could not remove {n} messages sent by the user",
|
||||
n,
|
||||
),
|
||||
n,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle whether the user is ignored or not.
|
||||
async fn toggle_ignored(&self) {
|
||||
let Some(sender) = self.sender.obj().and_upcast::<User>() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
let is_ignored = sender.is_ignored();
|
||||
|
||||
let label = if is_ignored {
|
||||
gettext("Stop ignoring user…")
|
||||
} else {
|
||||
gettext("Ignoring user…")
|
||||
};
|
||||
toast!(obj, label);
|
||||
|
||||
if is_ignored {
|
||||
if sender.stop_ignoring().await.is_err() {
|
||||
toast!(obj, gettext("Could not stop ignoring user"));
|
||||
}
|
||||
} else if sender.ignore().await.is_err() {
|
||||
toast!(obj, gettext("Could not ignore user"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// An avatar with a popover menu for room members.
|
||||
pub struct SenderAvatar(ObjectSubclass<imp::SenderAvatar>)
|
||||
@extends gtk::Widget, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl SenderAvatar {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ContentSenderAvatar" parent="GtkWidget">
|
||||
<accessibility>
|
||||
<property name="description" translatable="yes">Open Sender Context Menu</property>
|
||||
<property name="has-popup">true</property>
|
||||
</accessibility>
|
||||
<property name="focusable">true</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="Avatar" id="avatar">
|
||||
<property name="size">36</property>
|
||||
<property name="accessible-role">presentation</property>
|
||||
<binding name="data">
|
||||
<lookup name="avatar-data">
|
||||
<lookup name="sender">ContentSenderAvatar</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGestureClick">
|
||||
<signal name="released" handler="show_popover" swapped="true"/>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<object class="GtkButton" id="user_id_btn">
|
||||
<property name="tooltip-text" translatable="yes">Copy Matrix User ID</property>
|
||||
<property name="action-name">sender-avatar.copy-user-id</property>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
<class name="text-button" />
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="GtkLabel">
|
||||
<binding name="label">
|
||||
<lookup name="user-id-string">
|
||||
<lookup name="sender">ContentSenderAvatar</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="xalign">0.0</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
@ -15,6 +15,9 @@
|
||||
<property name="halign">start</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<style>
|
||||
<class name="body"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -49,7 +49,7 @@
|
||||
<property name="xalign">0</property>
|
||||
<style>
|
||||
<class name="heading"/>
|
||||
</style>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -159,7 +159,7 @@
|
||||
<property name="xalign">0</property>
|
||||
<style>
|
||||
<class name="heading"/>
|
||||
</style>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@ -249,6 +249,9 @@
|
||||
<property name="right-margin">12</property>
|
||||
<property name="top-margin">12</property>
|
||||
<property name="bottom-margin">12</property>
|
||||
<style>
|
||||
<class name="monospace"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
|
@ -130,7 +130,6 @@
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/read_receipts_list/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/read_receipts_list/read_receipts_popover.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/sender_avatar/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state/creation.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state/group_row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state/row.ui</file>
|
||||
|
@ -28,7 +28,10 @@ pub(crate) use queue::{IMAGE_QUEUE, ImageRequestPriority};
|
||||
|
||||
use super::{FrameDimensions, MediaFileError};
|
||||
use crate::{
|
||||
DISABLE_GLYCIN_SANDBOX, RUNTIME, components::AnimatedImagePaintable, spawn_tokio, utils::File,
|
||||
DISABLE_GLYCIN_SANDBOX, RUNTIME,
|
||||
components::AnimatedImagePaintable,
|
||||
spawn_tokio,
|
||||
utils::{File, TokioDrop, save_data_to_tmp_file},
|
||||
};
|
||||
|
||||
/// The maximum dimensions of a thumbnail in the timeline.
|
||||
@ -64,63 +67,111 @@ const THUMBNAIL_DIMENSIONS_THRESHOLD: u32 = 200;
|
||||
/// [supported image formats of glycin]: https://gitlab.gnome.org/GNOME/glycin/-/tree/main?ref_type=heads#supported-image-formats
|
||||
const SUPPORTED_ANIMATED_IMAGE_MIME_TYPES: &[&str] = &["image/gif", "image/png", "image/webp"];
|
||||
|
||||
/// Get an image loader for the given file.
|
||||
async fn image_loader(file: gio::File) -> Result<glycin::Image<'static>, glycin::ErrorCtx> {
|
||||
let mut loader = glycin::Loader::new(file);
|
||||
|
||||
if DISABLE_GLYCIN_SANDBOX {
|
||||
loader.sandbox_selector(glycin::SandboxSelector::NotSandboxed);
|
||||
}
|
||||
|
||||
spawn_tokio!(async move { loader.load().await })
|
||||
.await
|
||||
.unwrap()
|
||||
/// The source for decoding an image.
|
||||
enum ImageDecoderSource {
|
||||
/// The bytes containing the encoded image.
|
||||
Data(Vec<u8>),
|
||||
/// The file containing the encoded image.
|
||||
File(File),
|
||||
}
|
||||
|
||||
/// Load the given file as an image into a `GdkPaintable`.
|
||||
///
|
||||
/// Set `request_dimensions` if the image will be shown at specific dimensions.
|
||||
/// To show the image at its natural size, set it to `None`.
|
||||
async fn load_image(
|
||||
file: File,
|
||||
request_dimensions: Option<FrameDimensions>,
|
||||
) -> Result<Image, glycin::ErrorCtx> {
|
||||
let image_loader = image_loader(file.as_gfile()).await?;
|
||||
impl ImageDecoderSource {
|
||||
/// The maximum size of the `Data` variant. This is 1 MB.
|
||||
const MAX_DATA_SIZE: usize = 1_048_576;
|
||||
|
||||
let frame_request = request_dimensions.map(|request| {
|
||||
let image_info = image_loader.info();
|
||||
|
||||
let original_dimensions = FrameDimensions {
|
||||
width: image_info.width,
|
||||
height: image_info.height,
|
||||
};
|
||||
|
||||
original_dimensions.to_image_loader_request(request)
|
||||
});
|
||||
|
||||
spawn_tokio!(async move {
|
||||
let first_frame = if let Some(frame_request) = frame_request {
|
||||
image_loader.specific_frame(frame_request).await?
|
||||
/// Construct an `ImageSource` from the given bytes.
|
||||
///
|
||||
/// If the size of the bytes are too big to be kept in memory, they are
|
||||
/// written to a temporary file.
|
||||
async fn with_bytes(bytes: Vec<u8>) -> Result<Self, MediaFileError> {
|
||||
if bytes.len() > Self::MAX_DATA_SIZE {
|
||||
Ok(Self::File(save_data_to_tmp_file(bytes).await?))
|
||||
} else {
|
||||
image_loader.next_frame().await?
|
||||
Ok(Self::Data(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this image source into a loader.
|
||||
///
|
||||
/// Returns the created loader, and the image file, if any.
|
||||
fn into_loader(self) -> (glycin::Loader, Option<File>) {
|
||||
let (mut loader, file) = match self {
|
||||
Self::Data(bytes) => (glycin::Loader::new_vec(bytes), None),
|
||||
Self::File(file) => (glycin::Loader::new(file.as_gfile()), Some(file)),
|
||||
};
|
||||
Ok(Image {
|
||||
file,
|
||||
loader: image_loader.into(),
|
||||
first_frame: first_frame.into(),
|
||||
|
||||
if DISABLE_GLYCIN_SANDBOX {
|
||||
loader.sandbox_selector(glycin::SandboxSelector::NotSandboxed);
|
||||
}
|
||||
|
||||
(loader, file)
|
||||
}
|
||||
|
||||
/// Decode this image source into an [`Image`].
|
||||
///
|
||||
/// Set `request_dimensions` if the image will be shown at specific
|
||||
/// dimensions. To show the image at its natural size, set it to `None`.
|
||||
async fn decode_image(
|
||||
self,
|
||||
request_dimensions: Option<FrameDimensions>,
|
||||
) -> Result<Image, ImageError> {
|
||||
let (loader, file) = self.into_loader();
|
||||
|
||||
let decoder = spawn_tokio!(async move { loader.load().await })
|
||||
.await
|
||||
.expect("task was not aborted")?;
|
||||
|
||||
let frame_request = request_dimensions.map(|request| {
|
||||
let image_details = decoder.details();
|
||||
|
||||
let original_dimensions = FrameDimensions {
|
||||
width: image_details.width(),
|
||||
height: image_details.height(),
|
||||
};
|
||||
|
||||
original_dimensions.to_image_loader_request(request)
|
||||
});
|
||||
|
||||
spawn_tokio!(async move {
|
||||
let first_frame = if let Some(frame_request) = frame_request {
|
||||
decoder.specific_frame(frame_request).await?
|
||||
} else {
|
||||
decoder.next_frame().await?
|
||||
};
|
||||
|
||||
Ok(Image {
|
||||
file,
|
||||
decoder: TokioDrop::new(decoder).into(),
|
||||
first_frame: first_frame.into(),
|
||||
})
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("task was not aborted")
|
||||
.await
|
||||
.expect("task was not aborted")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for ImageDecoderSource {
|
||||
fn from(value: File) -> Self {
|
||||
Self::File(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gio::File> for ImageDecoderSource {
|
||||
fn from(value: gio::File) -> Self {
|
||||
Self::File(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// An image that was just loaded.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Image {
|
||||
/// The file of the image.
|
||||
file: File,
|
||||
/// The image loader.
|
||||
loader: Arc<glycin::Image<'static>>,
|
||||
/// The file containing the image, if any.
|
||||
///
|
||||
/// We need to keep a strong reference to the temporary file or it will be
|
||||
/// destroyed.
|
||||
file: Option<File>,
|
||||
/// The image decoder.
|
||||
decoder: Arc<TokioDrop<glycin::Image>>,
|
||||
/// The first frame of the image.
|
||||
first_frame: Arc<glycin::Frame>,
|
||||
}
|
||||
@ -134,7 +185,7 @@ impl fmt::Debug for Image {
|
||||
impl From<Image> for gdk::Paintable {
|
||||
fn from(value: Image) -> Self {
|
||||
if value.first_frame.delay().is_some() {
|
||||
AnimatedImagePaintable::new(value.file, value.loader, value.first_frame).upcast()
|
||||
AnimatedImagePaintable::new(value.decoder, value.first_frame, value.file).upcast()
|
||||
} else {
|
||||
value.first_frame.texture().upcast()
|
||||
}
|
||||
@ -157,9 +208,14 @@ impl ImageInfoLoader {
|
||||
async fn into_first_frame(self) -> Option<Frame> {
|
||||
match self {
|
||||
Self::File(file) => {
|
||||
let image_loader = image_loader(file).await.ok()?;
|
||||
let handle = spawn_tokio!(async move { image_loader.next_frame().await });
|
||||
Some(Frame::Glycin(handle.await.unwrap().ok()?))
|
||||
let (loader, _) = ImageDecoderSource::from(file).into_loader();
|
||||
|
||||
let frame = spawn_tokio!(async move { loader.load().await?.next_frame().await })
|
||||
.await
|
||||
.expect("task was not aborted")
|
||||
.ok()?;
|
||||
|
||||
Some(Frame::Glycin(frame))
|
||||
}
|
||||
Self::Texture(texture) => Some(Frame::Texture(texture)),
|
||||
}
|
||||
|
@ -19,13 +19,12 @@ use tokio::{
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::{Image, ImageError, load_image};
|
||||
use super::{Image, ImageDecoderSource, ImageError};
|
||||
use crate::{
|
||||
spawn_tokio,
|
||||
utils::{
|
||||
File,
|
||||
media::{FrameDimensions, MediaFileError},
|
||||
save_data_to_tmp_file,
|
||||
},
|
||||
};
|
||||
|
||||
@ -156,7 +155,7 @@ impl ImageRequestQueueInner {
|
||||
}
|
||||
|
||||
/// Add the given request to the queue.
|
||||
fn add_request(&mut self, request_id: ImageRequestId, request: ImageRequest) {
|
||||
fn queue_request(&mut self, request_id: ImageRequestId, request: ImageRequest) {
|
||||
let is_limit_reached = self.is_limit_reached();
|
||||
if !is_limit_reached || request.priority == ImageRequestPriority::High {
|
||||
// Spawn the request right away.
|
||||
@ -175,6 +174,31 @@ impl ImageRequestQueueInner {
|
||||
self.requests.insert(request_id, request);
|
||||
}
|
||||
|
||||
/// Add the given image request.
|
||||
///
|
||||
/// If another request for the same image already exists, this will reuse
|
||||
/// the same request.
|
||||
fn add_request(
|
||||
&mut self,
|
||||
inner: ImageLoaderRequest,
|
||||
priority: ImageRequestPriority,
|
||||
) -> ImageRequestHandle {
|
||||
let request_id = inner.source.request_id();
|
||||
|
||||
// If the request already exists, use the existing one.
|
||||
if let Some(request) = self.requests.get(&request_id) {
|
||||
let result_receiver = request.result_sender.subscribe();
|
||||
return ImageRequestHandle::new(result_receiver);
|
||||
}
|
||||
|
||||
// Build and add the request.
|
||||
let (request, result_receiver) = ImageRequest::new(inner, priority);
|
||||
|
||||
self.queue_request(request_id.clone(), request);
|
||||
|
||||
ImageRequestHandle::new(result_receiver)
|
||||
}
|
||||
|
||||
/// Add a request to download an image.
|
||||
///
|
||||
/// If another request for the same image already exists, this will reuse
|
||||
@ -186,24 +210,13 @@ impl ImageRequestQueueInner {
|
||||
dimensions: Option<FrameDimensions>,
|
||||
priority: ImageRequestPriority,
|
||||
) -> ImageRequestHandle {
|
||||
let data = DownloadRequestData {
|
||||
client,
|
||||
settings,
|
||||
dimensions,
|
||||
};
|
||||
let request_id = data.request_id();
|
||||
|
||||
// If the request already exists, use the existing one.
|
||||
if let Some(request) = self.requests.get(&request_id) {
|
||||
let result_receiver = request.result_sender.subscribe();
|
||||
return ImageRequestHandle::new(result_receiver);
|
||||
}
|
||||
|
||||
// Build and add the request.
|
||||
let (request, result_receiver) = ImageRequest::new(data, priority);
|
||||
self.add_request(request_id.clone(), request);
|
||||
|
||||
ImageRequestHandle::new(result_receiver)
|
||||
self.add_request(
|
||||
ImageLoaderRequest {
|
||||
source: ImageRequestSource::Download(DownloadRequest { client, settings }),
|
||||
dimensions,
|
||||
},
|
||||
priority,
|
||||
)
|
||||
}
|
||||
|
||||
/// Add a request to load an image from a file.
|
||||
@ -215,23 +228,15 @@ impl ImageRequestQueueInner {
|
||||
file: File,
|
||||
dimensions: Option<FrameDimensions>,
|
||||
) -> ImageRequestHandle {
|
||||
let data = FileRequestData { file, dimensions };
|
||||
let request_id = data.request_id();
|
||||
|
||||
// If the request already exists, use the existing one.
|
||||
if let Some(request) = self.requests.get(&request_id) {
|
||||
let result_receiver = request.result_sender.subscribe();
|
||||
return ImageRequestHandle::new(result_receiver);
|
||||
}
|
||||
|
||||
// Build and add the request.
|
||||
// Always use high priority because file requests should always be for
|
||||
// previewing a local image.
|
||||
let (request, result_receiver) = ImageRequest::new(data, ImageRequestPriority::High);
|
||||
|
||||
self.add_request(request_id.clone(), request);
|
||||
|
||||
ImageRequestHandle::new(result_receiver)
|
||||
self.add_request(
|
||||
ImageLoaderRequest {
|
||||
source: ImageRequestSource::File(file),
|
||||
dimensions,
|
||||
},
|
||||
ImageRequestPriority::High,
|
||||
)
|
||||
}
|
||||
|
||||
/// Mark the request with the given ID as stalled.
|
||||
@ -335,8 +340,8 @@ impl ImageRequestQueueInner {
|
||||
|
||||
/// A request for an image.
|
||||
struct ImageRequest {
|
||||
/// The data of the request.
|
||||
data: ImageRequestData,
|
||||
/// The request to the image loader.
|
||||
inner: ImageLoaderRequest,
|
||||
/// The priority of the request.
|
||||
priority: ImageRequestPriority,
|
||||
/// The sender of the channel to use to send the result.
|
||||
@ -352,13 +357,13 @@ struct ImageRequest {
|
||||
impl ImageRequest {
|
||||
/// Construct an image request with the given data and priority.
|
||||
fn new(
|
||||
data: impl Into<ImageRequestData>,
|
||||
inner: ImageLoaderRequest,
|
||||
priority: ImageRequestPriority,
|
||||
) -> (Self, broadcast::Receiver<Result<Image, ImageError>>) {
|
||||
let (result_sender, result_receiver) = broadcast::channel(1);
|
||||
(
|
||||
Self {
|
||||
data: data.into(),
|
||||
inner,
|
||||
priority,
|
||||
result_sender,
|
||||
retries_count: 0,
|
||||
@ -379,14 +384,14 @@ impl ImageRequest {
|
||||
|
||||
/// Spawn this request.
|
||||
fn spawn(&self) {
|
||||
let data = self.data.clone();
|
||||
let inner = self.inner.clone();
|
||||
let result_sender = self.result_sender.clone();
|
||||
let retries_count = self.retries_count;
|
||||
let task_handle = self.task_handle.clone();
|
||||
let stalled_timeout_source = self.stalled_timeout_source.clone();
|
||||
|
||||
let abort_handle = spawn_tokio!(async move {
|
||||
let request_id = data.request_id();
|
||||
let request_id = inner.source.request_id();
|
||||
|
||||
let stalled_timeout_source_clone = stalled_timeout_source.clone();
|
||||
let request_id_clone = request_id.clone();
|
||||
@ -404,7 +409,7 @@ impl ImageRequest {
|
||||
source.remove();
|
||||
}
|
||||
|
||||
let result = data.await;
|
||||
let result = inner.await;
|
||||
|
||||
// Cancel the timeout.
|
||||
if let Ok(Some(source)) = stalled_timeout_source.lock().map(|mut s| s.take()) {
|
||||
@ -451,7 +456,7 @@ impl Drop for ImageRequest {
|
||||
handle.abort();
|
||||
|
||||
// Broadcast that the request was aborted.
|
||||
let request_id = self.data.request_id();
|
||||
let request_id = self.inner.source.request_id();
|
||||
let result_sender = self.result_sender.clone();
|
||||
spawn_tokio!(async move {
|
||||
if let Err(error) = result_sender.send(Err(ImageError::Aborted)) {
|
||||
@ -462,26 +467,17 @@ impl Drop for ImageRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of a request to download an image.
|
||||
/// A request to download an image.
|
||||
#[derive(Clone)]
|
||||
struct DownloadRequestData {
|
||||
struct DownloadRequest {
|
||||
/// The Matrix client to use to make the request.
|
||||
client: Client,
|
||||
/// The settings of the request.
|
||||
settings: MediaRequestParameters,
|
||||
/// The dimensions to request.
|
||||
dimensions: Option<FrameDimensions>,
|
||||
}
|
||||
|
||||
impl DownloadRequestData {
|
||||
/// The ID of the image request with this data.
|
||||
fn request_id(&self) -> ImageRequestId {
|
||||
ImageRequestId::Download(self.settings.unique_key())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for DownloadRequestData {
|
||||
type Output = Result<File, MediaFileError>;
|
||||
impl IntoFuture for DownloadRequest {
|
||||
type Output = Result<ImageDecoderSource, MediaFileError>;
|
||||
type IntoFuture = BoxFuture<'static, Self::Output>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
@ -491,155 +487,76 @@ impl IntoFuture for DownloadRequestData {
|
||||
|
||||
Box::pin(async move {
|
||||
let media = client.media();
|
||||
let data = match media.get_media_content(&settings, true).await {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
return Err(MediaFileError::from(error));
|
||||
}
|
||||
};
|
||||
let data = media
|
||||
.get_media_content(&settings, true)
|
||||
.await
|
||||
.map_err(MediaFileError::from)?;
|
||||
|
||||
let file = ImageDecoderSource::with_bytes(data).await?;
|
||||
|
||||
let file = save_data_to_tmp_file(data).await?;
|
||||
Ok(file)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of a request to load an image file into a paintable.
|
||||
/// A request to the image loader.
|
||||
#[derive(Clone)]
|
||||
struct FileRequestData {
|
||||
/// The image file to load.
|
||||
file: File,
|
||||
struct ImageLoaderRequest {
|
||||
/// The source of the image data.
|
||||
source: ImageRequestSource,
|
||||
/// The dimensions to request.
|
||||
dimensions: Option<FrameDimensions>,
|
||||
}
|
||||
|
||||
impl FileRequestData {
|
||||
/// The ID of the image request with this data.
|
||||
fn request_id(&self) -> ImageRequestId {
|
||||
ImageRequestId::File(self.file.path().expect("file has a path"))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for FileRequestData {
|
||||
type Output = Result<Image, glycin::ErrorCtx>;
|
||||
type IntoFuture = BoxFuture<'static, Self::Output>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
let Self { file, dimensions } = self;
|
||||
|
||||
Box::pin(async move { load_image(file, dimensions).await })
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of an image request.
|
||||
#[derive(Clone)]
|
||||
enum ImageRequestData {
|
||||
/// The data for a download request.
|
||||
Download {
|
||||
/// The data to download the image.
|
||||
download_data: DownloadRequestData,
|
||||
/// The data to load the image into a paintable, after it was
|
||||
/// downloaded.
|
||||
file_data: Option<FileRequestData>,
|
||||
},
|
||||
/// The data for a file request.
|
||||
File(FileRequestData),
|
||||
}
|
||||
|
||||
impl ImageRequestData {
|
||||
/// The ID of the image request with this data.
|
||||
fn request_id(&self) -> ImageRequestId {
|
||||
match self {
|
||||
ImageRequestData::Download { download_data, .. } => download_data.request_id(),
|
||||
ImageRequestData::File(file_data) => file_data.request_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The data for the next request with this image request data.
|
||||
fn into_next_request_data(self) -> DownloadOrFileRequestData {
|
||||
match self {
|
||||
Self::Download {
|
||||
download_data,
|
||||
file_data,
|
||||
} => {
|
||||
if let Some(file_data) = file_data {
|
||||
file_data.into()
|
||||
} else {
|
||||
download_data.into()
|
||||
}
|
||||
}
|
||||
Self::File(file_data) => file_data.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for ImageRequestData {
|
||||
impl IntoFuture for ImageLoaderRequest {
|
||||
type Output = Result<Image, ImageError>;
|
||||
type IntoFuture = BoxFuture<'static, Self::Output>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let file_data = match self.into_next_request_data() {
|
||||
DownloadOrFileRequestData::Download(download_data) => {
|
||||
let dimensions = download_data.dimensions;
|
||||
// Load the data from the source.
|
||||
let source = self.source.try_into_decoder_source().await?;
|
||||
|
||||
// Download the image to a file.
|
||||
match download_data.await {
|
||||
Ok(file) => FileRequestData { file, dimensions },
|
||||
Err(error) => {
|
||||
warn!("Could not retrieve image: {error}");
|
||||
return Err(error.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
DownloadOrFileRequestData::File(file_data) => file_data,
|
||||
};
|
||||
|
||||
// Load the image from the file.
|
||||
match file_data.clone().await {
|
||||
Ok(image) => Ok(image),
|
||||
Err(error) => {
|
||||
warn!("Could not load image from file: {error}");
|
||||
Err(error.into())
|
||||
}
|
||||
}
|
||||
// Decode the image from the data.
|
||||
source
|
||||
.decode_image(self.dimensions)
|
||||
.await
|
||||
.inspect_err(|error| warn!("Could not decode image: {error}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DownloadRequestData> for ImageRequestData {
|
||||
fn from(download_data: DownloadRequestData) -> Self {
|
||||
Self::Download {
|
||||
download_data,
|
||||
file_data: None,
|
||||
/// The source for an image request.
|
||||
#[derive(Clone)]
|
||||
enum ImageRequestSource {
|
||||
/// The image must be downloaded from the media cache or the server.
|
||||
Download(DownloadRequest),
|
||||
/// The image is in the given file.
|
||||
File(File),
|
||||
}
|
||||
|
||||
impl ImageRequestSource {
|
||||
/// The ID of the image request with this source.
|
||||
fn request_id(&self) -> ImageRequestId {
|
||||
match self {
|
||||
Self::Download(download_request) => {
|
||||
ImageRequestId::Download(download_request.settings.unique_key())
|
||||
}
|
||||
Self::File(file) => ImageRequestId::File(file.path().expect("file should have a path")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileRequestData> for ImageRequestData {
|
||||
fn from(value: FileRequestData) -> Self {
|
||||
Self::File(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of a download request or a file request.
|
||||
#[derive(Clone)]
|
||||
enum DownloadOrFileRequestData {
|
||||
/// The data for a download request.
|
||||
Download(DownloadRequestData),
|
||||
/// The data for a file request.
|
||||
File(FileRequestData),
|
||||
}
|
||||
|
||||
impl From<DownloadRequestData> for DownloadOrFileRequestData {
|
||||
fn from(download_data: DownloadRequestData) -> Self {
|
||||
Self::Download(download_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileRequestData> for DownloadOrFileRequestData {
|
||||
fn from(value: FileRequestData) -> Self {
|
||||
Self::File(value)
|
||||
/// Try to download the image, if necessary.
|
||||
async fn try_into_decoder_source(self) -> Result<ImageDecoderSource, ImageError> {
|
||||
match self {
|
||||
Self::Download(download_request) => {
|
||||
// Download the image.
|
||||
Ok(download_request
|
||||
.await
|
||||
.inspect_err(|error| warn!("Could not retrieve image: {error}"))?)
|
||||
}
|
||||
Self::File(data) => Ok(data.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,9 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, OnceCell, RefCell},
|
||||
fmt, fs, io,
|
||||
io::Write,
|
||||
fmt, fs,
|
||||
io::{self, Write},
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{Arc, LazyLock},
|
||||
@ -384,36 +385,30 @@ impl<T> AsyncAction<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that requires the tokio runtime to be running when dropped.
|
||||
///
|
||||
/// This is basically usable as a [`OnceCell`].
|
||||
/// A wrapper that requires the tokio runtime to be running when dropped.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TokioDrop<T>(OnceCell<T>);
|
||||
pub struct TokioDrop<T>(Option<T>);
|
||||
|
||||
impl<T> TokioDrop<T> {
|
||||
/// Create a new empty `TokioDrop`;
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Gets a reference to the underlying value.
|
||||
///
|
||||
/// Returns `None` if the cell is empty.
|
||||
pub fn get(&self) -> Option<&T> {
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
/// Sets the contents of this cell to `value`.
|
||||
///
|
||||
/// Returns `Ok(())` if the cell was empty and `Err(value)` if it was full.
|
||||
pub(crate) fn set(&self, value: T) -> Result<(), T> {
|
||||
self.0.set(value)
|
||||
/// Create a new `TokioDrop` wrapping the given type.
|
||||
pub fn new(value: T) -> Self {
|
||||
Self(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for TokioDrop<T> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
impl<T> Deref for TokioDrop<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
.as_ref()
|
||||
.expect("TokioDrop should always contain a value")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for TokioDrop<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,35 +416,12 @@ impl<T> Drop for TokioDrop<T> {
|
||||
fn drop(&mut self) {
|
||||
let _guard = RUNTIME.enter();
|
||||
|
||||
if let Some(inner) = self.0.take() {
|
||||
drop(inner);
|
||||
if let Some(value) = self.0.take() {
|
||||
drop(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: glib::property::Property> glib::property::Property for TokioDrop<T> {
|
||||
type Value = T::Value;
|
||||
}
|
||||
|
||||
impl<T> glib::property::PropertyGet for TokioDrop<T> {
|
||||
type Value = T;
|
||||
|
||||
fn get<R, F: Fn(&Self::Value) -> R>(&self, f: F) -> R {
|
||||
f(self.get().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> glib::property::PropertySet for TokioDrop<T> {
|
||||
type SetValue = T;
|
||||
|
||||
fn set(&self, v: Self::SetValue) {
|
||||
assert!(
|
||||
self.set(v).is_ok(),
|
||||
"TokioDrop value was already initialized"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of a resource that can be loaded.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, glib::Enum)]
|
||||
#[enum_type(name = "LoadingState")]
|
||||
@ -539,6 +511,7 @@ pub(crate) async fn save_data_to_tmp_file(data: Vec<u8>) -> Result<File, std::io
|
||||
}
|
||||
}
|
||||
let mut file = NamedTempFile::new_in(dir)?;
|
||||
tracing::debug!("Created new tmp file: {}", file.path().to_string_lossy());
|
||||
file.write_all(&data)?;
|
||||
|
||||
Ok(file.into())
|
||||
|
Loading…
x
Reference in New Issue
Block a user