Compare commits

...

8 commits

26 changed files with 379 additions and 53 deletions

View file

@ -1250,3 +1250,32 @@ details.accordion .inner {
.CodeMirror-focused .CodeMirror-placeholder {
opacity: 50%;
}
.CodeMirror-gutters {
border-color: var(--color-super-lowered) !important;
background-color: var(--color-lowered) !important;
}
.CodeMirror-hints {
background: var(--color-raised) !important;
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
var(--color-shadow);
border-radius: var(--radius) !important;
padding: var(--pad-1) !important;
border-color: var(--color-super-lowered) !important;
}
.CodeMirror-hints li {
color: var(--color-text-raised) !important;
border-radius: var(--radius) !important;
transition:
background 0.15s,
color 0.15s;
font-size: 10px;
padding: calc(var(--pad-1) / 2) var(--pad-2);
}
.CodeMirror-hints li.CodeMirror-hint-active {
background-color: var(--color-primary) !important;
color: var(--color-text-primary) !important;
}

View file

@ -1928,7 +1928,7 @@
(text "{%- endif %}")
; note listings
(text "{% for note in notes %}")
(text "{% for note in notes %} {% if not view_mode or note.title != \"journal.css\" -%}")
(div
("class" "flex flex-row gap-1")
(a
@ -1958,6 +1958,6 @@
(icon (text "trash"))
(str (text "general:action.delete")))))
(text "{%- endif %}"))
(text "{% endfor %}"))
(text "{%- endif %} {% endfor %}"))
(text "{%- endif %}")
(text "{%- endmacro %}")

View file

@ -2,6 +2,27 @@
(text "{% if journal -%}") (title (text "{{ journal.title }}")) (text "{% else %}") (title (text "Journals - {{ config.name }}")) (text "{%- endif %}")
(link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/css/chats.css?v=tetratto-{{ random_cache_breaker }}"))
(style
(text "html, body {
overflow: hidden auto !important;
}
.sidebar {
position: sticky;
top: 42px;
}
@media screen and (max-width: 900px) {
.sidebar {
position: absolute;
top: unset;
}
body.sidebars_shown {
overflow: hidden !important;
}
}"))
(text "{% if view_mode and journal and is_editor -%} {% if note -%}")
; redirect to note
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}/{{ note.title }}"))
@ -9,6 +30,11 @@
; redirect to journal homepage
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}"))
(text "{%- endif %} {%- endif %}")
(text "{% if view_mode and journal -%}")
; add journal css
(link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/api/v1/journals/{{ journal.id }}/journal.css?v=tetratto-{{ random_cache_breaker }}"))
(text "{%- endif %}")
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"journals\") }}")
(text "{% if not view_mode -%}")
(nav
@ -63,17 +89,19 @@
("href" "/api/v1/auth/user/find/{{ journal.owner }}")
(text "{{ components::avatar(username=journal.owner, selector_type=\"id\", size=\"18px\") }}"))
(text "{% if (view_mode and owner) or not view_mode -%}")
(a
("class" "flush")
("href" "{% if view_mode -%} /@{{ owner.username }}/{{ journal.title }}/index {%- else -%} /journals/{{ journal.id }}/0 {%- endif %}")
("href" "{% if view_mode -%} /@{{ owner.username }}/{{ journal.title }} {%- else -%} /journals/{{ journal.id }}/0 {%- endif %}")
(b (text "{{ journal.title }}")))
(text "{%- endif %}")
(text "{% if note -%}")
(span (text "/"))
(b (text "{{ note.title }}"))
(text "{%- endif %}"))
(text "{% if user and user.id == journal.owner -%}")
(text "{% if user and user.id == journal.owner and (not note or note.title != \"journal.css\") -%}")
(div
("class" "pillmenu")
(a
@ -83,7 +111,7 @@
(icon (text "pencil")))
(a
("class" "{% if view_mode -%}active{%- endif %}")
("href" "/@{{ user.username }}/{{ journal.title }}/{% if note -%} {{ note.title }} {%- else -%} index {%- endif %}")
("href" "/@{{ user.username }}/{{ journal.title }}{% if note -%} /{{ note.title }} {%- endif %}")
(icon (text "eye"))))
(text "{%- endif %}"))
(text "{%- endif %}")
@ -96,6 +124,7 @@
("class" "card w-full flex flex-col gap-2")
(h3 (str (text "journals:label.welcome")))
(span (str (text "journals:label.select_a_journal")))
(span ("class" "mobile") (str (text "journals:label.mobile_click_my_journals")))
(button
("onclick" "create_journal()")
(icon (text "plus"))
@ -180,10 +209,36 @@
; import codemirror
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js") ("data-turbo-temporary" "true"))
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css") ("data-turbo-temporary" "true"))
(text "{% if note.title == \"journal.css\" -%}")
; css editor
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/edit/closebrackets.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/hint/css-hint.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/hint/show-hint.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/lint/css-lint.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/css/css.js") ("data-turbo-temporary" "true"))
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/addon/hint/show-hint.css") ("data-turbo-temporary" "true"))
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/addon/lint/lint.css") ("data-turbo-temporary" "true"))
(style
(text ".CodeMirror {
font-family: monospace !important;
font-size: 16px;
border: solid 1px var(--color-super-lowered);
border-radius: var(--radius);
}
.CodeMirror-line {
padding-left: 5px !important;
}"))
(text "{% else %}")
; markdown editor
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js") ("data-turbo-temporary" "true"))
(text "{%- endif %}")
; tab bar
(text "{% if note.title != \"journal.css\" -%}")
(div
("class" "pillmenu")
(a
@ -198,6 +253,7 @@
("data-tab-button" "preview")
("data-turbo" "false")
(str (text "journals:label.preview_pane"))))
(text "{%- endif %}")
; tabs
(div
@ -221,10 +277,15 @@
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}"))
(script
(text "setTimeout(() => {
if (!document.getElementById(\"preview_tab\").shadowRoot) {
document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" });
}
globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), {
value: document.getElementById(\"editor_content\").innerHTML,
mode: \"markdown\",
mode: \"{% if note.title == 'journal.css' -%} css {%- else -%} markdown {%- endif %}\",
lineWrapping: true,
lineNumbers: \"{{ note.title }}\" === \"journal.css\",
autoCloseBrackets: true,
autofocus: true,
viewportMargin: Number.POSITIVE_INFINITY,
@ -232,7 +293,8 @@
highlightFormatting: false,
fencedCodeBlockHighlighting: false,
xml: false,
smartIndent: false,
smartIndent: true,
indentUnit: 4,
placeholder: `# {{ note.title }}`,
extraKeys: {
Home: \"goLineLeft\",
@ -243,6 +305,15 @@
},
});
editor.on(\"keydown\", (cm, e) => {
if (e.key.length > 1) {
// ignore all keys that aren't a letter
return;
}
CodeMirror.showHint(cm, CodeMirror.hint.css);
});
document.querySelector(\"[data-tab-button=editor]\").addEventListener(\"click\", async (e) => {
e.preventDefault();
trigger(\"atto::hooks::tabs:switch\", [\"editor\"]);
@ -262,7 +333,10 @@
})
).text();
document.getElementById(\"preview_tab\").innerHTML = res;
const preview_token = window.crypto.randomUUID();
document.getElementById(\"preview_tab\").shadowRoot.innerHTML = `${res}<style>
@import url(\"/api/v1/journals/{{ journal.id }}/journal.css?v=preview-${preview_token}\");
</style>`;
trigger(\"atto::hooks::tabs:switch\", [\"preview\"]);
});
}, 150);"))
@ -272,7 +346,34 @@
("class" "flex flex-col gap-2 card")
(text "{{ note.content|markdown|safe }}"))
(span (text "Last updated: ") (span ("class" "date") (text "{{ note.edited }}")))
(div
("class" "flex w-full justify-between gap-2")
(span (text "Last updated: ") (span ("class" "date") (text "{{ note.edited }}")))
(text "{% if user and user.id == owner.id -%}")
(button
("class" "small")
("onclick" "{% if journal.privacy == \"Public\" -%}
trigger('atto::copy_text', ['{{ config.host }}/@{{ owner.username }}/{{ journal.title }}/{{ note.title }}'])
{%- else -%}
prompt_make_public();
trigger('atto::copy_text', ['{{ config.host }}/@{{ owner.username }}/{{ journal.title }}/{{ note.title }}'])
{%- endif %}")
(icon (text "share"))
(str (text "general:label.share")))
(script
(text "globalThis.prompt_make_public = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Would you like to make this journal public? This is required for others to view this note.\",
]))
) {
return;
}
change_journal_privacy({ target: { selectedOptions: [{ value: \"Public\" }] }, preventDefault: () => {} });
}"))
(text "{%- endif %}"))
(text "{%- endif %}")
(text "{%- endif %}")))
(style
@ -332,7 +433,7 @@
},
body: JSON.stringify({
title,
content: `# ${title}`,
content: title === \"journal.css\" ? `/* ${title} */\\n` : `# ${title}`,
journal: \"{{ selected_journal }}\",
}),
})
@ -431,8 +532,8 @@
globalThis.change_journal_privacy = async (e) => {
e.preventDefault();
const selected = event.target.selectedOptions[0];
fetch(\"/api/v1/journals/{{ selected_journal }}/privacy\", {
const selected = e.target.selectedOptions[0];
fetch(\"/api/v1/journals/{% if journal -%} {{ journal.id }} {%- else -%} {{ selected_journal }} {%- endif %}/privacy\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",

View file

@ -44,9 +44,10 @@
("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker"))))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&page=\", Number.parseInt(\"{{ page }}\") - 1]);
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});"))
(text "{% endblock %}")

View file

@ -564,14 +564,14 @@
(li
(text "Use custom CSS on your profile"))
(li
(text "Ability to use community emojis outside of
(text "Use community emojis outside of
their community"))
(li
(text "Ability to upload and use gif emojis"))
(text "Upload and use gif emojis"))
(li
(text "Create infinite stack timelines"))
(li
(text "Ability to upload images to posts"))
(text "Upload images to posts"))
(li
(text "Save infinite post drafts"))
(li
@ -579,7 +579,7 @@
(li
(text "Ability to create forges"))
(li
(text "Ability to create more than 1 app"))
(text "Create more than 1 app"))
(li
(text "Create up to 10 stack blocks"))
(li
@ -587,7 +587,9 @@
(li
(text "Increased proxied image size"))
(li
(text "Create infinite journals")))
(text "Create infinite journals"))
(li
(text "Create infinite notes in each journal")))
(a
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("class" "button")
@ -1401,6 +1403,11 @@
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
\"text\",
],
[
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
\"{{ profile.settings.paged_timelines }}\",
\"checkbox\",
],
[[], \"Fun\", \"title\"],
[
[\"disable_gpa_fun\", \"Disable GPA\"],

View file

@ -83,9 +83,10 @@
("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker")))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?stack_id={{ stack.id }}&page=\", Number.parseInt(\"{{ page }}\") - 1]);
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?stack_id={{ stack.id }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});"))
(text "{%- endif %}"))))

View file

@ -33,9 +33,10 @@
("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker"))))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=AllPosts&page=\", Number.parseInt(\"{{ page }}\") - 1]);
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=AllPosts&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});"))
(text "{% endblock %}")

View file

@ -11,9 +11,10 @@
("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker"))))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=FollowingPosts&page=\", Number.parseInt(\"{{ page }}\") - 1]);
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=FollowingPosts&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});"))
(text "{% endblock %}")

View file

@ -31,9 +31,10 @@
(div ("ui_ident" "io_data_marker")))
(text "{%- endif %}"))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=MyCommunities&page=\", Number.parseInt(\"{{ page }}\") - 1]);
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=MyCommunities&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});"))
(text "{% endblock %}")

View file

@ -11,9 +11,10 @@
("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker"))))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=PopularPosts&page=\", Number.parseInt(\"{{ page }}\") - 1]);
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=PopularPosts&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});"))
(text "{% endblock %}")

View file

@ -30,3 +30,7 @@
(str (text "chats:label.go_back")))
(text "{%- endif %}"))
(text "{%- endif %}")
(text "{% if paginated -%}")
(text "{{ components::pagination(page=page, items=list|length) }}")
(text "{%- endif %}")

View file

@ -1141,7 +1141,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
},
);
self.define("io_data_load", (_, tmpl, page) => {
self.define("io_data_load", (_, tmpl, page, paginated_mode = false) => {
self.IO_DATA_MARKER = document.querySelector(
"[ui_ident=io_data_marker]",
);
@ -1164,7 +1164,16 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
self.IO_DATA_PAGE = page;
self.IO_DATA_SEEN_IDS = [];
self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER);
if (!paginated_mode) {
self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER);
} else {
// immediately load first page
self.IO_DATA_TMPL = self.IO_DATA_TMPL.replace("&page=", "");
self.IO_DATA_TMPL += `&paginated=true&page=`;
self.io_load_data();
}
self.IO_PAGINATED = paginated_mode;
});
self.define("io_load_data", async () => {

View file

@ -213,7 +213,7 @@ pub async fn upload_avatar_request(
if mime == "image/gif" {
// gif image, don't encode
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
return Json(Error::DataTooLong("gif".to_string()).into());
return Json(Error::FileTooLarge.into());
}
std::fs::write(&path, img.0).unwrap();
@ -226,7 +226,7 @@ pub async fn upload_avatar_request(
// check file size
if img.0.len() > MAXIMUM_FILE_SIZE {
return Json(Error::DataTooLong("image".to_string()).into());
return Json(Error::FileTooLarge.into());
}
// upload image
@ -314,7 +314,7 @@ pub async fn upload_banner_request(
if mime == "image/gif" {
// gif image, don't encode
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
return Json(Error::DataTooLong("gif".to_string()).into());
return Json(Error::FileTooLarge.into());
}
std::fs::write(&path, img.0).unwrap();
@ -327,7 +327,7 @@ pub async fn upload_banner_request(
// check file size
if img.0.len() > MAXIMUM_FILE_SIZE {
return Json(Error::DataTooLong("image".to_string()).into());
return Json(Error::FileTooLarge.into());
}
// upload image

View file

@ -136,7 +136,7 @@ pub async fn upload_avatar_request(
// check file size
if img.0.len() > MAXIMUM_FILE_SIZE {
return Json(Error::DataTooLong("image".to_string()).into());
return Json(Error::FileTooLarge.into());
}
// upload image
@ -191,7 +191,7 @@ pub async fn upload_banner_request(
// check file size
if img.0.len() > MAXIMUM_FILE_SIZE {
return Json(Error::DataTooLong("image".to_string()).into());
return Json(Error::FileTooLarge.into());
}
// upload image

View file

@ -133,7 +133,7 @@ pub async fn create_request(
// check sizes
for img in &images {
if img.len() > MAXIMUM_FILE_SIZE {
return Json(Error::DataTooLong("image".to_string()).into());
return Json(Error::FileTooLarge.into());
}
}

View file

@ -9,11 +9,14 @@ use crate::{
routes::api::v1::{UpdateJournalPrivacy, CreateJournal, UpdateJournalTitle},
State,
};
use tetratto_core::model::{
journals::{Journal, JournalPrivacyPermission},
oauth,
permissions::FinePermission,
ApiReturn, Error,
use tetratto_core::{
database::NAME_REGEX,
model::{
journals::{Journal, JournalPrivacyPermission},
oauth,
permissions::FinePermission,
ApiReturn, Error,
},
};
pub async fn get_request(
@ -46,6 +49,20 @@ pub async fn get_request(
})
}
pub async fn get_css_request(
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let note = match data.get_note_by_journal_title(id, "journal.css").await {
Ok(x) => x,
Err(e) => return ([("Content-Type", "text/plain")], format!("/* {e} */")),
};
([("Content-Type", "text/css")], note.content)
}
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
@ -99,7 +116,17 @@ pub async fn update_title_request(
None => return Json(Error::NotAllowed.into()),
};
props.title = props.title.replace(" ", "_");
props.title = props.title.replace(" ", "_").to_lowercase();
// check name
let regex = regex::RegexBuilder::new(NAME_REGEX)
.multi_line(true)
.build()
.unwrap();
if regex.captures(&props.title).is_some() {
return Json(Error::MiscError("This title contains invalid characters".to_string()).into());
}
// make sure this title isn't already in use
if data

View file

@ -551,6 +551,7 @@ pub fn routes() -> Router {
.route("/journals", post(journals::create_request))
.route("/journals/{id}", get(journals::get_request))
.route("/journals/{id}", delete(journals::delete_request))
.route("/journals/{id}/journal.css", get(journals::get_css_request))
.route("/journals/{id}/title", post(journals::update_title_request))
.route(
"/journals/{id}/privacy",

View file

@ -10,12 +10,15 @@ use crate::{
routes::api::v1::{CreateNote, RenderMarkdown, UpdateNoteContent, UpdateNoteTitle},
State,
};
use tetratto_core::model::{
journals::{JournalPrivacyPermission, Note},
oauth,
permissions::FinePermission,
uploads::CustomEmoji,
ApiReturn, Error,
use tetratto_core::{
database::NAME_REGEX,
model::{
journals::{JournalPrivacyPermission, Note},
oauth,
permissions::FinePermission,
uploads::CustomEmoji,
ApiReturn, Error,
},
};
pub async fn get_request(
@ -135,7 +138,17 @@ pub async fn update_title_request(
Err(e) => return Json(e.into()),
};
props.title = props.title.replace(" ", "_");
props.title = props.title.replace(" ", "_").to_lowercase();
// check name
let regex = regex::RegexBuilder::new(NAME_REGEX)
.multi_line(true)
.build()
.unwrap();
if regex.captures(&props.title).is_some() {
return Json(Error::MiscError("This title contains invalid characters".to_string()).into());
}
// make sure this title isn't already in use
if data

View file

@ -116,7 +116,7 @@ pub async fn view_request(
}
// if we don't have a selected journal, we shouldn't be here probably
if selected_journal.is_empty() {
if selected_journal.is_empty() | (selected_note == "journal.css") {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &user).await,
));
@ -207,3 +207,81 @@ pub async fn view_request(
// return
Ok(Html(data.1.render("journals/app.html", &context).unwrap()))
}
/// `/@{owner}/{journal}`
pub async fn index_view_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path((owner, selected_journal)): Path<(String, String)>,
) -> impl IntoResponse {
let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) {
Some(ua) => Some(ua),
None => None,
};
// get owner
let owner = match data.0.get_user_by_username(&owner).await {
Ok(ua) => ua,
Err(e) => {
return Err(Html(render_error(e, &jar, &data, &user).await));
}
};
check_user_blocked_or_private!(user, owner, data, jar);
// get journal and check privacy settings
let journal = match data
.0
.get_journal_by_owner_title(owner.id, &selected_journal)
.await
{
Ok(p) => p,
Err(e) => {
return Err(Html(render_error(e, &jar, &data, &user).await));
}
};
if journal.privacy == JournalPrivacyPermission::Private {
if let Some(ref user) = user {
if user.id != journal.owner {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &Some(user.to_owned())).await,
));
}
} else {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &user).await,
));
}
}
// ...
let notes = match data.0.get_notes_by_journal(journal.id).await {
Ok(p) => Some(p),
Err(e) => {
return Err(Html(render_error(e, &jar, &data, &user).await));
}
};
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
if selected_journal.is_empty() {
context.insert("selected_journal", &0);
} else {
context.insert("selected_journal", &selected_journal);
}
context.insert("selected_note", &0);
context.insert("journal", &journal);
context.insert("owner", &owner);
context.insert("notes", &notes);
context.insert("view_mode", &true);
context.insert("is_editor", &false);
// return
Ok(Html(data.1.render("journals/app.html", &context).unwrap()))
}

View file

@ -576,6 +576,8 @@ pub struct TimelineQuery {
pub user_id: usize,
#[serde(default)]
pub tag: String,
#[serde(default)]
pub paginated: bool,
}
/// `/_swiss_army_timeline`
@ -697,6 +699,7 @@ pub async fn swiss_army_timeline_request(
context.insert("list", &list);
context.insert("page", &req.page);
context.insert("paginated", &req.paginated);
Ok(Html(
data.1
.render("timelines/swiss_army.html", &context)

View file

@ -134,7 +134,7 @@ pub fn routes() -> Router {
// journals
.route("/journals", get(journals::redirect_request))
.route("/journals/{journal}/{note}", get(journals::app_request))
.route("/@{owner}/{journal}", get(journals::view_request))
.route("/@{owner}/{journal}", get(journals::index_view_request))
.route("/@{owner}/{journal}/{note}", get(journals::view_request))
}

View file

@ -1,9 +1,10 @@
use oiseau::{cache::Cache, query_row};
use crate::{
database::common::NAME_REGEX,
model::{
auth::User,
permissions::FinePermission,
journals::{Journal, JournalPrivacyPermission},
permissions::FinePermission,
Error, Result,
},
};
@ -69,7 +70,7 @@ impl DataManager {
Ok(res.unwrap())
}
const MAXIMUM_FREE_JOURNALS: usize = 15;
const MAXIMUM_FREE_JOURNALS: usize = 5;
/// Create a new journal in the database.
///
@ -83,7 +84,19 @@ impl DataManager {
return Err(Error::DataTooLong("title".to_string()));
}
data.title = data.title.replace(" ", "_");
data.title = data.title.replace(" ", "_").to_lowercase();
// check name
let regex = regex::RegexBuilder::new(NAME_REGEX)
.multi_line(true)
.build()
.unwrap();
if regex.captures(&data.title).is_some() {
return Err(Error::MiscError(
"This title contains invalid characters".to_string(),
));
}
// make sure this title isn't already in use
if self

View file

@ -30,3 +30,4 @@ mod userblocks;
mod userfollows;
pub use drivers::DataManager;
pub use common::NAME_REGEX;

View file

@ -1,4 +1,5 @@
use oiseau::cache::Cache;
use crate::database::common::NAME_REGEX;
use crate::model::{auth::User, journals::Note, permissions::FinePermission, Error, Result};
use crate::{auto_method, DataManager};
use oiseau::{execute, get, params, query_row, query_rows, PostgresRow};
@ -64,6 +65,8 @@ impl DataManager {
Ok(res.unwrap())
}
const MAXIMUM_FREE_NOTES_PER_JOURNAL: usize = 10;
/// Create a new note in the database.
///
/// # Arguments
@ -82,7 +85,33 @@ impl DataManager {
return Err(Error::DataTooLong("content".to_string()));
}
data.title = data.title.replace(" ", "_");
data.title = data.title.replace(" ", "_").to_lowercase();
// check number of notes
let owner = self.get_user_by_id(data.owner).await?;
if !owner.permissions.check(FinePermission::SUPPORTER) {
let journals = self.get_notes_by_journal(data.owner).await?;
if journals.len() >= Self::MAXIMUM_FREE_NOTES_PER_JOURNAL {
return Err(Error::MiscError(
"You already have the maximum number of notes you can have in this journal"
.to_string(),
));
}
}
// check name
let regex = regex::RegexBuilder::new(NAME_REGEX)
.multi_line(true)
.build()
.unwrap();
if regex.captures(&data.title).is_some() {
return Err(Error::MiscError(
"This title contains invalid characters".to_string(),
));
}
// make sure this title isn't already in use
if self

View file

@ -231,6 +231,9 @@ pub struct UserSettings {
/// A list of strings the user has muted.
#[serde(default)]
pub muted: Vec<String>,
/// If timelines are paged instead of infinitely scrolled.
#[serde(default)]
pub paged_timelines: bool,
}
fn mime_avif() -> String {
@ -332,7 +335,7 @@ impl User {
// parse
for char in input.chars() {
if (char == '\\') && !escape {
if ((char == '\\') | (char == '/')) && !escape {
escape = true;
continue;
}

View file

@ -41,6 +41,7 @@ pub enum Error {
AlreadyAuthenticated,
DataTooLong(String),
DataTooShort(String),
FileTooLarge,
UsernameInUse,
TitleInUse,
QuestionsDisabled,
@ -62,6 +63,7 @@ impl Display for Error {
Self::AlreadyAuthenticated => "Already authenticated".to_string(),
Self::DataTooLong(name) => format!("Given {name} is too long!"),
Self::DataTooShort(name) => format!("Given {name} is too short!"),
Self::FileTooLarge => "Given file is too large".to_string(),
Self::UsernameInUse => "Username in use".to_string(),
Self::TitleInUse => "Title in use".to_string(),
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),