Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
6be729de50 | |||
ffdf320c14 | |||
fa72d6a59d | |||
dc50f3a8af | |||
f0d1a1e8e4 | |||
eb5a0d146f | |||
1b1c1c0bea | |||
97b7e873ed |
26 changed files with 379 additions and 53 deletions
|
@ -1250,3 +1250,32 @@ details.accordion .inner {
|
||||||
.CodeMirror-focused .CodeMirror-placeholder {
|
.CodeMirror-focused .CodeMirror-placeholder {
|
||||||
opacity: 50%;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1928,7 +1928,7 @@
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
|
||||||
; note listings
|
; note listings
|
||||||
(text "{% for note in notes %}")
|
(text "{% for note in notes %} {% if not view_mode or note.title != \"journal.css\" -%}")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex-row gap-1")
|
("class" "flex flex-row gap-1")
|
||||||
(a
|
(a
|
||||||
|
@ -1958,6 +1958,6 @@
|
||||||
(icon (text "trash"))
|
(icon (text "trash"))
|
||||||
(str (text "general:action.delete")))))
|
(str (text "general:action.delete")))))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
(text "{% endfor %}"))
|
(text "{%- endif %} {% endfor %}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
|
@ -2,6 +2,27 @@
|
||||||
(text "{% if journal -%}") (title (text "{{ journal.title }}")) (text "{% else %}") (title (text "Journals - {{ config.name }}")) (text "{%- endif %}")
|
(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 }}"))
|
(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 -%}")
|
(text "{% if view_mode and journal and is_editor -%} {% if note -%}")
|
||||||
; redirect to note
|
; redirect to note
|
||||||
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}/{{ note.title }}"))
|
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}/{{ note.title }}"))
|
||||||
|
@ -9,6 +30,11 @@
|
||||||
; redirect to journal homepage
|
; redirect to journal homepage
|
||||||
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}"))
|
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}"))
|
||||||
(text "{%- endif %} {%- endif %}")
|
(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 "{% endblock %} {% block body %} {{ macros::nav(selected=\"journals\") }}")
|
||||||
(text "{% if not view_mode -%}")
|
(text "{% if not view_mode -%}")
|
||||||
(nav
|
(nav
|
||||||
|
@ -63,17 +89,19 @@
|
||||||
("href" "/api/v1/auth/user/find/{{ journal.owner }}")
|
("href" "/api/v1/auth/user/find/{{ journal.owner }}")
|
||||||
(text "{{ components::avatar(username=journal.owner, selector_type=\"id\", size=\"18px\") }}"))
|
(text "{{ components::avatar(username=journal.owner, selector_type=\"id\", size=\"18px\") }}"))
|
||||||
|
|
||||||
|
(text "{% if (view_mode and owner) or not view_mode -%}")
|
||||||
(a
|
(a
|
||||||
("class" "flush")
|
("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 }}")))
|
(b (text "{{ journal.title }}")))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
(text "{% if note -%}")
|
(text "{% if note -%}")
|
||||||
(span (text "/"))
|
(span (text "/"))
|
||||||
(b (text "{{ note.title }}"))
|
(b (text "{{ note.title }}"))
|
||||||
(text "{%- endif %}"))
|
(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
|
(div
|
||||||
("class" "pillmenu")
|
("class" "pillmenu")
|
||||||
(a
|
(a
|
||||||
|
@ -83,7 +111,7 @@
|
||||||
(icon (text "pencil")))
|
(icon (text "pencil")))
|
||||||
(a
|
(a
|
||||||
("class" "{% if view_mode -%}active{%- endif %}")
|
("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"))))
|
(icon (text "eye"))))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
@ -96,6 +124,7 @@
|
||||||
("class" "card w-full flex flex-col gap-2")
|
("class" "card w-full flex flex-col gap-2")
|
||||||
(h3 (str (text "journals:label.welcome")))
|
(h3 (str (text "journals:label.welcome")))
|
||||||
(span (str (text "journals:label.select_a_journal")))
|
(span (str (text "journals:label.select_a_journal")))
|
||||||
|
(span ("class" "mobile") (str (text "journals:label.mobile_click_my_journals")))
|
||||||
(button
|
(button
|
||||||
("onclick" "create_journal()")
|
("onclick" "create_journal()")
|
||||||
(icon (text "plus"))
|
(icon (text "plus"))
|
||||||
|
@ -180,10 +209,36 @@
|
||||||
; import codemirror
|
; 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/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/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"))
|
(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
|
; tab bar
|
||||||
|
(text "{% if note.title != \"journal.css\" -%}")
|
||||||
(div
|
(div
|
||||||
("class" "pillmenu")
|
("class" "pillmenu")
|
||||||
(a
|
(a
|
||||||
|
@ -198,6 +253,7 @@
|
||||||
("data-tab-button" "preview")
|
("data-tab-button" "preview")
|
||||||
("data-turbo" "false")
|
("data-turbo" "false")
|
||||||
(str (text "journals:label.preview_pane"))))
|
(str (text "journals:label.preview_pane"))))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
; tabs
|
; tabs
|
||||||
(div
|
(div
|
||||||
|
@ -221,10 +277,15 @@
|
||||||
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}"))
|
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}"))
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(text "setTimeout(() => {
|
||||||
|
if (!document.getElementById(\"preview_tab\").shadowRoot) {
|
||||||
|
document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" });
|
||||||
|
}
|
||||||
|
|
||||||
globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), {
|
globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), {
|
||||||
value: document.getElementById(\"editor_content\").innerHTML,
|
value: document.getElementById(\"editor_content\").innerHTML,
|
||||||
mode: \"markdown\",
|
mode: \"{% if note.title == 'journal.css' -%} css {%- else -%} markdown {%- endif %}\",
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
|
lineNumbers: \"{{ note.title }}\" === \"journal.css\",
|
||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
viewportMargin: Number.POSITIVE_INFINITY,
|
viewportMargin: Number.POSITIVE_INFINITY,
|
||||||
|
@ -232,7 +293,8 @@
|
||||||
highlightFormatting: false,
|
highlightFormatting: false,
|
||||||
fencedCodeBlockHighlighting: false,
|
fencedCodeBlockHighlighting: false,
|
||||||
xml: false,
|
xml: false,
|
||||||
smartIndent: false,
|
smartIndent: true,
|
||||||
|
indentUnit: 4,
|
||||||
placeholder: `# {{ note.title }}`,
|
placeholder: `# {{ note.title }}`,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
Home: \"goLineLeft\",
|
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) => {
|
document.querySelector(\"[data-tab-button=editor]\").addEventListener(\"click\", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
trigger(\"atto::hooks::tabs:switch\", [\"editor\"]);
|
trigger(\"atto::hooks::tabs:switch\", [\"editor\"]);
|
||||||
|
@ -262,7 +333,10 @@
|
||||||
})
|
})
|
||||||
).text();
|
).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\"]);
|
trigger(\"atto::hooks::tabs:switch\", [\"preview\"]);
|
||||||
});
|
});
|
||||||
}, 150);"))
|
}, 150);"))
|
||||||
|
@ -272,7 +346,34 @@
|
||||||
("class" "flex flex-col gap-2 card")
|
("class" "flex flex-col gap-2 card")
|
||||||
(text "{{ note.content|markdown|safe }}"))
|
(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 %}")
|
||||||
(text "{%- endif %}")))
|
(text "{%- endif %}")))
|
||||||
(style
|
(style
|
||||||
|
@ -332,7 +433,7 @@
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title,
|
title,
|
||||||
content: `# ${title}`,
|
content: title === \"journal.css\" ? `/* ${title} */\\n` : `# ${title}`,
|
||||||
journal: \"{{ selected_journal }}\",
|
journal: \"{{ selected_journal }}\",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -431,8 +532,8 @@
|
||||||
|
|
||||||
globalThis.change_journal_privacy = async (e) => {
|
globalThis.change_journal_privacy = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const selected = event.target.selectedOptions[0];
|
const selected = e.target.selectedOptions[0];
|
||||||
fetch(\"/api/v1/journals/{{ selected_journal }}/privacy\", {
|
fetch(\"/api/v1/journals/{% if journal -%} {{ journal.id }} {%- else -%} {{ selected_journal }} {%- endif %}/privacy\", {
|
||||||
method: \"POST\",
|
method: \"POST\",
|
||||||
headers: {
|
headers: {
|
||||||
\"Content-Type\": \"application/json\",
|
\"Content-Type\": \"application/json\",
|
||||||
|
|
|
@ -44,9 +44,10 @@
|
||||||
("ui_ident" "io_data_load")
|
("ui_ident" "io_data_load")
|
||||||
(div ("ui_ident" "io_data_marker"))))
|
(div ("ui_ident" "io_data_marker"))))
|
||||||
|
|
||||||
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(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 %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -564,14 +564,14 @@
|
||||||
(li
|
(li
|
||||||
(text "Use custom CSS on your profile"))
|
(text "Use custom CSS on your profile"))
|
||||||
(li
|
(li
|
||||||
(text "Ability to use community emojis outside of
|
(text "Use community emojis outside of
|
||||||
their community"))
|
their community"))
|
||||||
(li
|
(li
|
||||||
(text "Ability to upload and use gif emojis"))
|
(text "Upload and use gif emojis"))
|
||||||
(li
|
(li
|
||||||
(text "Create infinite stack timelines"))
|
(text "Create infinite stack timelines"))
|
||||||
(li
|
(li
|
||||||
(text "Ability to upload images to posts"))
|
(text "Upload images to posts"))
|
||||||
(li
|
(li
|
||||||
(text "Save infinite post drafts"))
|
(text "Save infinite post drafts"))
|
||||||
(li
|
(li
|
||||||
|
@ -579,7 +579,7 @@
|
||||||
(li
|
(li
|
||||||
(text "Ability to create forges"))
|
(text "Ability to create forges"))
|
||||||
(li
|
(li
|
||||||
(text "Ability to create more than 1 app"))
|
(text "Create more than 1 app"))
|
||||||
(li
|
(li
|
||||||
(text "Create up to 10 stack blocks"))
|
(text "Create up to 10 stack blocks"))
|
||||||
(li
|
(li
|
||||||
|
@ -587,7 +587,9 @@
|
||||||
(li
|
(li
|
||||||
(text "Increased proxied image size"))
|
(text "Increased proxied image size"))
|
||||||
(li
|
(li
|
||||||
(text "Create infinite journals")))
|
(text "Create infinite journals"))
|
||||||
|
(li
|
||||||
|
(text "Create infinite notes in each journal")))
|
||||||
(a
|
(a
|
||||||
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
||||||
("class" "button")
|
("class" "button")
|
||||||
|
@ -1401,6 +1403,11 @@
|
||||||
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
|
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
|
||||||
\"text\",
|
\"text\",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
|
||||||
|
\"{{ profile.settings.paged_timelines }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
[[], \"Fun\", \"title\"],
|
[[], \"Fun\", \"title\"],
|
||||||
[
|
[
|
||||||
[\"disable_gpa_fun\", \"Disable GPA\"],
|
[\"disable_gpa_fun\", \"Disable GPA\"],
|
||||||
|
|
|
@ -83,9 +83,10 @@
|
||||||
("ui_ident" "io_data_load")
|
("ui_ident" "io_data_load")
|
||||||
(div ("ui_ident" "io_data_marker")))
|
(div ("ui_ident" "io_data_marker")))
|
||||||
|
|
||||||
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(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 %}"))))
|
(text "{%- endif %}"))))
|
||||||
|
|
||||||
|
|
|
@ -33,9 +33,10 @@
|
||||||
("ui_ident" "io_data_load")
|
("ui_ident" "io_data_load")
|
||||||
(div ("ui_ident" "io_data_marker"))))
|
(div ("ui_ident" "io_data_marker"))))
|
||||||
|
|
||||||
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(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 %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
("ui_ident" "io_data_load")
|
("ui_ident" "io_data_load")
|
||||||
(div ("ui_ident" "io_data_marker"))))
|
(div ("ui_ident" "io_data_marker"))))
|
||||||
|
|
||||||
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(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 %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -31,9 +31,10 @@
|
||||||
(div ("ui_ident" "io_data_marker")))
|
(div ("ui_ident" "io_data_marker")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
|
|
||||||
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(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 %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
("ui_ident" "io_data_load")
|
("ui_ident" "io_data_load")
|
||||||
(div ("ui_ident" "io_data_marker"))))
|
(div ("ui_ident" "io_data_marker"))))
|
||||||
|
|
||||||
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(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 %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -30,3 +30,7 @@
|
||||||
(str (text "chats:label.go_back")))
|
(str (text "chats:label.go_back")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(text "{% if paginated -%}")
|
||||||
|
(text "{{ components::pagination(page=page, items=list|length) }}")
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
|
@ -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(
|
self.IO_DATA_MARKER = document.querySelector(
|
||||||
"[ui_ident=io_data_marker]",
|
"[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_PAGE = page;
|
||||||
self.IO_DATA_SEEN_IDS = [];
|
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 () => {
|
self.define("io_load_data", async () => {
|
||||||
|
|
|
@ -213,7 +213,7 @@ pub async fn upload_avatar_request(
|
||||||
if mime == "image/gif" {
|
if mime == "image/gif" {
|
||||||
// gif image, don't encode
|
// gif image, don't encode
|
||||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
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();
|
std::fs::write(&path, img.0).unwrap();
|
||||||
|
@ -226,7 +226,7 @@ pub async fn upload_avatar_request(
|
||||||
|
|
||||||
// check file size
|
// check file size
|
||||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||||
return Json(Error::DataTooLong("image".to_string()).into());
|
return Json(Error::FileTooLarge.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
|
@ -314,7 +314,7 @@ pub async fn upload_banner_request(
|
||||||
if mime == "image/gif" {
|
if mime == "image/gif" {
|
||||||
// gif image, don't encode
|
// gif image, don't encode
|
||||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
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();
|
std::fs::write(&path, img.0).unwrap();
|
||||||
|
@ -327,7 +327,7 @@ pub async fn upload_banner_request(
|
||||||
|
|
||||||
// check file size
|
// check file size
|
||||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||||
return Json(Error::DataTooLong("image".to_string()).into());
|
return Json(Error::FileTooLarge.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
|
|
|
@ -136,7 +136,7 @@ pub async fn upload_avatar_request(
|
||||||
|
|
||||||
// check file size
|
// check file size
|
||||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||||
return Json(Error::DataTooLong("image".to_string()).into());
|
return Json(Error::FileTooLarge.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
|
@ -191,7 +191,7 @@ pub async fn upload_banner_request(
|
||||||
|
|
||||||
// check file size
|
// check file size
|
||||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||||
return Json(Error::DataTooLong("image".to_string()).into());
|
return Json(Error::FileTooLarge.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
|
|
|
@ -133,7 +133,7 @@ pub async fn create_request(
|
||||||
// check sizes
|
// check sizes
|
||||||
for img in &images {
|
for img in &images {
|
||||||
if img.len() > MAXIMUM_FILE_SIZE {
|
if img.len() > MAXIMUM_FILE_SIZE {
|
||||||
return Json(Error::DataTooLong("image".to_string()).into());
|
return Json(Error::FileTooLarge.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,14 @@ use crate::{
|
||||||
routes::api::v1::{UpdateJournalPrivacy, CreateJournal, UpdateJournalTitle},
|
routes::api::v1::{UpdateJournalPrivacy, CreateJournal, UpdateJournalTitle},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use tetratto_core::model::{
|
use tetratto_core::{
|
||||||
journals::{Journal, JournalPrivacyPermission},
|
database::NAME_REGEX,
|
||||||
oauth,
|
model::{
|
||||||
permissions::FinePermission,
|
journals::{Journal, JournalPrivacyPermission},
|
||||||
ApiReturn, Error,
|
oauth,
|
||||||
|
permissions::FinePermission,
|
||||||
|
ApiReturn, Error,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_request(
|
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 {
|
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
|
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()),
|
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
|
// make sure this title isn't already in use
|
||||||
if data
|
if data
|
||||||
|
|
|
@ -551,6 +551,7 @@ pub fn routes() -> Router {
|
||||||
.route("/journals", post(journals::create_request))
|
.route("/journals", post(journals::create_request))
|
||||||
.route("/journals/{id}", get(journals::get_request))
|
.route("/journals/{id}", get(journals::get_request))
|
||||||
.route("/journals/{id}", delete(journals::delete_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}/title", post(journals::update_title_request))
|
||||||
.route(
|
.route(
|
||||||
"/journals/{id}/privacy",
|
"/journals/{id}/privacy",
|
||||||
|
|
|
@ -10,12 +10,15 @@ use crate::{
|
||||||
routes::api::v1::{CreateNote, RenderMarkdown, UpdateNoteContent, UpdateNoteTitle},
|
routes::api::v1::{CreateNote, RenderMarkdown, UpdateNoteContent, UpdateNoteTitle},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use tetratto_core::model::{
|
use tetratto_core::{
|
||||||
journals::{JournalPrivacyPermission, Note},
|
database::NAME_REGEX,
|
||||||
oauth,
|
model::{
|
||||||
permissions::FinePermission,
|
journals::{JournalPrivacyPermission, Note},
|
||||||
uploads::CustomEmoji,
|
oauth,
|
||||||
ApiReturn, Error,
|
permissions::FinePermission,
|
||||||
|
uploads::CustomEmoji,
|
||||||
|
ApiReturn, Error,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_request(
|
pub async fn get_request(
|
||||||
|
@ -135,7 +138,17 @@ pub async fn update_title_request(
|
||||||
Err(e) => return Json(e.into()),
|
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
|
// make sure this title isn't already in use
|
||||||
if data
|
if data
|
||||||
|
|
|
@ -116,7 +116,7 @@ pub async fn view_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we don't have a selected journal, we shouldn't be here probably
|
// 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(
|
return Err(Html(
|
||||||
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
));
|
));
|
||||||
|
@ -207,3 +207,81 @@ pub async fn view_request(
|
||||||
// return
|
// return
|
||||||
Ok(Html(data.1.render("journals/app.html", &context).unwrap()))
|
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", ¬es);
|
||||||
|
|
||||||
|
context.insert("view_mode", &true);
|
||||||
|
context.insert("is_editor", &false);
|
||||||
|
|
||||||
|
// return
|
||||||
|
Ok(Html(data.1.render("journals/app.html", &context).unwrap()))
|
||||||
|
}
|
||||||
|
|
|
@ -576,6 +576,8 @@ pub struct TimelineQuery {
|
||||||
pub user_id: usize,
|
pub user_id: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tag: String,
|
pub tag: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub paginated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/_swiss_army_timeline`
|
/// `/_swiss_army_timeline`
|
||||||
|
@ -697,6 +699,7 @@ pub async fn swiss_army_timeline_request(
|
||||||
|
|
||||||
context.insert("list", &list);
|
context.insert("list", &list);
|
||||||
context.insert("page", &req.page);
|
context.insert("page", &req.page);
|
||||||
|
context.insert("paginated", &req.paginated);
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1
|
||||||
.render("timelines/swiss_army.html", &context)
|
.render("timelines/swiss_army.html", &context)
|
||||||
|
|
|
@ -134,7 +134,7 @@ pub fn routes() -> Router {
|
||||||
// journals
|
// journals
|
||||||
.route("/journals", get(journals::redirect_request))
|
.route("/journals", get(journals::redirect_request))
|
||||||
.route("/journals/{journal}/{note}", get(journals::app_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))
|
.route("/@{owner}/{journal}/{note}", get(journals::view_request))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use oiseau::{cache::Cache, query_row};
|
use oiseau::{cache::Cache, query_row};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
database::common::NAME_REGEX,
|
||||||
model::{
|
model::{
|
||||||
auth::User,
|
auth::User,
|
||||||
permissions::FinePermission,
|
|
||||||
journals::{Journal, JournalPrivacyPermission},
|
journals::{Journal, JournalPrivacyPermission},
|
||||||
|
permissions::FinePermission,
|
||||||
Error, Result,
|
Error, Result,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -69,7 +70,7 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAXIMUM_FREE_JOURNALS: usize = 15;
|
const MAXIMUM_FREE_JOURNALS: usize = 5;
|
||||||
|
|
||||||
/// Create a new journal in the database.
|
/// Create a new journal in the database.
|
||||||
///
|
///
|
||||||
|
@ -83,7 +84,19 @@ impl DataManager {
|
||||||
return Err(Error::DataTooLong("title".to_string()));
|
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
|
// make sure this title isn't already in use
|
||||||
if self
|
if self
|
||||||
|
|
|
@ -30,3 +30,4 @@ mod userblocks;
|
||||||
mod userfollows;
|
mod userfollows;
|
||||||
|
|
||||||
pub use drivers::DataManager;
|
pub use drivers::DataManager;
|
||||||
|
pub use common::NAME_REGEX;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use oiseau::cache::Cache;
|
use oiseau::cache::Cache;
|
||||||
|
use crate::database::common::NAME_REGEX;
|
||||||
use crate::model::{auth::User, journals::Note, permissions::FinePermission, Error, Result};
|
use crate::model::{auth::User, journals::Note, permissions::FinePermission, Error, Result};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
use oiseau::{execute, get, params, query_row, query_rows, PostgresRow};
|
use oiseau::{execute, get, params, query_row, query_rows, PostgresRow};
|
||||||
|
@ -64,6 +65,8 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAXIMUM_FREE_NOTES_PER_JOURNAL: usize = 10;
|
||||||
|
|
||||||
/// Create a new note in the database.
|
/// Create a new note in the database.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -82,7 +85,33 @@ impl DataManager {
|
||||||
return Err(Error::DataTooLong("content".to_string()));
|
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
|
// make sure this title isn't already in use
|
||||||
if self
|
if self
|
||||||
|
|
|
@ -231,6 +231,9 @@ pub struct UserSettings {
|
||||||
/// A list of strings the user has muted.
|
/// A list of strings the user has muted.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub muted: Vec<String>,
|
pub muted: Vec<String>,
|
||||||
|
/// If timelines are paged instead of infinitely scrolled.
|
||||||
|
#[serde(default)]
|
||||||
|
pub paged_timelines: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mime_avif() -> String {
|
fn mime_avif() -> String {
|
||||||
|
@ -332,7 +335,7 @@ impl User {
|
||||||
|
|
||||||
// parse
|
// parse
|
||||||
for char in input.chars() {
|
for char in input.chars() {
|
||||||
if (char == '\\') && !escape {
|
if ((char == '\\') | (char == '/')) && !escape {
|
||||||
escape = true;
|
escape = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub enum Error {
|
||||||
AlreadyAuthenticated,
|
AlreadyAuthenticated,
|
||||||
DataTooLong(String),
|
DataTooLong(String),
|
||||||
DataTooShort(String),
|
DataTooShort(String),
|
||||||
|
FileTooLarge,
|
||||||
UsernameInUse,
|
UsernameInUse,
|
||||||
TitleInUse,
|
TitleInUse,
|
||||||
QuestionsDisabled,
|
QuestionsDisabled,
|
||||||
|
@ -62,6 +63,7 @@ impl Display for Error {
|
||||||
Self::AlreadyAuthenticated => "Already authenticated".to_string(),
|
Self::AlreadyAuthenticated => "Already authenticated".to_string(),
|
||||||
Self::DataTooLong(name) => format!("Given {name} is too long!"),
|
Self::DataTooLong(name) => format!("Given {name} is too long!"),
|
||||||
Self::DataTooShort(name) => format!("Given {name} is too short!"),
|
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::UsernameInUse => "Username in use".to_string(),
|
||||||
Self::TitleInUse => "Title in use".to_string(),
|
Self::TitleInUse => "Title in use".to_string(),
|
||||||
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue