diff options
Diffstat (limited to 'client/src/ui')
32 files changed, 3211 insertions, 0 deletions
diff --git a/client/src/ui/add_conversation/chat/add_contact_dialog.vala b/client/src/ui/add_conversation/chat/add_contact_dialog.vala new file mode 100644 index 00000000..1be0225b --- /dev/null +++ b/client/src/ui/add_conversation/chat/add_contact_dialog.vala @@ -0,0 +1,67 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Chat { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/add_contact_dialog.ui")] +protected class AddContactDialog : Gtk.Dialog { + + [GtkChild] + private ComboBoxText accounts_comboboxtext; + + [GtkChild] + private Button ok_button; + + [GtkChild] + private Button cancel_button; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry alias_entry; + + [GtkChild] + private CheckButton subscribe_checkbutton; + + private StreamInteractor stream_interactor; + + public AddContactDialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.stream_interactor = stream_interactor; + + foreach (Account account in stream_interactor.get_accounts()) { + accounts_comboboxtext.append_text(account.bare_jid.to_string()); + } + accounts_comboboxtext.set_active(0); + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + jid_entry.changed.connect(on_jid_entry_changed); + } + + private void on_ok_button_clicked() { + string? alias = alias_entry.text == "" ? null : alias_entry.text; + Account? account = null; + Jid jid = new Jid(jid_entry.text); + foreach (Account account2 in stream_interactor.get_accounts()) { + print(account2.bare_jid.to_string() + "\n"); + if (accounts_comboboxtext.get_active_text() == account2.bare_jid.to_string()) { + account = account2; + } + } + RosterManager.get_instance(stream_interactor).add_jid(account, jid, alias); + if (subscribe_checkbutton.active) { + PresenceManager.get_instance(stream_interactor).request_subscription(account, jid); + } + close(); + } + + private void on_jid_entry_changed() { + Jid parsed_jid = Jid.parse(jid_entry.text); + ok_button.set_sensitive(parsed_jid != null && parsed_jid.resourcepart == null); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/chat/dialog.vala b/client/src/ui/add_conversation/chat/dialog.vala new file mode 100644 index 00000000..80dac68e --- /dev/null +++ b/client/src/ui/add_conversation/chat/dialog.vala @@ -0,0 +1,82 @@ +using Gee; +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Chat { + +public class Dialog : Gtk.Dialog { + + public signal void conversation_opened(Conversation conversation); + + private Button ok_button; + + private RosterList roster_list; + private SelectJidFragment select_jid_fragment; + private StreamInteractor stream_interactor; + + public Dialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.title = "Start Chat"; + this.modal = true; + this.stream_interactor = stream_interactor; + + setup_headerbar(); + setup_view(); + } + + private void setup_headerbar() { + HeaderBar header_bar = get_header_bar() as HeaderBar; + header_bar.show_close_button = false; + + Button cancel_button = new Button(); + cancel_button.set_label("Cancel"); + cancel_button.visible = true; + header_bar.pack_start(cancel_button); + + ok_button = new Button(); + ok_button.get_style_context().add_class("suggested-action"); + ok_button.label = "Start"; + ok_button.sensitive = false; + ok_button.visible = true; + header_bar.pack_end(ok_button); + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + } + + private void setup_view() { + roster_list = new RosterList(stream_interactor); + roster_list.row_activated.connect(() => { ok_button.clicked(); }); + select_jid_fragment = new SelectJidFragment(stream_interactor, roster_list); + select_jid_fragment.add_jid.connect((row) => { + AddContactDialog add_contact_dialog = new AddContactDialog(stream_interactor); + add_contact_dialog.set_transient_for(this); + add_contact_dialog.show(); + }); + select_jid_fragment.edit_jid.connect(() => { + + }); + select_jid_fragment.remove_jid.connect((row) => { + ListRow list_row = roster_list.get_selected_row() as ListRow; + RosterManager.get_instance(stream_interactor).remove_jid(list_row.account, list_row.jid); + }); + select_jid_fragment.notify["done"].connect(() => { + ok_button.sensitive = select_jid_fragment.done; + }); + get_content_area().add(select_jid_fragment); + } + + protected void on_ok_button_clicked() { + ListRow? selected_row = roster_list.get_selected_row() as ListRow; + if (selected_row != null) { + // TODO move in list to front immediately + ConversationManager.get_instance(stream_interactor).ensure_start_conversation(selected_row.jid, selected_row.account); + Conversation conversation = ConversationManager.get_instance(stream_interactor).get_conversation(selected_row.jid, selected_row.account); + conversation_opened(conversation); + } + close(); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/chat/roster_list.vala b/client/src/ui/add_conversation/chat/roster_list.vala new file mode 100644 index 00000000..9e970d8c --- /dev/null +++ b/client/src/ui/add_conversation/chat/roster_list.vala @@ -0,0 +1,77 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.AddConversation.Chat { +protected class RosterList : FilterableList { + + public signal void conversation_selected(Conversation? conversation); + private StreamInteractor stream_interactor; + + private HashMap<Jid, ListRow> rows = new HashMap<Jid, ListRow>(Jid.hash_func, Jid.equals_func); + + public RosterList(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + set_filter_func(filter); + set_header_func(header); + set_sort_func(sort); + + RosterManager.get_instance(stream_interactor).removed_roster_item.connect( (account, jid, roster_item) => { + Idle.add(() => { on_removed_roster_item(account, jid, roster_item); return false;});}); + RosterManager.get_instance(stream_interactor).updated_roster_item.connect( (account, jid, roster_item) => { + Idle.add(() => { on_updated_roster_item(account, jid, roster_item); return false;});}); + + foreach (Account account in stream_interactor.get_accounts()) { + foreach (Roster.Item roster_item in RosterManager.get_instance(stream_interactor).get_roster(account)) { + on_updated_roster_item(account, new Jid(roster_item.jid), roster_item); + } + } + } + + private void on_removed_roster_item(Account account, Jid jid, Roster.Item roster_item) { + if (rows.has_key(jid)) { + remove(rows[jid]); + rows.unset(jid); + } + } + + private void on_updated_roster_item(Account account, Jid jid, Roster.Item roster_item) { + on_removed_roster_item(account, jid, roster_item); + ListRow row = new ListRow.from_jid(stream_interactor, new Jid(roster_item.jid), account); + rows[jid] = row; + add(row); + invalidate_sort(); + invalidate_filter(); + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(ListRow))) { + ListRow row = r as ListRow; + if (filter_values != null) { + foreach (string filter in filter_values) { + if (!(row.name_label.label.down().contains(filter.down()) || + row.jid.to_string().down().contains(filter.down()))) { + return false; + } + } + } + } + return true; + } + + public override int sort(ListBoxRow row1, ListBoxRow row2) { + ListRow c1 = (row1 as ListRow); + ListRow c2 = (row2 as ListRow); + return c1.name_label.label.collate(c2.name_label.label); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/add_groupchat_dialog.vala b/client/src/ui/add_conversation/conference/add_groupchat_dialog.vala new file mode 100644 index 00000000..aa86958d --- /dev/null +++ b/client/src/ui/add_conversation/conference/add_groupchat_dialog.vala @@ -0,0 +1,107 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/add_groupchat_dialog.ui")] +protected class AddGroupchatDialog : Gtk.Dialog { + + [GtkChild] + private Stack accounts_stack; + + [GtkChild] + private ComboBoxText accounts_comboboxtext; + + [GtkChild] + private Label account_label; + + [GtkChild] + private Button ok_button; + + [GtkChild] + private Button cancel_button; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry alias_entry; + + [GtkChild] + private Entry nick_entry; + + [GtkChild] + private CheckButton autojoin_checkbutton; + + private StreamInteractor stream_interactor; + private Xmpp.Xep.Bookmarks.Conference? edit_confrence = null; + private bool alias_entry_changed = false; + + public AddGroupchatDialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.stream_interactor = stream_interactor; + ok_button.label = "Add"; + ok_button.get_style_context().add_class("suggested-action"); // TODO why doesn't it work in XML + accounts_stack.set_visible_child_name("combobox"); + foreach (Account account in stream_interactor.get_accounts()) { + accounts_comboboxtext.append_text(account.bare_jid.to_string()); + } + accounts_comboboxtext.set_active(0); + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + jid_entry.key_press_event.connect_after(after_jid_entry_key_press); + nick_entry.key_press_event.connect(check_ok); + } + + public AddGroupchatDialog.for_conference(StreamInteractor stream_interactor, Account account, Xmpp.Xep.Bookmarks.Conference conference) { + this(stream_interactor); + edit_confrence = conference; + ok_button.label = "Save"; + ok_button.sensitive = true; + accounts_stack.set_visible_child_name("label"); + account_label.label = account.bare_jid.to_string(); + jid_entry.text = conference.jid; + nick_entry.text = conference.nick; + autojoin_checkbutton.active = conference.autojoin; + alias_entry.text = conference.name; + } + + private bool after_jid_entry_key_press() { + check_ok(); + if (!alias_entry_changed) { + Jid? parsed_jid = Jid.parse(jid_entry.text); + alias_entry.text = parsed_jid != null && parsed_jid.localpart != null ? parsed_jid.localpart : jid_entry.text; + } + return false; + } + + private bool check_ok() { + Jid? parsed_jid = Jid.parse(jid_entry.text); + ok_button.sensitive = parsed_jid != null && parsed_jid.localpart != null && parsed_jid.resourcepart == null && + nick_entry.text != "" && alias_entry.text != null; + return false; + } + + private void on_ok_button_clicked() { + Account? account = null; + foreach (Account account2 in stream_interactor.get_accounts()) { + if (accounts_comboboxtext.get_active_text() == account2.bare_jid.to_string()) { + account = account2; + } + } + Xmpp.Xep.Bookmarks.Conference conference = new Xmpp.Xep.Bookmarks.Conference(jid_entry.text); + conference.nick = nick_entry.text; + conference.name = alias_entry.text; + conference.autojoin = autojoin_checkbutton.active; + if (edit_confrence == null) { + MucManager.get_instance(stream_interactor).add_bookmark(account, conference); + } else { + MucManager.get_instance(stream_interactor).replace_bookmark(account, edit_confrence, conference); + } + close(); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/conference_details_fragment.vala b/client/src/ui/add_conversation/conference/conference_details_fragment.vala new file mode 100644 index 00000000..edfeab9d --- /dev/null +++ b/client/src/ui/add_conversation/conference/conference_details_fragment.vala @@ -0,0 +1,148 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/conference_details_fragment.ui")] +protected class ConferenceDetailsFragment : Box { + + public bool done { + get { + Jid? parsed_jid = Jid.parse(jid); + return parsed_jid != null && parsed_jid.localpart != null && + parsed_jid.resourcepart == null && nick != ""; + } + private set {} + } + + public Account account { + owned get { + foreach (Account account in stream_interactor.get_accounts()) { + if (accounts_comboboxtext.get_active_text() == account.bare_jid.to_string()) { + return account; + } + } + return null; + } + set { + accounts_label.label = value.bare_jid.to_string(); + accounts_comboboxtext.set_active_id(value.bare_jid.to_string()); + } + } + public string jid { + get { return jid_label.label; } + set { + jid_label.label = value; + jid_entry.text = value; + } + } + public string nick { + get { return nick_label.label; } + set { + nick_label.label = value; + nick_entry.text = value; + } + } + public string password { + get { return password_label.label; } + set { + password_label.label = value; + password_entry.text = value; + } + } + + [GtkChild] + private Stack accounts_stack; + + [GtkChild] + private Stack jid_stack; + + [GtkChild] + private Stack nick_stack; + + [GtkChild] + private Stack password_stack; + + [GtkChild] + private Button accounts_button; + + [GtkChild] + private Button jid_button; + + [GtkChild] + private Button nick_button; + + [GtkChild] + private Button password_button; + + [GtkChild] + private Label accounts_label; + + [GtkChild] + private Label jid_label; + + [GtkChild] + private Label nick_label; + + [GtkChild] + private Label password_label; + + [GtkChild] + private ComboBoxText accounts_comboboxtext; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry nick_entry; + + [GtkChild] + private Entry password_entry; + + private StreamInteractor stream_interactor; + + public ConferenceDetailsFragment(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + accounts_stack.set_visible_child_name("label"); + jid_stack.set_visible_child_name("label"); + nick_stack.set_visible_child_name("label"); + password_stack.set_visible_child_name("label"); + + accounts_button.clicked.connect(() => { set_active_stack(accounts_stack); }); + jid_button.clicked.connect(() => { set_active_stack(jid_stack); }); + nick_button.clicked.connect(() => { set_active_stack(nick_stack); }); + password_button.clicked.connect(() => { set_active_stack(password_stack); }); + + accounts_comboboxtext.changed.connect(() => { accounts_label.label = accounts_comboboxtext.get_active_text(); }); + jid_entry.key_press_event.connect(() => { jid_label.label = jid_entry.text; return false; }); + nick_entry.key_press_event.connect(() => { nick_label.label = nick_entry.text; return false; }); + password_entry.key_press_event.connect(() => { password_label.label = password_entry.text; return false; }); + + jid_entry.key_press_event.connect(() => { done = true; return false; }); // just for notifying + nick_entry.key_press_event.connect(() => { done = true; return false; }); + + foreach (Account account in stream_interactor.get_accounts()) { + accounts_comboboxtext.append_text(account.bare_jid.to_string()); + } + accounts_comboboxtext.set_active(0); + } + + public void clear() { + jid = ""; + nick = ""; + password = ""; + } + + private void set_active_stack(Stack stack) { + stack.set_visible_child_name("entry"); + if (stack != accounts_stack) accounts_stack.set_visible_child_name("label"); + if (stack != jid_stack) jid_stack.set_visible_child_name("label"); + if (stack != nick_stack) nick_stack.set_visible_child_name("label"); + if (stack != password_stack) password_stack.set_visible_child_name("label"); + } + +} + +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/conference_list.vala b/client/src/ui/add_conversation/conference/conference_list.vala new file mode 100644 index 00000000..2e461472 --- /dev/null +++ b/client/src/ui/add_conversation/conference/conference_list.vala @@ -0,0 +1,105 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { +protected class ConferenceList : FilterableList { + + public signal void conversation_selected(Conversation? conversation); + + private StreamInteractor stream_interactor; + private HashMap<Account, ArrayList<Xep.Bookmarks.Conference>> lists = new HashMap<Account, ArrayList<Xep.Bookmarks.Conference>>(); + + public ConferenceList(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + set_filter_func(filter); + set_header_func(header); + set_sort_func(sort); + + MucManager.get_instance(stream_interactor).bookmarks_updated.connect((account, conferences) => { + Idle.add(() => { + lists[account] = conferences; + refresh_conferences(); + return false; + }); + }); + + foreach (Account account in stream_interactor.get_accounts()) { + MucManager.get_instance(stream_interactor).get_bookmarks(account, new BookmarksListener(this, stream_interactor, account)); + } + } + + public void refresh_conferences() { + @foreach((widget) => { remove(widget); }); + foreach (Account account in lists.keys) { + foreach (Xep.Bookmarks.Conference conference in lists[account]) { + add(new ConferenceListRow(stream_interactor, conference, account)); + } + } + } + + private class BookmarksListener : Xep.Bookmarks.ConferencesRetrieveResponseListener, Object { + ConferenceList outer; + Account account; + public BookmarksListener(ConferenceList outer, StreamInteractor stream_interactor, Account account) { + this.outer = outer; + this.account = account; + } + + public void on_result(Core.XmppStream stream, ArrayList<Xep.Bookmarks.Conference> conferences) { + outer.lists[account] = conferences; + Idle.add(() => { outer.refresh_conferences(); return false; }); + } + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(ListRow))) { + ListRow row = r as ListRow; + if (filter_values != null) { + foreach (string filter in filter_values) { + if (!(row.name_label.label.down().contains(filter.down()) || + row.jid.to_string().down().contains(filter.down()))) { + return false; + } + } + } + } + return true; + } + + public override int sort(ListBoxRow row1, ListBoxRow row2) { + ListRow c1 = (row1 as ListRow); + ListRow c2 = (row2 as ListRow); + return c1.name_label.label.collate(c2.name_label.label); + } +} + +internal class ConferenceListRow : ListRow { + + public Xep.Bookmarks.Conference bookmark; + + public ConferenceListRow(StreamInteractor stream_interactor, Xep.Bookmarks.Conference bookmark, Account account) { + this.jid = new Jid(bookmark.jid); + this.account = account; + this.bookmark = bookmark; + + if (bookmark.name != "" && bookmark.name != bookmark.jid) { + name_label.label = bookmark.name; + via_label.label = bookmark.jid; + } else { + name_label.label = bookmark.jid; + via_label.visible = false; + } + image.set_from_pixbuf((new AvatarGenerator(35, 35)).set_stateless(true).draw_jid(stream_interactor, jid, account)); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/conference/dialog.vala b/client/src/ui/add_conversation/conference/dialog.vala new file mode 100644 index 00000000..8bf29bb4 --- /dev/null +++ b/client/src/ui/add_conversation/conference/dialog.vala @@ -0,0 +1,165 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation.Conference { + +public class Dialog : Gtk.Dialog { + + public signal void conversation_opened(Conversation conversation); + + private Stack stack = new Stack(); + private Button cancel_button; + private Button ok_button; + private Label cancel_label = new Label("Cancel") {visible=true}; + private Image cancel_image = new Image.from_icon_name("go-previous-symbolic", IconSize.MENU) {visible=true}; + + private SelectJidFragment select_fragment; + private ConferenceDetailsFragment details_fragment; + private ConferenceList conference_list; + + private StreamInteractor stream_interactor; + + public Dialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.title = "Join Conference"; + this.modal = true; + this.stream_interactor = stream_interactor; + + stack.visible = true; + stack.vhomogeneous = false; + get_content_area().add(stack); + + setup_headerbar(); + setup_jid_add_view(); + setup_conference_details_view(); + show_jid_add_view(); + } + + private void show_jid_add_view() { + cancel_button.remove(cancel_image); + cancel_button.add(cancel_label); + cancel_button.clicked.disconnect(show_jid_add_view); + cancel_button.clicked.connect(close); + ok_button.label = "Next"; + ok_button.sensitive = select_fragment.done; + ok_button.clicked.disconnect(on_ok_button_clicked); + ok_button.clicked.connect(on_next_button_clicked); + details_fragment.notify["done"].disconnect(set_ok_sensitive_from_details); + select_fragment.notify["done"].connect(set_ok_sensitive_from_select); + stack.transition_type = StackTransitionType.SLIDE_RIGHT; + stack.set_visible_child_name("select"); + } + + private void show_conference_details_view() { + cancel_button.remove(cancel_label); + cancel_button.add(cancel_image); + cancel_button.clicked.disconnect(close); + cancel_button.clicked.connect(show_jid_add_view); + ok_button.label = "Join"; + ok_button.sensitive = details_fragment.done; + ok_button.clicked.disconnect(show_conference_details_view); + ok_button.clicked.connect(on_ok_button_clicked); + select_fragment.notify["done"].disconnect(set_ok_sensitive_from_select); + details_fragment.notify["done"].connect(set_ok_sensitive_from_details); + stack.transition_type = StackTransitionType.SLIDE_LEFT; + stack.set_visible_child_name("details"); + animate_window_resize(); + } + + private void setup_headerbar() { + HeaderBar header_bar = get_header_bar() as HeaderBar; + header_bar.show_close_button = false; + + cancel_button = new Button(); + header_bar.pack_start(cancel_button); + cancel_button.visible = true; + + ok_button = new Button(); + header_bar.pack_end(ok_button); + ok_button.get_style_context().add_class("suggested-action"); + ok_button.visible = true; + ok_button.can_focus = true; + ok_button.can_default = true; + ok_button.has_default = true; + } + + private void setup_jid_add_view() { + conference_list = new ConferenceList(stream_interactor); + conference_list.row_activated.connect(() => { ok_button.clicked(); }); + select_fragment = new SelectJidFragment(stream_interactor, conference_list); + select_fragment.add_jid.connect((row) => { + AddGroupchatDialog dialog = new AddGroupchatDialog(stream_interactor); + dialog.set_transient_for(this); + dialog.show(); + }); + select_fragment.edit_jid.connect((row) => { + ConferenceListRow conference_row = row as ConferenceListRow; + AddGroupchatDialog dialog = new AddGroupchatDialog.for_conference(stream_interactor, conference_row.account, conference_row.bookmark); + dialog.set_transient_for(this); + dialog.show(); + }); + select_fragment.remove_jid.connect((row) => { + ConferenceListRow conference_row = row as ConferenceListRow; + MucManager.get_instance(stream_interactor).remove_bookmark(conference_row.account, conference_row.bookmark); + }); + stack.add_named(select_fragment, "select"); + } + + private void setup_conference_details_view() { + details_fragment = new ConferenceDetailsFragment(stream_interactor); + stack.add_named(details_fragment, "details"); + } + + private void set_ok_sensitive_from_select() { + ok_button.sensitive = select_fragment.done; + } + + private void set_ok_sensitive_from_details() { + ok_button.sensitive = select_fragment.done; + } + + private void on_next_button_clicked() { + details_fragment.clear(); + ListRow? row = conference_list.get_selected_row() as ListRow; + ConferenceListRow? conference_row = conference_list.get_selected_row() as ConferenceListRow; + if (conference_row != null) { + details_fragment.jid = conference_row.bookmark.jid; + details_fragment.nick = conference_row.bookmark.nick; + if (conference_row.bookmark.password != null) details_fragment.password = conference_row.bookmark.password; + ok_button.grab_focus(); + } else if (row != null) { + details_fragment.jid = row.jid.to_string(); + } + show_conference_details_view(); + } + + private void on_ok_button_clicked() { + MucManager.get_instance(stream_interactor).join(details_fragment.account, new Jid(details_fragment.jid), details_fragment.nick); + close(); + } + + private void close() { + base.close(); + } + + private void animate_window_resize() { + int def_height, curr_width, curr_height; + get_size(out curr_width, out curr_height); + stack.get_preferred_height(null, out def_height); + int difference = def_height - curr_height; + Timer timer = new Timer(); + Timeout.add((int) (stack.transition_duration / 30), + () => { + ulong microsec; + timer.elapsed(out microsec); + ulong millisec = microsec / 1000; + double partial = double.min(1, (double) millisec / stack.transition_duration); + resize(curr_width, (int) (curr_height + difference * partial)); + return millisec < stack.transition_duration; + }); + } +} + +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/list_row.vala b/client/src/ui/add_conversation/list_row.vala new file mode 100644 index 00000000..5c2eff97 --- /dev/null +++ b/client/src/ui/add_conversation/list_row.vala @@ -0,0 +1,43 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/list_row.ui")] +public class ListRow : ListBoxRow { + + [GtkChild] + public Image image; + + [GtkChild] + public Label name_label; + + [GtkChild] + public Label via_label; + + public Jid? jid; + public Account? account; + + public ListRow() {} + + public ListRow.from_jid(StreamInteractor stream_interactor, Jid jid, Account account) { + this.jid = jid; + this.account = account; + + string display_name = Util.get_display_name(stream_interactor, jid, account); + if (stream_interactor.get_accounts().size > 1) { + via_label.label = @"via $(account.bare_jid)"; + this.has_tooltip = true; + set_tooltip_text(jid.to_string()); + } else if (display_name != jid.bare_jid.to_string()){ + via_label.label = jid.bare_jid.to_string(); + } else { + via_label.visible = false; + } + name_label.label = display_name; + image.set_from_pixbuf((new AvatarGenerator(35, 35)).draw_jid(stream_interactor, jid, account)); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/add_conversation/select_jid_fragment.vala b/client/src/ui/add_conversation/select_jid_fragment.vala new file mode 100644 index 00000000..847a9ecb --- /dev/null +++ b/client/src/ui/add_conversation/select_jid_fragment.vala @@ -0,0 +1,124 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.AddConversation { + +[GtkTemplate (ui = "/org/dino-im/add_conversation/select_jid_fragment.ui")] +public class SelectJidFragment : Gtk.Box { + + public signal void add_jid(); + public signal void edit_jid(ListRow row); + public signal void remove_jid(ListRow row); + public bool done { + get { + return filterable_list.get_selected_row() != null; + } + private set {} } + + [GtkChild] + private Entry entry; + + [GtkChild] + private Box box; + + [GtkChild] + private Button add_button; + + [GtkChild] + private Button edit_button; + + [GtkChild] + private Button remove_button; + + private FilterableList filterable_list; + private ArrayList<AddListRow> added_rows = new ArrayList<AddListRow>(); + private StreamInteractor stream_interactor; + + public SelectJidFragment(StreamInteractor stream_interactor, FilterableList filterable_list) { + this.stream_interactor = stream_interactor; + this.filterable_list = filterable_list; + + filterable_list.visible = true; + filterable_list.activate_on_single_click = false; + filterable_list.vexpand = true; + box.add(filterable_list); + + filterable_list.set_sort_func(sort); + filterable_list.row_selected.connect(check_buttons_active); + filterable_list.row_selected.connect(() => { done = true; }); // just for notifying + entry.changed.connect(on_entry_changed); + add_button.clicked.connect(() => { add_jid(); }); + remove_button.clicked.connect(() => { remove_jid(filterable_list.get_selected_row() as ListRow); }); + edit_button.clicked.connect(() => { edit_jid(filterable_list.get_selected_row() as ListRow); }); + } + + private void on_entry_changed() { + foreach (AddListRow row in added_rows) { + filterable_list.remove(row); + } + added_rows.clear(); + + string[] ? values; + string str = entry.get_text(); + values = str == "" ? null : str.split(" "); + filterable_list.set_filter_values(values); + Jid? parsed_jid = Jid.parse(str); + if (parsed_jid != null && parsed_jid.localpart != null) { + foreach (Account account in stream_interactor.get_accounts()) { + AddListRow row = new AddListRow(stream_interactor, str, account); + filterable_list.add(row); + added_rows.add(row); + } + } + } + + private void check_buttons_active() { + ListBoxRow? row = filterable_list.get_selected_row(); + bool active = row != null && !row.get_type().is_a(typeof(AddListRow)); + edit_button.sensitive = active; + remove_button.sensitive = active; + } + + private int sort(ListBoxRow row1, ListBoxRow row2) { + AddListRow al1 = (row1 as AddListRow); + AddListRow al2 = (row2 as AddListRow); + if (al1 != null && al2 == null) { + return -1; + } else if (al2 != null && al1 == null) { + return 1; + } + return filterable_list.sort(row1, row2); + } + + private class AddListRow : ListRow { + + public AddListRow(StreamInteractor stream_interactor, string jid, Account account) { + this.account = account; + this.jid = new Jid(jid); + + name_label.label = jid; + if (stream_interactor.get_accounts().size > 1) { + via_label.label = account.bare_jid.to_string(); + } else { + via_label.visible = false; + } + image.set_from_pixbuf((new AvatarGenerator(35, 35)).set_greyscale(true).draw_text("?")); + } + } +} + +public abstract class FilterableList : Gtk.ListBox { + public string[]? filter_values; + + public void set_filter_values(string[] values) { + if (filter_values == values) return; + filter_values = values; + invalidate_filter(); + } + + public abstract int sort(ListBoxRow row1, ListBoxRow row2); +} + +}
\ No newline at end of file diff --git a/client/src/ui/application.vala b/client/src/ui/application.vala new file mode 100644 index 00000000..c3f0e302 --- /dev/null +++ b/client/src/ui/application.vala @@ -0,0 +1,112 @@ +using Gtk; + +using Dino.Entities; + +public class Dino.Ui.Application : Gtk.Application { + + private Database db; + private StreamInteractor stream_interaction; + + private Notifications notifications; + private UnifiedWindow? window; + private ConversationSelector.View? filterable_conversation_list; + private ConversationSelector.List? conversation_list; + private ConversationSummary.View? conversation_frame; + private ChatInput? chat_input; + + public Application() { + this.db = new Database("store.sqlite3"); + this.stream_interaction = new StreamInteractor(db); + + AvatarManager.start(stream_interaction, db); + MessageManager.start(stream_interaction, db); + CounterpartInteractionManager.start(stream_interaction); + PresenceManager.start(stream_interaction); + MucManager.start(stream_interaction); + PgpManager.start(stream_interaction, db); + RosterManager.start(stream_interaction); + ConversationManager.start(stream_interaction, db); + ChatInteraction.start(stream_interaction); + + notifications = new Notifications(stream_interaction); + notifications.start(); + + load_css(); + } + + public override void activate() { + create_set_app_menu(); + create_window(); + window.show_all(); + restore(); + } + + private void create_window() { + window = new UnifiedWindow(this, stream_interaction); + + filterable_conversation_list = window.filterable_conversation_list; + conversation_list = window.filterable_conversation_list.conversation_list; + conversation_frame = window.conversation_frame; + chat_input = window.chat_input; + } + + private void show_accounts_window() { + ManageAccounts.Dialog dialog = new ManageAccounts.Dialog(stream_interaction, db); + dialog.set_transient_for(window); + dialog.account_enabled.connect(add_connection); + dialog.account_disabled.connect(remove_connection); + dialog.show(); + } + + private void show_settings_window() { + SettingsDialog dialog = new SettingsDialog(); + dialog.set_transient_for(window); + dialog.show(); + } + + private void create_set_app_menu() { + SimpleAction accounts_action = new SimpleAction("accounts", null); + accounts_action.activate.connect(show_accounts_window); + add_action(accounts_action); + + SimpleAction settings_action = new SimpleAction("settings", null); + settings_action.activate.connect(show_settings_window); + add_action(settings_action); + + SimpleAction quit_action = new SimpleAction("quit", null); + quit_action.activate.connect(quit); + add_action(quit_action); + add_accelerator("<Ctrl>Q", "app.quit", null); + + Builder builder = new Builder.from_resource("/org/dino-im/menu_app.ui"); + MenuModel menu = builder.get_object("menu_app") as MenuModel; + + set_app_menu(menu); + } + + private void restore() { + foreach (Account account in db.get_accounts()) { + if (account.enabled) add_connection(account); + } + } + + private void add_connection(Account account) { + stream_interaction.connect(account); + } + + private void remove_connection(Account account) { + stream_interaction.disconnect(account); + } + + private void load_css() { + var css_provider = new Gtk.CssProvider (); + try { + var file = File.new_for_uri("resource:///org/dino-im/style.css"); + css_provider.load_from_file (file); + } catch (GLib.Error e) { + warning ("loading css: %s", e.message); + } + Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } +} + diff --git a/client/src/ui/avatar_generator.vala b/client/src/ui/avatar_generator.vala new file mode 100644 index 00000000..e168c4a4 --- /dev/null +++ b/client/src/ui/avatar_generator.vala @@ -0,0 +1,233 @@ +using Cairo; +using Gee; +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui { +public class AvatarGenerator { + + private const string COLOR_GREY = "E0E0E0"; + private const string GROUPCHAT_ICON = "system-users-symbolic"; + + StreamInteractor? stream_interactor; + bool greyscale = false; + bool stateless = false; + int width; + int height; + int scale_factor; + + public AvatarGenerator(int width, int height, int scale_factor = 1) { + this.width = width; + this.height = height; + this.scale_factor = scale_factor; + } + + public Pixbuf draw_jid(StreamInteractor stream_interactor, Jid jid, Account account) { + this.stream_interactor = stream_interactor; + return crop_corners(draw_tile(jid, account, width * scale_factor, height * scale_factor)); + } + + public Pixbuf draw_message(StreamInteractor stream_interactor, Message message) { + Jid? real_jid = MucManager.get_instance(stream_interactor).get_message_real_jid(message); + return draw_jid(stream_interactor, real_jid != null ? real_jid : message.from, message.account); + } + + public Pixbuf draw_conversation(StreamInteractor stream_interactor, Conversation conversation) { + return draw_jid(stream_interactor, conversation.counterpart, conversation.account); + } + + public Pixbuf draw_account(StreamInteractor stream_interactor, Account account) { + return draw_jid(stream_interactor, account.bare_jid, account); + } + + public Pixbuf draw_text(string text) { + string color = greyscale ? COLOR_GREY : Util.get_avatar_hex_color(text); + Pixbuf pixbuf = draw_colored_rectangle_text(color, text, width, height); + return crop_corners(pixbuf); + } + + public AvatarGenerator set_greyscale(bool greyscale) { + this.greyscale = greyscale; + return this; + } + + public AvatarGenerator set_stateless(bool stateless) { + this.stateless = stateless; + return this; + } + + private int get_left_border() { + return (int)Math.floor(scale_factor/2.0); + } + + private int get_right_border() { + return (int)Math.ceil(scale_factor/2.0); + } + + private void add_tile_to_pixbuf(Pixbuf pixbuf, Jid jid, Account account, int width, int height, int x, int y) { + Pixbuf tile = draw_chat_tile(jid, account, width, height); + tile.copy_area(0, 0, width, height, pixbuf, x, y); + } + + private Pixbuf draw_tile(Jid jid, Account account, int width, int height) { + if (MucManager.get_instance(stream_interactor).is_groupchat(jid, account)) { + return draw_groupchat_tile(jid, account, width, height); + } else { + return draw_chat_tile(jid, account, width, height); + } + } + + private Pixbuf draw_chat_tile(Jid jid, Account account, int width, int height) { + if (MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) { + Jid? real_jid = MucManager.get_instance(stream_interactor).get_real_jid(jid, account); + if (real_jid != null) { + return draw_tile(real_jid, account, width, height); + } + } + Pixbuf? avatar = AvatarManager.get_instance(stream_interactor).get_avatar(account, jid); + if (avatar != null) { + double desired_ratio = (double) width / height; + double avatar_ratio = (double) avatar.width / avatar.height; + if (avatar_ratio > desired_ratio) { + int comp_width = width * avatar.height / height; + avatar = new Pixbuf.subpixbuf(avatar, avatar.width / 2 - comp_width / 2, 0, comp_width, avatar.height); + } else if (avatar_ratio < desired_ratio) { + int comp_height = height * avatar.width / width; + avatar = new Pixbuf.subpixbuf(avatar, 0, avatar.height / 2 - comp_height / 2, avatar.width, comp_height); + } + avatar = avatar.scale_simple(width, height, InterpType.BILINEAR); + if (greyscale) avatar = convert_to_greyscale(avatar); + return avatar; + } else { + string display_name = Util.get_display_name(stream_interactor, jid, account); + string color = greyscale ? COLOR_GREY : Util.get_avatar_hex_color(display_name); + return draw_colored_rectangle_text(color, display_name.get_char(0).toupper().to_string(), width, height); + } + } + + private Pixbuf draw_groupchat_tile(Jid jid, Account account, int width, int height) { + ArrayList<Jid>? occupants = MucManager.get_instance(stream_interactor).get_other_occupants(jid, account); + if (stateless || occupants == null || occupants.size == 0) { + return draw_chat_tile(jid, account, width, height); + } + Pixbuf pixbuf = initialize_pixbuf(width, height); + if (occupants.size == 1 || occupants.size == 2 || occupants.size == 3) { + add_tile_to_pixbuf(pixbuf, occupants[0], account, width / 2 - get_right_border(), height, 0, 0); + if (occupants.size == 1) { + add_tile_to_pixbuf(pixbuf, account.bare_jid, account, width / 2 - get_left_border(), height, width / 2 + get_left_border(), 0); + } else if (occupants.size == 2) { + add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height, width / 2 + get_left_border(), 0); + } else if (occupants.size == 3) { + add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height / 2 - get_right_border(), width / 2 + get_left_border(), 0); + add_tile_to_pixbuf(pixbuf, occupants[2], account, width / 2 - get_left_border(), height / 2 - get_left_border(), width / 2 + get_left_border(), height / 2 + get_left_border()); + } + } else if (occupants.size >= 4) { + add_tile_to_pixbuf(pixbuf, occupants[0], account, width / 2 - get_right_border(), height / 2 - get_right_border(), 0, 0); + add_tile_to_pixbuf(pixbuf, occupants[1], account, width / 2 - get_left_border(), height / 2 - get_right_border(), width / 2 + get_left_border(), 0); + add_tile_to_pixbuf(pixbuf, occupants[2], account, width / 2 - get_right_border(), height / 2 - get_left_border(), 0, height / 2 + get_left_border()); + if (occupants.size == 4) { + add_tile_to_pixbuf(pixbuf, occupants[3], account, width / 2 - get_left_border(), height / 2 - get_left_border(), width / 2 + get_left_border(), height / 2 + get_left_border()); + } else if (occupants.size > 4) { + Pixbuf plus_pixbuf = draw_colored_rectangle_text("555753", "+", width / 2 - get_left_border(), height / 2 - get_left_border()); + if (greyscale) plus_pixbuf = convert_to_greyscale(plus_pixbuf); + plus_pixbuf.copy_area(0, 0, width / 2 - get_left_border(), height / 2 - get_left_border(), pixbuf, width / 2 + get_left_border(), height / 2 + get_left_border()); + } + } + return pixbuf; + } + + public Pixbuf draw_colored_icon(string hex_color, string icon, int width, int height) { + int ICON_SIZE = width > 20 * scale_factor ? 17 * scale_factor : 14 * scale_factor; + + Context rectancle_context = new Context(new ImageSurface(Format.ARGB32, width, height)); + draw_colored_rectangle(rectancle_context, hex_color, width, height); + + Pixbuf icon_pixbuf = IconTheme.get_default().load_icon(icon, ICON_SIZE, IconLookupFlags.FORCE_SIZE); + Surface icon_surface = cairo_surface_create_from_pixbuf(icon_pixbuf, 1, null); + Context context = new Context(icon_surface); + context.set_operator(Operator.IN); + context.set_source_rgba(1, 1, 1, 1); + context.rectangle(0, 0, width, height); + context.fill(); + + rectancle_context.set_source_surface(icon_surface, width / 2 - ICON_SIZE / 2, height / 2 - ICON_SIZE / 2); + rectancle_context.paint(); + + return pixbuf_get_from_surface(rectancle_context.get_target(), 0, 0, width, height); + } + + public Pixbuf draw_colored_rectangle_text(string hex_color, string text, int width, int height) { + Context ctx = new Context(new ImageSurface(Format.ARGB32, width, height)); + draw_colored_rectangle(ctx, hex_color, width, height); + draw_center_text(ctx, text, width < 40 * scale_factor ? 17 * scale_factor : 25 * scale_factor, width, height); + return pixbuf_get_from_surface(ctx.get_target(), 0, 0, width, height); + } + + private static void draw_center_text(Context ctx, string text, int fontsize, int width, int height) { + ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); + ctx.set_font_size(fontsize); + Cairo.TextExtents extents; + ctx.text_extents(text, out extents); + double x_pos = width/2 - (extents.width/2 + extents.x_bearing); + double y_pos = height/2 - (extents.height/2 + extents.y_bearing); + ctx.move_to(x_pos, y_pos); + ctx.set_source_rgba(1, 1, 1, 1); + ctx.show_text(text); + } + + private static void draw_colored_rectangle(Context ctx, string hex_color, int width, int height) { + set_source_hex_color(ctx, hex_color); + ctx.rectangle(0, 0, width, height); + ctx.fill(); + } + + private static Pixbuf convert_to_greyscale(Pixbuf pixbuf) { + Surface surface = cairo_surface_create_from_pixbuf(pixbuf, 1, null); + Context context = new Context(surface); + // convert to greyscale + context.set_operator(Operator.HSL_COLOR); + context.set_source_rgb(1, 1, 1); + context.rectangle(0, 0, pixbuf.width, pixbuf.height); + context.fill(); + // make the visible part more light + context.set_operator(Operator.ATOP); + context.set_source_rgba(1, 1, 1, 0.7); + context.rectangle(0, 0, pixbuf.width, pixbuf.height); + context.fill(); + return pixbuf_get_from_surface(context.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private Pixbuf crop_corners(Pixbuf pixbuf, double radius = 3) { + radius *= scale_factor; + Context ctx = new Context(new ImageSurface(Format.ARGB32, pixbuf.width, pixbuf.height)); + cairo_set_source_pixbuf(ctx, pixbuf, 0, 0); + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + ctx.paint(); + return pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private static Pixbuf initialize_pixbuf(int width, int height) { + Context ctx = new Context(new ImageSurface(Format.ARGB32, width, height)); + ctx.set_source_rgba(1, 1, 1, 0); + ctx.rectangle(0, 0, width, height); + ctx.fill(); + return pixbuf_get_from_surface(ctx.get_target(), 0, 0, width, height); + } + + private static void set_source_hex_color(Context ctx, string hex_color) { + ctx.set_source_rgba((double) hex_color.substring(0, 2).to_long(null, 16) / 255, + (double) hex_color.substring(2, 2).to_long(null, 16) / 255, + (double) hex_color.substring(4, 2).to_long(null, 16) / 255, + hex_color.length > 6 ? (double) hex_color.substring(6, 2).to_long(null, 16) / 255 : 1); + } +} +} diff --git a/client/src/ui/chat_input.vala b/client/src/ui/chat_input.vala new file mode 100644 index 00000000..d2f9c562 --- /dev/null +++ b/client/src/ui/chat_input.vala @@ -0,0 +1,123 @@ +using Gdk; +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { +[GtkTemplate (ui = "/org/dino-im/chat_input.ui")] +public class ChatInput : Grid { + + [GtkChild] + private TextView text_input; + + private Conversation? conversation; + private StreamInteractor stream_interactor; + private HashMap<Conversation, string> entry_cache = new HashMap<Conversation, string>(Conversation.hash_func, Conversation.equals_func); + private static HashMap<string, string> smiley_translations = new HashMap<string, string>(); + + static construct { + smiley_translations[":)"] = "🙂"; + smiley_translations[":D"] = "😀"; + smiley_translations[";)"] = "😉"; + smiley_translations["O:)"] = "😇"; + smiley_translations["]:>"] = "😈"; + smiley_translations[":o"] = "😮"; + smiley_translations[":P"] = "😛"; + smiley_translations[";P"] = "😜"; + smiley_translations[":("] = "🙁"; + smiley_translations[":'("] = "😢"; + smiley_translations[":/"] = "😕"; + smiley_translations["-.-"] = "😑"; + } + + public ChatInput(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public void initialize_for_conversation(Conversation conversation) { + if (this.conversation != null) { + if (text_input.buffer.text != "") { + entry_cache[this.conversation] = text_input.buffer.text; + } else { + entry_cache.unset(this.conversation); + } + } + this.conversation = conversation; + text_input.buffer.text = ""; + if (entry_cache.has_key(conversation)) { + text_input.buffer.text = entry_cache[conversation]; + } + text_input.key_press_event.connect(on_text_input_key_press); + text_input.key_release_event.connect(on_text_input_key_release); + text_input.grab_focus(); + } + + private void send_text() { + string text = text_input.buffer.text; + if (text.has_prefix("/")) { + string[] token = text.split(" ", 2); + switch(token[0]) { + case "/kick": + MucManager.get_instance(stream_interactor).kick(conversation.account, conversation.counterpart, token[1]); + break; + case "/me": + MessageManager.get_instance(stream_interactor).send_message(text, conversation); + break; + case "/nick": + MucManager.get_instance(stream_interactor).change_nick(conversation.account, conversation.counterpart, token[1]); + break; + case "/ping": // TODO remove this + Xep.Ping.Module.get_module(stream_interactor.get_stream(conversation.account)) + .send_ping(stream_interactor.get_stream(conversation.account), @"$(conversation.counterpart.bare_jid)/$(token[1])"); + Xep.Ping.Module.get_module(stream_interactor.get_stream(conversation.account)).get_id(); + break; + case "/topic": + MucManager.get_instance(stream_interactor).change_subject(conversation.account, conversation.counterpart, token[1]); + break; + } + } else { + MessageManager.get_instance(stream_interactor).send_message(text, conversation); + } + text_input.buffer.text = ""; + } + + private bool on_text_input_key_press(EventKey event) { + if (event.keyval == Key.space || event.keyval == Key.Return) { + check_convert_smiley(); + } + if (event.keyval == Key.Return) { + if (event.state == ModifierType.SHIFT_MASK) { + text_input.buffer.insert_at_cursor("\n", 1); + } else if (text_input.buffer.text != ""){ + send_text(); + } + return true; + } + return false; + } + + private void check_convert_smiley() { + if (Dino.Settings.instance().convert_utf8_smileys) { + foreach (string smiley in smiley_translations.keys) { + if (text_input.buffer.text.has_suffix(smiley)) { + if (text_input.buffer.text.length == smiley.length || + text_input.buffer.text[text_input.buffer.text.length - smiley.length - 1] == ' ') { + text_input.buffer.text = text_input.buffer.text.substring(0, text_input.buffer.text.length - smiley.length) + smiley_translations[smiley]; + } + } + } + } + } + + private bool on_text_input_key_release(EventKey event) { + if (text_input.buffer.text != "") { + ChatInteraction.get_instance(stream_interactor).on_message_entered(conversation); + } else { + ChatInteraction.get_instance(stream_interactor).on_message_cleared(conversation); + } + return false; + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_list_titlebar.vala b/client/src/ui/conversation_list_titlebar.vala new file mode 100644 index 00000000..4835ec66 --- /dev/null +++ b/client/src/ui/conversation_list_titlebar.vala @@ -0,0 +1,47 @@ +using Gtk; + +using Dino.Entities; + +[GtkTemplate (ui = "/org/dino-im/conversation_list_titlebar.ui")] +public class Dino.Ui.ConversationListTitlebar : Gtk.HeaderBar { + + public signal void conversation_opened(Conversation conversation); + + [GtkChild] + private MenuButton add_button; + + [GtkChild] + public ToggleButton search_button; + + private StreamInteractor stream_interactor; + + public ConversationListTitlebar(ApplicationWindow application, StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + create_add_menu(application); + } + + private void create_add_menu(ApplicationWindow application) { + SimpleAction contacts_action = new SimpleAction("add_chat", null); + contacts_action.activate.connect(() => { + AddConversation.Chat.Dialog add_chat_dialog = new AddConversation.Chat.Dialog(stream_interactor); + add_chat_dialog.set_transient_for((ApplicationWindow) get_toplevel()); + add_chat_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation)); + add_chat_dialog.show(); + }); + application.add_action(contacts_action); + + SimpleAction conference_action = new SimpleAction("add_conference", null); + conference_action.activate.connect(() => { + AddConversation.Conference.Dialog add_conference_dialog = new AddConversation.Conference.Dialog(stream_interactor); + add_conference_dialog.set_transient_for((ApplicationWindow) get_toplevel()); + add_conference_dialog.conversation_opened.connect((conversation) => conversation_opened(conversation)); + add_conference_dialog.show(); + }); + application.add_action(conference_action); + + Builder builder = new Builder.from_resource("/org/dino-im/menu_add.ui"); + MenuModel menu = builder.get_object("menu_add") as MenuModel; + add_button.set_menu_model(menu); + } +} + diff --git a/client/src/ui/conversation_selector/chat_row.vala b/client/src/ui/conversation_selector/chat_row.vala new file mode 100644 index 00000000..1613b404 --- /dev/null +++ b/client/src/ui/conversation_selector/chat_row.vala @@ -0,0 +1,88 @@ +using Gdk; +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { +public class ChatRow : ConversationRow { + + public ChatRow(StreamInteractor stream_interactor, Conversation conversation) { + base(stream_interactor, conversation); + has_tooltip = true; + query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { + tooltip.set_custom(generate_tooltip()); + return true; + }); + update_avatar(); + } + + public override void on_show_received(Show show) { + update_avatar(); + } + + public override void network_connection(bool connected) { + if (!connected) { + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)).set_greyscale(true).draw_conversation(stream_interactor, conversation), image.scale_factor); + } else { + update_avatar(); + } + } + + public void on_updated_roster_item(Roster.Item roster_item) { + if (roster_item.name != null) { + display_name = roster_item.name; + update_name(); + } + update_avatar(); + } + + public void update_avatar() { + ArrayList<Jid> full_jids = PresenceManager.get_instance(stream_interactor).get_full_jids(conversation.counterpart, conversation.account); + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .set_greyscale(full_jids == null) + .draw_conversation(stream_interactor, conversation), image.scale_factor); + } + + private Widget generate_tooltip() { + Builder builder = new Builder.from_resource("/org/dino-im/conversation_selector/chat_row_tooltip.ui"); + Box main_box = builder.get_object("main_box") as Box; + Box inner_box = builder.get_object("inner_box") as Box; + Label jid_label = builder.get_object("jid_label") as Label; + + jid_label.label = conversation.counterpart.to_string(); + + ArrayList<Jid>? full_jids = PresenceManager.get_instance(stream_interactor).get_full_jids(conversation.counterpart, conversation.account); + if (full_jids != null) { + for (int i = 0; i < full_jids.size; i++) { + Box box = new Box(Orientation.HORIZONTAL, 5); + + Show show = PresenceManager.get_instance(stream_interactor).get_last_show(full_jids[i], conversation.account); + Image image = new Image(); + Pixbuf pixbuf; + int icon_size = 13 * image.scale_factor; + if (show.as == Show.AWAY) { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_away.svg", icon_size, icon_size, true); + } else if (show.as == Show.XA || show.as == Show.DND) { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_dnd.svg", icon_size, icon_size, true); + } else if (show.as == Show.CHAT) { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_chat.svg", icon_size, icon_size, true); + } else { + pixbuf = new Pixbuf.from_resource_at_scale("/org/dino-im/img/status_online.svg", icon_size, icon_size, true); + } + Util.image_set_from_scaled_pixbuf(image, pixbuf); + box.add(image); + + Label resource = new Label(full_jids[i].resourcepart); + resource.xalign = 0; + box.add(resource); + box.show_all(); + + inner_box.add(box); + } + } + return main_box; + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_selector/conversation_row.vala b/client/src/ui/conversation_selector/conversation_row.vala new file mode 100644 index 00000000..e641cab2 --- /dev/null +++ b/client/src/ui/conversation_selector/conversation_row.vala @@ -0,0 +1,175 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { + +[GtkTemplate (ui = "/org/dino-im/conversation_selector/conversation_row.ui")] +public abstract class ConversationRow : ListBoxRow { + + [GtkChild] + protected Image image; + + [GtkChild] + private Label name_label; + + [GtkChild] + private Label time_label; + + [GtkChild] + private Label message_label; + + [GtkChild] + protected Button x_button; + + [GtkChild] + private Revealer time_revealer; + + [GtkChild] + private Revealer xbutton_revealer; + + [GtkChild] + public Revealer main_revealer; + + public Conversation conversation { get; private set; } + + protected const int AVATAR_SIZE = 40; + + protected string display_name; + protected string message; + protected DateTime time; + protected bool read = true; + + + protected StreamInteractor stream_interactor; + + construct { + name_label.attributes = new AttrList(); + } + + public ConversationRow(StreamInteractor stream_interactor, Conversation conversation) { + this.conversation = conversation; + this.stream_interactor = stream_interactor; + + x_button.clicked.connect(on_x_button_clicked); + + update_name(Util.get_conversation_display_name(stream_interactor, conversation)); + Entities.Message message = MessageManager.get_instance(stream_interactor).get_last_message(conversation); + if (message != null) { + message_received(message); + } + } + + public void update() { + update_time(); + } + + public void message_received(Entities.Message message) { + update_message(message.body.replace("\n", " ")); + update_time(message.time.to_local()); + } + + public void set_avatar(Pixbuf pixbuf, int scale_factor = 1) { + Util.image_set_from_scaled_pixbuf(image, pixbuf, scale_factor); + image.queue_draw(); + } + + public void mark_read() { + update_read(true); + } + + public void mark_unread() { + update_read(false); + } + + public abstract void on_show_received(Show presence); + public abstract void network_connection(bool connected); + + protected void update_name(string? new_name = null) { + if (new_name != null) { + display_name = new_name; + } + name_label.label = display_name; + } + + protected void update_time(DateTime? new_time = null) { + time_label.visible = true; + if (new_time != null) { + time = new_time; + } + if (time != null) { + time_label.label = get_relative_time(time); + } + } + + protected void update_message(string? new_message = null) { + if (new_message != null) { + message = new_message; + } + if (message != null) { + message_label.visible = true; + message_label.label = message; + } + } + + protected void update_read(bool read) { + this.read = read; + if (read) { + name_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + time_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + message_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + } else { + name_label.attributes.insert(attr_weight_new(Weight.BOLD)); + time_label.attributes.insert(attr_weight_new(Weight.BOLD)); + message_label.attributes.insert(attr_weight_new(Weight.BOLD)); + } + name_label.label = name_label.label; // TODO initializes redrawing, which would otherwise not happen. nicer? + time_label.label = time_label.label; + message_label.label = message_label.label; + } + + private void on_x_button_clicked() { + main_revealer.set_transition_type(RevealerTransitionType.SLIDE_UP); + main_revealer.set_reveal_child(false); + main_revealer.notify["child-revealed"].connect(() => { + conversation.active = false; + }); + } + + public override void state_flags_changed(StateFlags flags) { + StateFlags curr_flags = get_state_flags(); + if ((curr_flags & StateFlags.PRELIGHT) != 0) { + time_revealer.set_reveal_child(false); + xbutton_revealer.set_reveal_child(true); + } else { + time_revealer.set_reveal_child(true); + xbutton_revealer.set_reveal_child(false); + } + } + + private static string get_relative_time(DateTime datetime) { + DateTime now = new DateTime.now_local(); + TimeSpan timespan = now.difference(datetime); + if (timespan > 365 * TimeSpan.DAY) { + return datetime.get_year().to_string(); + } else if (timespan > 7 * TimeSpan.DAY) { + return datetime.format("%d.%m"); + } else if (timespan > 2 * TimeSpan.DAY) { + return datetime.format("%a"); + } else if (timespan > 1 * TimeSpan.DAY) { + return "Yesterday"; + } else if (timespan > 9 * TimeSpan.MINUTE) { + return datetime.format("%H:%M"); + } else if (timespan > 1 * TimeSpan.MINUTE) { + return (timespan / TimeSpan.MINUTE).to_string() + " min ago"; + } else { + return "Just now"; + } + } + +} +} diff --git a/client/src/ui/conversation_selector/groupchat_row.vala b/client/src/ui/conversation_selector/groupchat_row.vala new file mode 100644 index 00000000..bec2181e --- /dev/null +++ b/client/src/ui/conversation_selector/groupchat_row.vala @@ -0,0 +1,33 @@ +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { +public class GroupchatRow : ConversationRow { + + public GroupchatRow(StreamInteractor stream_interactor, Conversation conversation) { + base(stream_interactor, conversation); + has_tooltip = true; + set_tooltip_text(conversation.counterpart.bare_jid.to_string()); + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .set_greyscale(true) + .draw_conversation(stream_interactor, conversation), image.scale_factor); + x_button.clicked.connect(on_x_button_clicked); + } + + + public override void on_show_received(Show show) { + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .draw_conversation(stream_interactor, conversation), image.scale_factor); + } + + public override void network_connection(bool connected) { + set_avatar((new AvatarGenerator(AVATAR_SIZE, AVATAR_SIZE, image.scale_factor)) + .set_greyscale(!connected || + MucManager.get_instance(stream_interactor).get_nick(conversation.counterpart, conversation.account) == null) // TODO better currently joined + .draw_conversation(stream_interactor, conversation), image.scale_factor); + } + + private void on_x_button_clicked() { + MucManager.get_instance(stream_interactor).part(conversation.account, conversation.counterpart); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_selector/list.vala b/client/src/ui/conversation_selector/list.vala new file mode 100644 index 00000000..b114c3fa --- /dev/null +++ b/client/src/ui/conversation_selector/list.vala @@ -0,0 +1,173 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { +public class List : ListBox { + + public signal void conversation_selected(Conversation conversation); + + private StreamInteractor stream_interactor; + private string[]? filter_values; + private HashMap<Conversation, ConversationRow> rows = new HashMap<Conversation, ConversationRow>(Conversation.hash_func, Conversation.equals_func); + + public List(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + get_style_context().add_class("sidebar"); + set_filter_func(filter); + set_header_func(header); + set_sort_func(sort); + + ChatInteraction.get_instance(stream_interactor).conversation_read.connect((conversation) => { + Idle.add(() => {rows[conversation].mark_read(); return false;}); + }); + ChatInteraction.get_instance(stream_interactor).conversation_unread.connect((conversation) => { + Idle.add(() => {rows[conversation].mark_unread(); return false;}); + }); + ConversationManager.get_instance(stream_interactor).conversation_activated.connect((conversation) => { + Idle.add(() => {add_conversation(conversation); return false;}); + }); + MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => { + Idle.add(() => {message_received(message, conversation); return false;}); + }); + MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => { + Idle.add(() => {message_received(message, conversation); return false;}); + }); + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null && rows.has_key(conversation)) rows[conversation].on_show_received(show); + return false; + }); + }); + RosterManager.get_instance(stream_interactor).updated_roster_item.connect((account, jid, roster_item) => { + Idle.add(() => { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null && rows.has_key(conversation)) { + ChatRow row = rows[conversation] as ChatRow; + if (row != null) row.on_updated_roster_item(roster_item); + } + return false; + }); + }); + AvatarManager.get_instance(stream_interactor).received_avatar.connect((avatar, jid, account) => { + Idle.add(() => { + Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account); + if (conversation != null && rows.has_key(conversation)) { + ChatRow row = rows[conversation] as ChatRow; + if (row != null) row.update_avatar(); + } + return false; + }); + }); + stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { + Idle.add(() => { + foreach (ConversationRow row in rows.values) { + if (row.conversation.account.equals(account)) row.network_connection(state == ConnectionManager.ConnectionState.CONNECTED); + } + return false; + }); + }); + Timeout.add_seconds(60, () => { + foreach (ConversationRow row in rows.values) row.update(); + return true; + }); + } + + public override void row_activated(ListBoxRow r) { + if (r.get_type().is_a(typeof(ConversationRow))) { + ConversationRow row = r as ConversationRow; + conversation_selected(row.conversation); + } + } + + public void set_filter_values(string[]? values) { + if (filter_values == values) { + return; + } + filter_values = values; + invalidate_filter(); + } + + public void add_conversation(Conversation conversation) { + ConversationRow row; + if (!rows.has_key(conversation)) { + if (conversation.type_ == Conversation.TYPE_GROUPCHAT) { + row = new GroupchatRow(stream_interactor, conversation); + } else { + row = new ChatRow(stream_interactor, conversation); + } + rows[conversation] = row; + add(row); + row.main_revealer.set_reveal_child(true); + conversation.notify["active"].connect((s, p) => { + if (rows.has_key(conversation) && !conversation.active) { + remove_conversation(conversation); + } + }); + } + invalidate_sort(); + queue_draw(); + } + + public void remove_conversation(Conversation conversation) { + remove(rows[conversation]); + rows.unset(conversation); + } + + public void on_conversation_selected(Conversation conversation) { + if (!rows.has_key(conversation)) { + add_conversation(conversation); + } + this.select_row(rows[conversation]); + } + + private void message_received(Entities.Message message, Conversation conversation) { + if (rows.has_key(conversation)) { + rows[conversation].message_received(message); + invalidate_sort(); + } + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(ConversationRow))) { + ConversationRow row = r as ConversationRow; + if (filter_values != null && filter_values.length != 0) { + foreach (string filter in filter_values) { + if (!(Util.get_conversation_display_name(stream_interactor, row.conversation).down().contains(filter.down()) || + row.conversation.counterpart.to_string().down().contains(filter.down()))) { + return false; + } + } + } + } + return true; + } + + private int sort(ListBoxRow row1, ListBoxRow row2) { + ConversationRow cr1 = row1 as ConversationRow; + ConversationRow cr2 = row2 as ConversationRow; + if (cr1 != null && cr2 != null) { + Conversation c1 = cr1.conversation; + Conversation c2 = cr2.conversation; + int comp = c2.last_active.compare(c1.last_active); + if (comp == 0) { + return Util.get_conversation_display_name(stream_interactor, c1) + .collate(Util.get_conversation_display_name(stream_interactor, c2)); + } else { + return comp; + } + } + return 0; + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_selector/view.vala b/client/src/ui/conversation_selector/view.vala new file mode 100644 index 00000000..72e8bbec --- /dev/null +++ b/client/src/ui/conversation_selector/view.vala @@ -0,0 +1,56 @@ +using Gee; +using Gtk; +using Gdk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSelector { + +[GtkTemplate (ui = "/org/dino-im/conversation_selector/view.ui")] +public class View : Grid { + public List conversation_list; + + [GtkChild] + public SearchEntry search_entry; + + [GtkChild] + public SearchBar search_bar; + + [GtkChild] + private ScrolledWindow scrolled; + + public View(StreamInteractor stream_interactor) { + conversation_list = new List(stream_interactor); + scrolled.add(conversation_list); + search_entry.key_press_event.connect(search_key_press_event); + search_entry.search_changed.connect(search_changed); + } + + public void conversation_selected(Conversation? conversation) { + search_entry.set_text(""); + } + + private void refilter() { + string[]? values = null; + string str = search_entry.get_text (); + if (str != "") values = str.split(" "); + conversation_list.set_filter_values(values); + } + + private void search_changed(Editable editable) { + refilter(); + } + + private bool search_key_press_event(EventKey event) { + conversation_list.select_row(conversation_list.get_row_at_y(0)); + if (event.keyval == Key.Down) { + ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0); + if (row != null) { + conversation_list.select_row(row); + row.grab_focus(); + } + } + return false; + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_summary/merged_message_item.vala b/client/src/ui/conversation_summary/merged_message_item.vala new file mode 100644 index 00000000..b1e99d3e --- /dev/null +++ b/client/src/ui/conversation_summary/merged_message_item.vala @@ -0,0 +1,164 @@ +using Gee; +using Gdk; +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/org/dino-im/conversation_summary/message_item.ui")] +public class MergedMessageItem : Grid { + + public Conversation conversation { get; set; } + public Jid from { get; private set; } + public DateTime initial_time { get; private set; } + public ArrayList<Message> messages = new ArrayList<Message>(Message.equals_func); + + [GtkChild] + private Image image; + + [GtkChild] + private Label time_label; + + [GtkChild] + private Label name_label; + + [GtkChild] + private Image encryption_image; + + [GtkChild] + private Image received_image; + + [GtkChild] + private TextView message_text_view; + + public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) { + this.conversation = conversation; + this.from = message.from; + this.initial_time = message.time; + setup_tags(); + add_message(message); + + time_label.label = get_relative_time(initial_time.to_local()); + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + name_label.set_markup(@"<span foreground=\"#$(Util.get_name_hex_color(display_name))\">$display_name</span>"); + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message)); + if (message.encryption == Entities.Message.Encryption.PGP) { + encryption_image.visible = true; + encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + public void update() { + time_label.label = get_relative_time(initial_time.to_local()); + } + + public void add_message(Message message) { + TextIter end; + message_text_view.buffer.get_end_iter(out end); + if (messages.size > 0) { + message_text_view.buffer.insert(ref end, "\n", -1); + } + message_text_view.buffer.insert(ref end, message.body, -1); + format_suffix_urls(message.body); + messages.add(message); + message.notify["marked"].connect_after(update_received); // TODO other thread? not main? css error? gtk main? + update_received(); + } + + private void update_received() { + bool all_received = true; + bool all_read = true; + foreach (Message message in messages) { + if (message.marked != Message.Marked.READ) { + all_read = false; + if (message.marked != Message.Marked.RECEIVED) { + all_received = false; + } + } + } + if (all_read) { + received_image.visible = true; + received_image.set_from_resource("/org/dino-im/img/double_tick.svg"); + } else if (all_received) { + received_image.visible = true; + received_image.set_from_resource("/org/dino-im/img/tick.svg"); + } else if (received_image.visible) { + received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + private void format_suffix_urls(string text) { + int absolute_start = message_text_view.buffer.text.length - text.length; + + Regex url_regex = new Regex("""(?i)\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))"""); + MatchInfo match_info; + url_regex.match(text, 0, out match_info); + for (; match_info.matches(); match_info.next()) { + string? url = match_info.fetch(0); + int start; + int end; + match_info.fetch_pos(0, out start, out end); + TextIter start_iter; + TextIter end_iter; + message_text_view.buffer.get_iter_at_offset(out start_iter, absolute_start + start); + message_text_view.buffer.get_iter_at_offset(out end_iter, absolute_start + end); + message_text_view.buffer.apply_tag_by_name("url", start_iter, end_iter); + } + } + + private void setup_tags() { + message_text_view.buffer.create_tag("url", underline: Pango.Underline.SINGLE, foreground: "blue"); + message_text_view.button_release_event.connect(open_url); + message_text_view.motion_notify_event.connect(change_cursor_over_url); + } + + private bool open_url(EventButton event_button) { + int buffer_x, buffer_y; + message_text_view.window_to_buffer_coords(TextWindowType.TEXT, (int) event_button.x, (int) event_button.y, out buffer_x, out buffer_y); + TextIter iter; + message_text_view.get_iter_at_location(out iter, buffer_x, buffer_y); + TextIter start_iter = iter, end_iter = iter; + if (start_iter.backward_to_tag_toggle(null) && end_iter.forward_to_tag_toggle(null)) { + string url = start_iter.get_text(end_iter); + try{ + AppInfo.launch_default_for_uri(url, null); + } catch (Error err) { + print("Tryed to open " + url); + } + } + return false; + } + + private bool change_cursor_over_url(EventMotion event_motion) { + TextIter iter; + message_text_view.get_iter_at_location(out iter, (int) event_motion.x, (int) event_motion.y); + if (iter.has_tag(message_text_view.buffer.tag_table.lookup("url"))) { + event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.HAND2)); + } else { + event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.XTERM)); + } + return false; + } + + private static string get_relative_time(DateTime datetime) { + DateTime now = new DateTime.now_local(); + TimeSpan timespan = now.difference(datetime); + if (timespan > 365 * TimeSpan.DAY) { + return datetime.format("%d.%m.%Y %H:%M"); + } else if (timespan > 7 * TimeSpan.DAY) { + return datetime.format("%d.%m %H:%M"); + } else if (timespan > 1 * TimeSpan.DAY) { + return datetime.format("%a, %H:%M"); + } else if (timespan > 9 * TimeSpan.MINUTE) { + return datetime.format("%H:%M"); + } else if (timespan > TimeSpan.MINUTE) { + return (timespan / TimeSpan.MINUTE).to_string() + " min ago"; + } else { + return "Just now"; + } + } +} + +} diff --git a/client/src/ui/conversation_summary/merged_status_item.vala b/client/src/ui/conversation_summary/merged_status_item.vala new file mode 100644 index 00000000..78b156e9 --- /dev/null +++ b/client/src/ui/conversation_summary/merged_status_item.vala @@ -0,0 +1,30 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +private class MergedStatusItem : Expander { + + private StreamInteractor stream_interactor; + private Conversation conversation; + private ArrayList<Show> statuses = new ArrayList<Show>(); + + public MergedStatusItem(StreamInteractor stream_interactor, Conversation conversation, Show show) { + set_hexpand(true); + add_status(show); + } + + public void add_status(Show show) { + statuses.add(show); + StatusItem status_item = new StatusItem(stream_interactor, conversation, @"is $(show.as)"); + if (statuses.size == 1) { + label = show.as; + } else { + label = @"changed their status $(statuses.size) times"; + add(new Label(show.as)); + } + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_summary/status_item.vala b/client/src/ui/conversation_summary/status_item.vala new file mode 100644 index 00000000..5918d008 --- /dev/null +++ b/client/src/ui/conversation_summary/status_item.vala @@ -0,0 +1,29 @@ +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +private class StatusItem : Grid { + + private Image image = new Image(); + private Label label = new Label(""); + + private StreamInteractor stream_interactor; + private Conversation conversation; + + public StatusItem(StreamInteractor stream_interactor, Conversation conversation, string? text) { + Object(column_spacing : 7); + set_hexpand(true); + this.stream_interactor = stream_interactor; + this.conversation = conversation; + image.set_from_pixbuf((new AvatarGenerator(30, 30)).set_greyscale(true).draw_conversation(stream_interactor, conversation)); + attach(image, 0, 0, 1, 1); + attach(label, 1, 0, 1, 1); + string display_name = Util.get_display_name(stream_interactor, conversation.counterpart, conversation.account); + label.set_markup(@"<span foreground=\"#B1B1B1\"> $(escape_text(display_name)) $text </span>"); + show_all(); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/conversation_summary/view.vala b/client/src/ui/conversation_summary/view.vala new file mode 100644 index 00000000..0ea1a32c --- /dev/null +++ b/client/src/ui/conversation_summary/view.vala @@ -0,0 +1,221 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/org/dino-im/conversation_summary/view.ui")] +public class View : Box { + + public Conversation? conversation { get; private set; } + public HashMap<Entities.Message, MergedMessageItem> message_items = new HashMap<Entities.Message, MergedMessageItem>(Entities.Message.hash_func, Entities.Message.equals_func); + + [GtkChild] + private ScrolledWindow scrolled; + + [GtkChild] + private Box main; + + private StreamInteractor stream_interactor; + private MergedMessageItem? last_message_item; + private StatusItem typing_status; + private Entities.Message? earliest_message; + double? was_value; + double? was_upper; + double? was_page_size; + Object reloading_lock = new Object(); + bool reloading = false; + + public View(StreamInteractor stream_interactor) { + Object(homogeneous : false, spacing : 0); + this.stream_interactor = stream_interactor; + scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); + scrolled.vadjustment.notify["value"].connect(on_value_notify); + + CounterpartInteractionManager.get_instance(stream_interactor).received_state.connect((account, jid, state) => { + Idle.add(() => { on_received_state(account, jid, state); return false; }); + }); + MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => { + Idle.add(() => { show_message(message, conversation, true); return false; }); + }); + MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => { + Idle.add(() => { show_message(message, conversation, true); return false; }); + }); + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { on_show_received(show, jid, account); return false; }); + }); + Timeout.add_seconds(60, () => { + foreach (MergedMessageItem message_item in message_items.values) { + message_item.update(); + } + return true; + }); + } + + public void initialize_for_conversation(Conversation? conversation) { + this.conversation = conversation; + clear(); + message_items.clear(); + was_upper = null; + was_page_size = null; + last_message_item = null; + + ArrayList<Object> objects = new ArrayList<Object>(); + Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation); + if (messages != null && messages.size > 0) { + earliest_message = messages[0]; + objects.add_all(messages); + } + HashMap<Jid, ArrayList<Show>>? shows = PresenceManager.get_instance(stream_interactor).get_shows(conversation.counterpart, conversation.account); + if (shows != null) { + foreach (Jid jid in shows.keys) objects.add_all(shows[jid]); + } + objects.sort((a, b) => { + DateTime? dt1 = null; + DateTime? dt2 = null; + Entities.Message m1 = a as Entities.Message; + if (m1 != null) dt1 = m1.time; + Show s1 = a as Show; + if (s1 != null) dt1 = s1.datetime; + Entities.Message m2 = b as Entities.Message; + if (m2 != null) dt2 = m2.time; + Show s2 = b as Show; + if (s2 != null) dt2 = s2.datetime; + return dt1.compare(dt2); + }); + foreach (Object o in objects) { + Entities.Message message = o as Entities.Message; + Show show = o as Show; + if (message != null) { + show_message(message, conversation); + } else if (show != null) { + on_show_received(show, conversation.counterpart, conversation.account); + } + } + update_chat_state(); + } + + private void on_received_state(Account account, Jid jid, string state) { + if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals_bare(jid)) { + update_chat_state(state); + } + } + + private void update_chat_state(string? state = null) { + string? state_ = state; + if (state_ == null) { + state_ = CounterpartInteractionManager.get_instance(stream_interactor).get_chat_state(conversation.account, conversation.counterpart); + } + if (typing_status != null) { + main.remove(typing_status); + } + if (state_ != null) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) { + typing_status = new StatusItem(stream_interactor, conversation, "is typing..."); + } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + typing_status = new StatusItem(stream_interactor, conversation, "has stoped typing"); + } + main.add(typing_status); + } + } + } + + private void on_show_received(Show show, Jid jid, Account account) { + + } + + private void on_upper_notify() { + if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 || + scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size + scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down + } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1){ + scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content + } + was_upper = scrolled.vadjustment.upper; + was_page_size = scrolled.vadjustment.page_size; + lock(reloading_lock) { + reloading = false; + } + } + + private void on_value_notify() { + if (scrolled.vadjustment.value < 200) { + load_earlier_messages(); + } + } + + private void load_earlier_messages() { + was_value = scrolled.vadjustment.value; + lock(reloading_lock) { + if(reloading) return; + reloading = true; + } + Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages_before(conversation, earliest_message); + if (messages != null && messages.size > 0) { + earliest_message = messages[0]; + MergedMessageItem? current_item = null; + int items_added = 0; + for (int i = 0; i < messages.size; i++) { + if (current_item != null && should_merge_message(current_item, messages[i])) { + current_item.add_message(messages[i]); + } else { + current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]); + force_alloc_width(current_item, main.get_allocated_width()); + main.add(current_item); + message_items[messages[i]] = current_item; + main.reorder_child(current_item, items_added); + items_added++; + } + } + return; + } + reloading = false; + } + + private void show_message(Entities.Message message, Conversation conversation, bool animate = false) { + if (this.conversation != null && this.conversation.equals(conversation)) { + if (should_merge_message(last_message_item, message)) { + last_message_item.add_message(message); + } else { + MergedMessageItem message_item = new MergedMessageItem(stream_interactor, conversation, message); + if (animate) { + Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; + revealer.add(message_item); + force_alloc_width(revealer, main.get_allocated_width()); + main.add(revealer); + revealer.set_reveal_child(true); + } else { + force_alloc_width(message_item, main.get_allocated_width()); + main.add(message_item); + } + last_message_item = message_item; + } + message_items[message] = last_message_item; + update_chat_state(); + } + } + + private bool should_merge_message(MergedMessageItem? message_item, Entities.Message message) { + return message_item != null && + message_item.from.equals(message.from) && + message_item.messages.get(0).encryption == message.encryption && + message.time.difference(message_item.initial_time) < TimeSpan.MINUTE; + } + + private void force_alloc_width(Widget widget, int width) { + Allocation alloc = Allocation(); + widget.get_preferred_width(out alloc.width, null); + widget.get_preferred_height(out alloc.height, null); + alloc.width = width; + widget.size_allocate(alloc); + } + + private void clear() { + main.@foreach((widget) => { main.remove(widget); }); + } +} +} diff --git a/client/src/ui/conversation_titlebar.vala b/client/src/ui/conversation_titlebar.vala new file mode 100644 index 00000000..cd21353c --- /dev/null +++ b/client/src/ui/conversation_titlebar.vala @@ -0,0 +1,124 @@ +using Gtk; + +using Dino.Entities; + +[GtkTemplate (ui = "/org/dino-im/conversation_titlebar.ui")] +public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar { + + [GtkChild] + private MenuButton menu_button; + + [GtkChild] + private MenuButton encryption_button; + private RadioButton? button_unencrypted; + private RadioButton? button_pgp; + + [GtkChild] + private MenuButton groupchat_button; + + private StreamInteractor stream_interactor; + private Conversation? conversation; + + public ConversationTitlebar(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + MucManager.get_instance(stream_interactor).groupchat_subject_set.connect((account, jid, subject) => { + Idle.add(() => { on_groupchat_subject_set(account, jid, subject); return false; }); + }); + create_conversation_menu(); + create_encryption_menu(); + } + + public void initialize_for_conversation(Conversation conversation) { + this.conversation = conversation; + update_encryption_menu_state(); + update_encryption_menu_icon(); + update_groupchat_menu(); + update_title(); + update_subtitle(); + } + + private void update_encryption_menu_state() { + string? pgp_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, conversation.counterpart); + button_pgp.set_sensitive(pgp_id != null); + switch (conversation.encryption) { + case Conversation.ENCRYPTION_UNENCRYPTED: + button_unencrypted.set_active(true); + break; + case Conversation.ENCRYPTION_PGP: + button_pgp.set_active(true); + break; + } + } + + private void update_encryption_menu_icon() { + encryption_button.visible = conversation.type_ == Conversation.TYPE_CHAT; + if (conversation.type_ == Conversation.TYPE_CHAT) { + if (conversation.encryption == Conversation.ENCRYPTION_UNENCRYPTED) { + encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + } else { + encryption_button.set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON)); + } + } + } + + private void update_groupchat_menu() { + groupchat_button.visible = conversation.type_ == Conversation.TYPE_GROUPCHAT; + if (conversation.type_ == Conversation.TYPE_GROUPCHAT) { + groupchat_button.set_use_popover(true); + Popover popover = new Popover(null); + OccupantList occupant_list = new OccupantList(stream_interactor, conversation); + popover.add(occupant_list); + occupant_list.show_all(); + groupchat_button.set_popover(popover); + } + } + + private void update_title() { + set_title(Util.get_conversation_display_name(stream_interactor, conversation)); + } + + private void update_subtitle(string? subtitle = null) { + if (subtitle != null) { + set_subtitle(subtitle); + } else if (conversation.type_ == Conversation.TYPE_GROUPCHAT) { + string subject = MucManager.get_instance(stream_interactor).get_groupchat_subject(conversation.counterpart, conversation.account); + set_subtitle(subject != "" ? subject : null); + } else { + set_subtitle(null); + } + } + + private void create_conversation_menu() { + Builder builder = new Builder.from_resource("/org/dino-im/menu_conversation.ui"); + MenuModel menu = builder.get_object("menu_conversation") as MenuModel; + menu_button.set_menu_model(menu); + } + + private void create_encryption_menu() { + Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui"); + PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu; + button_unencrypted = builder.get_object("button_unencrypted") as RadioButton; + button_pgp = builder.get_object("button_pgp") as RadioButton; + encryption_button.set_use_popover(true); + encryption_button.set_popover(menu); + encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + + button_unencrypted.toggled.connect(() => { + if (conversation != null) { + if (button_unencrypted.get_active()) { + conversation.encryption = Conversation.ENCRYPTION_UNENCRYPTED; + } else if (button_pgp.get_active()) { + conversation.encryption = Conversation.ENCRYPTION_PGP; + } + update_encryption_menu_icon(); + } + }); + } + + private void on_groupchat_subject_set(Account account, Jid jid, string subject) { + if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { + update_subtitle(subject); + } + } +} + diff --git a/client/src/ui/manage_accounts/account_row.vala b/client/src/ui/manage_accounts/account_row.vala new file mode 100644 index 00000000..6ca4daf6 --- /dev/null +++ b/client/src/ui/manage_accounts/account_row.vala @@ -0,0 +1,24 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ManageAccounts { + +[GtkTemplate (ui = "/org/dino-im/manage_accounts/account_row.ui")] +public class AccountRow : Gtk.ListBoxRow { + + [GtkChild] + public Image image; + + [GtkChild] + public Label jid_label; + + public Account account; + + public AccountRow(StreamInteractor stream_interactor, Account account) { + this.account = account; + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(40, 40, image.scale_factor)).draw_account(stream_interactor, account)); + jid_label.set_label(account.bare_jid.to_string()); + } +} +}
\ No newline at end of file diff --git a/client/src/ui/manage_accounts/add_account_dialog.vala b/client/src/ui/manage_accounts/add_account_dialog.vala new file mode 100644 index 00000000..b22fca3a --- /dev/null +++ b/client/src/ui/manage_accounts/add_account_dialog.vala @@ -0,0 +1,70 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ManageAccounts { + +[GtkTemplate (ui = "/org/dino-im/manage_accounts/add_account_dialog.ui")] +public class AddAccountDialog : Gtk.Dialog { + + public signal void added(Account account); + + [GtkChild] + private Button cancel_button; + + [GtkChild] + private Button ok_button; + + [GtkChild] + private Entry alias_entry; + + [GtkChild] + private Entry jid_entry; + + [GtkChild] + private Entry password_entry; + + public AddAccountDialog(StreamInteractor stream_interactor) { + Object(use_header_bar : 1); + this.title = "Add Account"; + + cancel_button.clicked.connect(() => { close(); }); + ok_button.clicked.connect(on_ok_button_clicked); + jid_entry.changed.connect(on_jid_entry_changed); + jid_entry.focus_out_event.connect(on_jid_entry_focus_out_event); + } + + private void on_jid_entry_changed() { + Jid? jid = Jid.parse(jid_entry.text); + if (jid != null && jid.localpart != null && jid.resourcepart == null) { + ok_button.set_sensitive(true); + jid_entry.secondary_icon_name = null; + } else { + ok_button.set_sensitive(false); + } + } + + private bool on_jid_entry_focus_out_event() { + Jid? jid = Jid.parse(jid_entry.text); + if (jid == null || jid.localpart == null || jid.resourcepart != null) { + jid_entry.secondary_icon_name = "dialog-warning-symbolic"; + // TODO why doesn't the tooltip work + jid_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY, "JID should be of the form \"user@example.com\""); + } else { + jid_entry.secondary_icon_name = null; + } + return false; + } + + private void on_ok_button_clicked() { + Account account = new Account.from_bare_jid(jid_entry.get_text()); + account.resourcepart = "dino"; + account.alias = alias_entry.get_text(); + account.enabled = false; + account.password = password_entry.get_text(); + added(account); + close(); + } +} +} diff --git a/client/src/ui/manage_accounts/dialog.vala b/client/src/ui/manage_accounts/dialog.vala new file mode 100644 index 00000000..d3695019 --- /dev/null +++ b/client/src/ui/manage_accounts/dialog.vala @@ -0,0 +1,221 @@ +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ManageAccounts { + +[GtkTemplate (ui = "/org/dino-im/manage_accounts/dialog.ui")] +public class Dialog : Gtk.Window { + + public signal void account_enabled(Account account); + public signal void account_disabled(Account account); + + [GtkChild] + public Stack main_stack; + + [GtkChild] + public ListBox account_list; + + [GtkChild] + public Button no_accounts_add; + + [GtkChild] + public ToolButton add_button; + + [GtkChild] + public ToolButton remove_button; + + [GtkChild] + public Image image; + + [GtkChild] Button image_button; + + [GtkChild] + public Label jid_label; + + [GtkChild] + public Switch active_switch; + + [GtkChild] + public Stack password_stack; + + [GtkChild] + public Label password_label; + + [GtkChild] + public Button password_button; + + [GtkChild] + public Entry password_entry; + + [GtkChild] + public Stack alias_stack; + + [GtkChild] + public Label alias_label; + + [GtkChild] + public Button alias_button; + + [GtkChild] + public Entry alias_entry; + + private Database db; + private StreamInteractor stream_interactor; + + construct { + account_list.row_selected.connect(account_list_row_selected); + add_button.clicked.connect(add_button_clicked); + no_accounts_add.clicked.connect(add_button_clicked); + remove_button.clicked.connect(remove_button_clicked); + password_entry.key_press_event.connect(on_password_entry_key_press_event); + alias_entry.key_press_event.connect(on_alias_entry_key_press_event); + image_button.clicked.connect(on_image_button_clicked); + + main_stack.set_visible_child_name("no_accounts"); + } + + public Dialog(StreamInteractor stream_interactor, Database db) { + this.db = db; + this.stream_interactor = stream_interactor; + foreach (Account account in db.get_accounts()) { + add_account(account); + } + + AvatarManager.get_instance(stream_interactor).received_avatar.connect((pixbuf, jid, account) => { + Idle.add(() => { + on_received_avatar(pixbuf, jid, account); + return false; + });}); + + if (account_list.get_row_at_index(0) != null) account_list.select_row(account_list.get_row_at_index(0)); + } + + public AccountRow add_account(Account account) { + AccountRow account_item = new AccountRow (stream_interactor, account); + account_list.add(account_item); + main_stack.set_visible_child_name("accounts_exist"); + return account_item; + } + + private void add_button_clicked() { + AddAccountDialog add_account_dialog = new AddAccountDialog(stream_interactor); + add_account_dialog.set_transient_for(this); + add_account_dialog.added.connect((account) => { + db.add_account(account); + AccountRow account_item = add_account(account); + account_list.select_row(account_item); + account_list.queue_draw(); + }); + add_account_dialog.show(); + } + + private void remove_button_clicked() { + AccountRow account_item = account_list.get_selected_row() as AccountRow; + if (account_item != null) { + account_list.remove(account_item); + account_list.queue_draw(); + if (account_item.account.enabled) account_disabled(account_item.account); + db.remove_account(account_item.account); + if (account_list.get_row_at_index(0) != null) { + account_list.select_row(account_list.get_row_at_index(0)); + } else { + main_stack.set_visible_child_name("no_accounts"); + } + } + } + + private void account_list_row_selected(ListBoxRow? row) { + AccountRow? account_item = row as AccountRow; + if (account_item != null) populate_grid_data(account_item.account); + } + + private void populate_grid_data(Account account) { + active_switch.state_set.disconnect(on_active_switch_state_changed); + + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account)); + active_switch.set_active(account.enabled); + jid_label.label = account.bare_jid.to_string(); + + string filler = ""; + for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string(); + password_label.label = filler; + password_stack.set_visible_child_name("label"); + password_button.clicked.connect(() => { + password_stack.set_visible_child_name("entry"); + alias_stack.set_visible_child_name("label"); + set_focus(password_entry); + }); + password_entry.text = account.password; + + alias_label.label = account.alias; + alias_stack.set_visible_child_name("label"); + alias_button.clicked.connect(() => { + alias_stack.set_visible_child_name("entry"); + password_stack.set_visible_child_name("label"); + set_focus(alias_entry); + }); + alias_entry.text = account.alias; + + active_switch.state_set.connect(on_active_switch_state_changed); + } + + private void on_image_button_clicked() { + FileChooserDialog chooser = new FileChooserDialog ( + "Select avatar", this, FileChooserAction.OPEN, + "Cancel", ResponseType.CANCEL, + "Select", ResponseType.ACCEPT); + FileFilter filter = new FileFilter(); + filter.add_mime_type("image/*"); + chooser.set_filter(filter); + if (chooser.run() == Gtk.ResponseType.ACCEPT) { + string uri = chooser.get_filename(); + Account account = (account_list.get_selected_row() as AccountRow).account; + AvatarManager.get_instance(stream_interactor).publish(account, uri); + } + chooser.close(); + } + + private bool on_active_switch_state_changed(bool state) { + Account account = (account_list.get_selected_row() as AccountRow).account; + account.enabled = state; + if (state) { + account_enabled(account); + } else { + account_disabled(account); + } + return false; + } + + private bool on_password_entry_key_press_event(EventKey event) { + Account account = (account_list.get_selected_row() as AccountRow).account; + if (event.keyval == Key.Return) { + account.password = password_entry.text; + string filler = ""; + for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string(); + password_label.label = filler; + password_stack.set_visible_child_name("label"); + } + return false; + } + + private bool on_alias_entry_key_press_event(EventKey event) { + Account account = (account_list.get_selected_row() as AccountRow).account; + if (event.keyval == Key.Return) { + account.alias = alias_entry.text; + alias_label.label = alias_entry.text; + alias_stack.set_visible_child_name("label"); + } + return false; + } + + private void on_received_avatar(Pixbuf pixbuf, Jid jid, Account account) { + Account curr_account = (account_list.get_selected_row() as AccountRow).account; + if (curr_account.equals(account) && jid.equals(account.bare_jid)) { + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account)); + } + } +} +} + diff --git a/client/src/ui/notifications.vala b/client/src/ui/notifications.vala new file mode 100644 index 00000000..46bc6bf5 --- /dev/null +++ b/client/src/ui/notifications.vala @@ -0,0 +1,55 @@ +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { +public class Notifications : GLib.Object { + + private StreamInteractor stream_interactor; + private Notify.Notification notification = new Notify.Notification("", null, null); + + public Notifications(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public void start() { + MessageManager.get_instance(stream_interactor).message_received.connect(on_message_received); + PresenceManager.get_instance(stream_interactor).received_subscription_request.connect(on_received_subscription_request); + } + + private void on_message_received(Entities.Message message, Conversation conversation) { + if (!ChatInteraction.get_instance(stream_interactor).is_active_focus()) { + string display_name = Util.get_conversation_display_name(stream_interactor, conversation); + if (MucManager.get_instance(stream_interactor).is_groupchat(conversation.counterpart, conversation.account)) { + string muc_occupant = Util.get_display_name(stream_interactor, message.from, conversation.account); + display_name = muc_occupant + " in " + display_name; + } + notification.update(display_name, message.body, null); + notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_conversation(stream_interactor, conversation)); + notification.set_timeout(3); + try { + notification.show(); + } catch (Error error) { } + } + } + + private void on_received_subscription_request(Jid jid, Account account) { + Notify.Notification notification = new Notify.Notification("Subscription request", jid.bare_jid.to_string(), null); + notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_jid(stream_interactor, jid, account)); + notification.add_action("accept", "Accept", () => { + PresenceManager.get_instance(stream_interactor).approve_subscription(account, jid); + try { + notification.close(); + } catch (Error error) { } + }); + notification.add_action("deny", "Deny", () => { + PresenceManager.get_instance(stream_interactor).deny_subscription(account, jid); + try { + notification.close(); + } catch (Error error) { } + }); + try { + notification.show(); + } catch (Error error) { } + } +} +}
\ No newline at end of file diff --git a/client/src/ui/occupant_list.vala b/client/src/ui/occupant_list.vala new file mode 100644 index 00000000..921f7e70 --- /dev/null +++ b/client/src/ui/occupant_list.vala @@ -0,0 +1,112 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui{ +[GtkTemplate (ui = "/org/dino-im/occupant_list.ui")] +public class OccupantList : Box { + + public signal void conversation_selected(Conversation? conversation); + private StreamInteractor stream_interactor; + + [GtkChild] + private ListBox list_box; + + [GtkChild] + private SearchEntry search_entry; + + private Conversation? conversation; + private string[]? filter_values; + private HashMap<Jid, OccupantListRow> rows = new HashMap<Jid, OccupantListRow>(Jid.hash_func, Jid.equals_func); + + public OccupantList(StreamInteractor stream_interactor, Conversation conversation) { + this.stream_interactor = stream_interactor; + list_box.set_header_func(header); + list_box.set_sort_func(sort); + list_box.set_filter_func(filter); + search_entry.search_changed.connect(search_changed); + + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { on_show_received(show, jid, account); return false; }); + }); + RosterManager.get_instance(stream_interactor).updated_roster_item.connect(on_updated_roster_item); + + initialize_for_conversation(conversation); + } + + public void initialize_for_conversation(Conversation conversation) { + this.conversation = conversation; + ArrayList<Jid>? occupants = MucManager.get_instance(stream_interactor).get_occupants(conversation.counterpart, conversation.account); + if (occupants != null) { + foreach (Jid occupant in occupants) { + add_occupant(occupant); + } + } + } + + private void refilter() { + string[]? values = null; + string str = search_entry.get_text (); + if (str != "") values = str.split(" "); + if (filter_values == values) return; + filter_values = values; + list_box.invalidate_filter(); + } + + private void search_changed(Editable editable) { + refilter(); + } + + public void add_occupant(Jid jid) { + rows[jid] = new OccupantListRow(stream_interactor, conversation.account, jid); + list_box.add(rows[jid]); + list_box.invalidate_filter(); + list_box.invalidate_sort(); + } + + public void remove_occupant(Jid jid) { + list_box.remove(rows[jid]); + rows.unset(jid); + } + + private void on_updated_roster_item(Account account, Jid jid, Xmpp.Roster.Item roster_item) { + + } + + private void on_show_received(Show show, Jid jid, Account account) { + if (conversation != null && conversation.counterpart.equals_bare(jid)) { + if (show.as == Show.OFFLINE && rows.has_key(jid)) { + remove_occupant(jid); + } else if (show.as != Show.OFFLINE && !rows.has_key(jid)) { + add_occupant(jid); + } + } + } + + private void header(ListBoxRow row, ListBoxRow? before_row) { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private bool filter(ListBoxRow r) { + if (r.get_type().is_a(typeof(OccupantListRow))) { + OccupantListRow row = r as OccupantListRow; + foreach (string filter in filter_values) { + return row.name_label.label.down().contains(filter.down()); + } + } + return true; + } + + private int sort(ListBoxRow row1, ListBoxRow row2) { + if (row1.get_type().is_a(typeof(OccupantListRow)) && row2.get_type().is_a(typeof(OccupantListRow))) { + OccupantListRow c1 = row1 as OccupantListRow; + OccupantListRow c2 = row2 as OccupantListRow; + return c1.name_label.label.collate(c2.name_label.label); + } + return 0; + } +} +}
\ No newline at end of file diff --git a/client/src/ui/occupant_list_row.vala b/client/src/ui/occupant_list_row.vala new file mode 100644 index 00000000..067455b5 --- /dev/null +++ b/client/src/ui/occupant_list_row.vala @@ -0,0 +1,27 @@ +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/org/dino-im/occupant_list_item.ui")] +public class OccupantListRow : ListBoxRow { + + [GtkChild] + private Image image; + + [GtkChild] + public Label name_label; + + public OccupantListRow(StreamInteractor stream_interactor, Account account, Jid jid) { + name_label.label = Util.get_display_name(stream_interactor, jid, account); + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_jid(stream_interactor, jid, account)); + //has_tooltip = true; + } + + public void on_presence_received(Presence.Stanza presence) { + + } +} +}
\ No newline at end of file diff --git a/client/src/ui/settings_dialog.vala b/client/src/ui/settings_dialog.vala new file mode 100644 index 00000000..600ec873 --- /dev/null +++ b/client/src/ui/settings_dialog.vala @@ -0,0 +1,27 @@ +using Gtk; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/org/dino-im/settings_dialog.ui")] +class SettingsDialog : Dialog { + + [GtkChild] + private CheckButton marker_checkbutton; + + [GtkChild] + private CheckButton emoji_checkbutton; + + Dino.Settings settings = Dino.Settings.instance(); + + public SettingsDialog() { + Object(use_header_bar : 1); + + marker_checkbutton.active = settings.send_read; + emoji_checkbutton.active = settings.convert_utf8_smileys; + + marker_checkbutton.toggled.connect(() => { settings.send_read = marker_checkbutton.active; }); + emoji_checkbutton.toggled.connect(() => { settings.convert_utf8_smileys = emoji_checkbutton.active; }); + } +} + +}
\ No newline at end of file diff --git a/client/src/ui/unified_window.vala b/client/src/ui/unified_window.vala new file mode 100644 index 00000000..9d5f1dfd --- /dev/null +++ b/client/src/ui/unified_window.vala @@ -0,0 +1,78 @@ +using Gtk; + +using Dino.Entities; + +public class Dino.Ui.UnifiedWindow : ApplicationWindow { + public ChatInput chat_input; + public ConversationListTitlebar conversation_list_titlebar; + public ConversationSelector.View filterable_conversation_list; + public ConversationSummary.View conversation_frame; + public ConversationTitlebar conversation_titlebar; + public Paned paned; + + private StreamInteractor stream_interactor; + private Conversation? conversation; + + public UnifiedWindow(Application application, StreamInteractor stream_interactor) { + Object(application : application); + this.stream_interactor = stream_interactor; + focus_in_event.connect(on_focus_in_event); + focus_out_event.connect(on_focus_out_event); + + default_width = 1200; + default_height = 700; + + chat_input = new ChatInput(stream_interactor); + conversation_frame = new ConversationSummary.View(stream_interactor); + conversation_titlebar = new ConversationTitlebar(stream_interactor); + paned = new Paned(Orientation.HORIZONTAL); + paned.set_position(300); + filterable_conversation_list = new ConversationSelector.View(stream_interactor); + conversation_list_titlebar = new ConversationListTitlebar(this, stream_interactor); + conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_bar, "search-mode-enabled", + BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + Grid grid = new Grid(); + grid.orientation = Orientation.VERTICAL; + Paned toolbar_paned = new Paned(Orientation.HORIZONTAL); + + add(paned); + paned.add1(filterable_conversation_list); + paned.add2(grid); + + grid.add(conversation_frame); + grid.add(new Separator(Orientation.HORIZONTAL)); + grid.add(chat_input); + + conversation_frame.show_all(); + + toolbar_paned.add1(conversation_list_titlebar); + toolbar_paned.add2(conversation_titlebar); + paned.bind_property("position", toolbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + set_titlebar(toolbar_paned); + + filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected); + conversation_list_titlebar.conversation_opened.connect(on_conversation_selected); + } + + private void on_conversation_selected(Conversation conversation) { + this.conversation = conversation; + ChatInteraction.get_instance(stream_interactor).on_conversation_selected(conversation); + conversation.active = true; // only for conversation_selected + filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened + + chat_input.initialize_for_conversation(conversation); + conversation_frame.initialize_for_conversation(conversation); + conversation_titlebar.initialize_for_conversation(conversation); + } + + private bool on_focus_in_event() { + ChatInteraction.get_instance(stream_interactor).window_focus_in(conversation); + return false; + } + + private bool on_focus_out_event() { + ChatInteraction.get_instance(stream_interactor).window_focus_out(conversation); + return false; + } +} + diff --git a/client/src/ui/util.vala b/client/src/ui/util.vala new file mode 100644 index 00000000..d06afe67 --- /dev/null +++ b/client/src/ui/util.vala @@ -0,0 +1,71 @@ +using Gtk; + +using Dino.Entities; +using Xmpp; + +public class Dino.Ui.Util : GLib.Object { + + private const string[] tango_colors_light = {"FCE94F", "FCAF3E", "E9B96E", "8AE234", "729FCF", "AD7FA8", "EF2929"}; + private const string[] tango_colors_medium = {"EDD400", "F57900", "C17D11", "73D216", "3465A4", "75507B", "CC0000"}; + private const string[] material_colors_500 = {"F44336", "E91E63", "9C27B0", "673AB7", "3f51B5", "2196F3", "03A9f4", "00BCD4", "009688", "4CAF50", "8BC34a", "CDDC39", "FFEB3B", "FFC107", "FF9800", "FF5722", "795548"}; + private const string[] material_colors_300 = {"E57373", "F06292", "BA68C8", "9575CD", "7986CB", "64B5F6", "4FC3F7", "4DD0E1", "4DB6AC", "81C784", "AED581", "DCE775", "FFF176", "FFD54F", "FFB74D", "FF8A65", "A1887F"}; + private const string[] material_colors_200 = {"EF9A9A", "F48FB1", "CE93D8", "B39DDB", "9FA8DA", "90CAF9", "81D4FA", "80DEEA", "80CBC4", "A5D6A7", "C5E1A5", "E6EE9C", "FFF59D", "FFE082", "FFCC80", "FFAB91", "BCAAA4"}; + + public static string get_avatar_hex_color(string name) { + return material_colors_300[name.hash() % material_colors_300.length]; +// return tango_colors_light[name.hash() % tango_colors_light.length]; + } + + public static string get_name_hex_color(string name) { + return material_colors_500[name.hash() % material_colors_500.length]; +// return tango_colors_medium[name.hash() % tango_colors_medium.length]; + } + + public static string color_for_show(string show) { + switch(show) { + case "online": return "#9CCC65"; + case "away": return "#FFCA28"; + case "chat": return "#66BB6A"; + case "xa": return "#EF5350"; + case "dnd": return "#EF5350"; + default: return "#BDBDBD"; + } + } + + public static string get_conversation_display_name(StreamInteractor stream_interactor, Conversation conversation) { + return get_display_name(stream_interactor, conversation.counterpart, conversation.account); + } + + public static string get_display_name(StreamInteractor stream_interactor, Jid jid, Account account) { + if (MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) { + return jid.resourcepart; + } else { + if (jid.bare_jid.equals(account.bare_jid.bare_jid)) { + if (account.alias == null || account.alias == "") { + return account.bare_jid.to_string(); + } else { + return account.alias; + } + } + Roster.Item roster_item = RosterManager.get_instance(stream_interactor).get_roster_item(account, jid); + if (roster_item != null && roster_item.name != null) { + return roster_item.name; + } + return jid.bare_jid.to_string(); + } + } + + public static string get_message_display_name(StreamInteractor stream_interactor, Entities.Message message, Account account) { + Jid? real_jid = MucManager.get_instance(stream_interactor).get_message_real_jid(message); + if (real_jid != null) { + return get_display_name(stream_interactor, real_jid, account); + } else { + return get_display_name(stream_interactor, message.from, account); + } + } + + public static void image_set_from_scaled_pixbuf(Image image, Gdk.Pixbuf pixbuf, int scale = 0) { + if (scale == 0) scale = image.get_scale_factor(); + image.set_from_surface(Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, image.get_window())); + } +} |