Server: Expose NSFW toggle, Dark Mode toggle

Ensure all submission view permission logic is the same
This commit is contained in:
asonix 2021-02-03 21:07:45 -06:00
parent 81edfa7123
commit 010dd2952f
13 changed files with 345 additions and 123 deletions

View file

@ -66,10 +66,16 @@ impl ViewBrowseState {
) -> Result<Self, Error> {
let store = state.profiles.clone();
let can_view_sensitive = if let Some(viewer) = viewer {
state.settings.for_profile(viewer).await?.sensitive
} else {
false
};
let state = actix_web::web::block(move || {
let mut cache = Cache::new();
let page = browse_page(viewer, &store, &mut cache, source);
let page = browse_page(viewer, can_view_sensitive, &store, &mut cache, source);
Ok(ViewBrowseState {
cache,

View file

@ -163,16 +163,16 @@ fn can_view_logged_out(
None => return Ok(false),
};
if submission.is_followers_only() {
return Ok(false);
}
let submissioner = match store.store.profiles.by_id(submission.profile_id())? {
Some(s) => s,
None => return Ok(false),
};
if submissioner.login_required() {
if crate::pagination::submission::can_view(
None,
&submission,
&store.store,
&mut Default::default(),
true,
false,
)
.is_none()
{
return Ok(false);
}
@ -217,6 +217,7 @@ fn can_view_comment_logged_out_no_recurse(
fn can_view(
profile: &Profile,
can_view_sensitive: bool,
comment: &Comment,
store: &hyaenidae_profiles::State,
) -> Result<bool, Error> {
@ -225,47 +226,19 @@ fn can_view(
None => return Ok(false),
};
let submissioner_id = submission.profile_id();
let blocking_submissioner = store
.store
.view
.blocks
.by_forward(submissioner_id, profile.id())?
.is_some();
if blocking_submissioner {
if crate::pagination::submission::can_view(
Some(profile.id()),
&submission,
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.is_none()
{
return Ok(false);
}
let blocked_by_submissioner = store
.store
.view
.blocks
.by_forward(profile.id(), submissioner_id)?
.is_some();
if blocked_by_submissioner {
return Ok(false);
}
if submission.is_followers_only() {
let is_submissioner = profile.id() == submissioner_id;
if !is_submissioner {
let follows_submissioner = store
.store
.view
.follows
.by_forward(submissioner_id, profile.id())?
.is_some();
if !follows_submissioner {
return Ok(false);
}
}
}
can_view_comment(profile, comment, store)
}
@ -325,8 +298,14 @@ async fn prepare_view(
profile: Option<&Profile>,
state: &State,
) -> Result<Option<CommentView>, Error> {
let can_view_sensitive = if let Some(profile) = profile {
state.settings.for_profile(profile.id()).await?.sensitive
} else {
false
};
match profile {
Some(profile) if !can_view(&profile, &comment, &state.profiles)? => {
Some(profile) if !can_view(&profile, can_view_sensitive, &comment, &state.profiles)? => {
return Ok(None);
}
None if !can_view_logged_out(&comment, &state.profiles)? => {
@ -550,7 +529,9 @@ async fn reply(
None => return Ok(crate::to_404()),
};
if !can_view(&profile, &comment, &state.profiles)? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if !can_view(&profile, can_view_sensitive, &comment, &state.profiles)? {
return Ok(crate::to_404());
}
@ -738,7 +719,9 @@ async fn report_page(
None => return Ok(crate::to_404()),
};
if !can_view(&profile, &comment, &state.profiles)? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if !can_view(&profile, can_view_sensitive, &comment, &state.profiles)? {
return Ok(crate::to_404());
}
@ -775,7 +758,9 @@ async fn report(
None => return Ok(crate::to_404()),
};
if !can_view(&profile, &comment, &state.profiles)? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if !can_view(&profile, can_view_sensitive, &comment, &state.profiles)? {
return Ok(crate::to_404());
}
@ -828,7 +813,9 @@ async fn report_success_page(
None => return Ok(crate::to_404()),
};
if !can_view(&profile, &comment, &state.profiles)? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if !can_view(&profile, can_view_sensitive, &comment, &state.profiles)? {
return Ok(crate::to_404());
}

View file

@ -221,6 +221,7 @@ struct State {
spawn: jobs::Spawn,
apub: apub::Apub,
images: images::Images,
settings: profiles::SettingStore,
domain: String,
base_url: url::Url,
db: Db,
@ -240,6 +241,7 @@ impl State {
let domain = base_url.domain().req()?.to_owned();
let admin = admin::Store::build(db)?;
let settings = profiles::SettingStore::build(db)?;
Ok(State {
profiles: hyaenidae_profiles::State::build(
@ -254,6 +256,7 @@ impl State {
spawn,
apub,
images,
settings,
base_url,
domain,
db: db.clone(),

View file

@ -63,6 +63,12 @@ impl FromRequest for NavState {
format!("{}?show_nav=true", path)
};
let dark = if let Some(profile) = &profile {
state.settings.for_profile(profile.id()).await?.dark
} else {
true
};
let notification_count = if let Some(profile) = &profile {
total_for_profile(profile.id(), &state).await.ok()
} else {
@ -99,7 +105,7 @@ impl FromRequest for NavState {
admin,
href,
is_open,
dark: true,
dark,
})
})
}

View file

@ -22,12 +22,14 @@ impl Cache {
pub(crate) fn browse_page(
viewer: Option<Uuid>,
can_view_sensitive: bool,
store: &hyaenidae_profiles::State,
cache: &mut Cache,
source: Option<PageSource>,
) -> Page {
Page::from_pagination(
BrowsePager {
can_view_sensitive,
viewer,
store,
cache,
@ -39,6 +41,7 @@ pub(crate) fn browse_page(
pub(crate) fn draft_page(
viewer: Option<Uuid>,
can_view_sensitive: bool,
profile_id: Uuid,
store: &hyaenidae_profiles::State,
cache: &mut Cache,
@ -46,6 +49,7 @@ pub(crate) fn draft_page(
) -> Page {
Page::from_pagination(
DraftPager {
can_view_sensitive,
viewer,
profile_id,
store,
@ -58,6 +62,7 @@ pub(crate) fn draft_page(
pub(crate) fn main_page(
viewer: Option<Uuid>,
can_view_sensitive: bool,
profile_id: Uuid,
store: &hyaenidae_profiles::State,
cache: &mut Cache,
@ -65,6 +70,7 @@ pub(crate) fn main_page(
) -> Page {
Page::from_pagination(
SubmissionPager {
can_view_sensitive,
viewer,
profile_id,
store,
@ -76,6 +82,7 @@ pub(crate) fn main_page(
}
struct BrowsePager<'b> {
can_view_sensitive: bool,
viewer: Option<Uuid>,
store: &'b hyaenidae_profiles::State,
cache: &'b mut Cache,
@ -93,6 +100,7 @@ impl<'b> Pagination for BrowsePager<'b> {
&self.store.store,
self.cache,
false,
self.can_view_sensitive,
)),
)
}
@ -108,6 +116,7 @@ impl<'b> Pagination for BrowsePager<'b> {
&self.store.store,
self.cache,
false,
self.can_view_sensitive,
)),
)
}
@ -123,12 +132,14 @@ impl<'b> Pagination for BrowsePager<'b> {
&self.store.store,
self.cache,
false,
self.can_view_sensitive,
)),
)
}
}
struct DraftPager<'b> {
can_view_sensitive: bool,
viewer: Option<Uuid>,
profile_id: Uuid,
store: &'b hyaenidae_profiles::State,
@ -147,6 +158,7 @@ impl<'b> Pagination for DraftPager<'b> {
&self.store.store,
self.cache,
true,
self.can_view_sensitive,
)),
)
}
@ -162,6 +174,7 @@ impl<'b> Pagination for DraftPager<'b> {
&self.store.store,
self.cache,
true,
self.can_view_sensitive,
)),
)
}
@ -177,12 +190,14 @@ impl<'b> Pagination for DraftPager<'b> {
&self.store.store,
self.cache,
true,
self.can_view_sensitive,
)),
)
}
}
struct SubmissionPager<'b> {
can_view_sensitive: bool,
viewer: Option<Uuid>,
profile_id: Uuid,
store: &'b hyaenidae_profiles::State,
@ -201,6 +216,7 @@ impl<'b> Pagination for SubmissionPager<'b> {
&self.store.store,
self.cache,
true,
self.can_view_sensitive,
)),
)
}
@ -216,6 +232,7 @@ impl<'b> Pagination for SubmissionPager<'b> {
&self.store.store,
self.cache,
true,
self.can_view_sensitive,
)),
)
}
@ -231,6 +248,7 @@ impl<'b> Pagination for SubmissionPager<'b> {
&self.store.store,
self.cache,
true,
self.can_view_sensitive,
)),
)
}
@ -241,6 +259,7 @@ fn filter_submissions<'a>(
store: &'a hyaenidae_profiles::store::Store,
cache: &'a mut Cache,
show_unlisted: bool,
can_view_sensitive: bool,
) -> impl FnMut(Uuid) -> Option<Uuid> + 'a {
move |submission_id| {
if !cache.submission_map.contains_key(&submission_id) {
@ -251,7 +270,14 @@ fn filter_submissions<'a>(
cache.profile_map.insert(profile.id(), profile);
}
let opt = can_view(viewer, &submission, store, cache, show_unlisted);
let opt = can_view(
viewer,
&submission,
store,
cache,
show_unlisted,
can_view_sensitive,
);
if let Some(file_id) = submission.files().get(0) {
if !cache.file_map.contains_key(file_id) {
@ -266,7 +292,14 @@ fn filter_submissions<'a>(
} else {
let submission = cache.submission_map.get(&submission_id)?.clone();
can_view(viewer, &submission, store, cache, show_unlisted)?;
can_view(
viewer,
&submission,
store,
cache,
show_unlisted,
can_view_sensitive,
)?;
Some(submission_id)
}
@ -279,11 +312,17 @@ pub(crate) fn can_view(
store: &hyaenidae_profiles::store::Store,
cache: &mut Cache,
show_unlisted: bool,
can_view_sensitive: bool,
) -> Option<()> {
if let Some(viewer) = viewer {
if viewer == submission.profile_id() {
return Some(());
}
if submission.is_sensitive() && !can_view_sensitive {
return None;
}
if let Some(block) = cache.blocks.get(&submission.profile_id()) {
if *block {
return None;

View file

@ -15,9 +15,11 @@ use i18n_embed_fl::fl;
use std::collections::HashMap;
use uuid::Uuid;
mod settings;
mod state;
mod update;
pub(crate) use settings::{SettingStore, Settings};
pub use state::{EditProfileState, ViewProfileState};
pub use update::HandleState;

52
src/profiles/settings.rs Normal file
View file

@ -0,0 +1,52 @@
use crate::error::Error;
use sled::{Db, Tree};
use uuid::Uuid;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct Settings {
pub(crate) dark: bool,
pub(crate) sensitive: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct SettingStore {
tree: Tree,
}
impl SettingStore {
pub(crate) fn build(db: &Db) -> Result<Self, sled::Error> {
Ok(SettingStore {
tree: db.open_tree("/main/settings")?,
})
}
pub(crate) async fn for_profile(&self, profile_id: Uuid) -> Result<Settings, Error> {
let this = self.clone();
let opt = actix_web::web::block(move || {
Ok(this
.tree
.get(profile_id.as_bytes())?
.and_then(|ivec| serde_json::from_slice(&ivec).ok()))
})
.await?;
Ok(opt.unwrap_or(Settings {
dark: true,
sensitive: false,
}))
}
pub(crate) async fn update(&self, profile_id: Uuid, settings: Settings) -> Result<(), Error> {
let this = self.clone();
actix_web::web::block(move || {
let vec = serde_json::to_vec(&settings)?;
this.tree.insert(profile_id.as_bytes(), vec)?;
Ok(()) as Result<_, Error>
})
.await?;
Ok(())
}
}

View file

@ -6,6 +6,7 @@ use crate::{
submission::{draft_page, main_page, Cache},
PageSource,
},
profiles::settings::Settings,
views::ProfileView,
ActixLoader, State,
};
@ -44,6 +45,8 @@ pub struct EditProfileState {
pub(crate) banner_error: Option<String>,
login_required_value: Option<bool>,
pub(crate) login_required_error: Option<String>,
pub(crate) settings_error: Option<String>,
pub(crate) settings: Settings,
}
impl ViewProfileState {
@ -226,6 +229,12 @@ impl ViewProfileState {
) -> Result<Self, Error> {
let store = state.profiles.clone();
let can_view_sensitive = if let Some(viewer) = viewer {
state.settings.for_profile(viewer).await?.sensitive
} else {
false
};
let state = actix_web::web::block(move || {
let mut cache = Cache::new();
@ -248,9 +257,23 @@ impl ViewProfileState {
cache.profile_map.insert(profile.id(), profile);
let page = if drafts {
draft_page(viewer, profile_id, &store, &mut cache, source)
draft_page(
viewer,
can_view_sensitive,
profile_id,
&store,
&mut cache,
source,
)
} else {
main_page(viewer, profile_id, &store, &mut cache, source)
main_page(
viewer,
can_view_sensitive,
profile_id,
&store,
&mut cache,
source,
)
};
let is_self = viewer.map(|id| id == profile_id).unwrap_or(false);
@ -308,7 +331,12 @@ impl ViewProfileState {
}
impl EditProfileState {
pub(super) fn new(profile: Profile, icon: Option<File>, banner: Option<File>) -> Self {
pub(super) fn new(
profile: Profile,
icon: Option<File>,
banner: Option<File>,
settings: Settings,
) -> Self {
EditProfileState {
profile,
icon,
@ -321,6 +349,8 @@ impl EditProfileState {
banner_error: None,
login_required_value: None,
login_required_error: None,
settings_error: None,
settings,
}
}
@ -330,6 +360,8 @@ impl EditProfileState {
let icon_id = profile.icon();
let banner_id = profile.banner();
let settings = state.settings.for_profile(profile.id()).await?;
let (icon, banner) = actix_web::web::block(move || {
let icon = if let Some(id) = icon_id {
store.store.files.by_id(id)?
@ -346,7 +378,7 @@ impl EditProfileState {
})
.await?;
Ok(Self::new(profile, icon, banner))
Ok(Self::new(profile, icon, banner, settings))
}
pub(crate) fn profile<'a>(&'a self) -> ProfileView<'a> {
@ -497,4 +529,9 @@ impl EditProfileState {
self.login_required_error = Some(err.to_owned());
self
}
pub(super) fn settings_error(&mut self, err: String) -> &mut Self {
self.settings_error = Some(err);
self
}
}

View file

@ -2,7 +2,7 @@ use crate::{
error::{Error, OptionExt},
middleware::{ProfileData, UserProfile},
nav::NavState,
profiles::{state::EditProfileState, to_current_profile},
profiles::{settings::Settings, state::EditProfileState, to_current_profile},
ActixLoader, State,
};
use actix_session::Session;
@ -66,6 +66,11 @@ pub(super) fn update_scope() -> Scope {
.route(web::post().to(update_require_login))
.route(web::get().to(to_current_profile)),
)
.service(
web::resource("/settings")
.route(web::post().to(update_settings))
.route(web::get().to(to_current_profile)),
)
}
pub(super) fn to_create() -> HttpResponse {
@ -328,6 +333,39 @@ async fn update_require_login(
})
}
#[derive(serde::Deserialize)]
struct SettingsForm {
sensitive: Option<String>,
dark: Option<String>,
}
async fn update_settings(
loader: ActixLoader,
form: web::Form<SettingsForm>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.0;
let settings = Settings {
sensitive: form.sensitive.is_some(),
dark: form.dark.is_some(),
};
let error = match state.settings.update(profile.id(), settings).await {
Ok(_) => return Ok(to_current_profile()),
Err(e) => e.to_string(),
};
let mut profile_state = EditProfileState::for_profile(profile, &state).await?;
profile_state.settings_error(error);
crate::rendered(HttpResponse::InternalServerError(), |cursor| {
crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct HandleForm {
pub(crate) handle: String,

View file

@ -5,6 +5,7 @@ use crate::{
images::{largest_image, FullImage, ImageType},
middleware::{CurrentSubmission, UserProfile},
nav::NavState,
profiles::Settings,
views::{OwnedProfileView, OwnedSubmissionView},
ActixLoader, State,
};
@ -195,7 +196,16 @@ async fn report_page(
let view = ReportView::build(submission_id.into_inner(), &state).await?;
if let Some(res) = can_view(Some(profile.id()), &view.author, &view.submission, &state).await? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(
Some(profile.id()),
can_view_sensitive,
&view.submission,
&state,
)
.await?
{
return Ok(res);
}
@ -223,7 +233,16 @@ async fn report(
let mut view = ReportView::build(submission_id.into_inner(), &state).await?;
if let Some(res) = can_view(Some(profile.id()), &view.author, &view.submission, &state).await? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(
Some(profile.id()),
can_view_sensitive,
&view.submission,
&state,
)
.await?
{
return Ok(res);
}
@ -267,7 +286,16 @@ async fn report_success_page(
let view = ReportView::build(submission_id.into_inner(), &state).await?;
if let Some(res) = can_view(Some(profile.id()), &view.author, &view.submission, &state).await? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(
Some(profile.id()),
can_view_sensitive,
&view.submission,
&state,
)
.await?
{
return Ok(res);
}
@ -293,12 +321,15 @@ pub struct SubmissionState {
pub(crate) delete_error: Option<String>,
pub(crate) visibility_error: Option<String>,
pub(crate) sensitive_error: Option<String>,
pub(crate) settings: Settings,
}
impl SubmissionState {
async fn new(submission: Submission, state: &State) -> Result<Self, Error> {
let file_ids: Vec<Uuid> = submission.files().iter().copied().collect();
let settings = state.settings.for_profile(submission.profile_id()).await?;
let store = state.profiles.clone();
let files = web::block(move || {
let mut files = HashMap::new();
@ -327,6 +358,7 @@ impl SubmissionState {
delete_error: None,
visibility_error: None,
sensitive_error: None,
settings,
})
}
@ -613,52 +645,29 @@ impl ViewSubmissionState {
async fn can_view(
viewer: Option<Uuid>,
poster: &Profile,
can_view_sensitive: bool,
submission: &Submission,
state: &State,
) -> Result<Option<HttpResponse>, Error> {
if submission.is_sensitive() {
if viewer.is_none() {
return Ok(Some(crate::to_404()));
}
}
let store = state.profiles.clone();
let submission = submission.clone();
let opt = web::block(move || {
Ok(crate::pagination::submission::can_view(
viewer,
&submission,
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)) as Result<Option<()>, Error>
})
.await?;
if poster.login_required() && viewer.is_none() {
if opt.is_none() {
return Ok(Some(crate::to_404()));
}
if poster.local_owner().is_none() && viewer.is_none() {
return Ok(Some(crate::to_404()));
}
let is_self = viewer
.as_ref()
.map(|pid| *pid == poster.id())
.unwrap_or(false);
if submission.published().is_none() && !is_self {
return Ok(Some(crate::to_404()));
}
let is_follower_or_self = if let Some(pid) = viewer {
let is_follower = state
.profiles
.store
.view
.follows
.by_forward(poster.id(), pid)?
.is_some();
is_follower || is_self
} else {
false
};
if submission.is_followers_only() && !is_follower_or_self {
return Ok(Some(crate::to_404()));
Ok(None)
}
Ok(None)
}
async fn files_for_submission(
@ -746,6 +755,7 @@ fn submission_nav(
async fn adjacent_submissions(
viewer: Option<Uuid>,
can_view_sensitive: bool,
submission: &Submission,
state: &State,
) -> Result<(Option<Uuid>, Option<Uuid>), Error> {
@ -769,6 +779,7 @@ async fn adjacent_submissions(
&inner_store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
@ -787,6 +798,7 @@ async fn adjacent_submissions(
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
@ -808,6 +820,7 @@ async fn adjacent_submissions(
&inner_store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
@ -826,6 +839,7 @@ async fn adjacent_submissions(
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
@ -862,12 +876,18 @@ async fn submission_page(
let cache = files_for_submission(&submission, cache, &state).await?;
let cache = files_for_profile(&poster, cache, &state).await?;
if let Some(res) = can_view(viewer, &poster, &submission, &state).await? {
let can_view_sensitive = if let Some(viewer) = viewer {
state.settings.for_profile(viewer).await?.sensitive
} else {
false
};
if let Some(res) = can_view(viewer, can_view_sensitive, &submission, &state).await? {
return Ok(res);
}
let (next_submission, previous_submission) =
adjacent_submissions(viewer, &submission, &state).await?;
adjacent_submissions(viewer, can_view_sensitive, &submission, &state).await?;
let (nav, current_file) = submission_nav(
page.map(|p| p.into_inner()),
@ -969,7 +989,10 @@ async fn create_comment(
let poster_id = submission.profile_id();
let poster = web::block(move || store.store.profiles.by_id(poster_id)?.req()).await?;
if let Some(res) = can_view(Some(profile.id()), &poster, &submission, &state).await? {
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(Some(profile.id()), can_view_sensitive, &submission, &state).await?
{
return Ok(res);
}
@ -999,7 +1022,7 @@ async fn create_comment(
let cache = files_for_profile(&poster, cache, &state).await?;
let (next_submission, previous_submission) =
adjacent_submissions(Some(profile.id()), &submission, &state).await?;
adjacent_submissions(Some(profile.id()), can_view_sensitive, &submission, &state).await?;
let (nav, current_file) = submission_nav(
page.map(|p| p.into_inner()),

View file

@ -30,6 +30,27 @@
@:button_group(&state.buttons(loader))
})
})
@:card(&Card::full_width().dark(nav_state.dark()), {
<form method="POST" action="/profiles/update/settings">
@:card_title({
@fl!(loader, "update-settings-heading")
})
@if let Some(error) = &state.settings_error {
@:card_body({
@error
})
}
@:card_body({
@:checkbox("sensitive", &fl!(loader, "sensitive-checkbox"), state.settings.sensitive)
@:checkbox("dark", &fl!(loader, "dark-checkbox"), state.settings.dark)
})
@:card_body({
@:button_group(&[
Button::primary(&fl!(loader, "update-settings-button")),
])
})
</form>
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({
@fl!(loader, "update-profile-heading")

View file

@ -70,21 +70,23 @@
</form>
})
}
@:card(&Card::full_width().dark(nav_state.dark()), {
<form method="POST" action="@state.sensitive_path()">
@:card_title({
@fl!(loader, "submission-sensitive-heading")
})
@:card_body({
@:checkbox("sensitive", &fl!(loader, "submission-sensitive-checkbox"), state.submission.is_sensitive())
})
@:card_body({
@:button_group(&[
Button::primary(&fl!(loader, "submission-sensitive-button")),
])
})
</form>
})
@if state.settings.sensitive {
@:card(&Card::full_width().dark(nav_state.dark()), {
<form method="POST" action="@state.sensitive_path()">
@:card_title({
@fl!(loader, "submission-sensitive-heading")
})
@:card_body({
@:checkbox("sensitive", &fl!(loader, "submission-sensitive-checkbox"), state.submission.is_sensitive())
})
@:card_body({
@:button_group(&[
Button::primary(&fl!(loader, "submission-sensitive-button")),
])
})
</form>
})
}
@:card(&Card::full_width().dark(nav_state.dark()), {
<form method="POST" action="@state.update_path()">
@:card_title({

View file

@ -138,6 +138,12 @@ file-num = file {$num}
profile-settings-title = Profile Settings
profile-settings-subtitle = Edit {$profileName}'s profile on {site-name}
update-settings-heading = Update Profile Settings
sensitive-checkbox = Show me NSFW content
dark-checkbox = Enable dark mode
update-settings-button = Save
update-profile-heading = Update Profile
update-bio-heading = Update Bio
update-bio-description =