mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-12-22 13:14:28 +00:00
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_direct_reply': direct_reply,
|
||||
},
|
||||
'$pull': {'meta.thread_children': self.id},
|
||||
|
||||
}):
|
||||
DB.outbox.update_one({'activity.object.id': reply.id}, {
|
||||
'$inc': {
|
||||
'meta.count_reply': 1,
|
||||
'meta.count_direct_reply': direct_reply,
|
||||
},
|
||||
'$pull': {'meta.thread_children': self.id},
|
||||
})
|
||||
|
||||
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>')
|
||||
def note_by_id(note_id):
|
||||
data = DB.outbox.find_one({'id': note_id})
|
||||
|
@ -414,39 +458,8 @@ def note_by_id(note_id):
|
|||
abort(404)
|
||||
if data['meta'].get('deleted', False):
|
||||
abort(410)
|
||||
|
||||
replies = list(DB.inbox.find({
|
||||
'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)
|
||||
thread = _build_thread(data)
|
||||
return render_template('note.html', me=ME, thread=thread, note=data)
|
||||
|
||||
|
||||
@app.route('/nodeinfo')
|
||||
|
@ -707,14 +720,33 @@ def admin():
|
|||
def new():
|
||||
reply_id = None
|
||||
content = ''
|
||||
thread = []
|
||||
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
|
||||
if reply.type_enum == ActivityType.CREATE:
|
||||
reply_id = reply.get_object().id
|
||||
actor = reply.get_actor()
|
||||
domain = urlparse(actor.id).netloc
|
||||
# FIXME(tsileo): if reply of reply, fetch all participants
|
||||
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')
|
||||
|
|
|
@ -165,8 +165,11 @@ button.bar-item {
|
|||
form.action-form {
|
||||
display: inline;
|
||||
}
|
||||
.perma {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.bottom-bar .perma-item {
|
||||
margin-right:5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.bottom-bar a.bar-item:hover {
|
||||
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">
|
||||
{% include "header.html" %}
|
||||
<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">
|
||||
<input type="hidden" name="redirect" value="/">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
|
|
@ -16,9 +16,7 @@
|
|||
{% block content %}
|
||||
<div id="container">
|
||||
{% include "header.html" %}
|
||||
{{ utils.display_note(note, perma=True) }}
|
||||
{% for reply in replies %}
|
||||
{{ utils.display_note(reply, perma=False) }}
|
||||
{% endfor %}
|
||||
{{ thread }}
|
||||
{{ utils.display_thread(thread) }}
|
||||
</div>
|
||||
{% 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_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 %}
|
||||
<a class="bar-item" href="/new?reply={{ aid }}">reply</a>
|
||||
|
@ -112,3 +112,13 @@
|
|||
|
||||
</div>
|
||||
{%- 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('@')
|
||||
actor_url = get_actor_url(mention)
|
||||
p = ACTOR_SERVICE.get(actor_url)
|
||||
print(p)
|
||||
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)
|
||||
return content, tags
|
||||
|
||||
|
|
Loading…
Reference in a new issue