diff --git a/briar-gtk/briar_gtk/actions/window.py b/briar-gtk/briar_gtk/actions/window.py new file mode 100644 index 0000000000000000000000000000000000000000..aed217e19fd471938010d08b9b50b2e1cfe05b02 --- /dev/null +++ b/briar-gtk/briar_gtk/actions/window.py @@ -0,0 +1,50 @@ +# Copyright (c) 2014-2020 Cedric Bellegarde <cedric.bellegarde@adishatz.org> +# Copyright (c) 2020 Nico Alt +# SPDX-License-Identifier: AGPL-3.0-only +# License-Filename: LICENSE.md +# +# Initial version based on GNOME Lollypop +# https://gitlab.gnome.org/World/lollypop/blob/1.2.20/lollypop/application_actions.py + +from gi.repository import Gio, GLib + +from briar_gtk.containers.main import MainContainer +from briar_gtk.define import APP + + +# pylint: disable=too-few-public-methods +class WindowActions: + + def __init__(self): + self._setup_actions() + + # pylint: disable=no-member + def _setup_actions(self): + back_to_sidebar_action = Gio.SimpleAction.new( + "back-to-sidebar", None) + back_to_sidebar_action.connect("activate", self._back_to_sidebar) + APP().set_accels_for_action("win.back-to-sidebar", ["<Ctrl>w"]) + self.add_action(back_to_sidebar_action) + + open_add_contact_action = Gio.SimpleAction.new( + "open-add-contact", None) + open_add_contact_action.connect("activate", self._open_add_contact) + self.add_action(open_add_contact_action) + + open_private_chat_action = Gio.SimpleAction.new( + "open-private-chat", GLib.VariantType.new("i")) + open_private_chat_action.connect("activate", self._open_private_chat) + self.add_action(open_private_chat_action) + + # pylint: disable=unused-argument + def _back_to_sidebar(self, action, parameter): + if isinstance(self.current_container, MainContainer): + self.current_container.show_sidebar() + + # pylint: disable=unused-argument + def _open_add_contact(self, action, parameter): + self.show_add_contact_container() + + # pylint: disable=unused-argument + def _open_private_chat(self, action, contact_id): + self.current_container.open_private_chat(contact_id.get_int32()) diff --git a/briar-gtk/briar_gtk/containers/chat.py b/briar-gtk/briar_gtk/containers/chat.py deleted file mode 100644 index 11e2e0b0414c67fa6ff8a2172e7c3b14bf6b5841..0000000000000000000000000000000000000000 --- a/briar-gtk/briar_gtk/containers/chat.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2019 Nico Alt -# SPDX-License-Identifier: AGPL-3.0-only -# License-Filename: LICENSE.md - -from gi.repository import GLib, Gtk - -from briar_wrapper.models.private_chat import PrivateChat - -from briar_gtk.container import Container -from briar_gtk.define import APP - - -class ChatContainer(Container): - - CONTAINER_UI = "/app/briar/gtk/ui/chat.ui" - - def __init__(self, contact_id): - super().__init__() - self._api = APP().api - self._contact_id = contact_id - self._setup_view() - self._load_content() - - def _setup_view(self): - self.builder.add_from_resource(self.CONTAINER_UI) - self.add(self.builder.get_object("chat")) - self.builder.connect_signals(self) - chat_entry = self.builder.get_object("chat_entry") - chat_entry.connect("key-press-event", self._key_pressed) - - def _load_content(self): - private_chat = PrivateChat(self._api, self._contact_id) - messages_list = private_chat.get() - self._messages_list_box = self.builder.get_object("messages_list") - for message in messages_list: - self._add_message(message["text"], message["local"]) - private_chat.watch_messages(self._add_message_async) - - def _add_message(self, message, local): - message_label = Gtk.Label(message) - message_label.set_halign(Gtk.Align.START) - if local: - message_label.set_halign(Gtk.Align.END) - message_label.show() - self._messages_list_box.add(message_label) - - def _add_message_async(self, message): - GLib.idle_add(self._add_message, message["text"], False) - - # pylint: disable=unused-argument - def _key_pressed(self, widget, event): - if event.hardware_keycode != 36 and event.hardware_keycode != 104: - return - chat_entry = self.builder.get_object("chat_entry") - message = chat_entry.get_text() - private_chat = PrivateChat(self._api, self._contact_id) - private_chat.send(message) - - self._add_message(message, True) - chat_entry.set_text("") diff --git a/briar-gtk/briar_gtk/containers/login.py b/briar-gtk/briar_gtk/containers/login.py index 7561f00bdc4d3b76623814bc6fa2ddc38286ad07..c71fc856a189c540b2b5b03dab7361a084c97989 100644 --- a/briar-gtk/briar_gtk/containers/login.py +++ b/briar-gtk/briar_gtk/containers/login.py @@ -64,7 +64,7 @@ class LoginContainer(Container): def _login_completed(self, succeeded): function = self._login_failed if succeeded: - function = self._window.on_startup_completed + function = self._window.show_main_container GLib.idle_add(function) def _login_failed(self): diff --git a/briar-gtk/briar_gtk/containers/main.py b/briar-gtk/briar_gtk/containers/main.py index 144aa969329de9195fe568bb178a53373104a4ff..7e567ec1d8dfe8cfd8890bc97e36afacfd743f73 100644 --- a/briar-gtk/briar_gtk/containers/main.py +++ b/briar-gtk/briar_gtk/containers/main.py @@ -1,12 +1,17 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +# +# Initial version based on GNOME Fractal +# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2 -from gi.repository import GLib, Gtk +from gi.repository import GLib from briar_wrapper.models.contacts import Contacts from briar_gtk.container import Container +from briar_gtk.widgets.contact_row import ContactRowWidget +from briar_gtk.containers.private_chat import PrivateChatContainer from briar_gtk.define import APP @@ -16,32 +21,114 @@ class MainContainer(Container): def __init__(self): super().__init__() - self._api = APP().api self._setup_view() self._load_content() + @property + def main_window_leaflet(self): + return self.builder.get_object("main_window_leaflet") + + @property + def room_name_label(self): + return self.builder.get_object("room_name") + + @property + def contacts_list_box(self): + return self.builder.get_object("contacts_list_box") + + @property + def main_content_stack(self): + return self.builder.get_object("main_content_stack") + + @property + def main_content_container(self): + return self.builder.get_object("main_content_container") + + @property + def chat_placeholder(self): + return self.main_content_stack.get_child_by_name("chat_placeholder") + + @property + def chat_view(self): + return self.main_content_stack.get_child_by_name("chat_view") + + @property + def history_container(self): + return self.builder.get_object("history_container") + + @property + def chat_entry(self): + return self.builder.get_object("chat_entry") + + def open_private_chat(self, contact_id): + contact_name = self._get_contact_name(contact_id) + self._prepare_chat_view(contact_name) + self._setup_private_chat_widget(contact_name, contact_id) + + def _prepare_chat_view(self, contact_name): + if self._no_chat_opened(): + self.chat_placeholder.hide() + else: + self._clear_history_container() + + self.chat_view.show() + self.main_window_leaflet.set_visible_child( + self.main_content_container) + self.room_name_label.set_text(contact_name) + + def _setup_private_chat_widget(self, contact_name, contact_id): + private_chat_widget = PrivateChatContainer(contact_name, contact_id) + self.history_container.add(private_chat_widget) + self.history_container.show_all() + self.chat_entry.connect("activate", private_chat_widget.send_message) + + def _no_chat_opened(self): + return self.chat_placeholder.get_visible() + + def _get_contact_name(self, contact_id): + name = "" + for contact in self.contacts_list: + if contact["contactId"] is contact_id: + name = contact["author"]["name"] + if "alias" in contact: + name = contact["alias"] + break + return name + + def _clear_history_container(self): + children = self.history_container.get_children() + for child in children: + self.history_container.remove(child) + def _setup_view(self): self.builder.add_from_resource(self.CONTAINER_UI) - self.add(self.builder.get_object("contacts_list")) self.builder.connect_signals(self) + self._setup_main_window_stack() + self._setup_headerbar_stack_holder() + self.room_name_label.set_text("") + + 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) + + def _setup_headerbar_stack_holder(self): + headerbar_stack_holder = self.builder.get_object( + "headerbar_stack_holder") + headerbar_stack_holder.show_all() + APP().window.set_titlebar(headerbar_stack_holder) + def _load_content(self): - self._contacts = Contacts(self._api) + self._contacts = Contacts(APP().api) self._load_contacts() self._contacts.watch_contacts(self._refresh_contacts_async) def _load_contacts(self): - contacts_list = self._contacts.get() - contacts_list_box = self.builder.get_object("contacts_list") - for contact in contacts_list: - name = contact["author"]["name"] - if "alias" in contact: - name = contact["alias"] - contact_button = Gtk.Button(name) - contact_button.connect("clicked", MainContainer._contact_clicked, - contact["contactId"]) - contact_button.show() - contacts_list_box.add(contact_button) + self.contacts_list = self._contacts.get() + for contact in self.contacts_list: + contact_row = ContactRowWidget(contact) + self.contacts_list_box.add(contact_row) def _refresh_contacts_async(self): GLib.idle_add(self._refresh_contacts) @@ -51,13 +138,16 @@ class MainContainer(Container): self._load_contacts() def _clear_contact_list(self): - contacts_list_box = self.builder.get_object("contacts_list") - contacts_list_box_children = contacts_list_box.get_children() + contacts_list_box_children = self.contacts_list_box.get_children() for child in contacts_list_box_children: - contacts_list_box.remove(child) + self.contacts_list_box.remove(child) # pylint: disable=unused-argument - @staticmethod - def _contact_clicked(widget, contact_id): - GLib.idle_add(APP().get_property("active_window"). - open_private_chat, contact_id) + def show_sidebar(self): + self.main_window_leaflet.set_visible_child( + self.builder.get_object("sidebar_box")) + self.chat_view.hide() + self.chat_placeholder.show() + self._clear_history_container() + self.contacts_list_box.unselect_all() + self.room_name_label.set_text("") diff --git a/briar-gtk/briar_gtk/containers/private_chat.py b/briar-gtk/briar_gtk/containers/private_chat.py new file mode 100644 index 0000000000000000000000000000000000000000..54c63d996a5580c214c952426b6d78b1f1eddfb3 --- /dev/null +++ b/briar-gtk/briar_gtk/containers/private_chat.py @@ -0,0 +1,83 @@ +# Copyright (c) 2019 Nico Alt +# SPDX-License-Identifier: AGPL-3.0-only +# License-Filename: LICENSE.md +# +# Initial version based on GNOME Fractal +# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2 + +import time + +from gi.repository import GLib, Gtk, Handy + +from briar_wrapper.models.private_chat import PrivateChat + +from briar_gtk.container import Container +from briar_gtk.define import APP +from briar_gtk.widgets.private_message import PrivateMessageWidget + + +# pylint: disable=too-few-public-methods +class PrivateChatContainer(Container): + + CONTAINER_UI = "/app/briar/gtk/ui/private_chat.ui" + + def __init__(self, contact_name, contact_id): + super().__init__() + + self._contact_name = contact_name + self._contact_id = contact_id + + self._setup_view() + self._load_content() + + def _setup_view(self): + self.builder.add_from_resource(self.CONTAINER_UI) + + self._messages_box = Gtk.ListBox() + self._messages_box.get_style_context().add_class("messages-history") + self._messages_box.show() + + column = Handy.Column() + column.set_maximum_width(800) + column.set_linear_growth_width(600) + column.set_hexpand(True) + column.set_vexpand(True) + column.add(self._messages_box) + column.show() + + messages_column = self.builder.get_object("messages_column") + messages_column.get_style_context().add_class("messages-box") + messages_column.add(column) + messages_column.show() + + self.add(self.builder.get_object("messages_scroll")) + + self.builder.connect_signals(self) + + def _load_content(self): + private_chat = PrivateChat(APP().api, self._contact_id) + messages_list = private_chat.get() + for message in messages_list: + self._add_message(message) + private_chat.watch_messages(self._add_message_async) + + def _add_message(self, message): + message = PrivateMessageWidget(self._contact_name, message) + self._messages_box.add(message) + + def _add_message_async(self, message): + GLib.idle_add(self._add_message, message) + + # pylint: disable=unused-argument + def send_message(self, widget): + message = widget.get_text() + private_chat = PrivateChat(APP().api, self._contact_id) + private_chat.send(message) + + self._add_message( + { + "text": message, + "local": True, + "timestamp": int(round(time.time() * 1000)) + }) + widget.set_text("") diff --git a/briar-gtk/briar_gtk/containers/registration.py b/briar-gtk/briar_gtk/containers/registration.py index d98b0b5c6c0ec10f3214ac550a6c7f989778291f..e4ba235c7c623e2ae560a2a691bd2fd6c50b4009 100644 --- a/briar-gtk/briar_gtk/containers/registration.py +++ b/briar-gtk/briar_gtk/containers/registration.py @@ -124,7 +124,7 @@ class RegistrationContainer(Container): def _registration_completed(self, succeeded): function = self._registration_failed if succeeded: - function = self._window.on_startup_completed + function = self._window.show_main_container GLib.idle_add(function) def _registration_failed(self): diff --git a/briar-gtk/briar_gtk/containers/startup.py b/briar-gtk/briar_gtk/containers/startup.py index d379f0075841b4011d3347905ac681ff2a47421d..8da951c337d35c474d483541c169a720ded10120 100644 --- a/briar-gtk/briar_gtk/containers/startup.py +++ b/briar-gtk/briar_gtk/containers/startup.py @@ -19,5 +19,4 @@ class StartupContainer(Container): if APP().api.has_account(): container = LoginContainer(window) - container.show() self.add(container) diff --git a/briar-gtk/briar_gtk/toolbar.py b/briar-gtk/briar_gtk/toolbar.py deleted file mode 100644 index 3c456f6a28e0f59d804a1e629fb0d331db9a26a5..0000000000000000000000000000000000000000 --- a/briar-gtk/briar_gtk/toolbar.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2019 Nico Alt -# Copyright (c) 2014-2019 Cedric Bellegarde <cedric.bellegarde@adishatz.org> -# SPDX-License-Identifier: AGPL-3.0-only -# License-Filename: LICENSE.md -# -# Initial version based on GNOME Lollypop -# https://gitlab.gnome.org/World/lollypop/blob/1.0.12/lollypop/toolbar.py - -from gi.repository import Gtk - -from briar_gtk.define import APPLICATION_NAME - - -class Toolbar(Gtk.HeaderBar): - - TOOLBAR_UI = "/app/briar/gtk/ui/toolbar_start.ui" - - def __init__(self): - super().__init__() - self._setup_builder() - self._setup_toolbar() - - def show_add_contact_button(self, show, callback=None): - add_contact_button = self._builder.get_object("add_contact_button") - if not show: - add_contact_button.hide() - return - if callback is None: - raise Exception("Callback needed when showing add contact button") - add_contact_button.show() - add_contact_button.connect("clicked", callback) - - def show_back_button(self, show, callback=None): - back_button = self._builder.get_object("back_button") - if not show: - back_button.hide() - return - if callback is None: - raise Exception("Callback needed when showing back button") - back_button.show() - back_button.connect("clicked", callback) - - def _setup_builder(self): - self._builder = Gtk.Builder() - - def _setup_toolbar(self): - self.set_title(APPLICATION_NAME) - - self._builder.add_from_resource(self.TOOLBAR_UI) - toolbar_start = self._builder.get_object("toolbar_start") - self.pack_start(toolbar_start) diff --git a/briar-gtk/briar_gtk/widgets/contact_row.py b/briar-gtk/briar_gtk/widgets/contact_row.py new file mode 100644 index 0000000000000000000000000000000000000000..09aaa5195df98e7e71771fe13496222dc432ec50 --- /dev/null +++ b/briar-gtk/briar_gtk/widgets/contact_row.py @@ -0,0 +1,54 @@ +# Copyright (c) 2020 Nico Alt +# SPDX-License-Identifier: AGPL-3.0-only +# License-Filename: LICENSE.md +# +# Initial version based on GNOME Fractal +# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2 + +from gi.repository import GLib, Gtk, Pango + + +class ContactRowWidget(Gtk.ListBoxRow): + + def __init__(self, contact): + super().__init__() + self._setup_view(contact) + + def _setup_view(self, contact): + name = ContactRowWidget._get_contact_name(contact) + contact_label = ContactRowWidget._create_contact_label(name) + contact_box = ContactRowWidget._create_contact_box() + contact_event_box = Gtk.EventBox() + + contact_box.pack_start(contact_label, True, True, 0) + contact_event_box.add(contact_box) + self.add(contact_event_box) + + self.show_all() + self._set_action(contact["contactId"]) + + @staticmethod + def _get_contact_name(contact): + name = contact["author"]["name"] + if "alias" in contact: + name = contact["alias"] + return name + + @staticmethod + def _create_contact_label(name): + contact_label = Gtk.Label(name) + contact_label.set_valign(Gtk.Align.CENTER) + contact_label.set_halign(Gtk.Align.START) + contact_label.set_ellipsize(Pango.EllipsizeMode.END) + return contact_label + + @staticmethod + def _create_contact_box(): + contact_box = Gtk.Box(Gtk.Orientation.HORIZONTAL, 5) + contact_box.get_style_context().add_class("room-row") + return contact_box + + def _set_action(self, contact_id): + data = GLib.Variant.new_int32(contact_id) + self.set_action_target_value(data) + self.set_action_name("win.open-private-chat") diff --git a/briar-gtk/briar_gtk/widgets/private_message.py b/briar-gtk/briar_gtk/widgets/private_message.py new file mode 100644 index 0000000000000000000000000000000000000000..9c665280b41d77ca4775aab4d106549a1c78e66e --- /dev/null +++ b/briar-gtk/briar_gtk/widgets/private_message.py @@ -0,0 +1,97 @@ +# Copyright (c) 2020 Nico Alt +# SPDX-License-Identifier: AGPL-3.0-only +# License-Filename: LICENSE.md +# +# Initial version based on GNOME Fractal +# https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2 + +import datetime +from gettext import gettext as _ + +from gi.repository import Gtk + + +class PrivateMessageWidget(Gtk.ListBoxRow): + + def __init__(self, contact_name, message): + super().__init__() + self._setup_view(contact_name, message) + + def _setup_view(self, contact_name, message): + self.set_selectable(False) + self.set_margin_top(12) + + username = contact_name + if message["local"]: + username = _("Myself") + + username_info = PrivateMessageWidget._create_username_info(username) + date_info = PrivateMessageWidget._create_date_info( + message["timestamp"] / 1000) + info = PrivateMessageWidget._create_info(username_info, date_info) + + body_content = PrivateMessageWidget._create_body_content( + message["text"]) + body = PrivateMessageWidget._create_body(body_content) + + content = PrivateMessageWidget._create_content(info, body) + message_box = PrivateMessageWidget._create_message_box(content) + + event_box = Gtk.EventBox() + event_box.add(message_box) + + self.add(event_box) + self.show_all() + + @staticmethod + def _create_username_info(username): + username_label = Gtk.Label.new(username) + username_label.set_justify(Gtk.Justification.LEFT) + username_label.set_halign(Gtk.Align.START) + username_label.get_style_context().add_class("username") + + username_event_box = Gtk.EventBox() + username_event_box.add(username_label) + return username_event_box + + @staticmethod + def _create_date_info(time): + date_label = Gtk.Label.new( + datetime.datetime.fromtimestamp(time).strftime("%I:%M")) + date_label.set_justify(Gtk.Justification.RIGHT) + date_label.set_valign(Gtk.Align.START) + date_label.set_halign(Gtk.Align.END) + date_label.get_style_context().add_class("timestamp") + return date_label + + @staticmethod + def _create_info(username_info, date_info): + info = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) + info.pack_start(username_info, True, True, 0) + info.pack_end(date_info, False, False, 0) + return info + + @staticmethod + def _create_body_content(text): + body_content = Gtk.Label.new(text) + body_content.set_halign(Gtk.Align.START) + return body_content + + @staticmethod + def _create_body(body_content): + body = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + body.add(body_content) + return body + + @staticmethod + def _create_content(info, body): + content = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + content.pack_start(info, False, False, 0) + content.pack_start(body, True, True, 0) + return content + + @staticmethod + def _create_message_box(content): + message_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10) + message_box.pack_start(content, True, True, 0) + return message_box diff --git a/briar-gtk/briar_gtk/window.py b/briar-gtk/briar_gtk/window.py index bd107602bd36a545211652c63c9af1f92b07b461..d54cf4552af16435b0afb21272a985b93d1e3c9f 100644 --- a/briar-gtk/briar_gtk/window.py +++ b/briar-gtk/briar_gtk/window.py @@ -2,44 +2,45 @@ # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md -from gi.repository.Gtk import ApplicationWindow, Grid +from gi.repository import Gtk +from briar_gtk.actions.window import WindowActions from briar_gtk.containers.add_contact import AddContactContainer -from briar_gtk.containers.chat import ChatContainer from briar_gtk.containers.main import MainContainer from briar_gtk.containers.startup import StartupContainer from briar_gtk.define import APP, APPLICATION_ID, APPLICATION_NAME -from briar_gtk.toolbar import Toolbar -class Window(ApplicationWindow): +class Window(Gtk.ApplicationWindow, WindowActions): - DEFAULT_WINDOW_SIZE = (600, 400) + DEFAULT_WINDOW_SIZE = (900, 600) def __init__(self): self._initialize_gtk_application_window() + WindowActions.__init__(self) self._setup_content() - @property - def container(self): - return self._container + def show_main_container(self): + self.current_container.destroy() + self._setup_main_container() - @property - def toolbar(self): - return self._toolbar + def show_add_contact_container(self): + self.current_container.destroy() + self._setup_add_contact_container() def _initialize_gtk_application_window(self): - ApplicationWindow.__init__(self, application=APP(), - title=APPLICATION_NAME, - icon_name=APPLICATION_ID) + Gtk.ApplicationWindow.__init__(self, application=APP(), + title=APPLICATION_NAME, + icon_name=APPLICATION_ID) def _setup_content(self): - self._setup_size(self.DEFAULT_WINDOW_SIZE) + self._resize_window(self.DEFAULT_WINDOW_SIZE) self._setup_startup_container() - def _setup_size(self, size): - if Window._size_is_valid(size): - self.resize(size[0], size[1]) + def _resize_window(self, size): + if not Window._size_is_valid(size): + raise Exception("Couldn't resize window; invalid size parameter") + self.resize(size[0], size[1]) @staticmethod def _size_is_valid(size): @@ -47,63 +48,16 @@ class Window(ApplicationWindow): isinstance(size[0], int) and\ isinstance(size[1], int) - def _setup_startup_container(self): - self._container = StartupContainer(self) - self._container.show() - self.add(self._container) - - def on_startup_completed(self): - self._container.destroy() - self._setup_grid() - self._setup_toolbar() - self._setup_main_container() - - def _setup_grid(self): - self._grid = Grid() - self._grid.show() - self.add(self._grid) + def _setup_container(self, container): + self.current_container = container + self.current_container.show_all() + self.add(self.current_container) - def _reset_window(self): - self._container.destroy() - self._grid.destroy() - self._setup_grid() - self._setup_toolbar() - - def _setup_toolbar(self): - self._toolbar = Toolbar() - self._toolbar.show() - self._toolbar.set_show_close_button(True) - self.set_titlebar(self._toolbar) + def _setup_startup_container(self): + self._setup_container(StartupContainer(self)) def _setup_main_container(self): - self._container = MainContainer() - self._container.show() - self._grid.add(self._container) - self._toolbar.show_add_contact_button(True, self.show_add_contact) - - # pylint: disable=unused-argument - def show_add_contact(self, widget): - self._setup_add_contact() - - def _setup_add_contact(self): - self._grid.destroy() - self._container = AddContactContainer(self) - self._container.show() - self.add(self._container) - - def open_private_chat(self, contact_id): - self._reset_window() - self._setup_private_chat(contact_id) - - def _setup_private_chat(self, contact_id): - self._container = ChatContainer(contact_id) - self._container.show() - self._grid.add(self._container) - self._toolbar.show_back_button(True, self.back_to_main) - self._toolbar.show_add_contact_button(False) - - # pylint: disable=unused-argument - def back_to_main(self, widget): - self._reset_window() - self._toolbar.show_back_button(False) - self._setup_main_container() + self._setup_container(MainContainer()) + + def _setup_add_contact_container(self): + self._setup_container(AddContactContainer()) diff --git a/briar-gtk/data/ui/app.briar.gtk.gresource.xml b/briar-gtk/data/ui/app.briar.gtk.gresource.xml index 8cd71a21eba4b0f8b5ea6ae09d30a4773a1e2a9d..5203c52d90d8787a3f725e45575c0d505ad1848b 100644 --- a/briar-gtk/data/ui/app.briar.gtk.gresource.xml +++ b/briar-gtk/data/ui/app.briar.gtk.gresource.xml @@ -3,10 +3,9 @@ <gresource prefix="/app/briar/gtk"> <file compressed="true">ui/application.css</file> <file compressed="true" preprocess="xml-stripblanks">ui/add_contact.ui</file> - <file compressed="true" preprocess="xml-stripblanks">ui/chat.ui</file> <file compressed="true" preprocess="xml-stripblanks">ui/login.ui</file> <file compressed="true" preprocess="xml-stripblanks">ui/main.ui</file> + <file compressed="true" preprocess="xml-stripblanks">ui/private_chat.ui</file> <file compressed="true" preprocess="xml-stripblanks">ui/registration.ui</file> - <file compressed="true" preprocess="xml-stripblanks">ui/toolbar_start.ui</file> </gresource> </gresources> diff --git a/briar-gtk/data/ui/application.css b/briar-gtk/data/ui/application.css index 430e31831668894f888ddc72520079eb3a4a67dc..27852f26d76561e1c2d13ea051aadd348db28a56 100644 --- a/briar-gtk/data/ui/application.css +++ b/briar-gtk/data/ui/application.css @@ -1,4 +1,87 @@ +/** + Copyright (c) 2019 - 2020 Nico Alt + SPDX-License-Identifier: AGPL-3.0-only + License-Filename: LICENSE.md + + Based on parts of GNOME Fractal + https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/app.css +**/ + +/** Dialog like windows **/ + .error-label { color: @error_color; } +/** Main window **/ + +.messages-box { + background-color: @theme_base_color; +} + +.messages-history { + background-color: @theme_base_color; + padding: 0 18px 18px; +} + +.history-view { + background-color: @theme_base_color; +} + +.messages-history > row:not(.msg-mention) { + padding: 6px 9px; +} + +.scroll_button { + border-radius: 9999px; + -gtk-outline-radius: 9999px; +} + +.message-input-area { + padding: 6px; +} + +.message-input-focused { + border: 2px solid @theme_selected_bg_color; + padding: 5px; +} + +.messages-scroll { + background-color: @theme_base_color; + border-bottom: 1px solid @borders; +} + +.chat-placeholder-title { + font-size: larger; + opacity: 0.5; +} + +.chat-placeholder-description { + font-size: smaller; + opacity: 0.5; +} + +row .username { + font-weight: bold; + font-size: small; +} + +row .username, +row.msg-emote, +.divider { + color: @theme_selected_bg_color; +} + +row .timestamp { + font-size: small; +} + +/** Sidebar **/ +.room-row { + padding: 6px 6px; +} + +.sidebar { + border: none; +} + diff --git a/briar-gtk/data/ui/chat.ui b/briar-gtk/data/ui/chat.ui deleted file mode 100644 index e779caf6b367c92139824e8d06b8276cfdf32be4..0000000000000000000000000000000000000000 --- a/briar-gtk/data/ui/chat.ui +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - Copyright (c) 2019 Nico Alt - SPDX-License-Identifier: AGPL-3.0-only - License-Filename: LICENSE.md ---> -<interface> - <requires lib="gtk+" version="3.20"/> - <object class="GtkGrid" id="chat"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="margin_left">18</property> - <property name="margin_right">18</property> - <property name="margin_top">18</property> - <property name="margin_bottom">18</property> - <child> - <object class="GtkListBox" id="messages_list"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="chat_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="placeholder_text" translatable="yes" context="chat page: input field">Type Message</property> - <property name="show_emoji_icon">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - </object> -</interface> diff --git a/briar-gtk/data/ui/main.ui b/briar-gtk/data/ui/main.ui index ab14dc861ee6e6121025f89f0eb7c2d9e0c4f6dc..2d1bf36736f5f29e9f9db291535ba853be418693 100644 --- a/briar-gtk/data/ui/main.ui +++ b/briar-gtk/data/ui/main.ui @@ -1,22 +1,452 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - Copyright (c) 2019 Nico Alt + Copyright (c) 2019 - 2020 Nico Alt SPDX-License-Identifier: AGPL-3.0-only License-Filename: LICENSE.md + + Based on parts of GNOME Fractal + https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/ui/main_window.ui --> <interface> - <requires lib="gtk+" version="3.20"/> - <object class="GtkListBox" id="contacts_list"> - <property name="visible">True</property> + <requires lib="gtk+" version="3.22"/> + <object class="GtkStack" id="main_window_stack"> <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="margin_left">18</property> - <property name="margin_right">18</property> - <property name="margin_top">18</property> - <property name="margin_bottom">18</property> - <property name="hexpand">True</property> + <property name="hhomogeneous">False</property> + <child> + <object class="HdyLeaflet" id="main_window_leaflet"> + <property name="child-transition-duration" bind-source="header_leaflet" bind-property="child-transition-duration" bind-flags="bidirectional|sync-create"/> + <property name="transition-type" bind-source="header_leaflet" bind-property="transition-type" bind-flags="bidirectional|sync-create"/> + <property name="mode-transition-duration" bind-source="header_leaflet" bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/> + <property name="visible-child-name" bind-source="header_leaflet" bind-property="visible-child-name" bind-flags="bidirectional|sync-create"/> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hhomogeneous-folded">True</property> + <child> + <object class="GtkBox" id="sidebar_box"> + <property name="width_request">200</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="hexpand">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow"> + <property name="width_request">200</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <child> + <object class="GtkViewport"> + <property name="width_request">200</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkListBox" id="contacts_list_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">center</property> + </object> + </child> + <style> + <class name="sidebar"/> + </style> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">sidebar</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="content_separator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <style> + <class name="sidebar"/> + </style> + </object> + </child> + <child> + <object class="GtkOverlay" id="main_content_container"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkStack" id="main_content_stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="name">room</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="history_container"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <child> + <object class="GtkEntry" id="chat_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="placeholder_text" translatable="yes" context="chat page: input field">Type Message</property> + <property name="show_emoji_icon">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + <style> + <class name="message-input-area" /> + </style> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="name">chat_view</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="margin_bottom">30</property> + <property name="halign">center</property> + <property name="valign">center</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_bottom">16</property> + <property name="pixel_size">128</property> + <property name="icon_name">app.briar.gtk</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">No contact selected</property> + <property name="margin_bottom">3</property> + <property name="justify">center</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + <style> + <class name="chat-placeholder-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Select a contact to start chatting</property> + <property name="justify">center</property> + <style> + <class name="chat-placeholder-description"/> + </style> + </object> + </child> + </object> + <packing> + <property name="name">chat_placeholder</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="index">-1</property> + </packing> + </child> + </object> + <packing> + <property name="name">main_content</property> + </packing> + </child> + </object> + <packing> + <property name="name">chat</property> + </packing> + </child> + </object> + <object class="HdyTitleBar" id="headerbar_stack_holder"> + <property name="visible">True</property> + <child> + <object class="GtkStack" id="headerbar_stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="hhomogeneous">False</property> + <child> + <object class="HdyLeaflet" id="header_leaflet"> <!--message view--> + <property name="visible">True</property> + <property name="transition-type">slide</property> + <property name="hhomogeneous-folded">True</property> + <child> + <object class="GtkHeaderBar" id="left-header"> <!--left titlebar--> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="show-close-button">True</property> + <child> + <object class="GtkMenuButton" id="add_room_menu"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="action_name">win.open-add-contact</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + </child> + <accessibility> + + </accessibility> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-add_room_menu"> + <property name="AtkObject::accessible_name" translatable="yes">Add</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">start</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">sidebar</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="header_separator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <style> + <class name="sidebar"/> + </style> + </object> + </child> + <child> + <object class="GtkHeaderBar" id="room_header_bar"> <!--right titlebar--> + <property name="show-close-button">True</property> + <property name="has-subtitle">False</property> + <property name="hexpand">true</property> + <property name="width-request">360</property> + <child> + <object class="GtkRevealer"> + <property name="reveal-child" bind-source="header_leaflet" bind-property="folded" bind-flags="sync-create"/> + <property name="transition-duration" bind-source="header_leaflet" bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/> + <property name="transition-type">crossfade</property> + <property name="visible">True</property> + <child> + <object class="GtkButton" id="leaflet_back_button"> + <property name="action_name">win.back-to-sidebar</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-previous-symbolic</property> + </object> + </child> + <accessibility> + + </accessibility> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-leaflet_back_button"> + <property name="AtkObject::accessible_name" translatable="yes">Back</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="title"> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">never</property> + <property name="propagate_natural_height">True</property> + <property name="propagate_natural_width">False</property> + <child> + <object class="GtkBox" id="room_info"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="room_name"> + <property name="can_focus">False</property> + <!-- Translators: This string is replaced not user-visible --> + <property name="label">Room name</property> + <property name="ellipsize">end</property> + <style> + <class name="title"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">main_content</property> + </packing> + </child> + </object> + <packing> + <property name="name">normal</property> + <property name="title">normal</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHeaderBar"> + <property name="can_focus">False</property> + <property name="show-close-button">True</property> + <property name="title">Briar</property> + </object> + </child> + </object> + <packing> + <property name="name">loading</property> + <property name="title">loading</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="HdyHeaderBar"> + <property name="can_focus">False</property> + <property name="show_close_button">True</property> + <property name="width_request">360</property> + <property name="centering_policy">HDY_CENTERING_POLICY_STRICT</property> + <child> + <object class="GtkButton" id="back_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="action_name">app.back</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-previous-symbolic</property> + </object> + </child> + <child internal-child="accessible"> + <object class="AtkObject" id="back_button-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes">Back</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="name">back</property> + <property name="title" translatable="yes">Back</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> </object> -</interface> +<!-- Synchronize left header and sidebar --> + <object class="GtkSizeGroup"> + <property name="mode">horizontal</property> + <widgets> + <widget name="left-header"/> + <widget name="sidebar_box"/> + </widgets> + </object> + <object class="GtkSizeGroup"> + <property name="mode">horizontal</property> + <widgets> + <widget name="main_content_container"/> + <widget name="room_header_bar"/> + </widgets> + </object> + <object class="GtkSizeGroup"> + <property name="mode">horizontal</property> + <widgets> + <widget name="header_separator"/> + <widget name="content_separator"/> + </widgets> + </object> + <object class="HdyHeaderGroup"> + <headerbars> + <headerbar name="left-header"/> + <headerbar name="room_header_bar"/> + </headerbars> + </object> +</interface> diff --git a/briar-gtk/data/ui/private_chat.ui b/briar-gtk/data/ui/private_chat.ui new file mode 100644 index 0000000000000000000000000000000000000000..b874d1a75eb86cf31d073c97de0280c0a125d20f --- /dev/null +++ b/briar-gtk/data/ui/private_chat.ui @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (c) 2019 Nico Alt + SPDX-License-Identifier: AGPL-3.0-only + License-Filename: LICENSE.md + + Based on parts of GNOME Fractal + https://gitlab.gnome.org/GNOME/fractal/blob/4.2.2/fractal-gtk/res/ui/scroll_widget.ui +--> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkScrolledWindow" id="messages_scroll"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">always</property> + <property name="window_placement">bottom-left</property> + <property name="min_content_width">300</property> + <property name="min_content_height">300</property> + <style> + <class name="messages-scroll" /> + </style> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">end</property> + <property name="vscroll_policy">natural</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="messages_column"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/briar-gtk/data/ui/toolbar_start.ui b/briar-gtk/data/ui/toolbar_start.ui deleted file mode 100644 index 31a939280b91719dace4365b13ed95fa7a30167b..0000000000000000000000000000000000000000 --- a/briar-gtk/data/ui/toolbar_start.ui +++ /dev/null @@ -1,52 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - Copyright (c) 2019 Nico Alt - SPDX-License-Identifier: AGPL-3.0-only - License-Filename: LICENSE.md - - Based on parts of GNOME Lollypop - https://gitlab.gnome.org/World/lollypop/blob/1.0.12/data/ToolbarPlayback.ui ---> -<interface> - <requires lib="gtk+" version="3.10"/> - <object class="GtkImage" id="back_image"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">go-previous-symbolic</property> - </object> - <object class="GtkImage" id="add_contact_image"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">list-add-symbolic</property> - </object> - <object class="GtkBox" id="toolbar_start"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">5</property> - <child> - <object class="GtkButton" id="back_button"> - <property name="visible">False</property> - <property name="valign">center</property> - <property name="image">back_image</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="add_contact_button"> - <property name="visible">False</property> - <property name="valign">center</property> - <property name="image">add_contact_image</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - </object> -</interface> - diff --git a/briar-gtk/tests/briar_gtk/test_toolbar.py b/briar-gtk/tests/briar_gtk/test_toolbar.py deleted file mode 100644 index d5215eccb2256c9c0cc1b28d3e5ceb57d9f41d98..0000000000000000000000000000000000000000 --- a/briar-gtk/tests/briar_gtk/test_toolbar.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2019 Nico Alt -# SPDX-License-Identifier: AGPL-3.0-only -# License-Filename: LICENSE.md - -import pytest -from unittest.mock import Mock - -from briar_gtk.toolbar import Toolbar - - -def test_show_back_button(mocker): - get_object_mock = mocker.patch("gi.repository.Gtk.Builder.get_object") - back_button_mock = get_object_mock.return_value - callback = Mock() - mocker.patch("briar_gtk.toolbar.Toolbar._setup_toolbar") - - Toolbar().show_back_button(True, callback) - - get_object_mock.assert_called_once_with("back_button") - back_button_mock.hide.assert_not_called() - back_button_mock.show.assert_called_once() - back_button_mock.connect.assert_called_once() - - -def test_show_back_button_without_callback(mocker): - get_object_mock = mocker.patch("gi.repository.Gtk.Builder.get_object") - back_button_mock = get_object_mock.return_value - mocker.patch("briar_gtk.toolbar.Toolbar._setup_toolbar") - - with pytest.raises(Exception, - match="Callback needed when showing back button"): - Toolbar().show_back_button(True) - - get_object_mock.assert_called_once_with("back_button") - back_button_mock.hide.assert_not_called() - back_button_mock.show.assert_not_called() - back_button_mock.connect.assert_not_called() - - -def test_hide_back_button(mocker): - get_object_mock = mocker.patch("gi.repository.Gtk.Builder.get_object") - back_button_mock = get_object_mock.return_value - mocker.patch("briar_gtk.toolbar.Toolbar._setup_toolbar") - - Toolbar().show_back_button(False) - - get_object_mock.assert_called_once_with("back_button") - back_button_mock.hide.assert_called_once() - back_button_mock.show.assert_not_called() - back_button_mock.connect.assert_not_called()