diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css
index f592c77..0603ee1 100644
--- a/crates/app/src/public/css/style.css
+++ b/crates/app/src/public/css/style.css
@@ -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;
+}
diff --git a/crates/app/src/public/html/journals/app.lisp b/crates/app/src/public/html/journals/app.lisp
index d148a0f..d65163d 100644
--- a/crates/app/src/public/html/journals/app.lisp
+++ b/crates/app/src/public/html/journals/app.lisp
@@ -9,6 +9,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
@@ -73,7 +78,7 @@
(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
@@ -181,10 +186,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
@@ -199,6 +230,7 @@
("data-tab-button" "preview")
("data-turbo" "false")
(str (text "journals:label.preview_pane"))))
+ (text "{%- endif %}")
; tabs
(div
@@ -222,10 +254,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,
@@ -233,7 +270,8 @@
highlightFormatting: false,
fencedCodeBlockHighlighting: false,
xml: false,
- smartIndent: false,
+ smartIndent: true,
+ indentUnit: 4,
placeholder: `# {{ note.title }}`,
extraKeys: {
Home: \"goLineLeft\",
@@ -244,6 +282,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\"]);
@@ -263,7 +310,10 @@
})
).text();
- document.getElementById(\"preview_tab\").innerHTML = res;
+ const preview_token = window.crypto.randomUUID();
+ document.getElementById(\"preview_tab\").shadowRoot.innerHTML = `${res}`;
trigger(\"atto::hooks::tabs:switch\", [\"preview\"]);
});
}, 150);"))
@@ -360,7 +410,7 @@
},
body: JSON.stringify({
title,
- content: `# ${title}`,
+ content: title === \"journal.css\" ? `/* ${title} */\\n` : `# ${title}`,
journal: \"{{ selected_journal }}\",
}),
})
diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp
index 8be4836..88c6d59 100644
--- a/crates/app/src/public/html/profile/settings.lisp
+++ b/crates/app/src/public/html/profile/settings.lisp
@@ -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")
diff --git a/crates/app/src/routes/api/v1/journals.rs b/crates/app/src/routes/api/v1/journals.rs
index d501f08..de6d501 100644
--- a/crates/app/src/routes/api/v1/journals.rs
+++ b/crates/app/src/routes/api/v1/journals.rs
@@ -49,6 +49,20 @@ pub async fn get_request(
})
}
+pub async fn get_css_request(
+ Path(id): Path,
+ Extension(data): Extension,
+) -> 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) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index f16b1ed..9217437 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -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",
diff --git a/crates/app/src/routes/pages/journals.rs b/crates/app/src/routes/pages/journals.rs
index 397b4cd..1f03dd7 100644
--- a/crates/app/src/routes/pages/journals.rs
+++ b/crates/app/src/routes/pages/journals.rs
@@ -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,
));
diff --git a/crates/core/src/database/journals.rs b/crates/core/src/database/journals.rs
index 8c2a637..a4a0d00 100644
--- a/crates/core/src/database/journals.rs
+++ b/crates/core/src/database/journals.rs
@@ -70,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.
///
diff --git a/crates/core/src/database/notes.rs b/crates/core/src/database/notes.rs
index e3fcdab..ea7da45 100644
--- a/crates/core/src/database/notes.rs
+++ b/crates/core/src/database/notes.rs
@@ -65,6 +65,8 @@ impl DataManager {
Ok(res.unwrap())
}
+ const MAXIMUM_FREE_NOTES_PER_JOURNAL: usize = 10;
+
/// Create a new note in the database.
///
/// # Arguments
@@ -85,6 +87,20 @@ impl DataManager {
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)