Compare commits

..

3 Commits

18 changed files with 617 additions and 40 deletions

View File

@ -5,7 +5,8 @@ from .auth import digest
from .routes.users import is_logged_in, get_active_user
from .constants import (
PermissionLevel, permission_level_string,
InfoboxKind, InfoboxIcons, InfoboxHTMLClass
InfoboxKind, InfoboxIcons, InfoboxHTMLClass,
REACTION_EMOJI,
)
from .lib.babycode import babycode_to_html, EMOJI
from datetime import datetime
@ -106,6 +107,7 @@ def create_app():
"PermissionLevel": PermissionLevel,
"__commit": commit,
"__emoji": EMOJI,
"REACTION_EMOJI": REACTION_EMOJI,
}
@app.context_processor

View File

@ -15,6 +15,38 @@ PermissionLevelString = {
PermissionLevel.ADMIN: 'Administrator',
}
REACTION_EMOJI = [
'smile',
'grin',
'neutral',
'wink',
'frown',
'angry',
'think',
'sob',
'surprised',
'smiletear',
'tongue',
'pensive',
'weary',
'imp',
'impangry',
'lobster',
'scissors',
]
def permission_level_string(perm):
return PermissionLevelString[PermissionLevel(int(perm))]

View File

@ -89,6 +89,9 @@ class DB:
self.table = table
self._where = [] # list of tuples
self._select = "*"
self._group_by = ""
self._order_by = ""
self._order_asc = True
def _build_where(self):
@ -104,6 +107,17 @@ class DB:
return " WHERE " + " AND ".join(conditions), params
def group_by(self, stmt):
self._group_by = stmt
return self
def order_by(self, stmt, asc = True):
self._order_by = stmt
self._order_asc = asc
return self
def select(self, columns = "*"):
self._select = columns
return self
@ -122,7 +136,16 @@ class DB:
def build_select(self):
sql = f"SELECT {self._select} FROM {self.table}"
where_clause, params = self._build_where()
return sql + where_clause, params
stmt = sql + where_clause
if self._group_by:
stmt += " GROUP BY " + self._group_by
if self._order_by:
stmt += " ORDER BY " + self._order_by + (" ASC" if self._order_asc else " DESC")
return stmt, params
def build_update(self, data):

View File

@ -2,6 +2,37 @@ from .babycode_parser import Parser
from markupsafe import escape
import re
NAMED_COLORS = [
'black', 'silver', 'gray', 'white', 'maroon', 'red',
'purple', 'fuchsia', 'green', 'lime', 'olive', 'yellow',
'navy', 'blue', 'teal', 'aqua', 'aliceblue', 'antiquewhite',
'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black',
'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue',
'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson',
'cyan', 'aqua', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray',
'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange',
'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray',
'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray',
'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia',
'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green',
'greenyellow', 'grey', 'gray', 'honeydew', 'hotpink', 'indianred',
'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen',
'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray',
'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue',
'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen',
'linen', 'magenta', 'fuchsia', 'maroon', 'mediumaquamarine', 'mediumblue',
'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite',
'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered',
'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip',
'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple',
'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue',
'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue',
'tan', 'teal', 'thistle', 'tomato', 'transparent', 'turquoise',
'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen',
]
def tag_code(children, attr):
is_inline = children.find('\n') == -1
if is_inline:
@ -16,16 +47,39 @@ def tag_list(children):
list_body = re.sub(r"\n\n+", "\1", list_body)
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
def tag_color(children, attr):
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
potential_color = attr.lower().strip()
if potential_color in NAMED_COLORS:
return f"<span style='color: {potential_color};'>{children}</span>"
m = re.match(hex_re, potential_color)
if m:
return f"<span style='color: #{m.group(1)};'>{children}</span>"
# return just the way it was if we can't parse it
return f"[color={attr}]{children}[/color]"
TAGS = {
"b": lambda children, attr: f"<strong>{children}</strong>",
"i": lambda children, attr: f"<em>{children}</em>",
"s": lambda children, attr: f"<del>{children}</del>",
"u": lambda children, attr: f"<u>{children}</u>",
"img": lambda children, attr: f"<div class=\"post-img-container\"><img class=\"block-img\" src=\"{attr}\" alt=\"{children}\"></div>",
"url": lambda children, attr: f"<a href={attr}>{children}</a>",
"quote": lambda children, attr: f"<blockquote>{children}</blockquote>",
"code": tag_code,
"ul": lambda children, attr: f"<ul>{tag_list(children)}</ul>",
"ol": lambda children, attr: f"<ol>{tag_list(children)}</ol>",
"big": lambda children, attr: f"<span style='font-size: 2rem;'>{children}</span>",
"small": lambda children, attr: f"<span style='font-size: 0.75rem;'>{children}</span>",
"color": tag_color,
"center": lambda children, attr: f"<div style='text-align: center;'>{children}</div>",
"right": lambda children, attr: f"<div style='text-align: right;'>{children}</div>",
}
def make_emoji(name, code):

View File

@ -256,3 +256,28 @@ class APIRateLimits(Model):
return True
else:
return False
class Reactions(Model):
table = "reactions"
@classmethod
def for_post(cls, post_id):
qb = db.QueryBuilder(cls.table)\
.select("reaction_text, COUNT(*) as c")\
.where({"post_id": post_id})\
.group_by("reaction_text")\
.order_by("c", False)
result = qb.all()
return result if result else []
@classmethod
def get_users(cls, post_id, reaction_text):
q = """
SELECT user_id, username FROM reactions
JOIN
users ON users.id = user_id
WHERE
post_id = ? AND reaction_text = ?
"""
return db.query(q, post_id, reaction_text)

View File

@ -1,7 +1,8 @@
from flask import Blueprint, request, url_for
from ..lib.babycode import babycode_to_html
from ..constants import REACTION_EMOJI
from .users import is_logged_in, get_active_user
from ..models import APIRateLimits, Threads
from ..models import APIRateLimits, Threads, Reactions
from ..db import db
bp = Blueprint("api", __name__, url_prefix="/api/")
@ -41,3 +42,57 @@ def babycode_preview():
return {'error': 'markup field missing or invalid type'}, 400
rendered = babycode_to_html(markup)
return {'html': rendered}
@bp.post('/add-reaction/<post_id>')
def add_reaction(post_id):
if not is_logged_in():
return {'error': 'not authorized', 'error_code': 401}, 401
user = get_active_user()
reaction_text = request.json.get('emoji')
if not reaction_text or not isinstance(reaction_text, str):
return {'error': 'emoji field missing or invalid type', 'error_code': 400}, 400
if reaction_text not in REACTION_EMOJI:
return {'error': 'unsupported reaction', 'error_code': 400}, 400
reaction = Reactions.find({
'user_id': user.id,
'post_id': int(post_id),
'reaction_text': reaction_text,
})
if reaction:
return {'error': 'reaction already exists', 'error_code': 409}, 409
reaction = Reactions.create({
'user_id': user.id,
'post_id': int(post_id),
'reaction_text': reaction_text,
})
return {'status': 'added'}
@bp.post('/remove-reaction/<post_id>')
def remove_reaction(post_id):
if not is_logged_in():
return {'error': 'not authorized'}, 401
user = get_active_user()
reaction_text = request.json.get('emoji')
if not reaction_text or not isinstance(reaction_text, str):
return {'error': 'emoji field missing or invalid type'}, 400
if reaction_text not in REACTION_EMOJI:
return {'error': 'unsupported reaction'}, 400
reaction = Reactions.find({
'user_id': user.id,
'post_id': int(post_id),
'reaction_text': reaction_text,
})
if not reaction:
return {'error': 'reaction does not exist'}, 404
reaction.delete()
return {'status': 'removed'}

View File

@ -3,7 +3,7 @@ from flask import (
)
from .users import login_required, mod_only, get_active_user, is_logged_in
from ..db import db
from ..models import Threads, Topics, Posts, Subscriptions
from ..models import Threads, Topics, Posts, Subscriptions, Reactions
from ..constants import InfoboxKind
from .posts import create_post
from slugify import slugify
@ -61,6 +61,7 @@ def thread(slug):
topic = topic,
topics = other_topics,
is_subscribed = is_subscribed,
Reactions = Reactions,
)

View File

@ -510,7 +510,9 @@ def inbox(username):
'thread_id': row['thread_id'],
'user_id': row['user_id'],
'original_markup': row['original_markup'],
'signature_rendered': row['signature_rendered']
'signature_rendered': row['signature_rendered'],
'thread_slug': row['thread_slug'],
})
return render_template("users/inbox.html", new_posts = new_posts, total_unreads_count = total_unreads_count, all_subscriptions = all_subscriptions)

View File

@ -76,6 +76,13 @@ SCHEMA = [
"signature_rendered" TEXT NOT NULL DEFAULT ''
)""",
"""CREATE TABLE IF NOT EXISTS "reactions" (
"id" INTEGER NOT NULL PRIMARY KEY,
"user_id" REFERENCES users(id) ON DELETE CASCADE,
"post_id" REFERENCES posts(id) ON DELETE CASCADE,
"reaction_text" TEXT NOT NULL DEFAULT ''
)""",
# INDEXES
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
@ -87,6 +94,9 @@ SCHEMA = [
"CREATE INDEX IF NOT EXISTS idx_topics_slug ON topics(slug)",
"CREATE INDEX IF NOT EXISTS session_keys ON sessions(key)",
"CREATE INDEX IF NOT EXISTS sessions_user_id ON sessions(user_id)",
"CREATE INDEX IF NOT EXISTS reaction_post_text ON reactions(post_id, reaction_text)",
"CREATE INDEX IF NOT EXISTS reaction_user_post_text ON reactions(user_id, post_id, reaction_text)",
]
def create():

View File

@ -14,23 +14,68 @@
</section>
<section class="babycode-guide-section">
<h2 id="text-formatting-tags">Text formatting tags</h2>
<ul>
<ul class='babycode-guide-list'>
<li>To make some text <strong>bold</strong>, enclose it in <code class="inline-code">[b][/b]</code>:<br>
[b]Hello World[/b]<br>
Will become<br>
<strong>Hello World</strong>
</li>
</ul>
<ul>
<ul class='babycode-guide-list'>
<li>To <em>italicize</em> text, enclose it in <code class="inline-code">[i][/i]</code>:<br>
[i]Hello World[/i]<br>
Will become<br>
<em>Hello World</em>
</li>
</ul>
<ul>
<ul class='babycode-guide-list'>
<li>To make some text <del>strikethrough</del>, enclose it in <code class="inline-code">[s][/s]</code>:<br>
[s]Hello World[/s]<br>
Will become<br>
<del>Hello World</del>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>To <u>underline</u> some text, enclose it in <code class="inline-code">[u][/u]</code>:<br>
[u]Hello World[/u]<br>
Will become<br>
<u>Hello World</u>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>To make some text {{ "[big]big[/big]" | babycode | safe }}, enclose it in <code class="inline-code">[big][/big]</code>:<br>
[big]Hello World[/big]<br>
Will become<br>
{{ "[big]Hello World[/big]" | babycode | safe }}
<li>Similarly, you can make text {{ "[small]small[/small]" | babycode | safe }} with <code class="inline-code">[small][/small]</code>:<br>
[small]Hello World[/small]<br>
Will become<br>
{{ "[small]Hello World[/small]" | babycode | safe }}
</li>
</ul>
<ul class='babycode-guide-list'>
<li>You can change the text color by using <code class="inline-code">[color][/color]</code>:<br>
[color=red]Red text[/color]<br>
[color=white]White text[/color]<br>
[color=#3b08f0]Blueish text[/color]<br>
Will become<br>
{{ "[color=red]Red text[/color]" | babycode | safe }}<br>
{{ "[color=white]White text[/color]" | babycode | safe }}<br>
{{ "[color=#3b08f0]Blueish text[/color]" | babycode | safe }}<br>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>You can center text by enclosing it in <code class="inline-code">[center][/center]</code>:<br>
[center]Hello World[/center]<br>
Will become<br>
{{ "[center]Hello World[/center]" | babycode | safe }}
</li>
<li>You can right-align text by enclosing it in <code class="inline-code">[right][/right]</code>:<br>
[right]Hello World[/right]<br>
Will become<br>
{{ "[right]Hello World[/right]" | babycode | safe }}
</li>
Note: the center and right tags will break the paragraph. See <a href="#paragraph-rules">Paragraph rules</a> for more details.
</ul>
</section>
<section class="babycode-guide-section">
@ -60,6 +105,15 @@
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
That will produce:<br>
{{ 'paragraph 1 \nstill paragraph 1' | babycode | safe }}
<p>Additionally, the following tags will break into a new paragraph:</p>
<ul>
<li><code class="inline-code">[code]</code> (code block, not inline);</li>
<li><code class="inline-code">[img]</code>;</li>
<li><code class="inline-code">[center]</code>;</li>
<li><code class="inline-code">[right]</code>;</li>
<li><code class="inline-code">[ul]</code> and <code class="inline-code">[ol]</code>;</li>
<li><code class="inline-code">[quote]</code>.</li>
</ul>
</section>
<section class="babycode-guide-section">
<h2 id="links">Links</h2>

View File

@ -53,6 +53,7 @@
<button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold"><strong>B</strong></button>
<button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics"><em>I</em></button>
<button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough"><del>S</del></button>
<button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline"><u>U</u></button>
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link"><code>://</code></button>
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block"><code>&lt;/&gt;</code></button>
<button class="babycode-button contain-svg full" type=button id="post-editor-img" title="Insert Image"><img src="/static/misc/image.svg"></button>
@ -89,13 +90,13 @@
</form>
{% endmacro %}
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None, no_reply = false) %}
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None, no_reply = false, Reactions = none) %}
{% set postclass = "post" %}
{% if editing %}
{% set postclass = postclass + " editing" %}
{% endif %}
{% set post_permalink = url_for("threads.thread", slug = post['thread_slug'], after = post['id'], _anchor = ("post-" + (post['id'] | string))) %}
<div class=" {{ postclass }}" id="post-{{ post['id'] }}">
<div class=" {{ postclass }}" id="post-{{ post['id'] }}" data-post-id="{{ post['id'] }}">
<div class="usercard">
<div class="usercard-inner">
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
@ -128,9 +129,11 @@
{% set show_reply = true %}
{% if active_user and post['thread_is_locked'] and not active_user.is_mod() %}
{% if not active_user %}
{% set show_reply = false %}
{% elif active_user and active_user.is_guest() %}
{% elif post['thread_is_locked'] and not active_user.is_mod() %}
{% set show_reply = false %}
{% elif active_user.is_guest() %}
{% set show_reply = false %}
{% elif editing %}
{% set show_reply = false %}
@ -160,7 +163,6 @@
<div class="post-inner" data-post-permalink="{{ post_permalink }}" data-author-username="{{ post.username }}">{{ post['content'] | safe }}</div>
{% if render_sig and post['signature_rendered'] %}
<div class="signature-container">
<hr>
{{ post['signature_rendered'] | safe }}
</div>
{% endif %}
@ -168,6 +170,37 @@
{{ babycode_editor_form(cancel_url = post_permalink, prefill = post['original_markup'], ta_name = "new_content") }}
{% endif %}
</div>
{% if Reactions -%}
{% set can_react = true -%}
{% if not active_user -%}
{% set can_react = false -%}
{% elif post['thread_is_locked'] and not active_user.is_mod() -%}
{% set can_react = false -%}
{% elif active_user.is_guest() -%}
{% set can_react = false -%}
{% elif editing -%}
{% set can_react = false -%}
{% endif -%}
{% set reactions = Reactions.for_post(post.id) -%}
<div class="post-reactions">
{% for reaction in reactions %}
{% 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 = active_user is not none and active_user.username in reactors %}
<span class="reaction-container" data-emoji="{{ reaction.reaction_text }}" data-post-id="{{ post.id }}"><button type="button" class="reduced reaction-button {{"active" if has_reacted else ""}}" {{ "disabled" if not can_react else ""}} title="{{reactors_str}}"><img class=emoji src="/static/emoji/{{reaction.reaction_text}}.png"> x<span class="reaction-count" data-emoji="{{ reaction.reaction_text }}">{{reaction.c}}</span></button>
</span>
{% endfor %}
{% if can_react %}
<button type="button" class="reduced add-reaction-button" data-post-id="{{ post.id }}">Add reaction</button>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endmacro %}

View File

@ -50,7 +50,7 @@
</div>
</nav>
{% for post in posts %}
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length)) }}
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length), Reactions = Reactions) }}
{% endfor %}
</main>
@ -72,6 +72,7 @@
</span>
</div>
</dialog>
<input type='hidden' id='allowed-reaction-emoji' value='{{ REACTION_EMOJI | join(' ') }}'>
<input type='hidden' id='thread-subscribe-endpoint' value='{{ url_for('api.thread_updates', thread_id=thread.id) }}'>
<div id="new-post-notification" class="new-concept-notification hidden">
<div class="new-notification-content">

View File

@ -43,7 +43,7 @@
{% if section == "header" %}
{% set latest_post_id = thread.posts[-1].id %}
{% set unread_posts_text = " (" + (thread.unread_count | string) + (" unread post" | pluralize(num=thread.unread_count)) %}
<a class="accordion-title" href="{{ url_for("threads.thread", slug=latest_post_slug, after=latest_post_id, _anchor="post-" + (latest_post_id | string)) }}" title="Jump to latest post">{{thread.thread_title + unread_posts_text}}, latest at {{ timestamp(thread.newest_post_time) }})</a>
<a class="accordion-title" href="{{ url_for("threads.thread", slug=thread.thread_slug, after=latest_post_id, _anchor="post-" + (latest_post_id | string)) }}" title="Jump to latest post">{{thread.thread_title + unread_posts_text}}, latest at {{ timestamp(thread.newest_post_time) }})</a>
<form class="modform" method="post" action="{{ url_for("threads.subscribe", slug = thread.thread_slug) }}">
<input type="hidden" name="subscribe" value="read">
<input type="submit" value="Mark thread as Read">

View File

@ -80,8 +80,8 @@
{% endif %}
</i></a>
</div>
<div class="post-content wider user-page-post-preview">
<div class="post-inner">{{ post.content | safe }}</div>
<div class="post-content user-page-post-preview">
<div class="post-inner wider">{{ post.content | safe }}</div>
</div>
</div>
{% endfor %}

View File

@ -42,6 +42,7 @@
const buttonBold = document.getElementById("post-editor-bold");
const buttonItalics = document.getElementById("post-editor-italics");
const buttonStrike = document.getElementById("post-editor-strike");
const buttonUnderline = document.getElementById("post-editor-underline");
const buttonUrl = document.getElementById("post-editor-url");
const buttonCode = document.getElementById("post-editor-code");
const buttonImg = document.getElementById("post-editor-img");
@ -105,6 +106,10 @@
e.preventDefault();
insertTag("s")
})
buttonUnderline.addEventListener("click", (e) => {
e.preventDefault();
insertTag("u")
})
buttonUrl.addEventListener("click", (e) => {
e.preventDefault();
insertTag("url=", false, "link label");

View File

@ -62,26 +62,26 @@
setTimeout(() => {
const valid = isQuoteSelectionValid();
if (isSelecting || !valid) {
removePopover();
removeQuotePopover();
return;
}
const selection = document.getSelection();
const selectionStr = selection.toString().trim();
if (selection.isCollapsed || selectionStr === "") {
removePopover();
removeQuotePopover();
return;
}
showPopover();
showQuotePopover();
}, 50)
}
function removePopover() {
function removeQuotePopover() {
quotePopover?.hidePopover();
}
function createPopover() {
function createQuotePopover() {
quotePopover = document.createElement("div");
quotePopover.popover = "auto";
quotePopover.className = "quote-popover";
@ -95,9 +95,9 @@
return quoteButton;
}
function showPopover() {
function showQuotePopover() {
if (!quotePopover) {
const quoteButton = createPopover();
const quoteButton = createQuotePopover();
quoteButton.addEventListener("click", () => {
console.log("Quoting:", document.getSelection().toString());
const postPermalink = quotedPostContainer.dataset.postPermalink;
@ -111,7 +111,7 @@
ta.focus();
document.getSelection().empty();
removePopover();
removeQuotePopover();
})
}
@ -196,4 +196,174 @@
.catch(error => console.log(error))
}
tryFetchUpdate();
if (supportsPopover()){
const reactionEmoji = document.getElementById("allowed-reaction-emoji").value.split(" ");
let reactionPopover = null;
let reactionTargetPostId = null;
function tryAddReaction(emoji, postId = reactionTargetPostId) {
const body = JSON.stringify({
"emoji": emoji,
});
fetch(`/api/add-reaction/${postId}`, {method: "POST", headers: {"Content-Type": "application/json"}, body: body})
.then(res => res.json())
.then(json => {
if (json.status === "added") {
const post = document.getElementById(`post-${postId}`);
const spans = Array.from(post.querySelectorAll(".reaction-count")).filter((span) => {
return span.dataset.emoji === emoji
});
if (spans.length > 0) {
const currentValue = spans[0].textContent;
spans[0].textContent = `${parseInt(currentValue) + 1}`;
const button = spans[0].closest(".reaction-button");
button.classList.add("active");
} else {
const span = document.createElement("span");
span.classList = "reaction-container";
span.dataset.emoji = emoji;
const button = document.createElement("button");
button.type = "button";
button.className = "reduced reaction-button active";
button.addEventListener("click", () => {
tryAddReaction(emoji, postId);
})
const img = document.createElement("img");
img.src = `/static/emoji/${emoji}.png`;
button.textContent = " x";
const reactionCountSpan = document.createElement("span")
reactionCountSpan.className = "reaction-count"
reactionCountSpan.textContent = "1"
button.insertAdjacentElement("afterbegin", img);
button.appendChild(reactionCountSpan);
span.appendChild(button);
const post = document.getElementById(`post-${postId}`);
post.querySelector(".post-reactions").insertBefore(span, post.querySelector(".add-reaction-button"));
}
} else if (json.error_code === 409) {
console.log("reaction exists, gonna try and remove");
tryRemoveReaction(emoji, postId);
} else {
console.warn(json)
}
})
.catch(error => console.error(error));
}
function tryRemoveReaction(emoji, postId = reactionTargetPostId) {
const body = JSON.stringify({
"emoji": emoji,
});
fetch(`/api/remove-reaction/${postId}`, {method: "POST", headers: {"Content-Type": "application/json"}, body: body})
.then(res => res.json())
.then(json => {
if (json.status === "removed") {
const post = document.getElementById(`post-${postId}`);
const spans = Array.from(post.querySelectorAll(".reaction-container")).filter((span) => {
return span.dataset.emoji === emoji
});
if (spans.length > 0) {
const reactionCountSpan = spans[0].querySelector(".reaction-count");
const currentValue = parseInt(reactionCountSpan.textContent);
if (currentValue - 1 === 0) {
spans[0].remove();
} else {
reactionCountSpan.textContent = `${parseInt(currentValue) - 1}`;
const button = reactionCountSpan.closest(".reaction-button");
button.classList.remove("active");
}
}
} else {
console.warn(json)
}
})
.catch(error => console.error(error));
}
function createReactionPopover() {
reactionPopover = document.createElement("div");
reactionPopover.className = "reaction-popover";
reactionPopover.popover = "auto";
const inner = document.createElement("div");
inner.className = "reaction-popover-inner";
reactionPopover.appendChild(inner);
for (let emoji of reactionEmoji) {
const img = document.createElement("img");
img.src = `/static/emoji/${emoji}.png`;
const button = document.createElement("button");
button.type = "button";
button.className = "reduced";
button.appendChild(img);
button.addEventListener("click", () => {
tryAddReaction(emoji);
})
button.dataset.emojiName = emoji;
inner.appendChild(button);
}
reactionPopover.addEventListener("beforetoggle", (e) => {
if (e.newState === "closed") {
reactionTargetPostId = null;
}
})
document.body.appendChild(reactionPopover);
}
function showReactionPopover() {
if (!reactionPopover) {
createReactionPopover();
}
if (!reactionPopover.matches(':popover-open')) {
reactionPopover.showPopover();
}
}
for (let button of document.querySelectorAll(".add-reaction-button")) {
button.addEventListener("click", (e) => {
showReactionPopover();
reactionTargetPostId = e.target.dataset.postId;
const rect = e.target.getBoundingClientRect();
const popoverRect = reactionPopover.getBoundingClientRect();
const scrollY = window.scrollY || window.pageYOffset;
reactionPopover.style.setProperty("top", `${rect.top + scrollY + rect.height}px`)
reactionPopover.style.setProperty("left", `${rect.left + rect.width/2 - popoverRect.width/2}px`)
})
}
for (let button of document.querySelectorAll(".reaction-button")) {
button.addEventListener("click", () => {
const reactionContainer = button.closest(".reaction-container")
const emoji = reactionContainer.dataset.emoji;
const postId = reactionContainer.dataset.postId;
console.log(reactionContainer);
tryAddReaction(emoji, postId);
})
}
} else {
for (let button of document.querySelectorAll(".add-reaction-button")) {
button.disabled = true;
button.title = "Enable JS to add reactions."
}
}
}

View File

@ -26,7 +26,7 @@
font-weight: bold;
font-style: italic;
}
.tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default;
color: black;
font-size: 0.9em;
@ -119,16 +119,18 @@ body {
.post-content-container {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 70px 2.5fr;
grid-template-rows: min-content 1fr min-content;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas: "post-info" "post-content";
grid-template-areas: "post-info" "post-content" "post-reactions";
grid-area: post-content-container;
min-height: 100%;
}
.post-info {
grid-area: post-info;
display: flex;
min-height: 70px;
justify-content: space-between;
padding: 5px 20px;
align-items: center;
@ -138,19 +140,36 @@ body {
.post-content {
grid-area: post-content;
padding: 20px;
margin-right: 25%;
padding: 20px 20px 0 20px;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #c1ceb1;
}
.post-content.wider {
margin-right: 12.5%;
.post-reactions {
grid-area: post-reactions;
min-height: 50px;
display: flex;
padding: 5px 20px;
align-items: center;
flex-wrap: wrap;
gap: 5px;
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
border-top: 2px dotted gray;
}
.post-inner {
height: 100%;
padding-right: 25%;
}
.post-inner.wider {
padding-right: 12.5%;
}
.signature-container {
border-top: 2px dotted gray;
padding: 10px 0;
}
pre code {
@ -773,7 +792,8 @@ ul, ol {
.babycode-button-container {
display: flex;
gap: 10px;
gap: 5px;
flex-wrap: wrap;
}
.babycode-button {
@ -797,3 +817,42 @@ ul, ol {
footer {
border-top: 1px solid black;
}
.reaction-button.active {
background-color: #beb1ce;
}
.reaction-button.active:hover {
background-color: rgb(203, 192.6, 215.8);
}
.reaction-button.active:active {
background-color: rgb(171.7642913386, 166.6881496063, 178.0118503937);
}
.reaction-button.active:disabled {
background-color: rgb(210.445, 209.535, 211.565);
}
.reaction-button.active.reduced {
margin: 0;
padding: 5px;
}
.reaction-popover {
position: relative;
margin: 0;
border: none;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.6901960784);
padding: 5px 10px;
width: 250px;
}
.reaction-popover-inner {
display: flex;
flex-wrap: wrap;
overflow: scroll;
margin: auto;
justify-content: center;
}
.babycode-guide-list {
border-bottom: 1px dashed;
}

View File

@ -166,18 +166,22 @@ body {
.post-content-container {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 70px 2.5fr;
grid-template-rows: min-content 1fr min-content;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas:
"post-info"
"post-content";
"post-content"
"post-reactions";
grid-area: post-content-container;
min-height: 100%;
}
.post-info {
grid-area: post-info;
display: flex;
min-height: 70px;
justify-content: space-between;
padding: 5px 20px;
align-items: center;
@ -187,19 +191,39 @@ body {
.post-content {
grid-area: post-content;
padding: 20px;
margin-right: 25%;
padding: 20px 20px 0 20px;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: $accent_color;
// min-height: 0;
}
.post-content.wider {
margin-right: 12.5%;
.post-reactions {
grid-area: post-reactions;
min-height: 50px;
display: flex;
padding: 5px 20px;
align-items: center;
flex-wrap: wrap;
gap: 5px;
background-color: $main_bg;
border-top: 2px dotted gray;
}
.post-inner {
height: 100%;
padding-right: 25%;
&.wider {
padding-right: 12.5%;
}
}
.signature-container {
border-top: 2px dotted gray;
padding: 10px 0;
}
pre code {
@ -763,7 +787,8 @@ ul, ol {
.babycode-button-container {
display: flex;
gap: 10px;
gap: 5px;
flex-wrap: wrap;
}
.babycode-button {
@ -788,3 +813,29 @@ ul, ol {
footer {
border-top: 1px solid black;
}
.reaction-button.active {
@include button($button_color2);
}
.reaction-popover {
position: relative;
margin: 0;
border: none;
border-radius: 4px;
background-color: #000000b0;
padding: 5px 10px;
width: 250px;
}
.reaction-popover-inner {
display: flex;
flex-wrap: wrap;
overflow: scroll;
margin: auto;
justify-content: center;
}
.babycode-guide-list {
border-bottom: 1px dashed;
}