mirror of
https://github.com/aljazceru/notedeck.git
synced 2026-01-14 13:54:19 +01:00
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -2607,6 +2607,7 @@ dependencies = [
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tracing-wasm",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"wasm-bindgen-futures",
|
||||
"winit",
|
||||
@@ -4679,6 +4680,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.37.0"
|
||||
|
||||
@@ -45,6 +45,7 @@ uuid = { version = "1.10.0", features = ["v4"] }
|
||||
indexmap = "2.6.0"
|
||||
dirs = "5.0.1"
|
||||
tracing-appender = "0.2.3"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13.0"
|
||||
|
||||
BIN
assets/icons/help_icon_dark_4x.png
Normal file
BIN
assets/icons/help_icon_dark_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
10
src/app.rs
10
src/app.rs
@@ -17,6 +17,7 @@ use crate::{
|
||||
profile::Profile,
|
||||
storage::{Directory, FileKeyStorage, KeyStorageType},
|
||||
subscriptions::{SubKind, Subscriptions},
|
||||
support::Support,
|
||||
thread::Thread,
|
||||
timeline::{Timeline, TimelineId, TimelineKind, ViewFilter},
|
||||
ui::{self, DesktopSidePanel},
|
||||
@@ -62,6 +63,7 @@ pub struct Damus {
|
||||
pub accounts: AccountManager,
|
||||
pub subscriptions: Subscriptions,
|
||||
pub app_rect_handler: AppSizeHandler,
|
||||
pub support: Support,
|
||||
|
||||
frame_history: crate::frame_history::FrameHistory,
|
||||
|
||||
@@ -751,6 +753,7 @@ impl Damus {
|
||||
frame_history: FrameHistory::default(),
|
||||
view_state: ViewState::default(),
|
||||
app_rect_handler: AppSizeHandler::default(),
|
||||
support: Support::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,6 +838,7 @@ impl Damus {
|
||||
frame_history: FrameHistory::default(),
|
||||
view_state: ViewState::default(),
|
||||
app_rect_handler: AppSizeHandler::default(),
|
||||
support: Support::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1024,7 +1028,11 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||
.show(ui);
|
||||
|
||||
if side_panel.response.clicked() {
|
||||
DesktopSidePanel::perform_action(app.columns_mut(), side_panel.action);
|
||||
DesktopSidePanel::perform_action(
|
||||
&mut app.columns,
|
||||
&mut app.support,
|
||||
side_panel.action,
|
||||
);
|
||||
}
|
||||
|
||||
// vertical sidebar line
|
||||
|
||||
@@ -31,6 +31,7 @@ pub mod relay_pool_manager;
|
||||
mod result;
|
||||
mod route;
|
||||
mod subscriptions;
|
||||
mod support;
|
||||
mod test_data;
|
||||
mod thread;
|
||||
mod time;
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::{
|
||||
add_column::{AddColumnResponse, AddColumnView},
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
note::PostAction,
|
||||
support::SupportView,
|
||||
RelayView, View,
|
||||
},
|
||||
Damus,
|
||||
@@ -128,6 +129,10 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) {
|
||||
col,
|
||||
ui,
|
||||
),
|
||||
Route::Support => {
|
||||
SupportView::new(&mut app.support).show(ui);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ pub enum Route {
|
||||
ComposeNote,
|
||||
AddColumn,
|
||||
Profile(Pubkey),
|
||||
Support,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -100,6 +101,7 @@ impl Route {
|
||||
Route::Profile(pubkey) => {
|
||||
format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey))
|
||||
}
|
||||
Route::Support => "Damus Support".to_owned(),
|
||||
};
|
||||
|
||||
TitledRoute {
|
||||
@@ -208,6 +210,7 @@ impl fmt::Display for Route {
|
||||
|
||||
Route::AddColumn => write!(f, "Add Column"),
|
||||
Route::Profile(_) => write!(f, "Profile"),
|
||||
Route::Support => write!(f, "Support"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
158
src/support.rs
Normal file
158
src/support.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use tracing::error;
|
||||
|
||||
use crate::{storage::Directory, DataPaths};
|
||||
|
||||
pub struct Support {
|
||||
directory: Option<Directory>,
|
||||
mailto_url: String,
|
||||
most_recent_log: Option<String>,
|
||||
}
|
||||
|
||||
fn new_log_dir() -> Option<Directory> {
|
||||
match DataPaths::Log.get_path() {
|
||||
Ok(path) => Some(Directory::new(path)),
|
||||
Err(e) => {
|
||||
error!("Support could not open directory: {}", e.to_string());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Support {
|
||||
fn default() -> Self {
|
||||
let directory = new_log_dir();
|
||||
|
||||
Self {
|
||||
mailto_url: MailtoBuilder::new(SUPPORT_EMAIL.to_string())
|
||||
.with_subject("Help Needed".to_owned())
|
||||
.with_content(EMAIL_TEMPLATE.to_owned())
|
||||
.build(),
|
||||
directory,
|
||||
most_recent_log: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static MAX_LOG_LINES: usize = 500;
|
||||
static SUPPORT_EMAIL: &str = "support@damus.io";
|
||||
static EMAIL_TEMPLATE: &str = "Describe the bug you have encountered:\n<-- your statement here -->\n\n===== Paste your log below =====\n\n";
|
||||
|
||||
impl Support {
|
||||
pub fn refresh(&mut self) {
|
||||
if let Some(directory) = &self.directory {
|
||||
self.most_recent_log = get_log_str(directory);
|
||||
} else {
|
||||
self.directory = new_log_dir();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mailto_url(&self) -> &str {
|
||||
&self.mailto_url
|
||||
}
|
||||
|
||||
pub fn get_log_dir(&self) -> Option<&str> {
|
||||
self.directory.as_ref()?.file_path.to_str()
|
||||
}
|
||||
|
||||
pub fn get_most_recent_log(&self) -> Option<&String> {
|
||||
self.most_recent_log.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_log_str(interactor: &Directory) -> Option<String> {
|
||||
match interactor.get_most_recent() {
|
||||
Ok(Some(most_recent_name)) => {
|
||||
match interactor.get_file_last_n_lines(most_recent_name.clone(), MAX_LOG_LINES) {
|
||||
Ok(file_output) => {
|
||||
return Some(
|
||||
get_prefix(
|
||||
&most_recent_name,
|
||||
file_output.output_num_lines,
|
||||
file_output.total_lines_in_file,
|
||||
) + &file_output.output,
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error retrieving the last lines from file {}: {:?}",
|
||||
most_recent_name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("No files were found.");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error fetching the most recent file: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_prefix(file_name: &str, lines_displayed: usize, num_total_lines: usize) -> String {
|
||||
format!(
|
||||
"===\nDisplaying the last {} of {} lines in file {}\n===\n\n",
|
||||
lines_displayed, num_total_lines, file_name,
|
||||
)
|
||||
}
|
||||
|
||||
struct MailtoBuilder {
|
||||
content: Option<String>,
|
||||
address: String,
|
||||
subject: Option<String>,
|
||||
}
|
||||
|
||||
impl MailtoBuilder {
|
||||
fn new(address: String) -> Self {
|
||||
Self {
|
||||
content: None,
|
||||
address,
|
||||
subject: None,
|
||||
}
|
||||
}
|
||||
|
||||
// will be truncated so the whole URL is at most 2000 characters
|
||||
pub fn with_content(mut self, content: String) -> Self {
|
||||
self.content = Some(content);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_subject(mut self, subject: String) -> Self {
|
||||
self.subject = Some(subject);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
let mut url = String::new();
|
||||
|
||||
url.push_str("mailto:");
|
||||
url.push_str(&self.address);
|
||||
|
||||
let has_subject = self.subject.is_some();
|
||||
|
||||
if has_subject || self.content.is_some() {
|
||||
url.push('?');
|
||||
}
|
||||
|
||||
if let Some(subject) = self.subject {
|
||||
url.push_str("subject=");
|
||||
url.push_str(&urlencoding::encode(&subject));
|
||||
}
|
||||
|
||||
if let Some(content) = self.content {
|
||||
if has_subject {
|
||||
url.push('&');
|
||||
}
|
||||
|
||||
url.push_str("body=");
|
||||
|
||||
let body = urlencoding::encode(&content);
|
||||
|
||||
url.push_str(&body);
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
}
|
||||
49
src/ui/button_hyperlink.rs
Normal file
49
src/ui/button_hyperlink.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use egui::{Button, Response, Ui, Widget};
|
||||
|
||||
pub struct ButtonHyperlink<'a> {
|
||||
url: String,
|
||||
button: Button<'a>,
|
||||
new_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> ButtonHyperlink<'a> {
|
||||
pub fn new(button: Button<'a>, url: impl ToString) -> Self {
|
||||
let url = url.to_string();
|
||||
Self {
|
||||
url: url.clone(),
|
||||
button,
|
||||
new_tab: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_new_tab(mut self, new_tab: bool) -> Self {
|
||||
self.new_tab = new_tab;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for ButtonHyperlink<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let response = ui.add(self.button);
|
||||
|
||||
if response.clicked() {
|
||||
let modifiers = ui.ctx().input(|i| i.modifiers);
|
||||
ui.ctx().open_url(egui::OpenUrl {
|
||||
url: self.url.clone(),
|
||||
new_tab: self.new_tab || modifiers.any(),
|
||||
});
|
||||
}
|
||||
if response.middle_clicked() {
|
||||
ui.ctx().open_url(egui::OpenUrl {
|
||||
url: self.url.clone(),
|
||||
new_tab: true,
|
||||
});
|
||||
}
|
||||
|
||||
if ui.style().url_in_tooltip {
|
||||
response.on_hover_text(self.url)
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ pub mod account_login_view;
|
||||
pub mod account_management;
|
||||
pub mod add_column;
|
||||
pub mod anim;
|
||||
pub mod button_hyperlink;
|
||||
pub mod mention;
|
||||
pub mod note;
|
||||
pub mod preview;
|
||||
pub mod profile;
|
||||
pub mod relay;
|
||||
pub mod side_panel;
|
||||
pub mod support;
|
||||
pub mod thread;
|
||||
pub mod timeline;
|
||||
pub mod username;
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
column::{Column, Columns},
|
||||
imgcache::ImageCache,
|
||||
route::Route,
|
||||
support::Support,
|
||||
user_account::UserAccount,
|
||||
Damus,
|
||||
};
|
||||
@@ -41,6 +42,7 @@ pub enum SidePanelAction {
|
||||
ComposeNote,
|
||||
Search,
|
||||
ExpandSidePanel,
|
||||
Support,
|
||||
}
|
||||
|
||||
pub struct SidePanelResponse {
|
||||
@@ -114,6 +116,8 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
let pfp_resp = self.pfp_button(ui);
|
||||
let settings_resp = ui.add(settings_button(dark_mode));
|
||||
|
||||
let support_resp = ui.add(support_button());
|
||||
|
||||
let optional_inner = if pfp_resp.clicked() {
|
||||
Some(egui::InnerResponse::new(
|
||||
SidePanelAction::Account,
|
||||
@@ -124,6 +128,11 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
SidePanelAction::Settings,
|
||||
settings_resp,
|
||||
))
|
||||
} else if support_resp.clicked() {
|
||||
Some(egui::InnerResponse::new(
|
||||
SidePanelAction::Support,
|
||||
support_resp,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -162,7 +171,7 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
helper.take_animation_response()
|
||||
}
|
||||
|
||||
pub fn perform_action(columns: &mut Columns, action: SidePanelAction) {
|
||||
pub fn perform_action(columns: &mut Columns, support: &mut Support, action: SidePanelAction) {
|
||||
let router = columns.get_first_router();
|
||||
match action {
|
||||
SidePanelAction::Panel => {} // TODO
|
||||
@@ -208,6 +217,14 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
// TODO
|
||||
info!("Clicked expand side panel button");
|
||||
}
|
||||
SidePanelAction::Support => {
|
||||
if router.routes().iter().any(|&r| r == Route::Support) {
|
||||
router.go_back();
|
||||
} else {
|
||||
support.refresh();
|
||||
router.route_to(Route::Support);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +369,28 @@ fn expand_side_panel_button() -> impl Widget {
|
||||
}
|
||||
}
|
||||
|
||||
fn support_button() -> impl Widget {
|
||||
|ui: &mut egui::Ui| -> egui::Response {
|
||||
let img_size = 16.0;
|
||||
|
||||
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
||||
let img_data = egui::include_image!("../../assets/icons/help_icon_dark_4x.png");
|
||||
let img = egui::Image::new(img_data).max_width(img_size);
|
||||
|
||||
let helper = AnimationHelper::new(ui, "help-button", vec2(max_size, max_size));
|
||||
|
||||
let cur_img_size = helper.scale_1d_pos(img_size);
|
||||
img.paint_at(
|
||||
ui,
|
||||
helper
|
||||
.get_animation_rect()
|
||||
.shrink((max_size - cur_img_size) / 2.0),
|
||||
);
|
||||
|
||||
helper.take_animation_response()
|
||||
}
|
||||
}
|
||||
|
||||
mod preview {
|
||||
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
@@ -390,7 +429,11 @@ mod preview {
|
||||
);
|
||||
let response = panel.show(ui);
|
||||
|
||||
DesktopSidePanel::perform_action(&mut self.app.columns, response.action);
|
||||
DesktopSidePanel::perform_action(
|
||||
&mut self.app.columns,
|
||||
&mut self.app.support,
|
||||
response.action,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
76
src/ui/support.rs
Normal file
76
src/ui/support.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use egui::{vec2, Button, Label, Layout, RichText};
|
||||
|
||||
use crate::{
|
||||
app_style::{get_font_size, NotedeckTextStyle},
|
||||
colors::PINK,
|
||||
fonts::NamedFontFamily,
|
||||
support::Support,
|
||||
};
|
||||
|
||||
use super::{button_hyperlink::ButtonHyperlink, padding};
|
||||
|
||||
pub struct SupportView<'a> {
|
||||
support: &'a mut Support,
|
||||
}
|
||||
|
||||
impl<'a> SupportView<'a> {
|
||||
pub fn new(support: &'a mut Support) -> Self {
|
||||
Self { support }
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||
padding(8.0, ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(0.0, 8.0);
|
||||
let font = egui::FontId::new(
|
||||
get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
);
|
||||
ui.add(Label::new(RichText::new("Running into a bug?").font(font)));
|
||||
ui.label(RichText::new("Step 1").text_style(NotedeckTextStyle::Heading3.text_style()));
|
||||
padding(8.0, ui, |ui| {
|
||||
ui.label("Open your default email client to get help from the Damus team");
|
||||
let size = vec2(120.0, 40.0);
|
||||
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.add(ButtonHyperlink::new(
|
||||
Button::new(
|
||||
RichText::new("Open Email")
|
||||
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Body)),
|
||||
)
|
||||
.fill(PINK)
|
||||
.min_size(size),
|
||||
self.support.get_mailto_url(),
|
||||
));
|
||||
})
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
if let Some(logs) = self.support.get_most_recent_log() {
|
||||
ui.label(
|
||||
RichText::new("Step 2").text_style(NotedeckTextStyle::Heading3.text_style()),
|
||||
);
|
||||
let size = vec2(80.0, 40.0);
|
||||
let copy_button = Button::new(
|
||||
RichText::new("Copy").size(get_font_size(ui.ctx(), &NotedeckTextStyle::Body)),
|
||||
)
|
||||
.fill(PINK)
|
||||
.min_size(size);
|
||||
padding(8.0, ui, |ui| {
|
||||
ui.add(Label::new("Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.").wrap());
|
||||
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||
if ui.add(copy_button).clicked() {
|
||||
ui.output_mut(|w| {
|
||||
w.copied_text = logs.to_string();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ui.label(
|
||||
egui::RichText::new("ERROR: Could not find logs on system")
|
||||
.color(egui::Color32::RED),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user