# HG changeset patch # User Goffi # Date 1665858010 -7200 # Node ID beedff600d2b23fff105a82eab1207f354be185c # Parent 38ca44d9675230a0004a9207071548904a5410f3 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 diff -r 38ca44d96752 -r beedff600d2b cagou/plugins/plugin_wid_blog.kv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/plugins/plugin_wid_blog.kv Sat Oct 15 20:20:10 2022 +0200 @@ -0,0 +1,168 @@ +# 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 . + +#:import date_fmt sat.tools.common.date_utils.date_fmt + +: + size_hint: None, 1 + symbol: "search" + width: dp(30) + font_size: dp(25) + color: 0.4, 0.4, 0.4, 1 + + +: + size_hint: None, 1 + symbol: "pencil" + width: dp(30) + font_size: dp(25) + color: 0.4, 0.4, 0.4, 1 + +: + padding: dp(20) + spacing: dp(10) + e2ee: e2ee_checkbox + Label: + size_hint: 1, None + color: 1, 1, 1, 1 + text: _("Publish a new post on {node} node of {service}").format(node=root.blog.node or "personal blog", service=root.blog.service or root.blog.profile) + text_size: root.width, None + size: self.texture_size + halign: "center" + bold: True + TextInput: + id: title + size_hint: 1, None + height: sp(30) + hint_text: _("title of your post (optional)") + TextInput: + id: content + size_hint: 1, None + height: sp(300) + hint_text: _("body of your post (markdown syntax allowed)") + BoxLayout + id: e2ee + size_hint: 1, None + padding_y: None + height: dp(25) + Widget: + CheckBox: + id: e2ee_checkbox + size_hint: None, 1 + width: dp(20) + active: False + color: 1, 1, 1, 1 + Label: + size_hint: None, None + text: _("encrypt post") + text_size: None, None + size: self.texture_size + padding_x: dp(10) + font_size: sp(15) + color: 1, 1, 1, 1 + Widget: + Button: + size_hint: 1, None + height: sp(50) + text: _("publish") + on_release: root.publish(title.text, content.text, e2ee=e2ee_checkbox.active) + Widget: + + +: + size_hint: None, None + size: dp(30), dp(30) + canvas.before: + Color: + rgba: (0.87,0.87,0.87,1) + RoundedRectangle: + radius: [dp(5)] + pos: self.pos + size: self.size + +: + size_hint: 1, None + avatar: avatar + header_box: header_box + height: self.minimum_height + orientation: "vertical" + Label: + color: 0, 0, 0, 1 + bold: True + font_size: root.title_font_size + text_size: None, None + size_hint: None, None + size: self.texture_size[0], self.texture_size[1] if root.blog_data.get("title") else 0 + opacity: 1 if root.blog_data.get("title") else 0 + padding: dp(5), 0 + text: root.blog_data.get("title", "") + BoxLayout: + id: header_box + size_hint: 1, None + height: dp(40) + BoxLayout: + orientation: 'vertical' + width: avatar.width + size_hint: None, 1 + BlogPostAvatar: + id: avatar + source: app.default_avatar + Label: + id: created_ts + color: (0, 0, 0, 1) + font_size: root.font_size + text_size: None, None + size_hint: None, None + size: self.texture_size + padding: dp(5), 0 + markup: True + valign: 'middle' + text: f"published on [b]{date_fmt(root.blog_data.get('published', 0), 'auto_day')}[/b]" + Symbol: + size_hint: None, None + height: created_ts.height + width: self.height + id: encrypted + symbol: 'lock-filled' if root.blog_data.get("encrypted") else 'lock-open' + font_size: created_ts.height + opacity: 1 if root.blog_data.get("encrypted") else 0 + color: 0.29,0.87,0.0,1 + SimpleXHTMLWidget: + size_hint: 1, None + height: self.minimum_height + xhtml: root.blog_data.get("content_xhtml") or self.escape(root.blog_data.get("content", "")) + color: (0, 0, 0, 1) + padding: dp(5), dp(5) + + +: + float_layout: float_layout + orientation: 'vertical' + posts_widget: posts_widget + FloatLayout: + id: float_layout + ScrollView: + size_hint: 1, 1 + pos_hint: {'x': 0, 'y': 0} + do_scroll_x: False + scroll_type: ['bars', 'content'] + bar_width: dp(6) + BoxLayout: + id: posts_widget + orientation: "vertical" + size_hint: 1, None + height: self.minimum_height + spacing: dp(10) diff -r 38ca44d96752 -r beedff600d2b cagou/plugins/plugin_wid_blog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/plugins/plugin_wid_blog.py Sat Oct 15 20:20:10 2022 +0200 @@ -0,0 +1,192 @@ +#!/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 . + + +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, + )