diff --git a/briar-gtk/briar_gtk/presenters/main_window.py b/briar-gtk/briar_gtk/presenters/main_window.py index cb81cd1c3771d3369fecc1042098fa38040645e7..4bafce689ef4051c09b4bbee3c19e7716b35c573 100644 --- a/briar-gtk/briar_gtk/presenters/main_window.py +++ b/briar-gtk/briar_gtk/presenters/main_window.py @@ -4,7 +4,6 @@ from briar_gtk.handlers.notification import NotificationHandler from briar_gtk.presenters.private_chat import PrivateChatPresenter -from briar_gtk.presenters.sidebar import SidebarPresenter from briar_gtk.define import APP from briar_gtk.views.private_chat import PrivateChatView from briar_gtk.views.sidebar import SidebarView @@ -13,12 +12,13 @@ from briar_gtk.widgets.about_dialog import AboutDialogWidget class MainWindowPresenter: - def __init__(self, main_window_view, builder): - self._main_window_view = main_window_view - self._builder = builder + def __init__(self, view): + self._notification_handler = NotificationHandler() + self._private_chat_presenter = None + self._sidebar_presenter = SidebarView(view.builder).presenter self._signals = list() + self._view = view - self._setup_children() self._setup_destroy_listener() @staticmethod @@ -27,45 +27,30 @@ class MainWindowPresenter: about_dialog.show() def open_change_contact_alias_dialog(self): - if self._private_chat_presenter is not None: + if isinstance(self._private_chat_presenter, PrivateChatPresenter): self._private_chat_presenter.open_change_contact_alias_dialog() def open_delete_all_messages_dialog(self): - if self._private_chat_presenter is not None: + if isinstance(self._private_chat_presenter, PrivateChatPresenter): self._private_chat_presenter.open_delete_all_messages_dialog() def open_delete_contact_dialog(self): - if self._private_chat_presenter is not None: + if isinstance(self._private_chat_presenter, PrivateChatPresenter): self._private_chat_presenter.open_delete_contact_dialog() def close_private_chat(self): - if self._private_chat_presenter is not None: + if isinstance(self._private_chat_presenter, PrivateChatPresenter): self._private_chat_presenter.close_private_chat() self._private_chat_presenter = None def open_private_chat(self, contact_id): - private_chat_view = PrivateChatView(self._builder) - self._private_chat_presenter = PrivateChatPresenter( - contact_id, private_chat_view, self._sidebar_presenter, - self._builder, APP().api) - - def _setup_children(self): - self._setup_notification_handler() - self._setup_sidebar_presenter() - self._private_chat_presenter = None - contact_name_label = self._builder.get_object("contact_name") - contact_name_label.set_text("") - - def _setup_notification_handler(self): - self._notification_handler = NotificationHandler() - - def _setup_sidebar_presenter(self): - sidebar_view = SidebarView(self._builder) - self._sidebar_presenter = SidebarPresenter( - sidebar_view, APP().api) + self.close_private_chat() + private_chat_view = PrivateChatView( + self._view.builder, contact_id, self._sidebar_presenter) + self._private_chat_presenter = private_chat_view.presenter def _setup_destroy_listener(self): - self._main_window_view.connect("destroy", self._on_destroy) + self._view.connect("destroy", self._on_destroy) # pylint: disable=unused-argument def _on_destroy(self, widget): diff --git a/briar-gtk/briar_gtk/presenters/private_chat.py b/briar-gtk/briar_gtk/presenters/private_chat.py index 6af1ca55abce12bed2cbcf57d0ebb254222293c5..dc9f4d1dd8cd907fb4b9dd8a84c336c467ee0924 100644 --- a/briar-gtk/briar_gtk/presenters/private_chat.py +++ b/briar-gtk/briar_gtk/presenters/private_chat.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +import time + from gettext import gettext as _ -from gi.repository import Gtk +from gi.repository import Gtk, GLib from briar_wrapper.models.contacts import Contacts from briar_wrapper.models.private_chat import PrivateChat @@ -14,48 +16,26 @@ from briar_gtk.widgets.edit_dialog import EditDialog # pylint: disable=too-many-arguments class PrivateChatPresenter: - _current_contact_id = 0 - - def __init__(self, contact_id, private_chat_view, sidebar_presenter, - builder, api): - self._private_chat_view = private_chat_view - self._sidebar_presenter = sidebar_presenter - self._builder = builder - self._api = api - self.open_private_chat(contact_id) - - def close_private_chat(self): # formerly `show_sidebar` - main_window_leaflet = self._builder.get_object("main_window_leaflet") - main_window_leaflet.set_visible_child( - self._builder.get_object("sidebar_box")) - - main_content_stack = self._builder.get_object("main_content_stack") - chat_view = main_content_stack.get_child_by_name("chat_view") - chat_view.hide() - - chat_placeholder = main_content_stack.get_child_by_name( - "chat_placeholder") - chat_placeholder.show() - self._clear_history_container() + def __init__(self, private_chat_view, sidebar_presenter): + self._signals = list() - contacts_list_box = self._builder.get_object("contacts_list_box") - contacts_list_box.unselect_all() + # TODO: Move whole sidebar presenter logic into briar_wrapper by + # notifying sidebar about changes to private chat from model + self._sidebar_presenter = sidebar_presenter - contact_name_label = self._builder.get_object("contact_name") - contact_name_label.set_text("") + self._view = private_chat_view - self._current_contact_id = 0 - self._builder.get_object("chat_menu_button").hide() + self._open_private_chat() def open_change_contact_alias_dialog(self): - if self._current_contact_id == 0: + if self._view.contact_id == 0: raise Exception("Can't change contact alias with ID 0") confirmation_dialog = EditDialog( parent=APP().window, flags=Gtk.DialogFlags.MODAL, - placeholder=self._get_contact_name(self._current_contact_id) + placeholder=self._get_contact_name(self._view.contact_id) ) confirmation_dialog.set_title(_("Change contact name")) @@ -64,14 +44,13 @@ class PrivateChatPresenter: user_alias = confirmation_dialog.get_entry().get_text() confirmation_dialog.destroy() if (response == Gtk.ResponseType.OK) and (user_alias != ''): - Contacts(APP().api).set_alias(self._current_contact_id, user_alias) - contact_name_label = self._builder.get_object("contact_name") - contact_name_label.set_text(user_alias) + Contacts(APP().api).set_alias(self._view.contact_id, user_alias) + self._set_contact_name_label(user_alias) self._sidebar_presenter.refresh_contacts() # TODO: Update name in chat history def open_delete_all_messages_dialog(self): - if self._current_contact_id == 0: + if self._view.contact_id == 0: raise Exception("Can't delete all messages with contact ID 0") confirmation_dialog = Gtk.MessageDialog( @@ -89,7 +68,7 @@ class PrivateChatPresenter: confirmation_dialog.show_all() def open_delete_contact_dialog(self): - if self._current_contact_id == 0: + if self._view.contact_id == 0: raise Exception("Can't delete contact with ID 0") confirmation_dialog = Gtk.MessageDialog( @@ -107,14 +86,30 @@ class PrivateChatPresenter: confirmation_dialog.connect("response", self._delete_contact) confirmation_dialog.show_all() - def open_private_chat(self, contact_id): - contact_name = self._get_contact_name(contact_id) + def close_private_chat(self): # formerly `show_sidebar` + main_content_stack = self._view.builder.get_object( + "main_content_stack") + self._hide_chat_view(main_content_stack) + self._show_chat_placeholder(main_content_stack) + self._show_sidebar_box() + self._clear_history_container() + self._unselect_contact() + self._set_contact_name_label("") + self._view.contact_id = 0 + self._hide_chat_menu_button() + self._disconnect_chat_entry_signals() + + def disconnect_signals(self): + for signal in self._signals: + APP().api.socket_listener.disconnect(signal) + + def _open_private_chat(self): + contact_name = self._get_contact_name(self._view.contact_id) self._prepare_chat_view(contact_name) - self._setup_private_chat_widget(contact_name, contact_id) - self._current_contact_id = contact_id + self._load_content(contact_name) @staticmethod - def _get_contact_name(contact_id): + def _get_contact_name(contact_id): # TODO: Move into briar_wrapper name = "" for contact in Contacts(APP().api).get(): if contact["contactId"] == contact_id: @@ -124,75 +119,154 @@ class PrivateChatPresenter: break return name - def _delete_all_messages(self, widget, response_id): - if response_id == Gtk.ResponseType.OK: - private_chat = PrivateChat(APP().api, self._current_contact_id) - private_chat.delete_all_messages() - self._sidebar_presenter.refresh_contacts() - self.close_private_chat() - widget.destroy() + def _prepare_chat_view(self, contact_name): + main_content_stack = self._view.builder.get_object( + "main_content_stack") - def _delete_contact(self, widget, response_id): - if response_id == Gtk.ResponseType.OK: - Contacts(APP().api).delete(self._current_contact_id) - self._sidebar_presenter.refresh_contacts() - self.close_private_chat() - widget.destroy() + self._hide_chat_placeholder(main_content_stack) + self._show_chat_view(main_content_stack) + self._show_main_content_container() + self._set_contact_name_label(contact_name) + self._show_chat_menu_button() - def _prepare_chat_view(self, contact_name): - main_content_stack = self._builder.get_object("main_content_stack") + @staticmethod + def _hide_chat_placeholder(main_content_stack): chat_placeholder = main_content_stack.get_child_by_name( "chat_placeholder") - if self._no_chat_opened(): - chat_placeholder.hide() - else: - self._clear_history_container() + chat_placeholder.hide() + @staticmethod + def _show_chat_view(main_content_stack): chat_view = main_content_stack.get_child_by_name("chat_view") chat_view.show() - main_window_leaflet = self._builder.get_object("main_window_leaflet") - main_content_container = self._builder.get_object( + + def _show_main_content_container(self): + main_window_leaflet = self._view.builder.get_object( + "main_window_leaflet") + main_content_container = self._view.builder.get_object( "main_content_container") main_window_leaflet.set_visible_child(main_content_container) - contact_name_label = self._builder.get_object("contact_name") + + def _set_contact_name_label(self, contact_name): + contact_name_label = self._view.builder.get_object("contact_name") contact_name_label.set_text(contact_name) - self._builder.get_object("chat_menu_button").show() - def _no_chat_opened(self): - main_content_stack = self._builder.get_object("main_content_stack") - chat_placeholder = main_content_stack.get_child_by_name( - "chat_placeholder") - return chat_placeholder.get_visible() + def _show_chat_menu_button(self): + chat_menu_button = self._view.builder.get_object("chat_menu_button") + chat_menu_button.show() - def _clear_history_container(self): - history_container = self._builder.get_object("history_container") - children = history_container.get_children() - for child in children: - child.destroy() + def _load_content(self, contact_name): + private_chat = PrivateChat(APP().api, self._view.contact_id) + messages = private_chat.get() + + self._view.setup_view(contact_name) + self._view.show_messages(messages) + self._setup_message_listener() + self._mark_messages_read(messages, private_chat) + self._setup_history_container() + self._setup_chat_entry() + + def _setup_message_listener(self): + # TODO: Move into briar_wrapper by adding function to PrivateChatModel + socket_listener = APP().api.socket_listener + signal_id = socket_listener.connect("ConversationMessageReceivedEvent", + self._view.add_message_async) + self._signals.append(signal_id) - def _setup_private_chat_widget(self, contact_name, contact_id): - self._private_chat_view.setup_view(contact_name, contact_id) - self._private_chat_view.load_content() - history_container = self._builder.get_object("history_container") - history_container.add(self._private_chat_view) + @staticmethod + def _mark_messages_read(messages, private_chat): + for message in messages: + if message.get("read", True) is False: + GLib.idle_add(private_chat.mark_read, message["id"]) + + def _setup_history_container(self): + history_container = self._view.builder.get_object("history_container") + history_container.add(self._view) history_container.show_all() - self._disconnect_chat_entry_signals() - chat_entry = self._builder.get_object("chat_entry") + def _setup_chat_entry(self): + chat_entry = self._view.builder.get_object("chat_entry") self._chat_entry_signal_id = chat_entry.connect( "activate", self._on_chat_entry_activate ) chat_entry.grab_focus() + @staticmethod + def _hide_chat_view(main_content_stack): + chat_view = main_content_stack.get_child_by_name("chat_view") + chat_view.hide() + + @staticmethod + def _show_chat_placeholder(main_content_stack): + chat_placeholder = main_content_stack.get_child_by_name( + "chat_placeholder") + chat_placeholder.show() + + def _unselect_contact(self): + contacts_list_box = self._view.builder.get_object("contacts_list_box") + contacts_list_box.unselect_all() + + def _hide_chat_menu_button(self): + chat_menu_button = self._view.builder.get_object("chat_menu_button") + chat_menu_button.hide() + + def _show_sidebar_box(self): + main_window_leaflet = self._view.builder.get_object( + "main_window_leaflet") + sidebar_box = self._view.builder.get_object("sidebar_box") + main_window_leaflet.set_visible_child(sidebar_box) + + def _delete_all_messages(self, widget, response_id): + if response_id == Gtk.ResponseType.OK: + private_chat = PrivateChat(APP().api, self._view.contact_id) + private_chat.delete_all_messages() + self._sidebar_presenter.refresh_contacts() + self.close_private_chat() + widget.destroy() + + def _delete_contact(self, widget, response_id): + if response_id == Gtk.ResponseType.OK: + Contacts(APP().api).delete(self._view.contact_id) + self._sidebar_presenter.refresh_contacts() + self.close_private_chat() + widget.destroy() + + def _clear_history_container(self): + history_container = self._view.builder.get_object("history_container") + children = history_container.get_children() + for child in children: + child.destroy() + def _disconnect_chat_entry_signals(self): if not hasattr(self, "_chat_entry_signal_id"): return - chat_entry = self._builder.get_object("chat_entry") + chat_entry = self._view.builder.get_object("chat_entry") chat_entry.disconnect(self._chat_entry_signal_id) del self._chat_entry_signal_id def _on_chat_entry_activate(self, widget): if len(widget.get_text()) == 0: return - self._private_chat_view.send_message(widget) + self._send_message(widget) self._sidebar_presenter.refresh_contacts() + + def _send_message(self, widget): + message = widget.get_text() + private_chat = PrivateChat(APP().api, self._view.contact_id) + private_chat.send(message) + + # TODO: Move into briar_wrapper by emitting event on private_chat.send + self._view.add_message( + { + "text": message, + "local": True, + "sent": False, + "seen": False, + + # TODO: Remove once web events updating is implemented + "no_stored_indicator": True, + + "timestamp": int(round(time.time() * 1000)) + }) + widget.set_text("") + GLib.idle_add(self._view.scroll_to_bottom) diff --git a/briar-gtk/briar_gtk/views/main_window.py b/briar-gtk/briar_gtk/views/main_window.py index 3a46d6ddc08109073c5e8fa0ae64cf9cf9217467..d063f2740609f28a8afda1829d519ffdcbe9793b 100644 --- a/briar-gtk/briar_gtk/views/main_window.py +++ b/briar-gtk/briar_gtk/views/main_window.py @@ -17,11 +17,9 @@ class MainWindowView(Gtk.Overlay): def __init__(self, window): super().__init__() - builder = self._setup_builder() - self.presenter = MainWindowPresenter(self, builder) - self._setup_view(builder, window) - self.show_all() - builder.get_object("chat_menu_button").hide() # TODO: Make default + self.builder = self._setup_builder() + self.presenter = MainWindowPresenter(self) + self._setup_view(window) def _setup_builder(self): builder = Gtk.Builder.new() @@ -37,18 +35,19 @@ class MainWindowView(Gtk.Overlay): builder.connect_signals(self) return builder - def _setup_view(self, builder, window): - self._setup_main_window_stack(builder) - self._setup_headerbar_stack_holder(builder, window) + def _setup_view(self, window): + self._setup_main_window_stack() + self._setup_header_bar_stack_holder(window) - def _setup_main_window_stack(self, builder): - main_window_stack = builder.get_object("main_window_stack") + def _setup_main_window_stack(self): + main_window_stack = self.builder.get_object("main_window_stack") main_window_stack.show_all() self.add(main_window_stack) - builder.get_object("chat_menu_button").hide() + self.show_all() - @staticmethod - def _setup_headerbar_stack_holder(builder, window): - headerbar_stack_holder = builder.get_object("headerbar_stack_holder") - headerbar_stack_holder.show_all() - window.set_titlebar(headerbar_stack_holder) + def _setup_header_bar_stack_holder(self, window): + header_bar_stack_holder = self.builder.get_object( + "headerbar_stack_holder") + header_bar_stack_holder.show_all() + self.builder.get_object("chat_menu_button").hide() + window.set_titlebar(header_bar_stack_holder) diff --git a/briar-gtk/briar_gtk/views/private_chat.py b/briar-gtk/briar_gtk/views/private_chat.py index 5833b26739802c497cd84fe3e465773a460e5208..560e0da01377f578525eb550a72258355a8fbabd 100644 --- a/briar-gtk/briar_gtk/views/private_chat.py +++ b/briar-gtk/briar_gtk/views/private_chat.py @@ -5,62 +5,89 @@ # Initial version based on GNOME Fractal # https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2 import os -import time from gi.repository import GLib, Gtk, Handy from briar_wrapper.models.private_chat import PrivateChat from briar_gtk.define import APP, RESOURCES_DIR +from briar_gtk.presenters.private_chat import PrivateChatPresenter from briar_gtk.widgets.private_message import PrivateMessageWidget # pylint: disable=too-many-instance-attributes class PrivateChatView(Gtk.Overlay): - # TODO: Move more logic into PrivateChatPresenter - CONTAINER_UI = "private_chat.ui" - def __init__(self, builder): + def __init__(self, builder, contact_id, sidebar_presenter): super().__init__() - self.builder = builder - - self._signals = list() self._contact_name = "" - self._contact_id = -1 + self._draw_signal_id = -1 + self._messages_box = None + self._messages_count = 0 self._previous_message = dict() - def send_message(self, widget): - message = widget.get_text() - private_chat = PrivateChat(APP().api, self._contact_id) - private_chat.send(message) + self.builder = builder + self.contact_id = contact_id + self.presenter = PrivateChatPresenter(self, sidebar_presenter) - # TODO: Doesn't work after Ctrl + W - self._add_message( - { - "text": message, - "local": True, - "sent": False, - "seen": False, + def setup_view(self, contact_name): + self._contact_name = contact_name + self._setup_builder() - # TODO: Remove once web events updating is implemented - "no_stored_indicator": True, + self._setup_messages_box() + clamp = self._setup_clamp() + self._setup_messages_column(clamp) + self._setup_messages_scroll() - "timestamp": int(round(time.time() * 1000)) - }) - widget.set_text("") - GLib.idle_add(self._scroll_to_bottom) + self.builder.connect_signals(self) + self._setup_destroy_listener() - def setup_view(self, contact_name, contact_id): - self._contact_name = contact_name - self._contact_id = contact_id - self._add_from_resource(self.CONTAINER_UI) + def show_messages(self, messages): + self._messages_count = len(messages) + for message in messages: + # Abusing idle_add function here because otherwise the message box + # is too small and scrolling cuts out messages + GLib.idle_add(self.add_message, message) + + def add_message(self, message): + if self._is_not_message(message): + return + message_widget = PrivateMessageWidget( + self._contact_name, + message, + self._previous_message + ) + self._previous_message = message + self._messages_box.add(message_widget) + + def scroll_to_bottom(self): + messages_scroll = self.builder.get_object("messages_scroll") + adjustment = messages_scroll.get_vadjustment() + adjustment.set_value( + adjustment.get_upper() - adjustment.get_page_size() + ) + + def add_message_async(self, message): + if message["data"]["contactId"] != self.contact_id: + return + GLib.idle_add(self._add_message_and_scroll, message["data"]) + if message["data"].get("read", True) is False: + private_chat = PrivateChat(APP().api, self.contact_id) + GLib.idle_add(private_chat.mark_read, message["data"]["id"]) + + def _setup_builder(self): + self.builder.add_from_resource( + os.path.join(RESOURCES_DIR, self.CONTAINER_UI) + ) + def _setup_messages_box(self): self._messages_box = Gtk.ListBox() self._messages_box.get_style_context().add_class("messages-history") self._messages_box.show() + def _setup_clamp(self): clamp = Handy.Clamp.new() clamp.set_maximum_size(800) clamp.set_tightening_threshold(600) @@ -68,92 +95,42 @@ class PrivateChatView(Gtk.Overlay): clamp.set_vexpand(True) clamp.add(self._messages_box) clamp.show() + return clamp + def _setup_messages_column(self, clamp): messages_column = self.builder.get_object("messages_column") messages_column.get_style_context().add_class("messages-box") messages_column.add(clamp) messages_column.show() + def _setup_messages_scroll(self): # TODO: (Hopefully) remove hack in GTK 4 messages_scroll = self.builder.get_object("messages_scroll") self._draw_signal_id = messages_scroll.connect( "draw", self._on_message_scroll_draw ) self.add(messages_scroll) - self.builder.connect_signals(self) - self._setup_destroy_listener() - - def _add_from_resource(self, ui_filename): - self.builder.add_from_resource( - os.path.join(RESOURCES_DIR, ui_filename) - ) - def _setup_destroy_listener(self): self.connect("destroy", self._on_destroy) # pylint: disable=unused-argument def _on_destroy(self, widget): - self._disconnect_signals() - - def _disconnect_signals(self): - for signal in self._signals: - APP().api.socket_listener.disconnect(signal) - - def load_content(self): - private_chat = PrivateChat(APP().api, self._contact_id) - messages_list = private_chat.get() - self._messages_count = len(messages_list) - for message in messages_list: - # Abusing idle_add function here because otherwise the message box - # is too small and scrolling cuts out messages - GLib.idle_add(self._add_message, message) - if message.get("read", True) is False: - GLib.idle_add(private_chat.mark_read, message["id"]) - socket_listener = APP().api.socket_listener - signal_id = socket_listener.connect("ConversationMessageReceivedEvent", - self._add_message_async) - self._signals.append(signal_id) - - def _add_message(self, message): - if self._is_not_message(message): - return - message_widget = PrivateMessageWidget( - self._contact_name, - message, - self._previous_message - ) - self._previous_message = message - self._messages_box.add(message_widget) + self.presenter.disconnect_signals() @staticmethod def _is_not_message(message): return "text" not in message - def _add_message_async(self, message): - if message["data"]["contactId"] != self._contact_id: - return - GLib.idle_add(self._add_message_and_scroll, message["data"]) - if message["data"].get("read", True) is False: - private_chat = PrivateChat(APP().api, self._contact_id) - GLib.idle_add(private_chat.mark_read, message["data"]["id"]) - def _add_message_and_scroll(self, message): - self._add_message(message) - GLib.idle_add(self._scroll_to_bottom) + self.add_message(message) + GLib.idle_add(self.scroll_to_bottom) # pylint: disable=unused-argument def _on_message_scroll_draw(self, widget, cairo_context): - self._scroll_to_bottom() + self.scroll_to_bottom() if self._draw_signal_is_not_needed(): widget.disconnect(self._draw_signal_id) - def _scroll_to_bottom(self): - messages_scroll = self.builder.get_object("messages_scroll") - adjustment = messages_scroll.get_vadjustment() - adjustment.set_value( - adjustment.get_upper() - adjustment.get_page_size() - ) - def _draw_signal_is_not_needed(self): if self._messages_count == 0: return True diff --git a/briar-gtk/briar_gtk/views/sidebar.py b/briar-gtk/briar_gtk/views/sidebar.py index 710a7faed03b36d5ecca9e82a3a4fd5e749d5841..d425300c6c039b2f5bb632c11b6e86ea5697aa1e 100644 --- a/briar-gtk/briar_gtk/views/sidebar.py +++ b/briar-gtk/briar_gtk/views/sidebar.py @@ -1,16 +1,16 @@ # Copyright (c) 2020 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md - +from briar_gtk.define import APP +from briar_gtk.presenters.sidebar import SidebarPresenter from briar_gtk.widgets.contact_row import ContactRowWidget class SidebarView: - # TODO: Move more logic into SidebarPresenter - def __init__(self, builder): self._builder = builder + self.presenter = SidebarPresenter(self, APP().api) def show_contacts(self, contact_list, selected_contact): self._clear_contact_list()