Cache associated records when iterating over comments or submissions

Rely more on the toolkit for UI
Fix a couple hard-coded dark modes
This commit is contained in:
asonix 2021-01-12 22:39:59 -06:00
parent 979c6386ef
commit 73d40e2f75
26 changed files with 325 additions and 707 deletions

View file

@ -50,6 +50,7 @@
.profile-box {
display: flex;
flex: 1;
width: 100%;
.profile-box--content {
flex: 1;
@ -62,7 +63,7 @@
.profile-box--all-meta {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: flex-start;
width: 100%;
}
@ -74,7 +75,7 @@
display: flex;
align-items: center;
flex-wrap: wrap;
margin-right: 4px;
margin-right: 8px;
}
.profile-box--meta--display .toolkit-link {
@ -82,21 +83,6 @@
font-size: 16px;
}
.profile-box--icon {
position: relative;
height: 64px;
width: 64px;
border-radius: 32px;
border: 3px solid #444;
background-color: #555;
overflow: hidden;
margin-right: 16px;
img {
width: 100%;
}
}
.profile-box--meta--display {
margin-right: 4px;
}
@ -106,83 +92,6 @@
}
}
.profile-view {
background-color: #444;
color: #f5f5f5;
overflow: hidden;
&.card-top {
border-radius: 3px 3px 0 0;
}
&.standalone {
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
margin-bottom: 32px;
}
img {
width: 100%;
}
.profile-view--banner {
height: 233px;
width: 100%;
}
.profile-view--banner--placeholder {
height: 100%;
min-height: 100%;
background-color: #555;
}
.profile-view--content {
padding-bottom: 16px;
}
.profile-view--content--top {
margin-top: -86px;
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
padding: 0 16px;
}
.profile-view--icon {
position: relative;
height: 128px;
width: 128px;
border-radius: 64px;
border: 3px solid #444;
background-color: #555;
overflow: hidden;
margin-right: 16px;
}
.profile-view--meta {
padding-top: 20px;
text-shadow: 1px 1px 1px #222;
span {
display: block;
}
}
.profile-view--meta--display {
font-weight: 600;
font-size: 20px;
}
.profile-view--meta--handle {
font-weight: 500;
}
.profile-view--description {
padding: 16px;
}
}
.text-section,
.button-section {
padding: 8px 0;
@ -208,89 +117,32 @@
justify-content: center;
}
.comment {
padding: 16px 0;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
}
& + .comment {
border-top: 1px solid #555;
}
.comment-body {
display: flex;
}
.comment-text {
flex: 1;
}
.comment-children {
padding-top: 16px;
.comment:first-child {
border-top: 1px solid #555;
padding-top: 16px;
}
}
}
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children,
> .comment-children > .comment > .comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children,
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children {
border-left: 4px solid #555;
padding-left: 16px;
}
}
.submission-tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
.submission-icon {
position: relative;
border-radius: 0px;
}
picture {
display: block;
width: 100%;
height: 100%;
}
img {
object-fit: cover;
}
.toolkit-link {
display: block;
height: 100%;
width: 100%;
font-style: none;
font-weight: 500;
color: #e5e5e5;
&:hover,
&:focus,
@ -332,15 +184,11 @@
}
}
.image-box {
max-width: 100%;
background-color: #f5f5f5;
overflow: hidden;
img {
display: block;
width: 100%;
}
.submission-box {
max-height: 70vh;
display: flex;
justify-content: center;
background-color: #000;
}
@media (max-width: 700px) {
@ -360,150 +208,7 @@
}
}
.profile-view {
&.standalone {
border-radius: 0;
}
.profile-view--banner {
height: 33vw;
}
}
}
@media (max-width: 680px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 660px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 640px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 620px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 600px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children > .comment > .comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 580px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children > .comment > .comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 560px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children > .comment >
.comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 540px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children > .comment > .comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 520px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children > .comment > .comment-children, {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 500px) {
.comment-box > .comment,
.comment-box > .comment-children > .comment {
> .comment-children {
border-left: 0;
padding-left: 0;
}
}
}
@media (max-width: 350px) {
.profile-view {
.profile-view--content--top {
margin-top: -56px;
display: block;
}
.profile-view--icon {
width: 96px;
height: 96px;
}
.profile-view--meta {
padding-bottom: 0;
}
.standalone {
border-radius: 0;
}
}

View file

@ -61,21 +61,42 @@ impl CommentNode {
let submission = Submission::from_id(submission_id, state)?;
let author = Profile::from_id(submission.profile_id(), state)?;
let items = state
let is_self = viewer.map(|id| id == author.id()).unwrap_or(false);
let mut profiles: HashMap<Uuid, Profile> = HashMap::new();
let children = state
.profiles
.store
.comments
.for_submission(submission_id)
.filter_map(|comment_id| state.profiles.store.comments.by_id(comment_id).ok()?)
.rev();
.filter_map(|comment| {
if let Some(profile) = profiles.get(&comment.profile_id()) {
CommentNode::from_root(comment, profile.clone(), viewer, &mut profiles, state)
} else {
let profile = Profile::from_id(comment.profile_id(), state).ok()?;
profiles.insert(profile.id(), profile.clone());
CommentNode::from_root(comment, profile, viewer, &mut profiles, state)
}
})
.collect();
Ok(into_nodes(submission, author, viewer, items, state))
Ok(CommentNode {
item: ItemWithAuthor {
parent: ItemWithAuthorInner::Submission(submission),
author,
},
is_self,
children,
})
}
pub(crate) fn from_root(
comment: Comment,
author: Profile,
self_profile: Option<Uuid>,
profiles: &mut HashMap<Uuid, Profile>,
state: &State,
) -> Option<Self> {
if let Some(self_id) = self_profile {
@ -95,11 +116,15 @@ impl CommentNode {
.replies_for(comment.id())
.filter_map(|comment_id| state.profiles.store.comments.by_id(comment_id).ok()?)
.filter_map(|comment| {
Some((Profile::from_id(comment.profile_id(), state).ok()?, comment))
})
.filter_map(|(profile, comment)| {
CommentNode::from_root(comment, profile, self_profile, state)
if let Some(profile) = profiles.get(&comment.profile_id()) {
CommentNode::from_root(comment, profile.clone(), self_profile, profiles, state)
} else {
let profile = Profile::from_id(comment.profile_id(), state).ok()?;
profiles.insert(profile.id(), profile.clone());
CommentNode::from_root(comment, profile, self_profile, profiles, state)
}
})
.rev()
.collect();
let is_self = self_profile.map(|id| id == author.id()).unwrap_or(false);
@ -113,115 +138,6 @@ impl CommentNode {
children,
})
}
fn insert(
&mut self,
mut indexes: Vec<usize>,
comment: Comment,
author: Profile,
is_self: bool,
) -> Option<Vec<usize>> {
let comment_id = comment.id();
if let Some(index) = indexes.pop() {
if let Some(node) = self.children.get_mut(index) {
if let Some(mut v) = node.insert(indexes, comment, author, is_self) {
v.push(index);
return Some(v);
} else {
log::warn!("Failed to insert comment {}", comment_id);
}
} else {
match &self.item.parent {
ItemWithAuthorInner::Comment(ref c) => log::error!(
"Error inserting {}: Failed to get child {} of comment {}",
comment_id,
index,
c.id(),
),
ItemWithAuthorInner::Submission(ref s) => log::error!(
"Error inserting {}: Failed to get child {} of submission {}",
comment_id,
index,
s.id()
),
}
}
} else {
let index = self.children.len();
self.children.push(CommentNode {
item: ItemWithAuthor {
parent: ItemWithAuthorInner::Comment(comment),
author,
},
is_self,
children: vec![],
});
return Some(vec![index]);
}
None
}
}
// Assumes comments are in-order by publish date
fn into_nodes(
submission: Submission,
author: Profile,
viewer: Option<Uuid>,
items: impl IntoIterator<Item = Comment>,
state: &State,
) -> CommentNode {
let is_self = viewer.map(|id| id == author.id()).unwrap_or(false);
let mut hashmap: HashMap<Uuid, Vec<usize>> = HashMap::new();
let mut node = CommentNode {
item: ItemWithAuthor {
parent: ItemWithAuthorInner::Submission(submission),
author,
},
is_self,
children: vec![],
};
for comment in items {
if let Some(viewer) = viewer {
if !can_view_comment_no_recurse(viewer, &comment, state).unwrap_or(false) {
continue;
}
} else {
if !can_view_comment_logged_out_no_recurse(&comment, state).unwrap_or(false) {
continue;
}
}
let comment_id = comment.id();
if let Ok(author) = Profile::from_id(comment.profile_id(), state) {
let is_self = viewer.map(|id| id == author.id()).unwrap_or(false);
if let Some(reply_to) = comment.comment_id() {
if let Some(indexes) = hashmap.get(&reply_to) {
if let Some(indexes) = node.insert(indexes.clone(), comment, author, is_self) {
hashmap.insert(comment_id, indexes);
} else {
log::warn!("Failed to insert nested comment {}", comment_id);
}
} else {
log::warn!("Reply To {} doesn't exist", reply_to);
}
} else {
if let Some(indexes) = node.insert(vec![], comment, author, is_self) {
hashmap.insert(comment_id, indexes);
} else {
log::warn!("Failed to insert top-level comment {}", comment_id);
}
}
} else {
log::warn!("Failed to get author for comment {}", comment_id);
}
}
node
}
fn to_comment_page(comment_id: Uuid) -> HttpResponse {
@ -530,11 +446,16 @@ fn prepare_view(
};
let author = Profile::from_id(comment.profile_id(), &state)?;
let node =
match CommentNode::from_root(comment, author, profile.as_ref().map(|p| p.id()), &state) {
Some(node) => node,
None => return Ok(None),
};
let node = match CommentNode::from_root(
comment,
author,
profile.as_ref().map(|p| p.id()),
&mut HashMap::new(),
&state,
) {
Some(node) => node,
None => return Ok(None),
};
let view = CommentView::new(
submission,

View file

@ -299,58 +299,6 @@ fn body_class(dark: bool) -> &'static str {
}
}
fn time(time: chrono::DateTime<chrono::Utc>) -> String {
time.format("%B %d, %Y, %I:%M %P").to_string()
}
fn published_time(time: chrono::DateTime<chrono::Utc>) -> String {
let duration = chrono::Utc::now() - time;
if let Some(years) =
duration
.num_days()
.checked_div(365)
.and_then(|years| if years > 0 { Some(years) } else { None })
{
if years == 1 {
format!("{} year ago", years)
} else {
format!("{} years ago", years)
}
} else if duration.num_weeks() > 0 {
if duration.num_weeks() == 1 {
format!("{} week ago", duration.num_weeks())
} else {
format!("{} weeks ago", duration.num_weeks())
}
} else if duration.num_days() > 0 {
if duration.num_days() == 1 {
format!("{} day ago", duration.num_days())
} else {
format!("{} days ago", duration.num_days())
}
} else if duration.num_hours() > 0 {
if duration.num_hours() == 1 {
format!("{} hour ago", duration.num_hours())
} else {
format!("{} hours ago", duration.num_hours())
}
} else if duration.num_minutes() > 0 {
if duration.num_minutes() == 1 {
format!("{} minute ago", duration.num_minutes())
} else {
format!("{} minutes ago", duration.num_minutes())
}
} else if duration.num_seconds() > 0 {
if duration.num_seconds() == 1 {
format!("{} second ago", duration.num_seconds())
} else {
format!("{} seconds ago", duration.num_seconds())
}
} else {
"now".to_owned()
}
}
async fn home(
req: HttpRequest,
page: Option<web::Query<SubmissionPage>>,

View file

@ -24,13 +24,11 @@ impl FromRequest for NavState {
let mut nav = vec![];
if let Some(logout_state) = logout {
let home = Button::primary("Home");
let submission = Button::secondary("New Submission");
let submission = Button::primary("New Submission");
let account = Button::secondary("Account");
let logout = Button::primary_outline("Logout");
logout_state.button(&logout);
home.href("/").dark(dark);
submission.href("/submissions/create").dark(dark);
account.href("/session/account").dark(dark);
logout.dark(dark);
@ -45,7 +43,6 @@ impl FromRequest for NavState {
btn
};
nav.push(home);
nav.push(submission);
nav.push(profile);
nav.push(account);

View file

@ -8,6 +8,7 @@ use actix_web::{web, HttpRequest, HttpResponse, Scope};
use hyaenidae_accounts::User;
use hyaenidae_profiles::store::{File, Submission};
use hyaenidae_toolkit::Button;
use std::collections::HashMap;
use uuid::Uuid;
mod middleware;
@ -315,6 +316,8 @@ pub(crate) fn build_submissions(
) -> SubmissionAggregation {
let mut nav = vec![];
let mut submissions: Vec<SubmissionView>;
let mut view_state = ViewState::new();
match page {
Some(SubmissionPage::Max { max }) => {
submissions = if profile.is_some() {
@ -325,7 +328,12 @@ pub(crate) fn build_submissions(
.submissions
.drafted_older_than_for_profile(max)
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(
submission_id,
viewed_by,
&mut view_state,
&state,
)
})
.take(per_page + 1)
.collect()
@ -336,7 +344,12 @@ pub(crate) fn build_submissions(
.submissions
.published_older_than_for_profile(max)
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(
submission_id,
viewed_by,
&mut view_state,
&state,
)
})
.take(per_page + 1)
.collect()
@ -348,7 +361,7 @@ pub(crate) fn build_submissions(
.submissions
.published_older_than(max)
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(submission_id, viewed_by, &mut view_state, &state)
})
.take(per_page + 1)
.collect()
@ -386,7 +399,12 @@ pub(crate) fn build_submissions(
.submissions
.drafted_newer_than_for_profile(min)
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(
submission_id,
viewed_by,
&mut view_state,
&state,
)
})
.take(per_page + 2)
.collect()
@ -397,7 +415,12 @@ pub(crate) fn build_submissions(
.submissions
.published_newer_than_for_profile(min)
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(
submission_id,
viewed_by,
&mut view_state,
&state,
)
})
.take(per_page + 2)
.collect()
@ -409,7 +432,7 @@ pub(crate) fn build_submissions(
.submissions
.published_newer_than(min)
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(submission_id, viewed_by, &mut view_state, &state)
})
.take(per_page + 2)
.collect()
@ -450,7 +473,12 @@ pub(crate) fn build_submissions(
.submissions
.drafted_for_profile(profile.id())
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(
submission_id,
viewed_by,
&mut view_state,
&state,
)
})
.take(per_page + 1)
.collect()
@ -461,7 +489,12 @@ pub(crate) fn build_submissions(
.submissions
.published_for_profile(profile.id())
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(
submission_id,
viewed_by,
&mut view_state,
&state,
)
})
.take(per_page + 1)
.collect()
@ -473,7 +506,7 @@ pub(crate) fn build_submissions(
.submissions
.published()
.filter_map(|submission_id| {
SubmissionView::from_id(submission_id, viewed_by, &state)
SubmissionView::from_id(submission_id, viewed_by, &mut view_state, &state)
})
.take(per_page + 1)
.collect()
@ -567,6 +600,22 @@ pub struct SubmissionView {
first_file: File,
}
struct ViewState {
profiles: HashMap<Uuid, Profile>,
blocks: HashMap<Uuid, bool>,
follows: HashMap<Uuid, bool>,
}
impl ViewState {
fn new() -> Self {
ViewState {
profiles: HashMap::new(),
blocks: HashMap::new(),
follows: HashMap::new(),
}
}
}
impl SubmissionView {
fn id(&self) -> Uuid {
self.submission.id()
@ -604,7 +653,12 @@ impl SubmissionView {
format!("/submissions/{}", self.submission.id())
}
fn from_id(submission_id: Uuid, viewed_by: Option<Uuid>, state: &State) -> Option<Self> {
fn from_id(
submission_id: Uuid,
viewed_by: Option<Uuid>,
view_state: &mut ViewState,
state: &State,
) -> Option<Self> {
let submission = state
.profiles
.store
@ -620,54 +674,79 @@ impl SubmissionView {
return None;
}
let poster = Profile::from_id(submission.profile_id(), state).ok()?;
let poster = if let Some(profile) = view_state.profiles.get(&submission.profile_id()) {
profile.clone()
} else {
let profile = Profile::from_id(submission.profile_id(), state).ok()?;
view_state.profiles.insert(profile.id(), profile.clone());
profile
};
if let Some(profile_id) = viewed_by {
let blocking = state
.profiles
.store
.view
.blocks
.by_forward(submission.profile_id(), profile_id)
.ok()?
.is_some();
if viewed_by.is_none() && poster.login_required() {
return None;
}
if blocking {
return None;
}
let blocked = state
.profiles
.store
.view
.blocks
.by_forward(profile_id, submission.profile_id())
.ok()?
.is_some();
if blocked {
if let Some(block) = view_state.blocks.get(&poster.id()) {
if *block {
return None;
}
} else {
if poster.login_required() {
return None;
if let Some(profile_id) = viewed_by {
let blocking = state
.profiles
.store
.view
.blocks
.by_forward(submission.profile_id(), profile_id)
.ok()?
.is_some();
if blocking {
view_state.blocks.insert(poster.id(), true);
return None;
}
let blocked = state
.profiles
.store
.view
.blocks
.by_forward(profile_id, submission.profile_id())
.ok()?
.is_some();
if blocked {
view_state.blocks.insert(poster.id(), true);
return None;
}
view_state.blocks.insert(poster.id(), false);
}
}
if submission.is_followers_only() {
let profile_id = viewed_by?;
if let Some(follow) = view_state.follows.get(&poster.id()) {
if !follow {
return None;
}
} else {
let profile_id = viewed_by?;
if !is_self
&& state
.profiles
.store
.view
.follows
.by_forward(submission.profile_id(), profile_id)
.ok()?
.is_none()
{
return None;
if !is_self
&& state
.profiles
.store
.view
.follows
.by_forward(submission.profile_id(), profile_id)
.ok()?
.is_none()
{
view_state.follows.insert(poster.id(), false);
return None;
}
view_state.follows.insert(poster.id(), true);
}
}

View file

@ -463,7 +463,7 @@ async fn update_page(
return Ok(crate::to_404());
}
let state = SubmissionState::new(submission, true);
let state = SubmissionState::new(submission, nav_state.dark());
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::update(cursor, &state, &nav_state)
@ -606,7 +606,7 @@ async fn update_submission(
};
}
let mut state = SubmissionState::new(submission, true);
let mut state = SubmissionState::new(submission, nav_state.dark());
if let Some(error) = title_error {
state.title_error(error);
}
@ -651,7 +651,7 @@ async fn add_file(
Err(e) => e.to_string(),
};
let mut state = SubmissionState::new(submission, true);
let mut state = SubmissionState::new(submission, nav_state.dark());
state.file_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
@ -690,7 +690,7 @@ async fn remove_file(
Err(e) => e.to_string(),
};
let mut state = SubmissionState::new(submission, true);
let mut state = SubmissionState::new(submission, nav_state.dark());
state.remove_file_error(form.file_id, error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
@ -724,7 +724,7 @@ async fn publish_submission(
}
};
let mut state = SubmissionState::new(submission, true);
let mut state = SubmissionState::new(submission, nav_state.dark());
state.publish_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
@ -754,7 +754,7 @@ async fn delete_submission(
Err(e) => e.to_string(),
};
let mut state = SubmissionState::new(submission, true);
let mut state = SubmissionState::new(submission, nav_state.dark());
state.delete_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {

View file

@ -4,6 +4,7 @@
@use crate::nav::NavState;
@use hyaenidae_toolkit::{templates::button_group, Button};
@use hyaenidae_toolkit::{templates::{card, card_title, card_body}, Card};
@use hyaenidae_toolkit::templates::nested;
@use hyaenidae_toolkit::{templates::text_input};
@(view: &CommentView, nav_state: &NavState)
@ -30,11 +31,11 @@
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Replies })
@:card_body({
<div class="comment-box">
@:nested(nav_state.dark(), {
@for child in &view.comments.children {
@:nodes(child, &view.comments.item, view.logged_in, nav_state.dark())
}
</div>
})
})
})
}

View file

@ -1,49 +1,38 @@
@use crate::comments::{CommentNode, ItemWithAuthor};
@use crate::templates::comments::{nodes, profile_box};
@use hyaenidae_toolkit::templates::link;
@use hyaenidae_toolkit::templates::{nested_children, nested_node};
@(node: &CommentNode, replying_to: &ItemWithAuthor, logged_in: bool, dark: bool)
@if let Some((comment, author)) = node.item.comment() {
<div class="comment">
<div class="comment-body">
@:profile_box(author, comment, replying_to, dark, {
<div class="comment-links">
<div>
@:link(&node.item.link(dark), {
@if logged_in {
reply
} else {
view
}
})
</div>
@if let Some(state) = node.edit_link(dark) {
<div>
@:link(&state, { edit })
</div>
@:nested_node({
@:profile_box(author, comment, replying_to, dark, {
<div class="comment-links">
<div>
@:link(&node.item.link(dark), {
@if logged_in {
reply
} else {
view
}
})
</div>
}, {
<div class="comment-text">
@comment.body()
</div>
})
</div>
@if node.has_children() {
<div class="comment-children">
@for child in &node.children {
@:nodes(child, &node.item, logged_in, dark)
@if let Some(state) = node.edit_link(dark) {
<div>
@:link(&state, { edit })
</div>
}
</div>
}
</div>
} else {
@if node.has_children() {
<div class="comment-children">
@for child in &node.children {
@:nodes(child, &node.item, logged_in, dark)
}
</div>
}
}, {
@comment.body()
})
})
}
@if node.has_children() {
@:nested_children({
@for child in &node.children {
@:nodes(child, &node.item, logged_in, dark)
}
})
}

View file

@ -1,46 +1,45 @@
@use crate::templates::profiles::icon;
@use crate::{profiles::Profile, comments::{Comment, ItemWithAuthor}};
@use hyaenidae_toolkit::{templates::link, Link};
@use hyaenidae_toolkit::templates::icon as tkicon;
@use hyaenidae_toolkit::templates::ago;
@(profile: &Profile, comment: &Comment, replying_to: &ItemWithAuthor, dark: bool, meta: Content, body: Content)
<div class="profile-box">
@:link(&Link::current_tab(&profile.view_path()).dark(dark), {
<div class="profile-box--icon">
@if let Some(key) = profile.icon_key() {
@:icon(key, &profile.name())
}
</div>
@:tkicon(&profile.view_path(), true, dark, {
@if let Some(key) = profile.icon_key() {
@:icon(key, &profile.name())
}
})
<div class="profile-box--content">
<div class="profile-box--all-meta">
<div class="profile-box--meta">
@if let Some(name) = profile.display_name() {
<div class="profile-box--meta--display">
<div>
<div class="profile-box--meta">
@if let Some(name) = profile.display_name() {
<div class="profile-box--meta--display">
@:link(&Link::current_tab(&profile.view_path()).plain(true).dark(dark), {
@name
})
</div>
}
<div class="profile-box--meta--handle">
@:link(&Link::current_tab(&profile.view_path()).plain(true).dark(dark), {
@name
@profile.full_handle()
})
</div>
}
<div class="profile-box--meta--handle">
@:link(&Link::current_tab(&profile.view_path()).plain(true).dark(dark), {
@profile.full_handle()
})
<div class="profile-box--meta--date">
posted @:ago(comment.published(), dark)
</div>
</div>
<div class="profile-box--meta--date">
posted
@:link(&Link::current_tab("#").title(&crate::time(comment.published())).plain(true).dark(dark), {
@crate::published_time(comment.published())
<div class="profile-box--replying-to">
@:link(&replying_to.link(dark), {
Replying to @replying_to.name()
})
</div>
</div>
<div class="profile-box--extra-meta">@:meta()</div>
</div>
<div class="profile-box--replying-to">
@:link(&replying_to.link(dark), {
Replying to @replying_to.name()
})
</div>
<div class="profile-box--body">@:body()</div>
</div>
</div>

View file

@ -5,6 +5,7 @@
@use hyaenidae_toolkit::{templates::button_group, Button};
@use hyaenidae_toolkit::{templates::{card, card_title, card_body}, Card};
@use hyaenidae_toolkit::templates::link;
@use hyaenidae_toolkit::templates::{nested, nested_children};
@use hyaenidae_toolkit::templates::text_input;
@(view: &CommentView, nav_state: &NavState)
@ -53,11 +54,13 @@
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Replies })
@:card_body({
<div class="comment-box">
@for child in &view.comments.children {
@:nodes(child, &view.comments.item, view.logged_in, nav_state.dark())
}
</div>
@:nested(nav_state.dark(), {
@:nested_children({
@for child in &view.comments.children {
@:nodes(child, &view.comments.item, view.logged_in, nav_state.dark())
}
})
})
})
})
}

View file

@ -34,14 +34,16 @@
</div>
})
<nav class="nav-links">
@:card(&Card::full_width().classes(&["nav"]).dark(nav_state.dark()), {
@:card_body({
@:nav(nav_state)
})
@:card_body({
@:button_group(&[
Button::primary_outline("Close").href(nav_state.href()).dark(nav_state.dark()),
])
@:centered(false, {
@:card(&Card::full_width().classes(&["nav"]).dark(nav_state.dark()), {
@:card_body({
@:nav(nav_state)
})
@:card_body({
@:button_group(&[
Button::primary_outline("Close").href(nav_state.href()).dark(nav_state.dark()),
])
})
})
})
</nav>

View file

@ -36,7 +36,7 @@
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Preview })
@:view("", profile)
@:view(profile, nav_state.dark())
})
})

View file

@ -30,6 +30,6 @@
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Preview })
@:view("", profile)
@:view(profile, nav_state.dark())
})
})

View file

@ -25,6 +25,6 @@
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Preview })
@:view("", profile)
@:view(profile, nav_state.dark())
})
})

View file

@ -36,7 +36,7 @@
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Preview })
@:view("", profile)
@:view(profile, nav_state.dark())
})
})

View file

@ -48,6 +48,6 @@
})
@:card(&Card::full_width().dark(true), {
@:card_title({ Preview })
@:view("", profile)
@:view(profile, nav_state.dark())
})
})

View file

@ -7,8 +7,8 @@
<script src="/toolkit/@file_input_js.name"></script>
<script src="/toolkit/@button_js.name"></script>
}, {
@:view("standalone", &state.profile)
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card(Card::full_width().dark(nav_state.dark()), { @:view(&state.profile, nav_state.dark()) })
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Profile Actions })
@:card_body({
@:button_group(&[
@ -17,7 +17,7 @@
])
})
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Update Profile })
@:card_body({
<form method="POST" action="/profiles/update/bio">
@ -50,9 +50,7 @@
<div class="columns">
<div class="columns--column">
@if let Some(key) = state.profile.icon_key() {
<div class="image-box">
@:icon(key, &state.profile.name())
</div>
@:icon(key, &state.profile.name())
} else {
<p>No icon set</p>
}
@ -85,9 +83,7 @@
<div class="columns">
<div class="columns--column">
@if let Some(key) = state.profile.banner_key() {
<div class="image-box">
@:banner(key, &state.profile.name())
</div>
@:banner(key, &state.profile.name())
} else {
<p>No banner set</p>
}
@ -144,7 +140,7 @@
</form>
})
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Danger })
@:card_body({
@:button_group(&[

View file

@ -6,8 +6,8 @@
@:home("Delete Profile", &format!("Delete {}", profile.name()), nav_state, {
<script src="/toolkit/@button_js.name"></script>
}, {
@:view("standalone account-page", profile)
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card(Card::full_width().dark(nav_state.dark()), { @:view(profile, nav_state.dark()) })
@:card(Card::full_width().dark(nav_state.dark()), {
<form method="POST" action="/profiles/delete">
@:card_title({ Delete Profile })
@:card_body({

View file

@ -5,7 +5,7 @@
@(pview: &ProfileView, nav_state: &NavState)
@:home(&pview.profile.name(), pview.profile.description().unwrap_or(&format!("{}'s profile on Hyaenidae", pview.profile.name())), nav_state, {}, {
@:view("standalone account-page", &pview.profile)
@:card(Card::full_width().dark(nav_state.dark()), { @:view(&pview.profile, nav_state.dark()) })
@if pview.is_self {
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Profile Actions })

View file

@ -6,7 +6,7 @@
@:home("Switch Profile", "Select the profile you wish to use", nav_state, {}, {
@for profile in profiles {
@:card(&Card::full_width().dark(nav_state.dark()), {
@:view("card-top", profile)
@:view(profile, nav_state.dark())
@:card_body({
<form method="POST" action="/profiles/change">
<input type="hidden" name="profile_id" value="@profile.id()" />

View file

@ -5,7 +5,7 @@
@(pview: &ProfileView, nav_state: &NavState)
@:home(&pview.profile.name(), pview.profile.description().unwrap_or(&format!("{}'s profile on Hyaenidae", pview.profile.name())), nav_state, {}, {
@:view("standalone", &pview.profile)
@:card(Card::full_width().dark(nav_state.dark()), { @:view(&pview.profile, nav_state.dark()) })
@if pview.is_self {
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Profile Actions })
@ -18,19 +18,21 @@
})
})
}
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Submissions })
@:card_section({
<div class="submission-tiles">
@for submission in &pview.submissions {
@:submission_tile(submission, pview.viewer, nav_state.dark())
}
</div>
})
@if pview.nav().len() > 0 {
@:card_body({
@:button_group(&pview.nav())
@if !pview.submissions.is_empty() {
@:card(Card::full_width().dark(nav_state.dark()), {
@:card_title({ Submissions })
@:card_section({
<div class="submission-tiles">
@for submission in &pview.submissions {
@:submission_tile(submission, pview.viewer, nav_state.dark())
}
</div>
})
}
})
@if pview.nav().len() > 0 {
@:card_body({
@:button_group(&pview.nav())
})
}
})
}
})

View file

@ -7,7 +7,7 @@
@if let Some(key) = submission.pictrs_key() {
@:link(&Link::current_tab(&submission.view_path()).plain(true).dark(dark), {
<div class="submission-icon image-box">
<div class="submission-icon">
@if submission.is_published() {
<div class="submission-icon--overlay">
<div class="submission-icon--meta">

View file

@ -1,35 +1,14 @@
@use crate::{templates::profiles::{banner, icon}, profiles::Profile};
@use hyaenidae_toolkit::templates::profile as tkprofile;
@(classes: &str, profile: &Profile)
@(profile: &Profile, dark: bool)
<div class="profile-view @classes">
<div class="profile-view--banner">
@if let Some(key) = profile.banner_key() {
@:banner(key, &profile.name())
} else {
<div class="profile-view--banner--placeholder"></div>
}
</div>
<div class="profile-view--content">
<div class="profile-view--content--top">
<div class="profile-view--icon">
@if let Some(key) = profile.icon_key() {
@:icon(key, &profile.name())
}
</div>
<div class="profile-view--meta">
<div class="profile-view--meta--display">
@if let Some(name) = profile.display_name() {
@name
} else {
&nbsp;
}
</div>
<div class="profile-view--meta--handle">@profile.full_handle()</div>
</div>
</div>
@if let Some(description) = profile.description() {
<div class="profile-view--description">@description</div>
}
</div>
</div>
@:tkprofile(&profile.view_path(), profile.display_name(), &profile.full_handle(), profile.description(), dark, {
@if let Some(key) = profile.icon_key() {
@:icon(key, &profile.name())
}
}, {
@if let Some(key) = profile.banner_key() {
@:banner(key, &profile.name())
}
})

View file

@ -1,16 +1,16 @@
@use crate::templates::profiles::icon;
@use crate::{profiles::Profile, submissions::Submission};
@use hyaenidae_toolkit::{templates::link, Link};
@use hyaenidae_toolkit::templates::ago;
@use hyaenidae_toolkit::templates::icon as tkicon;
@(profile: &Profile, submission: &Submission, dark: bool, body: Content)
<div class="profile-box">
@:link(&Link::current_tab(&profile.view_path()).dark(dark), {
<div class="profile-box--icon">
@if let Some(key) = profile.icon_key() {
@:icon(key, &profile.name())
}
</div>
@:tkicon(&profile.view_path(), true, dark, {
@if let Some(key) = profile.icon_key() {
@:icon(key, &profile.name())
}
})
<div class="profile-box--content">
<div class="profile-box--all-meta">
@ -30,9 +30,7 @@
@if let Some(published) = submission.published() {
<div class="profile-box--meta--date">
posted
@:link(&Link::current_tab("#").title(&crate::time(published)).plain(true).dark(dark), {
@crate::published_time(published)
})
@:ago(published, dark)
</div>
}
</div>

View file

@ -4,6 +4,7 @@
@use crate::{nav::NavState, submissions::SubmissionView};
@use hyaenidae_toolkit::{templates::button_group, Button};
@use hyaenidae_toolkit::{templates::{card, card_body, card_section, card_title}, Card};
@use hyaenidae_toolkit::templates::nested;
@use hyaenidae_toolkit::templates::text_input;
@(view: &SubmissionView, nav_state: &NavState)
@ -15,7 +16,7 @@
})
@:card_section({
@if let Some(file) = view.current_file.pictrs() {
<div class="submission-box image-box">
<div class="submission-box">
@:image(file.key(), &view.submission.title(), view.file_num)
</div>
} else {
@ -64,9 +65,9 @@
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Comments })
@:card_body({
<div class="comment-box">
@:nested(nav_state.dark(), {
@:nodes(&view.comments, &view.replying_to(), view.profile.is_some(), nav_state.dark())
</div>
})
})
})
}

View file

@ -45,9 +45,7 @@
@:card_body({
<div class="columns">
<div class="columns--column">
<div class="image-box">
@:image(file.key(), &state.submission.title(), tup.0)
</div>
@:image(file.key(), &state.submission.title(), tup.0)
</div>
<div class="columns--column">
<h3>Remove Image From Submission</h3>
@ -84,7 +82,7 @@
@:card_title({ View Submission })
@:card_body({
@:button_group(&[
&Button::primary("View").href(&state.view_path()),
&Button::primary("View").href(&state.view_path()).dark(nav_state.dark()),
])
})
})