Improve the replies/thread display

This commit is contained in:
Thomas Sileo 2018-06-03 21:28:06 +02:00
parent 6f7f2ae91c
commit 63b2d2870a
8 changed files with 96 additions and 43 deletions

View file

@ -958,12 +958,15 @@ class Note(BaseActivity):
'meta.count_reply': -1, 'meta.count_reply': -1,
'meta.count_direct_reply': direct_reply, 'meta.count_direct_reply': direct_reply,
}, },
'$pull': {'meta.thread_children': self.id},
}): }):
DB.outbox.update_one({'activity.object.id': reply.id}, { DB.outbox.update_one({'activity.object.id': reply.id}, {
'$inc': { '$inc': {
'meta.count_reply': 1, 'meta.count_reply': 1,
'meta.count_direct_reply': direct_reply, 'meta.count_direct_reply': direct_reply,
}, },
'$pull': {'meta.thread_children': self.id},
}) })
direct_reply = 0 direct_reply = 0

102
app.py
View file

@ -407,6 +407,50 @@ def index():
) )
def _build_thread(data, include_children=True):
data['_requested'] = True
root_id = data['meta'].get('thread_root_parent', data['activity']['object']['id'])
thread_ids = data['meta'].get('thread_parents', [])
if include_children:
thread_ids.extend(data['meta'].get('thread_children', []))
query = {
'activity.object.id': {'$in': thread_ids},
'type': 'Create',
'meta.deleted': False, # TODO(tsileo): handle Tombstone instead of filtering them
}
# Fetch the root replies, and the children
replies = [data] + list(DB.inbox.find(query)) + list(DB.outbox.find(query))
# Index all the IDs in order to build a tree
idx = {}
for rep in replies:
rep_id = rep['activity']['object']['id']
idx[rep_id] = rep.copy()
idx[rep_id]['_nodes'] = []
# Build the tree
for rep in replies:
rep_id = rep['activity']['object']['id']
if rep_id == root_id:
continue
reply_of = rep['activity']['object']['inReplyTo']
idx[reply_of]['_nodes'].append(rep)
# Flatten the tree
thread = []
def _flatten(node, level=0):
node['_level'] = level
thread.append(node)
for snode in sorted(idx[node['activity']['object']['id']]['_nodes'], key=lambda d: d['activity']['object']['published']):
_flatten(snode, level=level+1)
_flatten(idx[root_id])
return thread
@app.route('/note/<note_id>') @app.route('/note/<note_id>')
def note_by_id(note_id): def note_by_id(note_id):
data = DB.outbox.find_one({'id': note_id}) data = DB.outbox.find_one({'id': note_id})
@ -414,39 +458,8 @@ def note_by_id(note_id):
abort(404) abort(404)
if data['meta'].get('deleted', False): if data['meta'].get('deleted', False):
abort(410) abort(410)
thread = _build_thread(data)
replies = list(DB.inbox.find({ return render_template('note.html', me=ME, thread=thread, note=data)
'type': 'Create',
'activity.object.inReplyTo': data['activity']['object']['id'],
'meta.deleted': False,
}))
# Check for "replies of replies"
others = []
for rep in replies:
for rep_reply in rep.get('meta', {}).get('replies', []):
others.append(rep_reply['id'])
if others:
# Fetch the latest versions of the "replies of replies"
replies2 = list(DB.inbox.find({
'activity.id': {'$in': others},
}))
replies.extend(replies2)
replies2 = list(DB.outbox.find({
'activity.id': {'$in': others},
}))
replies.extend(replies2)
# Re-sort everything
replies = sorted(replies, key=lambda o: o['activity']['object']['published'])
return render_template('note.html', me=ME, note=data, replies=replies)
@app.route('/nodeinfo') @app.route('/nodeinfo')
@ -707,14 +720,33 @@ def admin():
def new(): def new():
reply_id = None reply_id = None
content = '' content = ''
thread = []
if request.args.get('reply'): if request.args.get('reply'):
reply = activitypub.parse_activity(OBJECT_SERVICE.get(request.args.get('reply'))) data = DB.inbox.find_one({'activity.object.id': request.args.get('reply')})
if not data:
data = DB.outbox.find_one({'activity.object.id': request.args.get('reply')})
if not data:
abort(400)
reply = activitypub.parse_activity(data['activity'])
reply_id = reply.id reply_id = reply.id
if reply.type_enum == ActivityType.CREATE:
reply_id = reply.get_object().id
actor = reply.get_actor() actor = reply.get_actor()
domain = urlparse(actor.id).netloc domain = urlparse(actor.id).netloc
# FIXME(tsileo): if reply of reply, fetch all participants
content = f'@{actor.preferredUsername}@{domain} ' content = f'@{actor.preferredUsername}@{domain} '
thread = _build_thread(
data,
include_children=False,
)
return render_template('new.html', reply=reply_id, content=content) return render_template(
'new.html',
reply=reply_id,
content=content,
thread=thread,
)
@app.route('/notifications') @app.route('/notifications')

View file

@ -165,6 +165,9 @@ button.bar-item {
form.action-form { form.action-form {
display: inline; display: inline;
} }
.perma {
font-size: 1.25em;
}
.bottom-bar .perma-item { .bottom-bar .perma-item {
margin-right: 5px; margin-right: 5px;
} }

View file

@ -1 +1 @@
.note-container p:first-child{margin-top:0}html,body{height:100%}body{background-color:#eee;color:#111;display:flex;flex-direction:column}.base-container{flex:1 0 auto}.footer{flex-shrink:0}a,h1,h2,h3,h4,h5,h6{color:#333}a{text-decoration:none}a:hover{text-decoration:underline}.gold{color:#1d781d}#header{margin-bottom:40px}#header .title{font-size:1.2em;padding-right:15px;color:#333}#header .title:hover{text-decoration:none}#header .subtitle-username{color:#111}#header .menu{padding:20px 0 10px 0}#header .menu ul{display:inline;list-style-type:none;padding:0}#header .menu ul li{float:left;padding-right:10px;margin-bottom:10px}#header .menu a{padding:2px 7px}#header .menu a.selected{background:#1d781d;color:#eee;border-radius:2px}#header .menu a:hover{background:#1d781d;color:#eee;text-decoration:none}#container{width:90%;max-width:720px;margin:40px auto}#container #notes{margin-top:20px}.actor-box{display:block;text-decoration:none;margin-bottom:40px}.actor-box .actor-icon{width:100%;max-width:120px;border-radius:2px}.actor-box h3{margin:0}.note{display:flex;margin-bottom:70px}.note .l{color:#333}.note .h-card{flex:initial;width:50px}.note .u-photo{width:50px;border-radius:2px}.note .note-wrapper{flex:1;padding-left:15px}.note .bottom-bar{margin-top:10px}.note .img-attachment{max-width:100%;border-radius:2px}.note h3{font-size:1.1em;color:#555}.note strong{font-weight:600}.note .note-container{clear:right;padding:10px 0}.bar-item{background:#ddd;padding:5px;color:#555;margin-right:5px;border-radius:2px}button.bar-item{border:0}form.action-form{display:inline}.bottom-bar .perma-item{margin-right:5px}.bottom-bar a.bar-item:hover{text-decoration:none}.footer>div{width:90%;max-width:720px;margin:40px auto}.footer a,.footer a:hover,.footer a:visited{text-decoration:underline;color:#111}.summary{color:#111;font-size:1.3em;margin-top:50px;margin-bottom:70px}.summary a,.summay a:hover{color:#111;text-decoration:underline}#followers,#following,#new{margin-top:50px}#admin{margin-top:50px}textarea,input{background:#ddd;padding:10px;color:#555;border:0px;border-radius:2px}input{padding:10px}input[type=submit]{color:#1d781d;text-transform:uppercase} .note-container p:first-child{margin-top:0}html,body{height:100%}body{background-color:#eee;color:#111;display:flex;flex-direction:column}.base-container{flex:1 0 auto}.footer{flex-shrink:0}a,h1,h2,h3,h4,h5,h6{color:#333}a{text-decoration:none}a:hover{text-decoration:underline}.gold{color:#1d781d}#header{margin-bottom:40px}#header .title{font-size:1.2em;padding-right:15px;color:#333}#header .title:hover{text-decoration:none}#header .subtitle-username{color:#111}#header .menu{padding:20px 0 10px 0}#header .menu ul{display:inline;list-style-type:none;padding:0}#header .menu ul li{float:left;padding-right:10px;margin-bottom:10px}#header .menu a{padding:2px 7px}#header .menu a.selected{background:#1d781d;color:#eee;border-radius:2px}#header .menu a:hover{background:#1d781d;color:#eee;text-decoration:none}#container{width:90%;max-width:720px;margin:40px auto}#container #notes{margin-top:20px}.actor-box{display:block;text-decoration:none;margin-bottom:40px}.actor-box .actor-icon{width:100%;max-width:120px;border-radius:2px}.actor-box h3{margin:0}.note{display:flex;margin-bottom:70px}.note .l{color:#333}.note .h-card{flex:initial;width:50px}.note .u-photo{width:50px;border-radius:2px}.note .note-wrapper{flex:1;padding-left:15px}.note .bottom-bar{margin-top:10px}.note .img-attachment{max-width:100%;border-radius:2px}.note h3{font-size:1.1em;color:#555}.note strong{font-weight:600}.note .note-container{clear:right;padding:10px 0}.bar-item{background:#ddd;padding:5px;color:#555;margin-right:5px;border-radius:2px}button.bar-item{border:0}form.action-form{display:inline}.perma{font-size:1.25em}.bottom-bar .perma-item{margin-right:5px}.bottom-bar a.bar-item:hover{text-decoration:none}.footer>div{width:90%;max-width:720px;margin:40px auto}.footer a,.footer a:hover,.footer a:visited{text-decoration:underline;color:#111}.summary{color:#111;font-size:1.3em;margin-top:50px;margin-bottom:70px}.summary a,.summay a:hover{color:#111;text-decoration:underline}#followers,#following,#new{margin-top:50px}#admin{margin-top:50px}textarea,input{background:#ddd;padding:10px;color:#555;border:0px;border-radius:2px}input{padding:10px}input[type=submit]{color:#1d781d;text-transform:uppercase}

View file

@ -5,6 +5,12 @@
<div id="container"> <div id="container">
{% include "header.html" %} {% include "header.html" %}
<div id="new"> <div id="new">
{% if thread %}
<h3 style="padding-bottom: 30px">Replying to {{ content }}</h3>
{{ utils.display_thread(thread) }}
{% else %}
<h3 style="padding-bottom:20px;">New note</h3>
{% endif %}
<form action="/api/new_note" method="POST"> <form action="/api/new_note" method="POST">
<input type="hidden" name="redirect" value="/"> <input type="hidden" name="redirect" value="/">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

View file

@ -16,9 +16,7 @@
{% block content %} {% block content %}
<div id="container"> <div id="container">
{% include "header.html" %} {% include "header.html" %}
{{ utils.display_note(note, perma=True) }} {{ thread }}
{% for reply in replies %} {{ utils.display_thread(thread) }}
{{ utils.display_note(reply, perma=False) }}
{% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -67,7 +67,7 @@
{% if item.meta.count_boost %}<a class ="bar-item" href="{{ item.activity.object.url }}">{{ item.meta.count_boost }} boosts</a>{% endif %} {% if item.meta.count_boost %}<a class ="bar-item" href="{{ item.activity.object.url }}">{{ item.meta.count_boost }} boosts</a>{% endif %}
{% if item.meta.count_like %}<a class ="bar-item" href="{{ item.activity.object.url }}">{{ item.meta.count_like }} likes</a>{% endif %} {% if item.meta.count_like %}<a class ="bar-item" href="{{ item.activity.object.url }}">{{ item.meta.count_like }} likes</a>{% endif %}
{% if ui %} {% if ui and session.logged_in %}
{% set aid = item.activity.object.id | quote_plus %} {% set aid = item.activity.object.id | quote_plus %}
<a class="bar-item" href="/new?reply={{ aid }}">reply</a> <a class="bar-item" href="/new?reply={{ aid }}">reply</a>
@ -112,3 +112,13 @@
</div> </div>
{%- endmacro %} {%- endmacro %}
{% macro display_thread(thread) -%}
{% for reply in thread %}
{% if reply._requested %}
{{ display_note(reply, perma=True, ui=False) }}
{% else %}
{{ display_note(reply, perma=False, ui=True) }}
{% endif %}
{% endfor %}
{% endmacro -%}

View file

@ -40,8 +40,9 @@ def mentionify(content: str) -> Tuple[str, List[Dict[str, str]]]:
_, username, domain = mention.split('@') _, username, domain = mention.split('@')
actor_url = get_actor_url(mention) actor_url = get_actor_url(mention)
p = ACTOR_SERVICE.get(actor_url) p = ACTOR_SERVICE.get(actor_url)
print(p)
tags.append(dict(type='Mention', href=p['id'], name=mention)) tags.append(dict(type='Mention', href=p['id'], name=mention))
link = f'<span class="h-card"><a href="{p.url}" class="u-url mention">@<span>{username}</span></a></span>' link = f'<span class="h-card"><a href="{p["url"]}" class="u-url mention">@<span>{username}</span></a></span>'
content = content.replace(mention, link) content = content.replace(mention, link)
return content, tags return content, tags