Commit 7cbc6a35 authored by Nico Alt's avatar Nico Alt

Combine contact list and chat view into one window

Thanks to libhandy [1], this is adaptive to different screen sizes.
This commit is heavily based on the work done at GNOME Fractal [2]
and GNOME Lollypop [3]. Huge kudos to them.

[1]: https://source.puri.sm/Librem5/libhandy
[2]: https://gitlab.gnome.org/GNOME/fractal/tree/4df16251f405335d3a7007301530d158fdcf774b
[3]: https://gitlab.gnome.org/World/lollypop/tree/1.2.20
parent 886acdb8
Pipeline #4109 failed with stage
in 3 minutes and 55 seconds
# 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())
......@@ -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):
......
# 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("")
# 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
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
class ChatContainer(Container):
# pylint: disable=too-few-public-methods
class PrivateChatContainer(Container):
CONTAINER_UI = "/app/briar/gtk/ui/chat.ui"
CONTAINER_UI = "/app/briar/gtk/ui/private_chat.ui"
def __init__(self, contact_id):
def __init__(self, contact_name, contact_id):
super().__init__()
self._api = APP().api
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.add(self.builder.get_object("chat"))
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)
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)
private_chat = PrivateChat(APP().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"])
self._add_message(message)
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(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["text"], False)
GLib.idle_add(self._add_message, message)
# 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)
def send_message(self, widget):
message = widget.get_text()
private_chat = PrivateChat(APP().api, self._contact_id)
private_chat.send(message)
self._add_message(message, True)
chat_entry.set_text("")
self._add_message(
{
"text": message,
"local": True,
"timestamp": int(round(time.time() * 1000))
})
widget.set_text("")
......@@ -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):
......
......@@ -19,5 +19,4 @@ class StartupContainer(Container):
if APP().api.has_account():
container = LoginContainer(window)
container.show()
self.add(container)
# 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)
# 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")
# 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
......@@ -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