diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 619f23c9..7d38235c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -80,8 +80,11 @@ SOURCES
src/ui/conversation_selector/groupchat_row.vala
src/ui/conversation_selector/list.vala
src/ui/conversation_selector/view.vala
+ src/ui/conversation_summary/conversation_item.vala
src/ui/conversation_summary/merged_message_item.vala
- src/ui/conversation_summary/merged_status_item.vala
+ src/ui/conversation_summary/message_item.vala
+ src/ui/conversation_summary/message_textview.vala
+ src/ui/conversation_summary/slashme_item.vala
src/ui/conversation_summary/status_item.vala
src/ui/conversation_summary/view.vala
src/ui/conversation_titlebar.vala
diff --git a/main/data/conversation_summary/message_item.ui b/main/data/conversation_summary/message_item.ui
index eac97279..e7b4f46f 100644
--- a/main/data/conversation_summary/message_item.ui
+++ b/main/data/conversation_summary/message_item.ui
@@ -1,6 +1,6 @@
-
+
True
7
horizontal
@@ -21,23 +21,10 @@
2
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
-
- 1
- 1
- 2
- 1
-
-
\ No newline at end of file
diff --git a/main/src/ui/conversation_summary/conversation_item.vala b/main/src/ui/conversation_summary/conversation_item.vala
new file mode 100644
index 00000000..480ccd48
--- /dev/null
+++ b/main/src/ui/conversation_summary/conversation_item.vala
@@ -0,0 +1,34 @@
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public enum MessageKind {
+ TEXT,
+ ME_COMMAND
+}
+
+public MessageKind get_message_kind(Message message) {
+ if (message.body.has_prefix("/me ")) {
+ return MessageKind.ME_COMMAND;
+ } else {
+ return MessageKind.TEXT;
+ }
+}
+
+public interface ConversationItem : Gtk.Widget {
+ public abstract bool merge(Entities.Message message);
+
+ public static ConversationItem create_for_message(StreamInteractor stream_interactor, Conversation conversation, Message message) {
+ switch (get_message_kind(message)) {
+ case MessageKind.TEXT:
+ return new MergedMessageItem(stream_interactor, conversation, message);
+ break;
+ case MessageKind.ME_COMMAND:
+ return new SlashMeItem(stream_interactor, conversation, message);
+ break;
+ }
+ return null;
+ }
+}
+
+}
\ No newline at end of file
diff --git a/main/src/ui/conversation_summary/merged_message_item.vala b/main/src/ui/conversation_summary/merged_message_item.vala
index b00dc18e..fa87ec59 100644
--- a/main/src/ui/conversation_summary/merged_message_item.vala
+++ b/main/src/ui/conversation_summary/merged_message_item.vala
@@ -7,176 +7,46 @@ using Dino.Entities;
namespace Dino.Ui.ConversationSummary {
-[GtkTemplate (ui = "/org/dino-im/conversation_summary/message_item.ui")]
-public class MergedMessageItem : Grid {
+public class MergedMessageItem : MessageItem {
- public Conversation conversation { get; set; }
- public Jid from { get; private set; }
- public DateTime initial_time { get; private set; }
- public ArrayList messages = new ArrayList(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;
-
- private StreamInteractor stream_interactor;
- private TextTag link_tag;
+ private Label name_label = new Label("") { xalign=0, visible=true };
+ private MessageTextView textview = new MessageTextView() { visible=true };
public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
- this.conversation = conversation;
- this.from = message.from;
- this.initial_time = message.time;
- this.stream_interactor = stream_interactor;
- setup_tags();
+ base(stream_interactor, conversation, message);
+ set_main_widget(textview);
+ set_title_widget(name_label);
+
add_message(message);
+ string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
+ name_label.set_markup(@"$display_name");
- time_label.label = get_relative_time(initial_time.to_local());
- Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message));
- if (message.encryption != Encryption.NONE) {
- encryption_image.visible = true;
- encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
- }
- name_label.label = Util.get_message_display_name(stream_interactor, message, conversation.account);
-
+ textview.style_updated.connect(update_display_style);
update_display_style();
- Util.force_base_background(message_text_view, "textview, text:not(:selected)");
- message_text_view.style_updated.connect(update_display_style);
+ }
+
+ public override void add_message(Message message) {
+ base.add_message(message);
+ if (messages.size > 1) textview.add_text("\n");
+ textview.add_text(message.body);
+ }
+
+ public override bool merge(Message message) {
+ if (get_message_kind(message) == MessageKind.TEXT &&
+ this.from.equals(message.from) &&
+ this.messages[0].encryption == message.encryption &&
+ message.time.difference(initial_time) < TimeSpan.MINUTE &&
+ this.messages[0].marked != Entities.Message.Marked.WONTSEND) {
+ add_message(message);
+ return true;
+ }
+ return false;
+
}
private void update_display_style() {
- RGBA bg = message_text_view.get_style_context().get_background_color(StateFlags.NORMAL);
- bool dark_theme = (bg.red < 0.5 && bg.green < 0.5 && bg.blue < 0.5);
-
string display_name = Util.get_message_display_name(stream_interactor, messages[0], conversation.account);
- name_label.set_markup(@"$display_name");
-
- LinkButton lnk = new LinkButton("http://example.com");
- RGBA link_color = lnk.get_style_context().get_color(StateFlags.LINK);
- link_tag.foreground_rgba = link_color;
- }
-
- 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(() => {
- Idle.add(() => { update_received(); return false; });
- });
- update_received();
- }
-
- private void update_received() {
- bool all_received = true;
- bool all_read = true;
- foreach (Message message in messages) {
- if (message.marked == Message.Marked.WONTSEND) {
- received_image.visible = true;
- received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR);
- Util.force_error_color(received_image);
- Util.force_error_color(encryption_image);
- Util.force_error_color(time_label);
- return;
- } else 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_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR);
- } else if (all_received) {
- received_image.visible = true;
- received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR);
- } 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);
- start = text[0:start].char_count();
- end = text[0:end].char_count();
- 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() {
- link_tag = 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";
- }
+ name_label.set_markup(@"$display_name");
}
}
diff --git a/main/src/ui/conversation_summary/merged_status_item.vala b/main/src/ui/conversation_summary/merged_status_item.vala
deleted file mode 100644
index 1fe8ecf3..00000000
--- a/main/src/ui/conversation_summary/merged_status_item.vala
+++ /dev/null
@@ -1,31 +0,0 @@
-using Gee;
-using Gtk;
-
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-private class MergedStatusItem : Expander {
-
- private StreamInteractor stream_interactor;
- private Conversation conversation;
- private ArrayList statuses = new ArrayList();
-
- 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/main/src/ui/conversation_summary/message_item.vala b/main/src/ui/conversation_summary/message_item.vala
new file mode 100644
index 00000000..fed67945
--- /dev/null
+++ b/main/src/ui/conversation_summary/message_item.vala
@@ -0,0 +1,112 @@
+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 MessageItem : Grid, ConversationItem {
+
+ [GtkChild] private Image image;
+ [GtkChild] private Label time_label;
+ [GtkChild] private Image encryption_image;
+ [GtkChild] private Image received_image;
+
+ public StreamInteractor stream_interactor;
+ public Conversation conversation { get; set; }
+ public Jid from { get; private set; }
+ public DateTime initial_time { get; private set; }
+ public ArrayList messages = new ArrayList(Message.equals_func);
+
+ public MessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
+ this.conversation = conversation;
+ this.stream_interactor = stream_interactor;
+ this.initial_time = message.time;
+ this.from = message.from;
+
+ if (message.encryption != Encryption.NONE) {
+ encryption_image.visible = true;
+ encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
+ }
+
+ time_label.label = get_relative_time(initial_time.to_local());
+ Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message));
+ }
+
+ public void set_title_widget(Widget w) {
+ attach(w, 1, 0, 1, 1);
+ }
+
+ public void set_main_widget(Widget w) {
+ attach(w, 1, 1, 2, 1);
+ }
+
+ public void update() {
+ time_label.label = get_relative_time(initial_time.to_local());
+ }
+
+ public virtual void add_message(Message message) {
+ messages.add(message);
+
+ message.notify["marked"].connect_after(() => {
+ Idle.add(() => { update_received(); return false; });
+ });
+ update_received();
+ }
+
+ public virtual bool merge(Message message) {
+ return false;
+ }
+
+ private void update_received() {
+ bool all_received = true;
+ bool all_read = true;
+ foreach (Message message in messages) {
+ if (message.marked == Message.Marked.WONTSEND) {
+ received_image.visible = true;
+ received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR);
+ Util.force_error_color(received_image);
+ Util.force_error_color(encryption_image);
+ Util.force_error_color(time_label);
+ return;
+ } else 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_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR);
+ } else if (all_received) {
+ received_image.visible = true;
+ received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR);
+ } else if (received_image.visible) {
+ received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR);
+ }
+ }
+
+ 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/main/src/ui/conversation_summary/message_textview.vala b/main/src/ui/conversation_summary/message_textview.vala
new file mode 100644
index 00000000..6474a1a3
--- /dev/null
+++ b/main/src/ui/conversation_summary/message_textview.vala
@@ -0,0 +1,87 @@
+using Gdk;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class MessageTextView : TextView {
+
+ private TextTag link_tag;
+
+ public MessageTextView() {
+ Object(editable:false, hexpand:true, wrap_mode:WrapMode.WORD_CHAR);
+
+ link_tag = buffer.create_tag("url", underline: Pango.Underline.SINGLE, foreground: "blue");
+ button_release_event.connect(open_url);
+ motion_notify_event.connect(change_cursor_over_url);
+
+ update_display_style();
+ Util.force_base_background(this, "textview, text:not(:selected)");
+ style_updated.connect(update_display_style);
+ }
+
+ public void add_text(string text) {
+ TextIter end;
+ buffer.get_end_iter(out end);
+ buffer.insert(ref end, text, -1);
+ format_suffix_urls(text);
+ }
+
+ private void update_display_style() {
+ LinkButton lnk = new LinkButton("http://example.com");
+ RGBA link_color = lnk.get_style_context().get_color(StateFlags.LINK);
+ link_tag.foreground_rgba = link_color;
+ }
+
+ private void format_suffix_urls(string text) {
+ int absolute_start = 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);
+ start = text[0:start].char_count();
+ end = text[0:end].char_count();
+ TextIter start_iter;
+ TextIter end_iter;
+ buffer.get_iter_at_offset(out start_iter, absolute_start + start);
+ buffer.get_iter_at_offset(out end_iter, absolute_start + end);
+ buffer.apply_tag_by_name("url", start_iter, end_iter);
+ }
+ }
+
+ private bool open_url(EventButton event_button) {
+ int buffer_x, buffer_y;
+ window_to_buffer_coords(TextWindowType.TEXT, (int) event_button.x, (int) event_button.y, out buffer_x, out buffer_y);
+ TextIter iter;
+ 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;
+ get_iter_at_location(out iter, (int) event_motion.x, (int) event_motion.y);
+ if (iter.has_tag(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;
+ }
+}
+
+}
\ No newline at end of file
diff --git a/main/src/ui/conversation_summary/slashme_item.vala b/main/src/ui/conversation_summary/slashme_item.vala
new file mode 100644
index 00000000..cd387bc9
--- /dev/null
+++ b/main/src/ui/conversation_summary/slashme_item.vala
@@ -0,0 +1,43 @@
+using Gdk;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class SlashMeItem : MessageItem {
+
+ private Box box = new Box(Orientation.VERTICAL, 0) { visible=true, vexpand=true };
+ private MessageTextView textview = new MessageTextView() { visible=true };
+ private string text;
+ private TextTag nick_tag;
+
+ public SlashMeItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
+ base(stream_interactor, conversation, message);
+ box.set_center_widget(textview);
+ set_title_widget(box);
+ text = message.body.substring(3);
+
+ string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
+ nick_tag = textview.buffer.create_tag("nick", foreground: @"#$(Util.get_name_hex_color(display_name, false))");
+ TextIter iter;
+ textview.buffer.get_start_iter(out iter);
+ textview.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
+ textview.add_text(text);
+ add_message(message);
+
+ textview.style_updated.connect(update_display_style);
+ update_display_style();
+ }
+
+ public override bool merge(Message message) {
+ return false;
+ }
+
+ private void update_display_style() {
+ string display_name = Util.get_message_display_name(stream_interactor, messages[0], conversation.account);
+ nick_tag.foreground = @"#$(Util.get_name_hex_color(display_name, Util.is_dark_theme(textview)))";
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/status_item.vala b/main/src/ui/conversation_summary/status_item.vala
index 0775d8f3..1704356c 100644
--- a/main/src/ui/conversation_summary/status_item.vala
+++ b/main/src/ui/conversation_summary/status_item.vala
@@ -22,7 +22,7 @@ private class StatusItem : Grid {
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(@" $(escape_text(display_name)) $text ");
+ label.set_markup(@"$(escape_text(display_name)) $text");
show_all();
}
}
diff --git a/main/src/ui/conversation_summary/view.vala b/main/src/ui/conversation_summary/view.vala
index 2cf695b2..d884a04a 100644
--- a/main/src/ui/conversation_summary/view.vala
+++ b/main/src/ui/conversation_summary/view.vala
@@ -11,13 +11,13 @@ namespace Dino.Ui.ConversationSummary {
public class View : Box {
public Conversation? conversation { get; private set; }
- public HashMap message_items = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func);
+ public HashMap conversation_items = new HashMap(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 ConversationItem? last_conversation_item;
private StatusItem typing_status;
private Entities.Message? earliest_message;
double? was_value;
@@ -44,8 +44,9 @@ public class View : Box {
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();
+ foreach (ConversationItem conversation_item in conversation_items.values) {
+ MessageItem message_item = conversation_item as MessageItem;
+ if (message_item != null) message_item.update();
}
return true;
});
@@ -56,10 +57,10 @@ public class View : Box {
public void initialize_for_conversation(Conversation? conversation) {
this.conversation = conversation;
clear();
- message_items.clear();
+ conversation_items.clear();
was_upper = null;
was_page_size = null;
- last_message_item = null;
+ last_conversation_item = null;
ArrayList