301 lines
14 KiB
HTML
301 lines
14 KiB
HTML
{%- from 'common/icons.html' import icn_info, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_dragger -%}
|
|
|
|
{% macro dict_to_attr(attrs) -%}
|
|
{%- for key, value in attrs.items() if value is not none -%}{{' '}}{{key}}="{{value}}"{%- endfor -%}
|
|
{%- endmacro %}
|
|
|
|
{% macro timestamp(unix_ts) -%}
|
|
<time data-r="localizeTimestamps" datetime="{{ unix_ts | iso8601 }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></time>
|
|
{%- endmacro %}
|
|
|
|
{% macro subheader(title, desc='') -%}
|
|
<div id="subheader" class="plank secondary-bg">
|
|
<h1 class="info">{{title}}</h1>
|
|
{%- if desc -%}<span>{{desc}}</span>{%- endif -%}
|
|
<div class="actions-group">{% if caller %}{{- caller() -}}{% endif %}</div>
|
|
</div>
|
|
{%- endmacro %}
|
|
|
|
{% macro pager(current_page, page_count, classes='', url='', args={}) -%}
|
|
{%- set args = dict(args.items() | rejectattr(0, 'equalto', 'page')) -%}
|
|
{%- if args -%}
|
|
{#- remove the page query argument -#}
|
|
{%- set url = url + (args | dict_to_query_string) + '&page=' -%}
|
|
{%- else -%}
|
|
{%- set url = url + '?page=' -%}
|
|
{%- endif -%}
|
|
<span class="button-row {{classes}}">
|
|
{%- if current_page == 0 -%}
|
|
{%- if page_count <= 3 -%}
|
|
{%- for i in range(page_count) -%}
|
|
<a href="{{url}}{{i+1}}" class="linkbutton minimal">{{i+1}}</a>
|
|
{%- endfor -%}
|
|
{%- else -%}
|
|
<a href="{{url}}1" class="linkbutton minimal">1</a>
|
|
<a href="{{url}}2" class="linkbutton minimal">2</a>
|
|
<button class="minimal" disabled>…</button>
|
|
<a href="{{url}}{{page_count - 1}}" class="linkbutton minimal">{{page_count - 1}}</a>
|
|
<a href="{{url}}{{page_count}}" class="linkbutton minimal">{{page_count}}</a>
|
|
{%- endif -%}
|
|
{%- else -%}
|
|
{%- set left_start = [2, current_page - 1] | max -%}
|
|
{%- set right_end = [page_count - 1, current_page + 1] | min -%}
|
|
|
|
{%- if current_page != 1 -%}
|
|
<a href="{{url}}1" class="linkbutton minimal">1</a>
|
|
{%- endif -%}
|
|
|
|
{%- if left_start > 2 -%}
|
|
<button class="minimal" disabled>…</button>
|
|
{%- endif -%}
|
|
|
|
{%- for i in range(left_start, current_page) -%}
|
|
<a href="{{url}}{{i}}" class="linkbutton minimal">{{i}}</a>
|
|
{%- endfor -%}
|
|
|
|
{%- if page_count > 0 -%}
|
|
<button class="minimal" disabled>{{current_page}}</button>
|
|
{%- endif -%}
|
|
|
|
{%- for i in range(current_page + 1, right_end + 1) -%}
|
|
<a href="{{url}}{{i}}" class="linkbutton minimal">{{i}}</a>
|
|
{%- endfor -%}
|
|
|
|
{%- if right_end < page_count - 1 -%}
|
|
<button class="minimal" disabled>…</button>
|
|
{%- endif -%}
|
|
|
|
{%- if page_count > 1 and current_page != page_count -%}
|
|
<a href="{{url}}{{page_count}}" class="linkbutton minimal">{{page_count}}</a>
|
|
{%- endif -%}
|
|
{%- endif -%}
|
|
</span>
|
|
{%- endmacro %}
|
|
|
|
{% macro tabs(prefix='', labels=[], signal_ss=[], signal_rs=[]) -%}
|
|
<div class="tab-container" data-r="setTab">
|
|
<div class="tab-bar" role="tablist">
|
|
{%- for tab_label in labels -%}
|
|
<button type="button" class="tab-button" role="tab" aria-selected="{{'true' if loop.index0==0 else 'false'}}" id="{{prefix+'-'+(tab_label | lower)+'-tab'}}" aria-controls="{{prefix+'-'+(tab_label | lower)+'-content'}}" disabled data-r="enhance" data-s="setTab {{signal_ss[loop.index0] if signal_ss[loop.index0] else ''}}" data-tab-index="{{loop.index0}}">{{tab_label}}</button>
|
|
{%- endfor -%}
|
|
</div>
|
|
{%- for tab_label in labels -%}
|
|
<div class="plank secondary-bg even no-shadow tab-content {{'hidden' if loop.index0!=0 else ''}}" role="tabpanel" aria-labelledby="{{prefix+'-'+(tab_label | lower)+'-tab'}}" id="{{prefix+'-'+(tab_label | lower)+'-content'}}" data-r="{{signal_rs[loop.index0] if signal_rs[loop.index0] else ''}}">
|
|
{{- caller(loop.index0) -}}
|
|
</div>
|
|
{%- endfor -%}
|
|
</div>
|
|
{%- endmacro %}
|
|
|
|
{% macro babycode_editor_component(
|
|
placeholder='Post content',
|
|
prefill='',
|
|
required=true,
|
|
id='babycode-content',
|
|
banned_tags=[]
|
|
) -%}
|
|
{%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview'], signal_ss=[none, 'babycodePreviewInit'], signal_rs=[none, 'babycodePreview']) -%}
|
|
{%- if idx == 0 -%}
|
|
<span class="babycode-editor-controls">
|
|
<span class="button-row js-only" data-r="enhance">
|
|
<button type="button" title="insert bold" class="minimal" data-babycode-tag="b" data-s="insertBabycode"><b>B</b></button>
|
|
<button type="button" title="insert italic" class="minimal" data-babycode-tag="i" data-s="insertBabycode"><i>i</i></button>
|
|
<button type="button" title="insert strikethrough" class="minimal" data-babycode-tag="s" data-s="insertBabycode"><s>S</s></button>
|
|
<button type="button" title="insert underline" class="minimal" data-babycode-tag="u" data-s="insertBabycode"><u>U</u></button>
|
|
<button type="button" title="insert link" class="minimal" data-babycode-tag="url" data-prefill="link label" data-s="insertBabycode"><code>://</code></button>
|
|
<button type="button" title="insert code block" class="minimal" data-babycode-tag="code" data-break-line data-s="insertBabycode"><code></></code></button>
|
|
<button type="button" title="insert ordered list" class="minimal" data-babycode-tag="ol" data-break-line data-s="insertBabycode">1.</button>
|
|
<button type="button" title="insert unordered list" class="minimal" data-babycode-tag="ul" data-break-line data-s="insertBabycode">•</button>
|
|
<button type="button" title="insert spoiler" class="minimal" data-babycode-tag="spoiler=" data-break-line data-prefill="spoiler content" data-s="insertBabycode">s</button>
|
|
<button type="button" title="insert emoji…" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
|
</span>
|
|
<span class="flex-last js-only" data-r="enhance babycodeEditorCharCount">0/</span>
|
|
</span>
|
|
<input type="hidden" name="babycode_banned_tags" id="{{id}}-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
|
|
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}} autocomplete="off" maxlength="5000" data-r="insertBabycode babycodePreviewInit babycodeEditorCharCountInit babycodeEditorQuote" data-listen="input" data-s="babycodeEditorCharCount" data-banned-tags="{{banned_tags | unique | list | tojson | forceescape}}">{{ prefill }}</textarea>
|
|
{%- if banned_tags -%}
|
|
<div>
|
|
<span>Forbidden tags:</span>
|
|
<ul class="horizontal">
|
|
{%- for tag in banned_tags -%}
|
|
<li><code class="inline-code">{{tag}}</code></li>
|
|
{%- endfor -%}
|
|
</ul>
|
|
</div>
|
|
{%- endif -%}
|
|
<a href="##">babycode help</a>
|
|
{%- else -%}
|
|
<div data-r="showBabycodePreview"></div>
|
|
{%- endif -%}
|
|
{%- endcall -%}
|
|
{%- endmacro %}
|
|
|
|
{% macro avatar(url) -%}
|
|
<div class="avatar-container">
|
|
<img src="{{url}}" class="avatar">
|
|
</div>
|
|
{%- endmacro %}
|
|
|
|
{% macro bookmark_button(kind, id, text='Bookmark') -%}
|
|
<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}}…</button>
|
|
{%- endmacro %}
|
|
|
|
{% macro full_post(
|
|
post, render_sig=true, is_latest=false,
|
|
show_toolbar=true, is_editing=false, thread=none,
|
|
show_reactions=true, show_thread=false, allow_reacting=true,
|
|
tb_edit=true, tb_quote=true, tb_delete=true, tb_bookmark=true,
|
|
bookmark_btn='Bookmark', tb_pretext=''
|
|
) -%}
|
|
{%- if is_logged_in() -%}
|
|
{%- set can_delete = post.user_id == get_active_user().id or is_mod() -%}
|
|
{%- else -%}
|
|
{%- set show_toolbar = false -%}
|
|
{%- endif -%}
|
|
{%- set owns = is_logged_in() and post.user_id == get_active_user().id -%}
|
|
{%- set can_reply = (is_logged_in()) and (not thread.locked or is_mod()) -%}
|
|
<div class="usercard plank even contrast-bg minimal no-shadow">
|
|
<div class="usercard-inner">
|
|
{{avatar(post.avatar_path)}}
|
|
<div class="usercard-rest">
|
|
<a href="{{url_for('users.user_page', username=post.username)}}" class="usercard-username">{{post.display_name if post.display_name else post.username}}</a>
|
|
<abbr title="mention">@{{post.username}}</abbr>
|
|
<i>{{post.status}}</i>
|
|
{%- set badges=post.badges_json | fromjson -%}
|
|
<div class="badges-container">
|
|
{%- for badge in badges -%}
|
|
{%- if badge.link -%}<a href="{{badge.link}}">{%- endif -%}
|
|
<img src="{{badge.file_path}}" alt="{{badge.label}}" title="{{badge.label}}" class="badge-button">
|
|
{%- if badge.link -%}</a>{%- endif -%}
|
|
{%- endfor -%}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="post-content">
|
|
<div class="plank even minimal secondary-bg no-shadow post-info">
|
|
<span>
|
|
{%- if tb_pretext -%}
|
|
<span>{{tb_pretext}} • </span>
|
|
{%- endif -%}
|
|
<a href="{{get_post_url(post.id, _anchor=true)}}">
|
|
{%- if post.edited_at <= post.created_at -%}
|
|
<i>Posted on {{timestamp(post.created_at)}}</i>
|
|
{%- else -%}
|
|
<i>Edited on {{timestamp(post.edited_at)}}</i>
|
|
{%- endif -%}
|
|
</a>
|
|
{%- if show_thread -%}
|
|
<span> in thread <a href="{{url_for('threads.thread_by_id', thread_id=post.thread_id)}}"> {{post.thread_title}}</a></span>
|
|
{%- endif -%}
|
|
</span>
|
|
{%- if show_toolbar -%}
|
|
<span class="subheader-actions">
|
|
{%- if owns and tb_edit -%}
|
|
<a class="linkbutton" href="{{url_for('posts.edit', post_id=post.id, _anchor='babycode-content')}}">Edit</a>
|
|
{%- endif -%}
|
|
{%- if can_reply and tb_quote -%}
|
|
<button autocomplete='off' data-r="enhance" data-s="babycodeEditorQuote" disabled title="This feature requires JavaScript to be enabled." data-quote="{{post.original_markup}}" data-poster-name="{{ post.display_name if post.display_name else post.username }}">Quote</button>
|
|
{%- endif -%}
|
|
{%- if can_delete and tb_delete -%}
|
|
<a class="linkbutton critical" href="{{url_for('posts.delete', post_id=post.id)}}">Delete</a>
|
|
{%- endif -%}
|
|
{%- if tb_bookmark -%}
|
|
{{ bookmark_button('post', post.id, bookmark_btn) }}
|
|
{%- endif -%}
|
|
</span>
|
|
{%- endif -%}
|
|
</div>
|
|
<div class="plank even no-shadow post-content-inner minimal">
|
|
{%- if not is_editing -%}
|
|
{{post.content | safe}}
|
|
{%- if render_sig and post.signature_rendered -%}
|
|
<aside class="post-signature">{{post.signature_rendered | safe}}</aside>
|
|
{%- endif -%}
|
|
{%- else -%}
|
|
{{- babycode_editor_component(prefill=post.original_markup) -}}
|
|
{%- endif -%}
|
|
</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>
|
|
{%- 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 -%}
|
|
{%- elif is_editing -%}
|
|
<input type="submit" value="Save">
|
|
<a href="{{get_post_url(post.id, _anchor=true)}}" class="linkbutton warn">Cancel</a>
|
|
{%- endif -%}
|
|
</div>
|
|
|
|
</div>
|
|
{%- endmacro %}
|
|
|
|
{% macro bookmark_menu() -%}
|
|
{%- if is_logged_in() -%}
|
|
<div id="bookmark-popover" data-r="showBookmarkMenu" class="plank even" popover>
|
|
<div class="bookmark-menu-header">
|
|
<span>Bookmark collections</span>
|
|
<a href="{{url_for('users.bookmarks', username=get_active_user().username)}}">View bookmarks</a>
|
|
</div>
|
|
<div class="bookmark-menu-inner" data-r="fillBookmarkMenu">Loading…</div>
|
|
</div>
|
|
{%- endif -%}
|
|
{%- endmacro %}
|
|
|
|
{% macro infobox(message, kind=InfoboxKind.INFO) -%}
|
|
<div class="infobox plank top contain-svg horizontal {{InfoboxHTMLClass[kind]}}">
|
|
{%- if kind == InfoboxKind.INFO -%}
|
|
{{- icn_info() -}}
|
|
{%- elif kind == InfoboxKind.WARN -%}
|
|
{{- icn_warn() -}}
|
|
{%- elif kind == InfoboxKind.ERROR -%}
|
|
{{- icn_error() -}}
|
|
{%- endif -%}
|
|
{%- set m = message.split(';', maxsplit=1) -%}
|
|
<strong>{{m[0]}}</strong>
|
|
{%- if m[1] -%}
|
|
{{m[1]}}
|
|
{%- endif -%}
|
|
</div>
|
|
{%- endmacro %}
|
|
|
|
{% macro motd(motd_objs) -%}
|
|
{%- if motd_objs -%}
|
|
<div class="motd plank contrast-bg">
|
|
<div class="contain-svg">
|
|
{{ icn_megaphone(64) }}
|
|
<i><abbr title="Message of the Day">MOTD</abbr></i>
|
|
</div>
|
|
<div class="motd-content mobile-fill-flex">
|
|
<h2 class="info">{{ motd_objs[0].title }}</h2>
|
|
<div class="motd-body">{{ motd_objs[0].body_rendered | safe }}</div>
|
|
</div>
|
|
</div>
|
|
{%- endif -%}
|
|
{%- endmacro %}
|
|
|
|
{% macro sortable_list(attr=none) -%}
|
|
<ol class="sortable-list plank even no-shadow minimal tertiary-bg" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
|
{%- if caller -%}
|
|
{{ caller() }}
|
|
{%- endif -%}
|
|
</ol>
|
|
{%- endmacro %}
|
|
|
|
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
|
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'secondary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
|
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
|
<div class="sortable-item-inner">{{ caller() }}</div>
|
|
</li>
|
|
{%- endmacro %}
|