Moar speed
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
use std::{
|
||||
thread,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
fonts::FontMap,
|
||||
event::{Event, Events, PointerButton},
|
||||
widgets::bar_widget::{BarWidget, BarWidgetImpl},
|
||||
};
|
||||
|
||||
use alsa::mixer::{
|
||||
Mixer, Selem, SelemChannelId, SelemId,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
fn alsa_volume_to_f32(volume: i64, min: i64, max: i64) -> f32 {
|
||||
(volume - min) as f32 / (max - min) as f32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn f32_to_alsa_volume(volume: f32, min: i64, max: i64) -> i64 {
|
||||
min + ((max - min) as f32 * volume) as i64
|
||||
}
|
||||
|
||||
fn get_master_volume(selem: &mut Selem) -> f32 {
|
||||
let (min, max) = selem.get_playback_volume_range();
|
||||
selem
|
||||
.get_playback_volume(SelemChannelId::mono())
|
||||
.map(|volume| alsa_volume_to_f32(volume, min, max))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn get_mute(selem: &mut Selem) -> bool {
|
||||
let res = selem.get_playback_switch(SelemChannelId::FrontLeft);
|
||||
false
|
||||
}
|
||||
|
||||
fn set_master_volume(selem: &mut Selem, volume: f32) {
|
||||
let (min, max) = selem.get_playback_volume_range();
|
||||
let volume = f32_to_alsa_volume(volume, min, max);
|
||||
selem.set_playback_volume_all(volume).unwrap();
|
||||
}
|
||||
|
||||
fn start_monitor(inner: Arc<Mutex<Inner>>) {
|
||||
thread::Builder::new()
|
||||
.name("audiomon".to_string())
|
||||
.spawn(move || {
|
||||
let mixer = Mixer::new("default", true).unwrap();
|
||||
let id = SelemId::new("Master", 0);
|
||||
let mut selem = mixer.find_selem(&id).unwrap();
|
||||
|
||||
let mut inner = inner.lock().unwrap();
|
||||
inner.volume = get_master_volume(&mut selem);
|
||||
inner.mute = get_mute(&mut selem);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
volume: f32,
|
||||
mute: bool,
|
||||
}
|
||||
|
||||
pub struct AlsaAudio {
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
}
|
||||
|
||||
impl AlsaAudio {
|
||||
pub fn new(fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
|
||||
let inner = Arc::new(Mutex::new(Inner{
|
||||
mute: false,
|
||||
volume: 0.,
|
||||
}));
|
||||
start_monitor(inner.clone());
|
||||
let dev = AlsaAudio{ inner };
|
||||
BarWidget::new(Box::new(dev), fm, font, size)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidgetImpl for AlsaAudio {
|
||||
fn name(&self) -> &'static str {
|
||||
"audio"
|
||||
}
|
||||
fn get_dirty(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn value(&mut self) -> f32 {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.volume
|
||||
}
|
||||
fn color(&self) -> Color {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
if inner.mute {
|
||||
Color::DARKORANGE
|
||||
} else {
|
||||
Color::WHITE
|
||||
}
|
||||
}
|
||||
fn click(&mut self, pos: f32, btn: PointerButton) {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
match btn {
|
||||
// PointerButton::Left => inner.set_master_volume(pos),
|
||||
//PointerButton::Right => Some(Change::ToggleMute),
|
||||
//PointerButton::ScrollVertical(val) => Some(Change::VolumeInc((val / 512.) as f32)),
|
||||
//PointerButton::ScrollHorizontal(val) => Some(Change::VolumeInc((val / 100.) as f32)),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl BarWidgetImpl for Backlight {
|
||||
fn name(&self) -> &'static str {
|
||||
"backlight"
|
||||
}
|
||||
fn value(&self) -> f32 {
|
||||
fn value(&mut self) -> f32 {
|
||||
if self.cur > self.max {
|
||||
// what.
|
||||
return 1.0;
|
||||
|
||||
@@ -14,7 +14,7 @@ pub trait BarWidgetImpl {
|
||||
false
|
||||
}
|
||||
fn name(&self) -> &'static str;
|
||||
fn value(&self) -> f32;
|
||||
fn value(&mut self) -> f32;
|
||||
fn color(&self) -> Color;
|
||||
fn click(&mut self, _pos: f32, _btn: PointerButton) {}
|
||||
}
|
||||
@@ -50,9 +50,8 @@ impl Widget for BarWidget {
|
||||
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
let fg = Color::WHITE;
|
||||
let bg = Color::BLACK;
|
||||
|
||||
font.draw_text(view, bg, fg, self.inner_widget.name())
|
||||
font.draw_text(view, fg, self.inner_widget.name())
|
||||
.unwrap();
|
||||
let size = self.size.ceil() as u32;
|
||||
let bar_offset = 4 * size;
|
||||
|
||||
+93
-67
@@ -1,81 +1,111 @@
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
error::Error,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dbus::blocking::{
|
||||
stdintf::org_freedesktop_dbus::{Properties, PropertiesPropertiesChanged},
|
||||
LocalConnection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
event::{Event, Events},
|
||||
fonts::FontMap,
|
||||
event::{Event, Events},
|
||||
widgets::bar_widget::{BarWidget, BarWidgetImpl},
|
||||
};
|
||||
|
||||
use upower_dbus::BatteryState;
|
||||
enum UpowerBatteryState {
|
||||
Charging,
|
||||
Discharging,
|
||||
Empty,
|
||||
Full,
|
||||
NotCharging,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
struct InnerBattery {
|
||||
value: f32,
|
||||
state: BatteryState,
|
||||
state: UpowerBatteryState,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
async fn monitor(inner: Arc<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) -> Result<(), Box<dyn Error>> {
|
||||
use zbus::Connection;
|
||||
use upower_dbus::UPowerProxy;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
let conn = Connection::system().await?;
|
||||
let proxy = UPowerProxy::new(&conn).await?;
|
||||
|
||||
let device = proxy.get_display_device().await?;
|
||||
|
||||
let local_inner = inner.clone();
|
||||
let local_events = events.clone();
|
||||
let mut percent_stream = device.receive_percentage_changed().await;
|
||||
let a = tokio::spawn(async move {
|
||||
while let Some(percent) = percent_stream.next().await {
|
||||
let percent = percent.get().await.unwrap() as f32 / 100.;
|
||||
let mut inner = local_inner.lock().unwrap();
|
||||
inner.value = percent;
|
||||
inner.dirty = true;
|
||||
drop(inner);
|
||||
|
||||
let mut events = local_events.lock().unwrap();
|
||||
events.add_event(Event::PowerUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
let local_inner = inner.clone();
|
||||
let local_events = events.clone();
|
||||
let mut state_stream = device.receive_state_changed().await;
|
||||
let b = tokio::spawn(async move {
|
||||
while let Some(state) = state_stream.next().await {
|
||||
let state = state.get().await.unwrap();
|
||||
let mut inner = local_inner.lock().unwrap();
|
||||
inner.state = state;
|
||||
inner.dirty = true;
|
||||
drop(inner);
|
||||
|
||||
let mut events = local_events.lock().unwrap();
|
||||
events.add_event(Event::PowerUpdate);
|
||||
}
|
||||
});
|
||||
a.await?;
|
||||
b.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_monitor(inner: Arc<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) {
|
||||
use tokio::runtime;
|
||||
thread::Builder::new()
|
||||
.name("battmon".to_string())
|
||||
.spawn(move || {
|
||||
let rt = runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.build()
|
||||
let conn = LocalConnection::new_system().unwrap();
|
||||
let device_path = "/org/freedesktop/UPower/devices/DisplayDevice";
|
||||
let proxy = conn.with_proxy(
|
||||
"org.freedesktop.UPower",
|
||||
device_path,
|
||||
Duration::from_millis(500),
|
||||
);
|
||||
|
||||
let capacity: f64 = proxy
|
||||
.get("org.freedesktop.UPower.Device", "Percentage")
|
||||
.expect("unable to get property");
|
||||
let state: u32 = proxy
|
||||
.get("org.freedesktop.UPower.Device", "State")
|
||||
.expect("unable to get state");
|
||||
|
||||
{
|
||||
let mut inner = inner.lock().unwrap();
|
||||
inner.value = capacity as f32 / 100.;
|
||||
inner.state = match state {
|
||||
1 => UpowerBatteryState::Charging,
|
||||
2 => UpowerBatteryState::Discharging,
|
||||
3 => UpowerBatteryState::Empty,
|
||||
4 => UpowerBatteryState::Full,
|
||||
5 => UpowerBatteryState::NotCharging,
|
||||
6 => UpowerBatteryState::Discharging,
|
||||
_ => UpowerBatteryState::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
proxy
|
||||
.match_signal(
|
||||
move |c: PropertiesPropertiesChanged,
|
||||
_: &LocalConnection,
|
||||
_: &dbus::Message| {
|
||||
if c.interface_name != "org.freedesktop.UPower.Device" {
|
||||
return true;
|
||||
}
|
||||
let mut inner = inner.lock().unwrap();
|
||||
for (key, value) in c.changed_properties {
|
||||
match key.as_str() {
|
||||
"State" => {
|
||||
inner.state = match value.0.as_u64().unwrap() {
|
||||
1 => UpowerBatteryState::Charging,
|
||||
2 => UpowerBatteryState::Discharging,
|
||||
3 => UpowerBatteryState::Empty,
|
||||
4 => UpowerBatteryState::Full,
|
||||
5 => UpowerBatteryState::NotCharging,
|
||||
6 => UpowerBatteryState::Discharging,
|
||||
_ => UpowerBatteryState::Unknown,
|
||||
};
|
||||
inner.dirty = true;
|
||||
let mut events = events.lock().unwrap();
|
||||
events.add_event(Event::PowerUpdate);
|
||||
}
|
||||
"Percentage" => {
|
||||
inner.value = value.0.as_f64().unwrap() as f32 / 100.;
|
||||
inner.dirty = true;
|
||||
let mut events = events.lock().unwrap();
|
||||
events.add_event(Event::PowerUpdate);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
rt.block_on(async move { monitor(inner, events).await }).unwrap();
|
||||
|
||||
loop {
|
||||
conn.process(Duration::from_millis(60000)).unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -89,7 +119,7 @@ impl Battery {
|
||||
let battery = Battery {
|
||||
inner: Arc::new(Mutex::new(InnerBattery {
|
||||
value: 0.,
|
||||
state: BatteryState::Unknown,
|
||||
state: UpowerBatteryState::Unknown,
|
||||
dirty: false,
|
||||
})),
|
||||
};
|
||||
@@ -105,7 +135,7 @@ impl BarWidgetImpl for Battery {
|
||||
fn name(&self) -> &'static str {
|
||||
"battery"
|
||||
}
|
||||
fn value(&self) -> f32 {
|
||||
fn value(&mut self) -> f32 {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.dirty = false;
|
||||
inner.value
|
||||
@@ -113,15 +143,11 @@ impl BarWidgetImpl for Battery {
|
||||
fn color(&self) -> Color {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
match inner.state {
|
||||
BatteryState::Charging | BatteryState::FullyCharged => Color::LIGHTGREEN,
|
||||
BatteryState::PendingCharge => Color::LIGHTRED,
|
||||
_ => if inner.value > 0.25 {
|
||||
Color::WHITE
|
||||
} else if inner.value > 0.1 {
|
||||
Color::DARKORANGE
|
||||
} else {
|
||||
Color::RED
|
||||
}
|
||||
UpowerBatteryState::Charging | UpowerBatteryState::Full => Color::LIGHTGREEN,
|
||||
UpowerBatteryState::NotCharging => Color::LIGHTRED,
|
||||
_ if inner.value > 0.25 => Color::WHITE,
|
||||
_ if inner.value > 0.1 => Color::DARKORANGE,
|
||||
_ => Color::RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ impl Calendar {
|
||||
orig: NaiveDate,
|
||||
mut time: NaiveDate,
|
||||
) {
|
||||
let black = Color::BLACK;
|
||||
let white = Color::WHITE;
|
||||
let dim = Color::GREY80;
|
||||
let dimmer = Color::GREY75;
|
||||
@@ -85,7 +84,6 @@ impl Calendar {
|
||||
y_off * (self.size * LINE_HEIGHT).ceil() as u32
|
||||
+ (self.size * 1.).ceil() as u32,
|
||||
)),
|
||||
black,
|
||||
white,
|
||||
&wk_chr,
|
||||
)
|
||||
@@ -100,7 +98,6 @@ impl Calendar {
|
||||
year_font
|
||||
.draw_text(
|
||||
&mut view.offset(((self.size * 17.).ceil() as u32, 0)),
|
||||
black,
|
||||
dim,
|
||||
&format!("{:}", time.year()),
|
||||
)
|
||||
@@ -124,7 +121,7 @@ impl Calendar {
|
||||
_ => panic!("impossible value"),
|
||||
};
|
||||
cal_font
|
||||
.draw_text(&mut view, black, white, month_str)
|
||||
.draw_text(&mut view, white, month_str)
|
||||
.unwrap();
|
||||
|
||||
let mut done = false;
|
||||
@@ -152,7 +149,6 @@ impl Calendar {
|
||||
y_off * (self.size * LINE_HEIGHT).ceil() as u32
|
||||
+ (self.size * MONTH_FACTOR).ceil() as u32,
|
||||
)),
|
||||
black,
|
||||
dimmer,
|
||||
&format!("{:02}", wk.week()),
|
||||
)
|
||||
@@ -177,7 +173,6 @@ impl Calendar {
|
||||
y_off * (self.size * LINE_HEIGHT).ceil() as u32
|
||||
+ (self.size * MONTH_FACTOR).ceil() as u32,
|
||||
)),
|
||||
black,
|
||||
c,
|
||||
&format!("{:02}", time.day()),
|
||||
)
|
||||
|
||||
@@ -54,14 +54,12 @@ impl<'a> Widget for Clock {
|
||||
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
|
||||
let time = Local::now().naive_local();
|
||||
let fg = Color::WHITE;
|
||||
let bg = Color::BLACK;
|
||||
|
||||
let digit = self.digit_width;
|
||||
let colon = self.colon_width;
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
font.draw_text_fixed_width(
|
||||
view,
|
||||
bg,
|
||||
fg,
|
||||
&[digit, digit, colon, digit, digit],
|
||||
&format!("{:02}:{:02}", time.hour(), time.minute()),
|
||||
|
||||
@@ -50,11 +50,9 @@ impl<'a> Widget for Date {
|
||||
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
|
||||
let time = Local::now().naive_local();
|
||||
let fg = Color::WHITE;
|
||||
let bg = Color::BLACK;
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
font.draw_text(
|
||||
view,
|
||||
bg,
|
||||
fg,
|
||||
&format!(
|
||||
"{}, {:02}/{:02}/{:4}",
|
||||
|
||||
+7
-13
@@ -287,7 +287,6 @@ impl InterfaceWidget for Launcher {
|
||||
view: &mut BufferView,
|
||||
) -> Geometry {
|
||||
let fg = Color::WHITE;
|
||||
let bg = Color::BLACK;
|
||||
|
||||
let line_height = intf.size.ceil() as u32;
|
||||
|
||||
@@ -299,14 +298,13 @@ impl InterfaceWidget for Launcher {
|
||||
let mut x_max = font
|
||||
.auto_draw_text_with_cursor(
|
||||
&mut prompt_line,
|
||||
bg,
|
||||
fg,
|
||||
&format!(" {}", &intf.prompt.input),
|
||||
intf.prompt.cursor + 3,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
font.auto_draw_text(&mut prompt_line, bg, fg, ">")
|
||||
font.auto_draw_text(&mut prompt_line, fg, ">")
|
||||
.unwrap();
|
||||
|
||||
// Draw entries
|
||||
@@ -336,13 +334,13 @@ impl InterfaceWidget for Launcher {
|
||||
}
|
||||
x_max = max(
|
||||
x_max,
|
||||
font.auto_draw_text_individual_colors(&mut line, bg, &colors, &m.name)
|
||||
font.auto_draw_text_individual_colors(&mut line, &colors, &m.name)
|
||||
.unwrap(),
|
||||
);
|
||||
} else {
|
||||
x_max = max(
|
||||
x_max,
|
||||
font.auto_draw_text(&mut line, bg, dimfg, &m.name)
|
||||
font.auto_draw_text(&mut line, dimfg, &m.name)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
@@ -386,7 +384,6 @@ impl InterfaceWidget for Shell {
|
||||
view: &mut BufferView,
|
||||
) -> Geometry {
|
||||
let fg = Color::WHITE;
|
||||
let bg = Color::BLACK;
|
||||
|
||||
let line_height = intf.size.ceil() as u32;
|
||||
|
||||
@@ -399,13 +396,12 @@ impl InterfaceWidget for Shell {
|
||||
let x_max = font
|
||||
.auto_draw_text_with_cursor(
|
||||
&mut prompt_line,
|
||||
bg,
|
||||
fg,
|
||||
&format!(" {}", &intf.prompt.input),
|
||||
intf.prompt.cursor + 3,
|
||||
)
|
||||
.unwrap();
|
||||
font.auto_draw_text(&mut prompt_line, bg, Color::BUFF, "!")
|
||||
font.auto_draw_text(&mut prompt_line, Color::BUFF, "!")
|
||||
.unwrap();
|
||||
|
||||
Geometry {
|
||||
@@ -447,7 +443,6 @@ impl InterfaceWidget for Calc {
|
||||
view: &mut BufferView,
|
||||
) -> Geometry {
|
||||
let fg = Color::WHITE;
|
||||
let bg = Color::BLACK;
|
||||
|
||||
let line_height = intf.size.ceil() as u32;
|
||||
|
||||
@@ -460,20 +455,19 @@ impl InterfaceWidget for Calc {
|
||||
let res_off = font
|
||||
.auto_draw_text_with_cursor(
|
||||
&mut prompt_line,
|
||||
bg,
|
||||
fg,
|
||||
&format!(" {} ", &intf.prompt.input),
|
||||
intf.prompt.cursor + 3,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
font.auto_draw_text(&mut prompt_line, bg, Color::BUFF, "=")
|
||||
font.auto_draw_text(&mut prompt_line, Color::BUFF, "=")
|
||||
.unwrap();
|
||||
|
||||
if let Some(res) = &self.result {
|
||||
let resfg = Color::LIGHTORANGE;
|
||||
let mut result_line = view.offset((res_off.0, prompt_offset));
|
||||
font.auto_draw_text(&mut result_line, bg, resfg, &format!(" = {}", &res))
|
||||
font.auto_draw_text(&mut result_line, resfg, &format!(" = {}", &res))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -484,7 +478,7 @@ impl InterfaceWidget for Calc {
|
||||
}
|
||||
prompt_offset -= line_height;
|
||||
let mut result_line = view.offset((0, prompt_offset));
|
||||
font.auto_draw_text(&mut result_line, bg, dimfg, &m)
|
||||
font.auto_draw_text(&mut result_line, dimfg, &m)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,4 +1,5 @@
|
||||
mod audio;
|
||||
//mod alsaaudio;
|
||||
mod pulseaudio;
|
||||
mod backlight;
|
||||
mod bar_widget;
|
||||
mod battery;
|
||||
@@ -10,7 +11,8 @@ mod line;
|
||||
mod widget;
|
||||
mod layout;
|
||||
|
||||
pub use audio::Audio;
|
||||
//pub use alsaaudio::AlsaAudio;
|
||||
pub use pulseaudio::PulseAudio;
|
||||
pub use backlight::Backlight;
|
||||
pub use battery::Battery;
|
||||
pub use calendar::Calendar;
|
||||
|
||||
@@ -177,7 +177,7 @@ enum Change {
|
||||
ToggleMute,
|
||||
}
|
||||
|
||||
pub struct InnerAudio {
|
||||
struct InnerAudio {
|
||||
volume: f32,
|
||||
channel_volume: Option<ChannelVolumes>,
|
||||
mute: bool,
|
||||
@@ -186,14 +186,14 @@ pub struct InnerAudio {
|
||||
pipe: RawFd,
|
||||
}
|
||||
|
||||
pub struct Audio {
|
||||
pub struct PulseAudio {
|
||||
inner: Arc<Mutex<InnerAudio>>,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
impl PulseAudio {
|
||||
pub fn new(events: Arc<Mutex<Events>>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
|
||||
let (a, b) = pipe().unwrap();
|
||||
let audio = Audio {
|
||||
let audio = PulseAudio {
|
||||
inner: Arc::new(Mutex::new(InnerAudio{
|
||||
volume: 0.0,
|
||||
channel_volume: None,
|
||||
@@ -208,15 +208,17 @@ impl Audio {
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidgetImpl for Audio {
|
||||
impl BarWidgetImpl for PulseAudio {
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.inner.lock().unwrap().dirty
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"volume"
|
||||
}
|
||||
fn value(&self) -> f32 {
|
||||
self.inner.lock().unwrap().volume
|
||||
fn value(&mut self) -> f32 {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.dirty = false;
|
||||
inner.volume
|
||||
}
|
||||
fn color(&self) -> Color {
|
||||
match self.inner.lock().unwrap().mute {
|
||||
Reference in New Issue
Block a user