forked from forks/microblog.pub
Improve the replies/thread display
This commit is contained in:
parent
6f7f2ae91c
commit
63b2d2870a
8 changed files with 96 additions and 43 deletions
|
@ -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
102
app.py
|
@ -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')
|
||||||
|
|
|
@ -165,8 +165,11 @@ 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;
|
||||||
}
|
}
|
||||||
.bottom-bar a.bar-item:hover {
|
.bottom-bar a.bar-item:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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() }}">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 -%}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue