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)