From 50a23afe7a293fe358c563737ea4b6137cae1c8c Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 8 Jan 2019 10:40:58 -0600 Subject: [PATCH] Moved basic post displaying logic to MVP. Comments and audio still are not moved --- src/controller/buffers.py | 8 +- src/controller/mainController.py | 4 +- src/interactors/__init__.py | 1 + src/interactors/postCreation.py | 6 +- src/interactors/postDisplayer.py | 98 ++++++++++ src/presenters/__init__.py | 1 + src/presenters/postCreation.py | 4 +- src/presenters/postDisplayer.py | 323 +++++++++++++++++++++++++++++++ 8 files changed, 434 insertions(+), 11 deletions(-) create mode 100644 src/interactors/postDisplayer.py create mode 100644 src/presenters/postDisplayer.py diff --git a/src/controller/buffers.py b/src/controller/buffers.py index 73465e5..bb013eb 100644 --- a/src/controller/buffers.py +++ b/src/controller/buffers.py @@ -120,7 +120,7 @@ class baseBuffer(object): """ Create a post in the current user's wall. This process is handled in two parts. This is the first part, where the GUI is created and user can send the post. During the second part (threaded), the post will be sent to the API.""" - p = presenters.postPresenter(session=self.session, interactor=interactors.postInteractor(), view=views.post(title=_("Write your post"), message="", text="")) + p = presenters.createPostPresenter(session=self.session, interactor=interactors.postInteractor(), view=views.post(title=_("Write your post"), message="", text="")) if hasattr(p, "text") or hasattr(p, "privacy"): call_threaded(self.do_last, p=p) @@ -265,7 +265,7 @@ class baseBuffer(object): post = self.get_post() if post == None: return - comment = presenters.postPresenter(session=self.session, interactor=interactors.postInteractor(), view=views.post(title=_("Add a comment"), message="", text="", mode="comment")) + comment = presenters.createPostPresenter(session=self.session, interactor=interactors.postInteractor(), view=views.post(title=_("Add a comment"), message="", text="", mode="comment")) if hasattr(comment, "text") or hasattr(comment, "privacy"): msg = comment.text try: @@ -347,7 +347,7 @@ class baseBuffer(object): elif "type" in post and post["type"] == "friend": pub.sendMessage("open-post", post_object=post, controller_="friendship") else: - pub.sendMessage("open-post", post_object=post, controller_="postController") + pub.sendMessage("open-post", post_object=post, controller_="displayPostPresenter") def pause_audio(self, *args, **kwargs): """ pauses audio playback.""" @@ -444,7 +444,7 @@ class feedBuffer(baseBuffer): return super(feedBuffer, self).post() owner_id = self.kwargs["owner_id"] user = self.session.get_user_name(owner_id) - p = presenters.postPresenter(session=self.session, interactor=interactors.postInteractor(), view=views.post(title=_("Write your post"), message="", text="")) + p = presenters.createPostPresenter(session=self.session, interactor=interactors.postInteractor(), view=views.post(title=_("Write your post"), message="", text="")) if hasattr(p, "text") or hasattr(p, "privacy"): call_threaded(self.do_last, p=p, owner_id=owner_id) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 37d06c4..30dfa71 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -20,7 +20,7 @@ from mysc import localization from sessionmanager import session, utils, renderers from wxUI import (mainWindow, commonMessages) from wxUI.dialogs import search as searchDialogs -from wxUI.dialogs import timeline, creation +from wxUI.dialogs import timeline, creation, postDialogs from update import updater from issueReporter import issueReporter from . import buffers @@ -253,7 +253,7 @@ class Controller(object): player.player.play_all(audios, shuffle=self.window.player_shuffle.IsChecked()) def view_post(self, post_object, controller_): - p = getattr(posts, controller_)(self.session, post_object) + p = getattr(presenters, controller_)(session=self.session, postObject=post_object, interactor=interactors.displayPostInteractor(), view=postDialogs.post()) p.dialog.get_response() p.dialog.Destroy() diff --git a/src/interactors/__init__.py b/src/interactors/__init__.py index 6860927..1586425 100644 --- a/src/interactors/__init__.py +++ b/src/interactors/__init__.py @@ -2,4 +2,5 @@ from .attach import * from . audioRecorder import * from .configuration import * from .postCreation import * +from .postDisplayer import * from .profiles import * \ No newline at end of file diff --git a/src/interactors/postCreation.py b/src/interactors/postCreation.py index 031336c..cb2c7e8 100644 --- a/src/interactors/postCreation.py +++ b/src/interactors/postCreation.py @@ -6,7 +6,7 @@ from pubsub import pub from extra import translator from .import base -class postInteractor(base.baseInteractor): +class createPostInteractor(base.baseInteractor): def set(self, control, value): if not hasattr(self.view, control): @@ -17,7 +17,7 @@ class postInteractor(base.baseInteractor): self.view.text.SetValue(self.view.text.GetValue()+", ".join(users)) def install(self, *args, **kwargs): - super(postInteractor, self).install(*args, **kwargs) + super(createPostInteractor, self).install(*args, **kwargs) widgetUtils.connect_event(self.view.spellcheck, widgetUtils.BUTTON_PRESSED, self.on_spellcheck) widgetUtils.connect_event(self.view.translateButton, widgetUtils.BUTTON_PRESSED, self.on_translate) widgetUtils.connect_event(self.view.mention, widgetUtils.BUTTON_PRESSED, self.on_mention) @@ -27,7 +27,7 @@ class postInteractor(base.baseInteractor): pub.subscribe(self.add_tagged_users, self.modulename+"_add_tagged_users") def uninstall(self): - super(postInteractor, self).uninstall() + super(createPostInteractor, self).uninstall() pub.unsubscribe(self.set, self.modulename+"_set") pub.unsubscribe(self.add_tagged_users, self.modulename+"_add_tagged_users") diff --git a/src/interactors/postDisplayer.py b/src/interactors/postDisplayer.py new file mode 100644 index 0000000..73e054e --- /dev/null +++ b/src/interactors/postDisplayer.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import six +import widgetUtils +import wx +from pubsub import pub +from wxUI.dialogs import postDialogs, urlList +from wxUI import menus +from .import base + +class displayPostInteractor(base.baseInteractor): + + def set(self, control, value): + if not hasattr(self.view, control): + raise AttributeError("The control is not present in the view.") + getattr(self.view, control).SetValue(value) + + def load_image(self, image): + image = wx.Image(stream=six.BytesIO(image.content)) + try: + self.view.image.SetBitmap(wx.Bitmap(image)) + except ValueError: + return + self.view.panel.Layout() + + def add_items(self, control, items): + if not hasattr(self.view, control): + raise AttributeError("The control is not present in the view.") + for i in items: + getattr(self.view, control).insert_item(False, *i) + + def enable_attachments(self): + self.view.attachments.list.Enable(True) + + def enable_photo_controls(self, navigation): + self.view.enable_photo_controls(navigation) + + def clean_list(self, list): + if not hasattr(self.view, list): + raise AttributeError("The control is not present in the view.") + getattr(self.view, control).clear() + + def install(self, *args, **kwargs): + super(displayPostInteractor, self).install(*args, **kwargs) + self.view.comments.list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_show_comment) + widgetUtils.connect_event(self.view.like, widgetUtils.BUTTON_PRESSED, self.on_like) + widgetUtils.connect_event(self.view.comment, widgetUtils.BUTTON_PRESSED, self.on_add_comment) + widgetUtils.connect_event(self.view.tools, widgetUtils.BUTTON_PRESSED, self.on_show_tools_menu) + widgetUtils.connect_event(self.view.repost, widgetUtils.BUTTON_PRESSED, self.on_repost) +# self.view.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_show_menu, self.view.comments.list) +# self.view.Bind(wx.EVT_LIST_KEY_DOWN, self.on_show_menu_by_key, self.view.comments.list) + pub.subscribe(self.set, self.modulename+"_set") + pub.subscribe(self.load_image, self.modulename+"_load_image") + pub.subscribe(self.add_items, self.modulename+"_add_items") + pub.subscribe(self.enable_attachments, self.modulename+"_enable_attachments") + pub.subscribe(self.enable_photo_controls, self.modulename+"_enable_photo_controls") + + def uninstall(self): + pub.unsubscribe(self.set, self.modulename+"_set") + pub.unsubscribe(self.load_image, self.modulename+"_load_image") + pub.unsubscribe(self.add_items, self.modulename+"_add_items") + pub.unsubscribe(self.enable_attachments, self.modulename+"_enable_attachments") + pub.unsubscribe(self.enable_photo_controls, self.modulename+"_enable_photo_controls") + + def on_like(self, *args, **kwargs): + self.presenter.post_like() + + def on_repost(self, *args, **kwargs): + self.presenter.post_repost() + + def on_add_comment(self, *args, **kwargs): + self.presenter.add_comment() + + def on_show_tools_menu(self, *args, **kwargs): + menu = menus.toolsMenu() + widgetUtils.connect_event(self.view, widgetUtils.MENU, self.on_open_url, menuitem=menu.url) + widgetUtils.connect_event(self.view, widgetUtils.MENU, self.on_translate, menuitem=menu.translate) + widgetUtils.connect_event(self.view, widgetUtils.MENU, self.on_spellcheck, menuitem=menu.CheckSpelling) + self.view.PopupMenu(menu, self.view.tools.GetPosition()) + + def on_open_url(self, *args, **kwargs): + pass + + def on_translate(self, *args, **kwargs): + dlg = translator.gui.translateDialog() + if dlg.get_response() == widgetUtils.OK: + text_to_translate = self.view.get_text() + dest = [x[0] for x in translator.translator.available_languages()][dlg.get("dest_lang")] + self.presenter.translate(text_to_translate, dest) + dlg.Destroy() + + def on_spellcheck(self, event=None): + text = self.view.get_text() + self.presenter.spellcheck(text) + + def on_show_comment(self, *args, **kwargs): + comment = self.view.comments.get_selected() + self.presenter.show_comment(comment) \ No newline at end of file diff --git a/src/presenters/__init__.py b/src/presenters/__init__.py index cab6a51..7e7e8ea 100644 --- a/src/presenters/__init__.py +++ b/src/presenters/__init__.py @@ -14,5 +14,6 @@ from .attach import * from .audioRecorder import * from .postCreation import * +from .postDisplayer import * from .configuration import * from .profiles import * \ No newline at end of file diff --git a/src/presenters/postCreation.py b/src/presenters/postCreation.py index 96f928a..0d45435 100644 --- a/src/presenters/postCreation.py +++ b/src/presenters/postCreation.py @@ -12,10 +12,10 @@ from .import base log = getLogger("controller.message") -class postPresenter(base.basePresenter): +class createPostPresenter(base.basePresenter): def __init__(self, session, view, interactor): - super(postPresenter, self).__init__(view=view, interactor=interactor, modulename="messages") + super(createPostPresenter, self).__init__(view=view, interactor=interactor, modulename="create_post") self.session = session self.images = [] self.tagged_people = [] diff --git a/src/presenters/postDisplayer.py b/src/presenters/postDisplayer.py new file mode 100644 index 0000000..58fe30c --- /dev/null +++ b/src/presenters/postDisplayer.py @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import re +import os +import six +import threading +import arrow +import requests +import languageHandler +import views +import interactors +import output +import webbrowser +import logging +from sessionmanager import session, renderers, utils # We'll use some functions from there +from pubsub import pub +from extra import SpellChecker, translator +from controller.posts import comment, audio +from mysc.thread_utils import call_threaded +from .import base +from .postCreation import createPostPresenter + +def get_user(id, profiles): + """ Returns an user name and last name based in the id receibed.""" + for i in profiles: + if i["id"] == id: + return "{0} {1}".format(i["first_name"], i["last_name"]) + # Translators: This string is used when socializer can't find the right user information. + return _("Unknown username") + +def get_message(status): + message = "" + if "text" in status: + message = renderers.clean_text(status["text"]) + return message + +class displayPostPresenter(base.basePresenter): + """ Base class for post representation.""" + + def __init__(self, session, postObject, view, interactor): + super(displayPostPresenter, self).__init__(view=view, interactor=interactor, modulename="display_post") + self.session = session + self.post = postObject + # Posts from newsfeed contains this source_id instead from_id in walls. Also it uses post_id and walls use just id. + if "source_id" in self.post: + self.user_identifier = "source_id" + self.post_identifier = "post_id" + else: + # In wall's posts, if someone has posted in user's wall, owner_id should be used instead from_id + # This will help for retrieving comments, do likes, etc. + if "owner_id" not in self.post: + self.user_identifier = "from_id" + else: + self.user_identifier = "owner_id" + self.post_identifier = "id" + self.worker = threading.Thread(target=self.load_all_components) + self.worker.finished = threading.Event() + self.worker.start() + self.attachments = [] + self.load_images = False + # We'll put images here, so it will be easier to work with them. + self.images = [] + self.imageIndex = 0 + self.run() + + def get_comments(self): + """ Get comments and insert them in a list.""" + user = self.post[self.user_identifier] + id = self.post[self.post_identifier] + self.comments = self.session.vk.client.wall.getComments(owner_id=user, post_id=id, need_likes=1, count=100, extended=1, preview_length=0) + comments_ = [] + for i in self.comments["items"]: + # If comment has a "deleted" key it should not be displayed, obviously. + if "deleted" in i: + continue + from_ = get_user(i["from_id"], self.comments["profiles"]) + if "reply_to_user" in i: + extra_info = get_user(i["reply_to_user"], self.comments["profiles"]) + from_ = _("{0} > {1}").format(from_, extra_info) + # As we set the comment reply properly in the from_ field, let's remove the first username from here if it exists. + fixed_text = re.sub("^\[id\d+\|\D+\], ", "", i["text"]) + if len(fixed_text) > 140: + text = fixed_text[:141] + else: + text = fixed_text + original_date = arrow.get(i["date"]) + created_at = original_date.humanize(locale=languageHandler.curLang[:2]) + likes = str(i["likes"]["count"]) + comments_.append((from_, text, created_at, likes)) + self.send_message("add_items", control="comments", items=comments_) + + def get_post_information(self): + from_ = self.session.get_user_name(self.post[self.user_identifier]) + if "copy_history" in self.post: + # Translators: {0} will be replaced with an user. + title = _("repost from {0}").format(from_,) + else: + if ("from_id" in self.post and "owner_id" in self.post) and (self.post["from_id"] != self.post["owner_id"]): + # Translators: {0} will be replaced with the user who is posting, and {1} with the wall owner. + title = _("Post from {0} in the {1}'s wall").format(self.session.get_user_name(self.post["from_id"]), self.session.get_user_name(self.post["owner_id"])) + else: + title = _("Post from {0}").format(from_,) + self.send_message("set_title", value=title) + message = "" + message = get_message(self.post) + if "copy_history" in self.post: + nm = "\n" + for i in self.post["copy_history"]: + nm += "{0}: {1}\n\n".format(self.session.get_user_name(i["from_id"]), get_message(i)) + self.get_attachments(i, get_message(i)) + message += nm + self.send_message("set", control="post_view", value=message) + self.get_attachments(self.post, message) + self.check_image_load() + + def get_attachments(self, post, text): + attachments = [] + if "attachments" in post: + for i in post["attachments"]: + # We don't need the photos_list attachment, so skip it. + if i["type"] == "photos_list": + continue + if i["type"] == "photo": + if self.load_images == False: self.load_images = True + self.images.append(i) + attachments.append(renderers.add_attachment(i)) + self.attachments.append(i) + # Links in text are not treated like normal attachments, so we'll have to catch and add those to the list without title + # We can't get a title because title is provided by the VK API and it will not work for links as simple text. + urls = utils.find_urls_in_text(text) + if len(urls) > 0: + links = [] + for i in urls: + links.append({"link": {"title": _("Untitled link"), "url": i}, "type": "link"}) + for i in links: + attachments.append(renderers.add_attachment(i)) + self.attachments.append(i) + if len(self.attachments) > 0: + self.send_message("enable_attachments") + self.send_message("add_items", control="attachments", items=attachments) + + def check_image_load(self): + if self.load_images and len(self.images) > 0 and self.session.settings["general"]["load_images"]: + self.send_message("enable_control", control="image") + nav = False # Disable navigation controls in photos + if len(self.images) > 1: + nav = True + self.send_message("enable_photo_controls", navigation=nav) + self.set_image(0) + + def set_next_image(self, *args, **kwargs): + if self.imageIndex < -1 or self.imageIndex == len(self.images)-1: + self.imageIndex = -1 + if len(self.images) <= self.imageIndex+1: + self.imageIndex = 0 + else: + self.imageIndex = self.imageIndex + 1 + self.set_image(self.imageIndex) + + def set_previous_image(self, *args, **kwargs): + if self.imageIndex <= 0: + self.imageIndex = len(self.images) + self.imageIndex = self.imageIndex - 1 + self.set_image(self.imageIndex) + + def set_image(self, index): + if len(self.images) < index-1: + return + # Get's photo URL. + url = self.get_photo_url(self.images[index]["photo"], "x") + if url != "": + img = requests.get(url) + self.send_message("load_image", image=img) + # Translators: {0} is the number of the current photo and {1} is the total number of photos. + output.speak(_("Loaded photo {0} of {1}").format(index+1, len(self.images))) + return + + def get_photo_url(self, photo, size="x"): + url = "" + for i in photo["sizes"]: + if i["type"] == size: + url = i["url"] + break + return url + + def load_all_components(self): + self.get_post_information() + self.get_likes() + self.get_reposts() + self.get_comments() + if self.post["comments"]["can_post"] == 0: + self.send_message("disable_control", control="comment") + if self.post["likes"]["can_like"] == 0 and self.post["likes"]["user_likes"] == 0: + self.send_message("disable_control", "like") + elif self.post["likes"]["user_likes"] == 1: + self.send_message("set_label", control="like", label=_("&Dislike")) + if self.post["likes"]["can_publish"] == 0: + self.send_message("disable_control", control="repost") + + def post_like(self): + if ("owner_id" in self.post) == False: + user = int(self.post[self.user_identifier]) + else: + user = int(self.post["owner_id"]) + id = int(self.post[self.post_identifier]) + if "type" in self.post: + type_ = self.post["type"] + else: + type_ = "post" + if self.post["likes"]["user_likes"] == 1: + l = self.session.vk.client.likes.delete(owner_id=user, item_id=id, type=type_) + output.speak(_("You don't like this")) + self.post["likes"]["count"] = l["likes"] + self.post["likes"]["user_likes"] = 2 + self.get_likes() + self.send_message("set_label", control="like", label=_("&Like")) + else: + l = self.session.vk.client.likes.add(owner_id=user, item_id=id, type=type_) + output.speak(_("You liked this")) + self.send_message("set_label", control="like", label=_("&Dislike")) + self.post["likes"]["count"] = l["likes"] + self.post["likes"]["user_likes"] = 1 + self.get_likes() + + def post_repost(self): + object_id = "wall{0}_{1}".format(self.post[self.user_identifier], self.post[self.post_identifier]) + p = createPostPresenter(session=self.session, interactor=interactors.createPostInteractor(), view=views.post(title=_("Repost"), message=_("Add your comment here"), text="", mode="comment")) + if hasattr(p, "text") or hasattr(p, "privacy"): + msg = p.text + self.session.vk.client.wall.repost(object=object_id, message=msg) + + def get_likes(self): + self.send_message("set_label", control="likes", label=_("{0} people like this").format(self.post["likes"]["count"],)) + + def get_reposts(self): + self.send_message("set_label", control="shares", label=_("Shared {0} times").format(self.post["reposts"]["count"],)) + + def add_comment(self): + comment = createPostPresenter(session=self.session, interactor=interactors.createPostInteractor(), view=views.post(title=_("Add a comment"), message="", text="", mode="comment")) + if hasattr(comment, "text") or hasattr(comment, "privacy"): + msg = comment.text + try: + user = self.post[self.user_identifier] + id = self.post[self.post_identifier] + self.session.vk.client.wall.addComment(owner_id=user, post_id=id, text=msg) + output.speak(_("You've posted a comment")) + if self.comments["count"] < 100: + self.clear_comments_list() + self.get_comments() + except Exception as msg: + log.error(msg) + + def clear_comments_list(self): + self.send_message("clear_list", list="comments") + + def show_comment(self, comment_index): + c = comment(self.session, self.comments["items"][comment_index]) + c.dialog.get_response() + + def comment_like(self, comment): + comment_id = self.comments["data"][comment]["id"] + self.session.like(comment_id) + output.speak(_("You do like this comment")) + + def comment_dislike(self, comment): + comment_id = self.comments["data"][comment]["id"] + self.session.unlike(comment_id) + output.speak(_("You don't like this comment")) + + def translate(self, text, language): + msg = translator.translator.translate(text, language) + self.send_message("set", control="post_view", value=msg) + self.send_message("focus_control", control="post_view") + output.speak(_("Translated")) + + def spellcheck(self, text): + checker = SpellChecker.spellchecker.spellChecker(text, "") + if hasattr(checker, "fixed_text"): + self.send_message("set", control="post_view", value=checker.fixed_text) + self.send_message("focus_control", control="post_view") + checker.clean() + + def open_attachment(self, index): + attachment = self.attachments[index] + if attachment["type"] == "audio": + a = audio(session=self.session, postObject=[attachment["audio"]]) + a.dialog.get_response() + a.dialog.Destroy() + if attachment["type"] == "link": + output.speak(_("Opening URL..."), True) + webbrowser.open_new_tab(attachment["link"]["url"]) + elif attachment["type"] == "doc": + output.speak(_("Opening document in web browser...")) + webbrowser.open(attachment["doc"]["url"]) + elif attachment["type"] == "video": + # it seems VK doesn't like to attach video links as normal URLS, so we'll have to + # get the full video object and use its "player" key which will open a webbrowser in their site with a player for the video. + # see https://vk.com/dev/attachments_w and and https://vk.com/dev/video.get + # However, the flash player isn't good for visually impaired people (when you press play you won't be able to close the window with alt+f4), so it could be good to use the HTML5 player. + # For firefox, see https://addons.mozilla.org/ru/firefox/addon/force-html5-video-player-at-vk/ + # May be I could use a dialogue here for inviting people to use this addon in firefox. It seems it isn't possible to use this html5 player from the player URL. + object_id = "{0}_{1}".format(attachment["video"]["owner_id"], attachment["video"]["id"]) + video_object = self.session.vk.client.video.get(owner_id=attachment["video"]["owner_id"], videos=object_id) + video_object = video_object["items"][0] + output.speak(_("Opening video in web browser..."), True) + webbrowser.open_new_tab(video_object["player"]) + elif attachment["type"] == "photo": + output.speak(_("Opening photo in web browser..."), True) + # Possible photo sizes for looking in the attachment information. Try to use the biggest photo available. + possible_sizes = [1280, 604, 130, 75] + url = "" + for i in possible_sizes: + if "photo_{0}".format(i,) in attachment["photo"]: + url = attachment["photo"]["photo_{0}".format(i,)] + break + if url != "": + webbrowser.open_new_tab(url) + else: + log.debug("Unhandled attachment: %r" % (attachment,)) + + def __del__(self): + if hasattr(self, "worker"): + self.worker.finished.set()