~fabrixxm/activist

d60d68593d4e4924faf7f2df748048423e6d46b0 — fabrixxm a month ago eeb99f1
UI: reply form inline with actions
M activist/activities.py => activist/activities.py +10 -8
@@ 163,7 163,7 @@ def delete(obj:dict, **props):



def new_object(fnc:Callable, listid:str = "", **props):
def new_object(fnc:Callable, listid:str = "", **props) -> str:
    objtags = props.get('tags', [])

    if "content" in props:


@@ 201,18 201,19 @@ def new_object(fnc:Callable, listid:str = "", **props):
        db.List.append(listid, obj['id'])
    db.List.append("network", obj['id'])
    tasks.publish(activity['id'])
    return obj['id']

def note(text:str, **props):
def note(text:str, **props) -> str:
    """
    Create and send a note with 'text'

    Optional named arguments will be added as object properties
    (e.g. 'to = [list of recipients url]' or 'inReplyTo = url')
    """
    new_object(activitystream.new_note, content=text, **props)
    return new_object(activitystream.new_note, content=text, **props)


def photo(imagename:str, mediatype:str, **props):
def photo(imagename:str, mediatype:str, **props) -> str:
    """
    Create an Image object and publish it



@@ 224,17 225,17 @@ def photo(imagename:str, mediatype:str, **props):
    # but I think I should do that in web.py.
    # or create a task

    new_object(activitystream.new_image, 'photos', url=imageurl, mediaType=mediatype, **props)
    return new_object(activitystream.new_image, 'photos', url=imageurl, mediaType=mediatype, **props)


def article(title:str, text:str, **props):
def article(title:str, text:str, **props) -> str:
    """
    Create an Article with 'title' and 'text' and publish it
    """
    new_object(activitystream.new_article, 'homepage', name=title, content=text)    
    return new_object(activitystream.new_article, 'homepage', name=title, content=text)    


def share_page(url:str, title:Optional[str] = None):
def share_page(url:str, title:Optional[str] = None) -> str:

    # TODO: fetch page title (and description maybe?)
    title = title if title else url


@@ 248,6 249,7 @@ def share_page(url:str, title:Optional[str] = None):
    db.List.append("homepage", obj['id'])
    db.List.append("network", obj['id'])
    tasks.publish(activity['id'])
    return obj['id']


def follow(actordata:dict):

M activist/static/style.css => activist/static/style.css +9 -0
@@ 129,6 129,11 @@ p:last-child {
    flex-grow: 2;
}

.hidden {
    display: none;
}


/* search bar */
.searchbar {
    width: 100%;


@@ 258,6 263,10 @@ body.article article>header>h1::after {
    overflow-y: auto;
}

.obj-image .scrollview .smaller-tile:first-child {
    padding-left: 0.8rem;
}


/** grid - images in post */
.img-grid {

M activist/template.py => activist/template.py +10 -4
@@ 85,13 85,15 @@ def mentions(ref:ASRef) -> str:
    obj = db.Object.get(ref)
    if obj is None:
        return ""
    
    mentions = [""]
    # this should be there? mmh.
    # mentions = [obj.attributedTo.acct]
    mentions = []
    mentions.append(obj.attributedTo.acct)
    for m in obj.data.get('tag', []):
        if m['type'] == "Mention":
            mentions.append(acct(m['href']))
    return "@" + " @".join(m for m in mentions if m != settings.USER_ACCT)

    return " @".join(m for m in mentions if m != settings.USER_ACCT)


@app.template_filter('firstof')


@@ 148,7 150,6 @@ def urlsafe(url:str) -> str:
    return urlsafe_b64encode(url.encode("utf8")).decode("utf8")



@app.template_filter('datetime')
def fmtdatetime(dt:datetime, fmt:str = "%Y-%m-%d  %H:%M") -> str:
    """


@@ 203,6 204,11 @@ def seconds(secs:float, style:str = ""):
    return r if r else "now"


@app.template_filter('hash')
def varhash(var:Any) -> str:
    return hash(var)


@app.context_processor
def helpers():
    def class_helper(*args, **kwargs) -> str:

M activist/templates/f/actions.tpl.j2 => activist/templates/f/actions.tpl.j2 +42 -31
@@ 9,35 9,46 @@
{% set likes = obj.activities().where(type="Like").count() %}
{% set liked = obj|inlist('liked') %}
{% set conversation = obj.conversation().count() - 1 %} {# 'conversation' counts also first object #}

<form method="POST" action="{{ url_for('action') }}" class="object-action-bar"
<form method="POST" action="{{ url_for('action') }}"
        hx-post="{{ url_for('action') }}" hx-target="this" hx-swap="outerHTML">
    <a class="btn btn-link text-small text-gray" href="{{ obj.id }}" title="{{ obj.published|datetime }}" data-datetime="{{ obj.published|datetime }}">{{ obj.published|ago }}</a>

{% if g.user %}
    <input type="hidden" name="obj" value="{{ obj.id}}">
    <button title="Reshare" type="submit" name="action" value="reshare"
        class="{{ class('btn btn-link',  { 'bg-secondary': reshared  }) }}">
            {{ reshares if reshares else "" }} <i class="icon i-rotate-ccw-square"></i>
    </button>
    <button title="Like"  name="action" value="like"
        class="{{ class('btn btn-link', { 'bg-secondary': liked }) }}">
            {{ likes if likes else "" }} <i class="icon i-star"></i>
    </button>
    <a href="{{ url_for('new', uri=obj.id|urlsafe) }}" title="Reply" class="btn-btn-link" 
            hx-get="{{ url_for('new', uri=obj.id|urlsafe) }}"
            hx-on::after-request="location.hash = '#new-modal'"
            hx-target="#new-modal .modal-container" hx-swap="outerHTML">
        <i class="icon i-reply"></i>
    </a>

{% else %}
    <small title="Reshares">{{ reshares if reshares else ""  }} <i class="icon i-rotate-ccw-square"></i></small>
    <small title="Likes">{{ likes if reshares else "" }} <i class="icon i-star"></i></small>
{% endif %}

    <a href="{{ url_for('thread', uri=obj.id|urlsafe) }}"  title="Comments" class="btn  btn-link">
        {{ conversation - 1 if conversation > 1 else "" }} <i class="icon i-message-circle"></i>
    </a>

</form>
\ No newline at end of file

    <div class="object-action-bar">
        <a class="btn btn-link text-small text-gray" href="{{ obj.id }}" title="{{ obj.published|datetime }}" data-datetime="{{ obj.published|datetime }}">{{ obj.published|ago }}</a>

    {% if g.user %}
        <input type="hidden" name="obj" value="{{ obj.id}}">
        <button title="Reshare" type="submit" name="action" value="reshare"
            class="{{ class('btn btn-link',  { 'bg-secondary': reshared  }) }}">
                {{ reshares if reshares else "" }} <i class="icon i-rotate-ccw-square"></i>
        </button>
        <button title="Like"  name="action" value="like"
            class="{{ class('btn btn-link', { 'bg-secondary': liked }) }}">
                {{ likes if likes else "" }} <i class="icon i-star"></i>
        </button>
        <a href="#o{{ obj|hash }} .reply" title="Reply" class="btn-btn-link"
                onclick="event.preventDefault(); document.querySelector(this.getAttribute('href')).classList.toggle('hidden')">
            <i class="icon i-reply"></i>
        </a>

    {% else %}
        <small title="Reshares">{{ reshares if reshares else ""  }} <i class="icon i-rotate-ccw-square"></i></small>
        <small title="Likes">{{ likes if reshares else "" }} <i class="icon i-star"></i></small>
    {% endif %}

        <a href="{{ url_for('thread', uri=obj.id|urlsafe) }}"  title="Comments" class="btn btn-link">
            {{ conversation if conversation > 0 else "" }} <i class="icon i-message-circle"></i>
        </a>

    </div>


    {% if g.user %}
    <div class="reply hidden">
        <div class="form-group">
            <textarea class="form-input" id="content" name="content" placeholder="Reply" rows="3">{{ obj|mentions }}</textarea>
        </div>
        <a href="#" onclick="event.preventDefault(); this.parentElement.classList.toggle('hidden')" class="btn btn-default">Cancel</a>
        <button class="btn btn-primary" type="submit" name="action" value="reply">Reply</button>
    </div>
    {% endif %}
</form>

M activist/templates/f/object-small.tpl.j2 => activist/templates/f/object-small.tpl.j2 +1 -1
@@ 6,7 6,7 @@
{% set actor = obj.attributedTo %}
{% set avatarsize = "sm" %}

<div class="smaller-tile">
<div class="smaller-tile" id="o{{ obj|hash }}">
    {% include "f/avatar.tpl.j2" %}

    <div class="smalle-tile-main">

M activist/templates/f/object.tpl.j2 => activist/templates/f/object.tpl.j2 +2 -2
@@ 6,7 6,7 @@


{% set actor = obj.attributedTo %}
<div class="card">
<div class="card" id="o{{ obj|hash }}">
    <div class="object-activities-summary text-tiny text-gray">
        {% if obj.inReplyTo_id %}
            <i class="icon i-message-circle-reply"></i> 


@@ 20,7 20,7 @@
                'i-star-off': firstactivity.type == 'Dislike',
                'i-rotate-ccw-square': firstactivity.type == 'Announce',
            }) }}"></i>
            {{ firstactivity.type|pastaction(Announce = 'reshared') }} by <a href="{{ firstactivity.actor_id }}">{{ firstactivity.actor.data|firstof('name', 'preferredUsername') }}</a>
            {{ firstactivity.type|pastaction(Announce = 'reshared') }} by <a href="{{ firstactivity.actor_id }}">{{ firstactivity.actor.data|firstof('name', 'preferredUsername') }}</a> {{ firstactivity.published|dtrelative }}
        {% endif %}
    </div>


M activist/templates/object/Image.html.j2 => activist/templates/object/Image.html.j2 +1 -1
@@ 6,7 6,7 @@

{% block body %}
{% set actor = obj.attributedTo %}
<div class="container grid-xl obj-image">
<div class="container grid-xl obj-image" id="o{{ obj|hash }}">
    <div class="columns">
        <div class="col-8 col-md-12 imageview">
            <img src="{{ obj.data.url }}" class="img-responsive" title="{{ obj.data.name }}">

M activist/web.py => activist/web.py +16 -26
@@ 24,6 24,7 @@ from .utils import rawactivity_by_id, activityfy
from .consts import MIME_AS, MIME_HTML, AS_PUBLIC, OBJECTS, ACTORS, ACTIVITES
from . import template
from .background import Background, TaskType
from . import tasks as bgtasks

PER_PAGE = 20



@@ 327,7 328,7 @@ def network():
    #return _collection_html("network", "network.html.j2")


@app.route("/thread/<uri>")
@app.route("/thread/<uri>", methods=['GET'])
def thread(uri:str):
    uri = urlsafe_b64decode(uri.encode("utf8")).decode("utf8")
    obj = selected_obj = db.Object.get(uri)


@@ 369,44 370,33 @@ def logout():
def action():
    action = request.form.get('action')

    def _get_object_data(obj_id):
        objdata, cached = activitypub.get_remote_object(obj_id)
    obj_id = request.form.get('obj')
    objdata, cached = activitypub.get_remote_object(obj_id)

        if objdata is None or objdata['type'] == "Tombstone":
            logger.info("Action %r on %r : Object not found", action, obj_id)
            abort(400)
    if objdata is None or objdata['type'] == "Tombstone":
        logger.info("Action %r on %r : Object not found", action, obj_id)
        abort(400)

        if not cached:
            activitypub.save_recursive(objdata)
    if not cached:
        activitypub.save_recursive(objdata)

        return db.Object.from_data(objdata)
        
    obj = db.Object.from_data(objdata)
    

    redirect_url = request.form.get('redirect', url_for('network'))
    match action:
        case "like":
            obj =  _get_object_data(request.form.get('obj'))
            activities.like(obj.data)
        case "reshare":
            obj =  _get_object_data(request.form.get('obj'))
            activities.announce(obj.data)
        case "post":
            repliedObjId = request.form.get('inReplyTo')
            repliedObjData = None
            if repliedObjId:
                repliedObjData = _get_object_data(repliedObjId)

        case "reply":
            text = request.form.get('content', '')
            if text == "":
                # TODO: flash messages
                abort(400, "No text. No post. Sad life. Go back.")
            props = {}
            if repliedObjData is not None:
                props['inReplyTo'] = repliedObjData['id']
            activities.note(text, **props)

            # TODO: AAAAH
            return redirect(redirect_url)
                abort(400, "No text. No reply. Sad life. Go back.")
            props = {'inReplyTo': obj_id}
            new_obj_id = activities.note(text, **props)
            bgtasks.update_conversations(new_obj_id)
        case _:
            abort(400, "Bad action. Such shame.")