You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

464 lines
14 KiB

use sourceview::prelude::BufferExt;
/// Spawn a future on the default `MainContext`
///
/// This was taken from `gtk-macros`
/// but allows setting optionally the priority
///
/// FIXME: this should maybe be upstreamed
#[macro_export]
macro_rules! spawn {
($future:expr) => {
let ctx = glib::MainContext::default();
ctx.spawn_local($future);
};
($priority:expr, $future:expr) => {
let ctx = glib::MainContext::default();
ctx.spawn_local_with_priority($priority, $future);
};
}
/// Spawn a future on the tokio runtime
#[macro_export]
macro_rules! spawn_tokio {
($future:expr) => {
$crate::RUNTIME.spawn($future)
};
}
/// Show a toast with the given message on the ancestor window of `widget`.
///
/// The simplest way to use this macros is for displaying a simple message. It
/// can be anything that implements `AsRef<str>`.
///
/// ```ignore
/// toast!(widget, gettext("Something happened"));
/// ```
///
/// This macro also supports replacing named variables with their value. It
/// supports both the `var` and the `var = expr` syntax. In this case the
/// message and the variables must be `String`s.
///
/// ```ignore
/// toast!(
/// widget,
/// gettext("Error number {n}: {msg}"),
/// n = error_nb.to_string(),
/// msg,
/// );
/// ```
///
/// To add `Pill`s to the toast, you can precede a [`Room`] or [`User`] with
/// `@`.
///
/// ```ignore
/// let room = Room::new(session, room_id);
/// let member = Member::new(room, user_id);
///
/// toast!(
/// widget,
/// gettext("Could not contact {user} in {room}",
/// @user = member,
/// @room,
/// );
/// ```
///
/// For this macro to work, the ancestor window be a [`Window`](crate::Window)
/// or an [`adw::PreferencesWindow`].
///
/// [`Room`]: crate::session::room::Room
/// [`User`]: crate::session::user::User
#[macro_export]
macro_rules! toast {
($widget:expr, $message:expr) => {
{
$crate::_add_toast!($widget, adw::Toast::new($message.as_ref()));
}
};
($widget:expr, $message:expr, $($tail:tt)+) => {
{
let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+);
let string_dict: Vec<_> = string_vars
.iter()
.map(|(key, val): &(&str, String)| (key.as_ref(), val.as_ref()))
.collect();
let message = $crate::utils::freplace($message.into(), &*string_dict);
let toast = if pill_vars.is_empty() {
adw::Toast::new($message.as_ref())
} else {
let pill_vars = std::collections::HashMap::<&str, $crate::components::Pill>::from(pill_vars);
let mut swapped_label = String::new();
let mut widgets = Vec::with_capacity(pill_vars.len());
let mut last_end = 0;
let mut matches = pill_vars
.keys()
.map(|key: &&str| {
message
.match_indices(&format!("{{{key}}}"))
.map(|(start, _)| (start, key))
.collect::<Vec<_>>()
})
.flatten()
.collect::<Vec<_>>();
matches.sort_unstable();
for (start, key) in matches {
swapped_label.push_str(&message[last_end..start]);
swapped_label.push_str($crate::components::DEFAULT_PLACEHOLDER);
last_end = start + key.len() + 2;
widgets.push(pill_vars.get(key).unwrap().clone())
}
swapped_label.push_str(&message[last_end..message.len()]);
let widget = $crate::components::LabelWithWidgets::with_label_and_widgets(
&swapped_label,
widgets,
)
.upcast::<gtk::Widget>();
adw::Toast::builder()
.custom_title(&widget)
.build()
};
$crate::_add_toast!($widget, toast);
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _toast_accum {
([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident, $($tail:tt)*) => {
$crate::_toast_accum!([$($string_vars)* (stringify!($var), $var),], [$($pill_vars)*], $($tail)*)
};
([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => {
$crate::_toast_accum!([$($string_vars)* (stringify!($var), $val),], [$($pill_vars)*], $($tail)*)
};
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident, $($tail:tt)*) => {
{
let pill: $crate::components::Pill = $var.to_pill();
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
}
};
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => {
{
let pill: $crate::components::Pill = $val.to_pill();
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
}
};
([$($string_vars:tt)*], [$($pill_vars:tt)*],) => { ([$($string_vars)*], [$($pill_vars)*]) };
}
#[doc(hidden)]
#[macro_export]
macro_rules! _add_toast {
($widget:expr, $toast:expr) => {{
use gtk::prelude::WidgetExt;
if let Some(root) = $widget.root() {
if let Some(window) = root.downcast_ref::<$crate::Window>() {
window.add_toast($toast.as_ref());
} else if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
use adw::prelude::PreferencesWindowExt;
window.add_toast($toast.as_ref());
} else {
panic!("Trying to display a toast when the parent doesn't support it");
}
}
}};
}
use std::{convert::TryInto, path::PathBuf, str::FromStr};
use gettextrs::gettext;
use gtk::{
gio::{self, prelude::*},
glib::{self, closure, Object},
};
use matrix_sdk::ruma::{
events::room::MediaSource, EventId, OwnedEventId, OwnedTransactionId, TransactionId, UInt,
};
use mime::Mime;
// Returns an expression that is the and’ed result of the given boolean
// expressions.
#[allow(dead_code)]
pub fn and_expr<E: AsRef<gtk::Expression>>(a_expr: E, b_expr: E) -> gtk::ClosureExpression {
gtk::ClosureExpression::new::<bool, _, _>(
&[a_expr, b_expr],
closure!(|_: Option<Object>, a: bool, b: bool| { a && b }),
)
}
// Returns an expression that is the or’ed result of the given boolean
// expressions.
pub fn or_expr<E: AsRef<gtk::Expression>>(a_expr: E, b_expr: E) -> gtk::ClosureExpression {
gtk::ClosureExpression::new::<bool, _, _>(
&[a_expr, b_expr],
closure!(|_: Option<Object>, a: bool, b: bool| { a || b }),
)
}
// Returns an expression that is the inverted result of the given boolean
// expressions.
#[allow(dead_code)]
pub fn not_expr<E: AsRef<gtk::Expression>>(a_expr: E) -> gtk::ClosureExpression {
gtk::ClosureExpression::new::<bool, _, _>(
&[a_expr],
closure!(|_: Option<Object>, a: bool| { !a }),
)
}
pub fn cache_dir() -> PathBuf {
let mut path = glib::user_cache_dir();
path.push("fractal");
if !path.exists() {
let dir = gio::File::for_path(path.clone());
dir.make_directory_with_parents(gio::Cancellable::NONE)
.unwrap();
}
path
}
/// Converts a `UInt` to `i32`.
///
/// Returns `-1` if the conversion didn't work.
pub fn uint_to_i32(u: Option<UInt>) -> i32 {
u.and_then(|ui| {
let u: Option<u16> = ui.try_into().ok();
u
})
.map(|u| {
let i: i32 = u.into();
i
})
.unwrap_or(-1)
}
pub fn setup_style_scheme(buffer: &sourceview::Buffer) {
let manager = adw::StyleManager::default();
buffer.set_style_scheme(style_scheme().as_ref());
manager.connect_dark_notify(glib::clone!(@weak buffer => move |_| {
buffer.set_style_scheme(style_scheme().as_ref());
}));
}
pub fn style_scheme() -> Option<sourceview::StyleScheme> {
let manager = adw::StyleManager::default();
let scheme_name = if manager.is_dark() {
"Adwaita-dark"
} else {
"Adwaita"
};
sourceview::StyleSchemeManager::default().scheme(scheme_name)
}
/// Get the unique id of the given `MediaSource`.
///
/// It is built from the underlying `MxcUri` and can be safely used in a
/// filename.
///
/// The id is not guaranteed to be unique for malformed `MxcUri`s.
pub fn media_type_uid(media_type: Option<MediaSource>) -> String {
if let Some(mxc) = media_type
.map(|media_type| match media_type {
MediaSource::Plain(uri) => uri,
MediaSource::Encrypted(file) => file.url,
})
.filter(|mxc| mxc.is_valid())
{
format!("{}_{}", mxc.server_name().unwrap(), mxc.media_id().unwrap())
} else {
"media_uid".to_owned()
}
}
/// Get a default filename for a mime type.
///
/// Tries to guess the file extension, but it might not find it.
///
/// If the mime type is unknown, it uses the name for `fallback`. The fallback
/// mime types that are recognized are `mime::IMAGE`, `mime::VIDEO`
/// and `mime::AUDIO`, other values will behave the same as `None`.
pub fn filename_for_mime(mime_type: Option<&str>, fallback: Option<mime::Name>) -> String {
let (type_, extension) = if let Some(mime) = mime_type.and_then(|m| Mime::from_str(m).ok()) {
let extension =
mime_guess::get_mime_extensions(&mime).map(|extensions| extensions[0].to_owned());
(Some(mime.type_().as_str().to_owned()), extension)
} else {
(fallback.map(|type_| type_.as_str().to_owned()), None)
};
let name = match type_.as_deref() {
// Translators: Default name for image files.
Some("image") => gettext("image"),
// Translators: Default name for video files.
Some("video") => gettext("video"),
// Translators: Default name for audio files.
Some("audio") => gettext("audio"),
// Translators: Default name for files.
_ => gettext("file"),
};
extension
.map(|extension| format!("{}.{}", name, extension))
.unwrap_or(name)
}
/// Generate temporary IDs for pending events.
///
/// Returns a `(transaction_id, event_id)` tuple. The `event_id` is derived from
/// the `transaction_id`.
pub fn pending_event_ids() -> (OwnedTransactionId, OwnedEventId) {
let txn_id = TransactionId::new();
let event_id = EventId::parse(format!("${}:fractal.gnome.org", txn_id)).unwrap();
(txn_id, event_id)
}
pub enum TimeoutFuture {
Timeout,
}
use futures::{
future::{self, Either, Future},
pin_mut,
};
pub async fn timeout_future<T>(
timeout: std::time::Duration,
fut: impl Future<Output = T>,
) -> Result<T, TimeoutFuture> {
let timeout = glib::timeout_future(timeout);
pin_mut!(fut);
match future::select(fut, timeout).await {
Either::Left((x, _)) => Ok(x),
_ => Err(TimeoutFuture::Timeout),
}
}
pub struct TemplateCallbacks {}
#[gtk::template_callbacks(functions)]
impl TemplateCallbacks {
#[template_callback]
fn string_not_empty(string: Option<&str>) -> bool {
!string.unwrap_or_default().is_empty()
}
#[template_callback]
fn object_is_some(obj: Option<glib::Object>) -> bool {
obj.is_some()
}
#[template_callback]
fn invert_boolean(boolean: bool) -> bool {
!boolean
}
}
/// The result of a password validation.
#[derive(Debug, Default, Clone, Copy)]
pub struct PasswordValidity {
/// Whether the password includes at least one lowercase letter.
pub has_lowercase: bool,
/// Whether the password includes at least one uppercase letter.
pub has_uppercase: bool,
/// Whether the password includes at least one number.
pub has_number: bool,
/// Whether the password includes at least one symbol.
pub has_symbol: bool,
/// Whether the password is at least 8 characters long.
pub has_length: bool,
/// The percentage of checks passed for the password, between 0 and 100.
///
/// If progress is 100, the password is valid.
pub progress: u32,
}
impl PasswordValidity {
pub fn new() -> Self {
Self::default()
}
}
/// Validate a password according to the Matrix specification.
///
/// A password should include a lower-case letter, an upper-case letter, a
/// number and a symbol and be at a minimum 8 characters in length.
///
/// See: <https://spec.matrix.org/v1.1/client-server-api/#notes-on-password-management>
pub fn validate_password(password: &str) -> PasswordValidity {
let mut validity = PasswordValidity::new();
for char in password.chars() {
if char.is_numeric() {
validity.has_number = true;
} else if char.is_lowercase() {
validity.has_lowercase = true;
} else if char.is_uppercase() {
validity.has_uppercase = true;
} else {
validity.has_symbol = true;
}
}
validity.has_length = password.len() >= 8;
let mut passed = 0;
if validity.has_number {
passed += 1;
}
if validity.has_lowercase {
passed += 1;
}
if validity.has_uppercase {
passed += 1;
}
if validity.has_symbol {
passed += 1;
}
if validity.has_length {
passed += 1;
}
validity.progress = passed * 100 / 5;
validity
}
/// Replace variables in the given string with the given dictionary.
///
/// The expected format to replace is `{name}`, where `name` is the first string
/// in the dictionary entry tuple.
pub fn freplace(s: String, args: &[(&str, &str)]) -> String {
let mut s = s;
for (k, v) in args {
s = s.replace(&format!("{{{}}}", k), v);
}
s
}
pub async fn check_if_reachable(hostname: &impl AsRef<str>) -> bool {
let address = gio::NetworkAddress::parse_uri(hostname.as_ref(), 80).unwrap();
let monitor = gio::NetworkMonitor::default();
match monitor.can_reach_future(&address).await {
Ok(()) => true,
Err(error) => {
log::error!(
"Homeserver {} isn't reachable: {}",
hostname.as_ref(),
error
);
false
}
}
}