diff --git a/activitypub.py b/activitypub.py index 3646c0a..bb1ad48 100644 --- a/activitypub.py +++ b/activitypub.py @@ -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 diff --git a/app.py b/app.py index 60e786d..b215a00 100644 --- a/app.py +++ b/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/') 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') diff --git a/sass/base_theme.scss b/sass/base_theme.scss index f8c7771..eba792b 100644 --- a/sass/base_theme.scss +++ b/sass/base_theme.scss @@ -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; diff --git a/static/css/theme.css b/static/css/theme.css index a2fe8c7..6b25941 100644 --- a/static/css/theme.css +++ b/static/css/theme.css @@ -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} diff --git a/templates/new.html b/templates/new.html index 7e76445..bf658aa 100644 --- a/templates/new.html +++ b/templates/new.html @@ -5,6 +5,12 @@
{% include "header.html" %}
+{% if thread %} +

Replying to {{ content }}

+{{ utils.display_thread(thread) }} +{% else %} +

New note

+{% endif %}
diff --git a/templates/note.html b/templates/note.html index 9311c43..b682238 100644 --- a/templates/note.html +++ b/templates/note.html @@ -16,9 +16,7 @@ {% block content %}
{% 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) }}
{% endblock %} diff --git a/templates/utils.html b/templates/utils.html index 33dc7e1..b33185c 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -67,7 +67,7 @@ {% if item.meta.count_boost %}{{ item.meta.count_boost }} boosts{% endif %} {% if item.meta.count_like %}{{ item.meta.count_like }} likes{% endif %} -{% if ui %} +{% if ui and session.logged_in %} {% set aid = item.activity.object.id | quote_plus %} reply @@ -112,3 +112,13 @@
{%- 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 -%} diff --git a/utils/content_helper.py b/utils/content_helper.py index 18fabf4..b254e2b 100644 --- a/utils/content_helper.py +++ b/utils/content_helper.py @@ -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'@{username}' + link = f'@{username}' content = content.replace(mention, link) return content, tags