view cagou/plugins/plugin_wid_blog.py @ 488:beedff600d2b

blog: blog widget implementation: this patch implements a basic blog widget. The search bare can be used to change node (only node for now, will be improved to do search and all). Publication on current node can be done by pressing the pencil icon. A checkbox can be activated to use end-to-end encryption. No pagination or comments are supported for now. Due to lack of HTML rendering in Kivy, only simple formatting is supported. If item is end-2-end encrypted, a green closed locker is shown next to publication date. rel 380
author Goffi <goffi@goffi.org>
date Sat, 15 Oct 2022 20:20:10 +0200
parents
children 203755bbe0fe
line wrap: on
line source

#!/usr/bin/env python3

#desktop/mobile frontend for Libervia XMPP client
# Copyright (C) 2016-2022 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


from functools import partial
import json
from typing import Any, Dict, Optional

from kivy import properties
from kivy.metrics import sp
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from sat.core import log as logging
from sat.core.i18n import _
from sat.tools.common import data_format
from sat_frontends.bridge.bridge_frontend import BridgeException
from sat_frontends.quick_frontend import quick_widgets
from sat_frontends.tools import jid

from cagou import G
from cagou.core.menu import SideMenu

from ..core import cagou_widget
from ..core.common import SymbolButton
from ..core.constants import Const as C
from ..core.image import Image

log = logging.getLogger(__name__)

PLUGIN_INFO = {
    "name": _("blog"),
    "main": "Blog",
    "description": _("(micro)blog"),
    "icon_symbol": "pencil",
}


class SearchButton(SymbolButton):
    blog = properties.ObjectProperty()

    def on_release(self, *args):
        self.blog.header_input.dispatch('on_text_validate')


class NewPostButton(SymbolButton):
    blog = properties.ObjectProperty()

    def on_release(self, *args):
        self.blog.show_new_post_menu()


class NewPosttMenu(SideMenu):
    blog = properties.ObjectProperty()
    size_hint_close = (1, 0)
    size_hint_open = (1, 0.9)

    def _publish_cb(self, item_id: str) -> None:
        G.host.addNote(
            _("blog post published"),
            _("your blog post has been published with ID {item_id}").format(
                item_id=item_id
            )
        )
        self.blog.load_blog()

    def _publish_eb(self, exc: BridgeException) -> None:
        G.host.addNote(
            _("Problem while publish blog post"),
            _("Can't publish blog post at {node!r} from {service}: {problem}").format(
                node=self.blog.node or G.host.ns_map.get("microblog"),
                service=(
                    self.blog.service if self.blog.service
                    else G.host.profiles[self.blog.profile].whoami,
                ),
                problem=exc
            ),
            C.XMLUI_DATA_LVL_ERROR
        )

    def publish(
            self,
            title: str,
            content: str,
            e2ee: bool = False
    ) -> None:
        self.hide()
        mb_data: Dict[str, Any] = {"content_rich": content}
        if e2ee:
            mb_data["encrypted"] = True
        title = title.strip()
        if title:
            mb_data["title_rich"] = title
        G.host.bridge.mbSend(
            self.blog.service,
            self.blog.node,
            data_format.serialise(mb_data),
            self.blog.profile,
            callback=self._publish_cb,
            errback=self._publish_eb,
        )


class BlogPostAvatar(ButtonBehavior, Image):
    pass


class BlogPostWidget(BoxLayout):
    blog_data = properties.DictProperty()
    font_size = properties.NumericProperty(sp(12))
    title_font_size = properties.NumericProperty(sp(14))


class Blog(quick_widgets.QuickWidget, cagou_widget.CagouWidget):
    posts_widget = properties.ObjectProperty()
    service = properties.StringProperty()
    node = properties.StringProperty()
    use_header_input = True

    def __init__(self, host, target, profiles):
        quick_widgets.QuickWidget.__init__(self, G.host, target, profiles)
        cagou_widget.CagouWidget.__init__(self)
        search_btn = SearchButton(blog=self)
        self.headerInputAddExtra(search_btn)
        new_post_btn = NewPostButton(blog=self)
        self.headerInputAddExtra(new_post_btn)
        self.load_blog()

    def on_kv_post(self, __):
        self.bind(
            service=lambda __, value: self.load_blog(),
            node=lambda __, value: self.load_blog(),
        )

    def onHeaderInput(self):
        text = self.header_input.text.strip()
        # for now we only use text as node
        self.node = text

    def show_new_post_menu(self):
        """Show the "add a contact" menu"""
        NewPosttMenu(blog=self).show()

    def _mb_get_cb(self, blog_data_s: str) -> None:
        blog_data = json.loads(blog_data_s)
        for item in blog_data["items"]:
            self.posts_widget.add_widget(BlogPostWidget(blog_data=item))

    def _mb_get_eb(
        self,
        exc: BridgeException,
    ) -> None:
        G.host.addNote(
            _("Problem while getting blog data"),
            _("Can't get blog for {node!r} at {service}: {problem}").format(
                node=self.node or G.host.ns_map.get("microblog"),
                service=self.service if self.service else G.host.profiles[self.profile].whoami,
                problem=exc
            ),
            C.XMLUI_DATA_LVL_ERROR
        )

    def load_blog(
        self,
    ) -> None:
        """Retrieve a blog and display it"""
        extra = {}
        self.posts_widget.clear_widgets()
        G.host.bridge.mbGet(
            self.service,
            self.node,
            20,
            [],
            data_format.serialise(extra),
            self.profile,
            callback=self._mb_get_cb,
            errback=self._mb_get_eb,
        )