Compare commits

..

7 Commits

17 changed files with 525 additions and 139 deletions

View File

@@ -162,6 +162,13 @@ def clear_api_limits():
for l in limits:
l.delete()
def ensure_default_collection():
from .db import db
from .models import BookmarkCollections
with db.transaction():
for missing_user in BookmarkCollections.get_users_without_default():
BookmarkCollections.create_default(missing_user)
cache = Cache()
def create_app():
@@ -243,6 +250,8 @@ def create_app():
reparse_babycode()
ensure_default_collection()
bind_default_badges(app.config['BADGES_PATH'])
app.config['SESSION_COOKIE_SECURE'] = True

View File

@@ -6,7 +6,7 @@ from pygments.lexers import get_lexer_by_name
from pygments.util import ClassNotFound as PygmentsClassNotFound
import re
BABYCODE_VERSION = 10
BABYCODE_VERSION = 11
class BabycodeError(Exception):
@@ -183,7 +183,7 @@ class HTMLRenderer(BabycodeRenderer):
if mention_data not in self.mentions:
self.mentions.append(mention_data)
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.user_page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.user_page', username=target_user.username)}' title='@{target_user.username}' data-r='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
def render(self, ast):
out = super().render(ast)

View File

@@ -506,10 +506,24 @@ class BookmarkCollections(Model):
@classmethod
def create_default(cls, user_id):
q = """INSERT INTO bookmark_collections (user_id, name, is_default, sort_order)
VALUES (?, "Bookmarks", 1, 0) RETURNING id
"""
res = db.fetch_one(q, user_id)
return cls.create({
'user_id': user_id,
'name': 'Bookmarks',
'is_default': True,
'sort_order': 0,
})
@staticmethod
def get_users_without_default():
q = """
SELECT users.id FROM users
WHERE NOT EXISTS (
SELECT 1 FROM bookmark_collections bc
WHERE bc.user_id = users.id
AND bc.is_default = 1
)
"""
return [row['id'] for row in db.query(q)]
@classmethod
def get_for_user(cls, user_id):

View File

@@ -1,10 +1,21 @@
from flask import Blueprint, request
from ..auth import is_logged_in, hard_login_required, get_active_user
from ..lib.babycode import babycode_to_html
from ..models import APIRateLimits
from ..models import APIRateLimits, Posts, Threads, Reactions
from ..constants import REACTION_EMOJI
bp = Blueprint('api', __name__, url_prefix='/api/')
@bp.before_request
def ensure_json():
if request.method == 'POST':
if not request.is_json:
return {'error': 'unsupported media type'}, 415
elif not request.content_length:
return {'error': 'body expected'}, 400
elif not isinstance(request.json, dict):
return {'error': 'body must be an object'}, 400
@bp.post('/babycode-preview/')
@hard_login_required
def babycode_preview():
@@ -19,3 +30,60 @@ def babycode_preview():
return {'error': 'banned_tags field is invalid type'}, 400
rendered = babycode_to_html(markup, banned_tags).result
return {'html': rendered}
@bp.get('/whoami/')
def whoami():
user = get_active_user()
if not user:
return {}
return {
'id': user.id,
'username': user.username,
'display_name': user.display_name,
}
@bp.post('/toggle-reaction/')
@hard_login_required
def toggle_reaction():
user = get_active_user()
emoji = request.json.get('reaction')
if emoji not in REACTION_EMOJI:
return {'error': f'invalid reaction string, given: {emoji}'}, 400
post_id = request.json.get('post', -1)
post = Posts.find({'id': post_id})
if not post:
return {'error': 'post not found'}, 404
thread = Threads.find({'id': post.thread_id})
if not user.can_post_to_thread_or_topic(thread):
return {'error': 'thread is locked'}, 403
reaction_obj = {
'user_id': int(user.id),
'post_id': int(post_id),
'reaction_text': emoji,
}
r = Reactions.find(reaction_obj)
if r:
# remove
r.delete()
return {'status': 'ok', 'added': False}
else:
# add
r = Reactions.create(reaction_obj)
return {'status': 'ok', 'added': True}
@bp.get('/thread-permission/<int:thread_id>')
def thread_permission(thread_id):
user = get_active_user()
if not user:
return {'can_post': False}
thread = Threads.find({'id': thread_id})
if not thread:
return {'can_post': False}
return {'can_post': user.can_post_to_thread_or_topic(thread)}

View File

@@ -1,6 +1,6 @@
from flask import Blueprint, render_template, request, url_for
from ..auth import get_active_user, is_logged_in, hard_login_required
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts, Badges, BadgeUploads
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts, Badges, BadgeUploads, Reactions
from functools import wraps
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
@@ -133,3 +133,7 @@ def badge_editor():
badges = Badges.get_for_user(user.id)
badge_uploads = BadgeUploads.get_for_user(user.id)
return render_template('hyper/badge_editor.html', badges=badges, badge_uploads=badge_uploads)
@bp.get('/reactions/<int:post_id>')
def get_reaction_buttons(post_id):
return render_template('hyper/reaction_buttons.html', Reactions=Reactions, post_id=post_id)

View File

@@ -35,6 +35,13 @@ def ownership_or_mod_required(view_func):
return view_func(*args, **kwargs)
return wrapper
@bp.get('/<int:post_id>/')
def post_by_id(post_id):
post = get_post_url(post_id, _anchor=True)
if not post:
abort(404)
return redirect(post)
@bp.get('/<int:post_id>/edit/')
@login_required
@ownership_required

View File

@@ -658,7 +658,6 @@ def save_badges(username):
('id', 'NOT IN', ids),
('user_id', '=', user.id),
])
print(list(map(lambda x: x.id, deleted_badges)))
with db.transaction():
for b in deleted_badges:

View File

@@ -140,6 +140,19 @@
<button autocomplete='off' data-r="enhance" data-s="showBookmarkMenu" disabled title="This feature requires JavaScript to be enabled." data-concept-kind="{{kind}}" data-concept-id="{{id}}">{{icn_bookmark(24)}}{{text}}&hellip;</button>
{%- endmacro %}
{% macro reaction_buttons(post_id) -%}
{%- for reaction in Reactions.for_post(post_id) -%}
{% set reactors = Reactions.get_users(post_id, reaction.reaction_text) | map(attribute='username') | list %}
{% set reactors_trimmed = reactors[:10] %}
{% set reactors_str = reactors_trimmed | join (',\n') %}
{% if reactors | count > 10 %}
{% set reactors_str = reactors_str + '\n...and many others' %}
{% endif %}
{% set has_reacted = get_active_user() is not none and get_active_user().username in reactors %}
<button autocomplete="off" type="button" title="{{reactors_str}}" class="minimal {{'alt' if has_reacted else ''}}" data-emoji="{{reaction.reaction_text}}" data-s="toggleReaction" data-r="enableReactionButtons" disabled><img src="/static/emoji/{{reaction.reaction_text}}.png">{{reaction.c}}</button>
{%- endfor -%}
{%- endmacro %}
{% macro full_post(
post, render_sig=true, is_latest=false,
show_toolbar=true, is_editing=false, thread=none,
@@ -172,7 +185,7 @@
</div>
</div>
</div>
<div class="post-content">
<div class="post-content" data-r="collectImages">
<div class="plank even minimal secondary-bg no-shadow post-info">
<span>
{%- if tb_pretext -%}
@@ -218,19 +231,10 @@
</div>
<div class="plank even secondary-bg minimal no-shadow">
{%- if show_reactions -%}
<span class="button-row">
{%- for reaction in Reactions.for_post(post.id) -%}
{% set reactors = Reactions.get_users(post.id, reaction.reaction_text) | map(attribute='username') | list %}
{% set reactors_trimmed = reactors[:10] %}
{% set reactors_str = reactors_trimmed | join (',\n') %}
{% if reactors | count > 10 %}
{% set reactors_str = reactors_str + '\n...and many others' %}
{% endif %}
{% set has_reacted = get_active_user() is not none and get_active_user().username in reactors %}
<button data-r="enhance" type="button" disabled title="{{reactors_str}}" class="minimal {{'alt' if has_reacted else ''}}"><img src="/static/emoji/{{reaction.reaction_text}}.png">{{reaction.c}}</button>
{%- endfor -%}
<span class="button-row" data-r="replaceReactionButtons">
{{- reaction_buttons(post.id) -}}
</span>
{%- if is_logged_in() and allow_reacting -%}<button autocomplete='off' data-r="enhance" disabled title="This feature requires JavaScript to be enabled.">Add reaction</button>{%- endif -%}
{%- if is_logged_in() and allow_reacting -%}<button autocomplete='off' data-r="disableReactionMenuButton enableReactionMenuButton" disabled title="This feature requires JavaScript to be enabled." data-s="openReactionMenu">Add reaction</button>{%- endif -%}
{%- elif is_editing -%}
<input type="submit" value="Save">
<a href="{{get_post_url(post.id, _anchor=true)}}" class="linkbutton warn">Cancel</a>

View File

@@ -0,0 +1,2 @@
{%- from 'common/macros.html' import reaction_buttons with context -%}
{{- reaction_buttons(post_id) -}}

View File

@@ -5,6 +5,7 @@
{%- block title -%}{{thread.title}}{%- endblock -%}
{%- block content -%}
<bitty-8 data-connect="/static/js/bits/bookmark-menu.js"></bitty-8>
<bitty-8 data-connect="/static/js/bits/thread.js"></bitty-8>
{%- set td -%}
<ul class="horizontal">
<li>Started by <a href="{{url_for('users.user_page', username=started_by.username)}}">{{started_by.get_readable_name()}}</a> in topic <a href="{{url_for('topics.topic_by_id', topic_id=topic.id)}}">{{topic.name}}</a></li>
@@ -64,7 +65,7 @@
{%- endcall -%}
<main>
{%- for post in posts -%}
<article id="post-{{post.id}}" class="post plank">
<article id="post-{{post.id}}" class="post plank" data-postid="{{post.id}}">
{{full_post(post)}}
</article>
{%- endfor -%}
@@ -84,6 +85,25 @@
</span>
</div>
{{ bookmark_menu() }}
<dialog closedby="any" class="plank thread-lighbox" data-r="showLightbox closeLightbox">
<div class="menu">
<button data-s="closeLightbox">Close</button>
<a href="" target="_blank" rel="noreferrer noopener" class="linkbutton alt">Open original</a>
</div>
<img class="lightbox-image" src="https://placehold.co/900x710">
<div class="menu">
<button data-s="lightboxPrevious">Previous</button>
<span data-r="lightboxSetCounter">0/0</span>
<button data-s="lightboxNext">Next</button>
</div>
</dialog>
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(thread) -%}
<div class="plank even" id="reaction-popover" popover data-r="openReactionMenu closeReactionMenu">
{%- for emoji in REACTION_EMOJI -%}
<button class="minimal emoji-button" title=":{{emoji}}:" data-emoji="{{emoji}}" data-s="toggleReaction"><img src="/static/emoji/{{emoji}}.png"></button>
{%- endfor -%}
</div>
{%- endif -%}
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(thread) -%}
<form action="{{url_for('threads.reply', thread_id=thread.id)}}" method="POST" class="plank post-edit-form" data-listen="submit" data-r="clearThreadDraft" data-s="clearThreadDraft">
<h2 class="info">Reply to "{{thread.title}}"</h2>

View File

@@ -208,7 +208,6 @@ input[type="text"], input[type="password"], input[type="url"], textarea, select
background-color: var(--main-color);
border-radius: var(--border-radius);
border: solid var(--border-thickness) var(--border-color);
resize: vertical;
padding: var(--small-padding) var(--medium-padding);
margin: var(--base-padding) 0px;
@@ -219,7 +218,8 @@ input[type="text"], input[type="password"], input[type="url"], textarea, select
}
textarea {
font-family: 'Atkinson Hyperlegible Mono'
font-family: 'Atkinson Hyperlegible Mono';
resize: vertical;
}
h1 {
@@ -696,6 +696,39 @@ details.inner {
justify-content: center;
}
#reaction-popover {
position: absolute;
margin-block: var(--small-padding);
margin-inline: 0;
width: 300px;
--button-size: calc(var(--huge-padding) * 2);
.emoji-button {
min-width: var(--button-size);
min-height: var(--button-size);
img {
image-rendering: crisp-edges;
width: 30px;
}
}
}
#reaction-popover:popover-open {
--gap: var(--base-padding);
--max-columns: 4;
display: grid;
gap: var(--gap);
justify-items: center;
--grid-item-size: calc((100% - var(--gap) * var(--max-columns)) / var(--max-columns));
grid-template-columns: repeat(
auto-fit,
minmax(max(var(--button-size), var(--grid-item-size)), 1fr)
);
}
#bookmark-popover {
position: absolute;
min-width: 400px;
@@ -743,6 +776,125 @@ table {
}
}
ol.sortable-list {
display: flex;
gap: var(--base-padding);
flex-direction: column;
list-style: none;
flex-grow: 1;
margin: 0;
padding-left: 0;
li {
display: flex;
gap: var(--big-padding);
}
li.immovable .dragger {
cursor: not-allowed;
}
}
.plank.dragger {
display: flex;
align-items: center;
/*background-color: var(--bg-color-tertiary);*/
padding: var(--base-padding);
cursor: move;
}
.sortable-item-inner {
display: flex;
gap: var(--base-padding);
flex-grow: 1;
flex-direction: column;
& > * {
flex-grow: 1;
}
&.row {
flex-direction: row;
}
&:not(.row):not(.full) > * {
margin-right: auto;
}
}
.badge-editor-badge-container {
display: flex;
align-items: baseline;
gap: var(--base-padding);
& > input[type=text], & > input[type=url] {
width: 100%;
}
}
.badge-editor-file-picker {
display: flex;
gap: var(--base-padding);
flex-direction: column;
align-items: center;
min-width: 150px;
& > input[type=file] {
width: 100%;
&::file-selector-button {
display: none;
}
}
&.hidden {
display: none;
}
}
.badge-editor-badge-select {
display: flex;
gap: var(--base-padding);
flex-direction: column;
align-items: center;
min-width: 200px;
& > select {
width: 100%;
}
}
.js-only {
display: none;
}
dialog.plank.thread-lighbox {
margin: auto;
min-width: 80vw;
min-height: 70vh;
max-width: 80vw;
max-height: 70vh;
flex-direction: column;
justify-content: space-between;
align-items: center;
& > .menu {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
&:open {
display: flex;
}
}
.lightbox-image {
max-width: 50vw;
max-height: 50vh;
}
/* babycode tags */
.inline-code {
background-color: var(--code-bg-color);
@@ -924,97 +1076,6 @@ a.mention {
}
}
ol.sortable-list {
display: flex;
gap: var(--base-padding);
flex-direction: column;
list-style: none;
flex-grow: 1;
margin: 0;
padding-left: 0;
li {
display: flex;
gap: var(--big-padding);
}
li.immovable .dragger {
cursor: not-allowed;
}
}
.plank.dragger {
display: flex;
align-items: center;
/*background-color: var(--bg-color-tertiary);*/
padding: var(--base-padding);
cursor: move;
}
.sortable-item-inner {
display: flex;
gap: var(--base-padding);
flex-grow: 1;
flex-direction: column;
& > * {
flex-grow: 1;
}
&.row {
flex-direction: row;
}
&:not(.row):not(.full) > * {
margin-right: auto;
}
}
.badge-editor-badge-container {
display: flex;
align-items: baseline;
gap: var(--base-padding);
& > input[type=text], & > input[type=url] {
width: 100%;
}
}
.badge-editor-file-picker {
display: flex;
gap: var(--base-padding);
flex-direction: column;
align-items: center;
min-width: 150px;
& > input[type=file] {
width: 100%;
&::file-selector-button {
display: none;
}
}
&.hidden {
display: none;
}
}
.badge-editor-badge-select {
display: flex;
gap: var(--base-padding);
flex-direction: column;
align-items: center;
min-width: 200px;
& > select {
width: 100%;
}
}
.js-only {
display: none;
}
@media (max-width: 768px) {
body {
margin-left: 0;
@@ -1085,4 +1146,17 @@ ol.sortable-list {
flex-direction: column;
align-items: center;
}
dialog.plank.thread-lighbox {
margin: 0;
min-width: 100vw;
min-height: 100vh;
max-width: unset;
max-height: unset;
}
.lightbox-image {
max-width: 100%;
max-height: 100%;
}
}

View File

@@ -91,8 +91,10 @@ export function badgeEditorToggleFilePicker(ev, sender, el) {
export function badgeEditorAddBadge(ev, sender, el) {
// TODO: page templates do not get updated on mutation
const badge = document.getElementById('badge-template').innerText;
el.innerHTML += badge;
const badgeTemplate = document.getElementById('badge-template').innerText;
const parser = new DOMParser();
const e = parser.parseFromString(badgeTemplate, 'text/html').body.firstElementChild;
el.appendChild(e);
b.trigger('badgeEditorAssignImgId');
badgesCount++;
b.trigger('setBadgeCount');

View File

@@ -14,28 +14,28 @@ async function getHTML(endpoint, options = {}) {
return { body: await res.text(), status: res.status };
}
export const b = {
bookmarksCollectionEndpoint: '/hyperapi/bookmarks/dropdown/',
bookmarkMenuState: {},
}
export const b = {};
const BOOKMARKS_COLLECTION_ENDPOINT = '/hyperapi/bookmarks/dropdown/';
let bookmarkMenuState = {};
export async function showBookmarkMenu(ev, sender, el) {
if (b.bookmarkMenuState.state === undefined) {
if (bookmarkMenuState.state === undefined) {
el.addEventListener('toggle', e => {
if (e.newState === 'closed') {
b.bookmarkMenuState.state = 'closed';
bookmarkMenuState.state = 'closed';
}
});
}
// dismiss if open and last invoker is the same button that opened it
if (b.bookmarkMenuState.state === 'open' && b.bookmarkMenuState.invoker === sender) {
if (bookmarkMenuState.state === 'open' && bookmarkMenuState.invoker === sender) {
el.hidePopover();
return;
}
b.bookmarkMenuState.invoker = sender;
b.bookmarkMenuState.state = 'open';
bookmarkMenuState.invoker = sender;
bookmarkMenuState.state = 'open';
b.send({ 'plain': 'Loading…' }, 'fillBookmarkMenu');
el.showPopover();
const bRect = sender.getBoundingClientRect();
@@ -52,13 +52,13 @@ export async function showBookmarkMenu(ev, sender, el) {
}
el.style.top = `${bRect.bottom + scrollY}px`;
b.bookmarkMenuState.kind = sender.dataset.conceptKind;
b.bookmarkMenuState.id = sender.dataset.conceptId;
bookmarkMenuState.kind = sender.dataset.conceptKind;
bookmarkMenuState.id = sender.dataset.conceptId;
const bookmarkCollections = await getHTML(b.bookmarksCollectionEndpoint, {
const bookmarkCollections = await getHTML(BOOKMARKS_COLLECTION_ENDPOINT, {
_query: {
concept_kind: b.bookmarkMenuState.kind,
concept_id: b.bookmarkMenuState.id,
concept_kind: bookmarkMenuState.kind,
concept_id: bookmarkMenuState.id,
}
});
b.send({ 'html': bookmarkCollections.body }, 'fillBookmarkMenu');
@@ -85,10 +85,10 @@ export async function bookmarkMenuSubmit(ev, _, el) {
return;
}
const newCollections = await getHTML(b.bookmarksCollectionEndpoint, {
const newCollections = await getHTML(BOOKMARKS_COLLECTION_ENDPOINT, {
_query: {
concept_kind: b.bookmarkMenuState.kind,
concept_id: b.bookmarkMenuState.id,
concept_kind: bookmarkMenuState.kind,
concept_id: bookmarkMenuState.id,
saved: true,
}
});

View File

@@ -1,7 +1,9 @@
export const b = {}
export function addCollection(ev, sender, el) {
el.innerHTML += b.templates.collectionItem;
const parser = new DOMParser();
const e = parser.parseFromString(b.templates.collectionItem, 'text/html').body.firstElementChild;
el.appendChild(e);
}
export function deleteCollection(ev, sender, el) {

View File

@@ -0,0 +1,179 @@
export const b = {
init: 'activatePostImages getUserData',
}
const POST_IMAGES_SELECTOR = 'img.post-image:not(aside img.post-image)'
const WHOAMI_ENDPOINT = '/api/whoami/'
const THREAD_PERM_ENDPOINT = '/api/thread-permission/'
const TOGGLE_REACTION_ENDPOINT = '/api/toggle-reaction/'
const REPLACE_REACTIONS_ENDPOINT = '/hyperapi/reactions/'
const getThreadId = () => {
const scheme = window.location.pathname.split("/");
if (scheme[1] !== 'threads' || scheme[2] === 'new') {
return -1;
}
return parseInt(scheme[2]);
}
let images = [];
let currentIndex = 0;
let currentUser = null;
let reactionMenuState = {};
export function activatePostImages(_, __, ___) {
const images = document.querySelectorAll(POST_IMAGES_SELECTOR);
images.forEach(image => {
image.style.cursor = 'pointer';
image.dataset.s = 'collectImages';
});
}
export function collectImages(_, sender, el) {
if (!el.contains(sender)) return;
images = Array.from(el.querySelectorAll(POST_IMAGES_SELECTOR));
currentIndex = images.indexOf(sender);
b.trigger('showLightbox');
}
export function showLightbox(_, __, el) {
const originalImg = images[currentIndex];
const lightboxImg = el.querySelector('img');
const anchor = el.querySelector('a');
anchor.href = originalImg.src;
lightboxImg.src = originalImg.src;
lightboxImg.alt = originalImg.alt;
if (!el.open) {
el.showModal();
}
b.trigger('lightboxSetCounter');
}
export function closeLightbox(_, __, el) {
el.close();
}
export function lightboxSetCounter(_, __, el) {
el.innerText = `${currentIndex + 1}/${images.length}`;
}
export function lightboxNext(_, __, ___) {
if (images.length == 1) return;
currentIndex++;
if (currentIndex >= images.length) {
currentIndex = 0;
}
b.trigger('showLightbox');
}
export function lightboxPrevious(_, __, ___) {
if (images.length == 1) return;
currentIndex--;
if (currentIndex < 0) {
currentIndex = images.length - 1;
}
b.trigger('showLightbox');
}
export async function getUserData(_, __, ___) {
currentUser = await b.getData(WHOAMI_ENDPOINT);
b.trigger('highlightMentions');
const d = (await b.getData(`${THREAD_PERM_ENDPOINT}${getThreadId()}`)).can_post;
if (d) {
b.trigger('enableReactionMenuButton');
b.trigger('enableReactionButtons');
} else {
b.trigger('disableReactionMenuButton');
}
}
export function highlightMentions(_, __, el) {
if (!el) return;
if (el.dataset.username === currentUser.username) {
el.classList.add('me');
}
}
export function openReactionMenu(ev, sender, el) {
if (!el) return;
if (reactionMenuState.state === undefined) {
el.addEventListener('toggle', e => {
if (e.newState === 'closed') {
reactionMenuState.state = 'closed';
}
});
}
if (reactionMenuState.state === 'open' && reactionMenuState.invoker === sender) {
el.hidePopover();
return;
}
// TODO: [el, sender].prop(key) searches for ancestors with attr [data-${key}] if current element does not have `dataset[key]` but dataset transforms key names whereas css does not
reactionMenuState.post = sender.prop('postid');
reactionMenuState.invoker = sender;
reactionMenuState.state = 'open';
el.showPopover();
const bRect = sender.getBoundingClientRect();
const scrollY = window.scrollY;
el.style.left = `${bRect.left}px`;
el.style.top = `${bRect.bottom + scrollY}px`;
}
export function closeReactionMenu(_, __, el) {
el.hidePopover();
}
export async function toggleReaction(_, sender, __) {
const emoji = sender.dataset.emoji;
const post = sender.prop('postid') ? sender.prop('postid') : reactionMenuState.post;
const res = await fetch(TOGGLE_REACTION_ENDPOINT, {
method: 'POST',
body: JSON.stringify({ reaction: emoji, post: post }),
headers: {
'content-type': 'application/json',
},
});
if (res.status !== 200) {
return;
}
b.send({ postId: post }, 'replaceReactionButtons');
}
export async function replaceReactionButtons(payload, __, el) {
if (payload.postId !== el.prop('postid')) return;
const res = await fetch(`${REPLACE_REACTIONS_ENDPOINT}${payload.postId}`);
if (res.status !== 200) {
return;
}
const body = await res.text();
const p = new DOMParser();
const e = p.parseFromString(body, 'text/html').body;
el.replaceChildren(...e.children);
el.childNodes.forEach(b => {
if (!b instanceof HTMLButtonElement) return;
b.disabled = false;
})
}
export function disableReactionMenuButton(_, __, el) {
el.title = 'You do not have permission to add reactions to this post.';
}
export function enableReactionMenuButton(_, __, el) {
el.disabled = false;
el.title = '';
}
export function enableReactionButtons(_, __, el) {
el.disabled = false;
}

View File

@@ -1,8 +1,9 @@
export const b = {
babycodePreviewEndpoint: '/api/babycode-preview/',
init: 'babycodeEditorCharCountInit localizeTimestamps',
}
const BABYCODE_PREVIEW_ENDPOINT = '/api/babycode-preview/';
const getThreadId = () => {
const scheme = window.location.pathname.split("/");
if (scheme[1] !== 'threads' || scheme[2] === 'new') {
@@ -114,6 +115,7 @@ export function babycodeEditorCharCount(evOrPayload, sender, el) {
export function clearThreadDraft(_, __, ___) {
const threadId = getThreadId();
if (threadId === -1) return;
localStorage.removeItem(`thread-${threadId}`);
}
@@ -157,7 +159,7 @@ export async function babycodePreview(payload, _, el) {
}),
}
const f = await fetch(b.babycodePreviewEndpoint, options);
const f = await fetch(BABYCODE_PREVIEW_ENDPOINT, options);
try {
if (!f.ok) {
console.error(f);

View File

@@ -106,7 +106,7 @@
ta.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 'Enter') {
if (ta.form.reportValidity()) {
ta.form.submit();
ta.form.requestSubmit();
}
}
})