parent
7ab4752b24
commit
f277db6cb4
|
@ -80,8 +80,11 @@ SOURCES
|
||||||
src/ui/conversation_selector/groupchat_row.vala
|
src/ui/conversation_selector/groupchat_row.vala
|
||||||
src/ui/conversation_selector/list.vala
|
src/ui/conversation_selector/list.vala
|
||||||
src/ui/conversation_selector/view.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_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/status_item.vala
|
||||||
src/ui/conversation_summary/view.vala
|
src/ui/conversation_summary/view.vala
|
||||||
src/ui/conversation_titlebar.vala
|
src/ui/conversation_titlebar.vala
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<interface>
|
<interface>
|
||||||
<template class="DinoUiConversationSummaryMergedMessageItem">
|
<template class="DinoUiConversationSummaryMessageItem">
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="column-spacing">7</property>
|
<property name="column-spacing">7</property>
|
||||||
<property name="orientation">horizontal</property>
|
<property name="orientation">horizontal</property>
|
||||||
|
@ -21,23 +21,10 @@
|
||||||
<property name="height">2</property>
|
<property name="height">2</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="name_label">
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="top_attach">0</property>
|
|
||||||
<property name="width">1</property>
|
|
||||||
<property name="height">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="time_label">
|
<object class="GtkLabel" id="time_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="xalign">1</property>
|
<property name="xalign">1</property>
|
||||||
<property name="yalign">0</property>
|
|
||||||
<style>
|
<style>
|
||||||
<class name="dim-label"/>
|
<class name="dim-label"/>
|
||||||
</style>
|
</style>
|
||||||
|
@ -79,19 +66,5 @@
|
||||||
<property name="height">1</property>
|
<property name="height">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="GtkTextView" id="message_text_view">
|
|
||||||
<property name="editable">False</property>
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
<property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="width">2</property>
|
|
||||||
<property name="height">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</template>
|
</template>
|
||||||
</interface>
|
</interface>
|
34
main/src/ui/conversation_summary/conversation_item.vala
Normal file
34
main/src/ui/conversation_summary/conversation_item.vala
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,176 +7,46 @@ using Dino.Entities;
|
||||||
|
|
||||||
namespace Dino.Ui.ConversationSummary {
|
namespace Dino.Ui.ConversationSummary {
|
||||||
|
|
||||||
[GtkTemplate (ui = "/org/dino-im/conversation_summary/message_item.ui")]
|
public class MergedMessageItem : MessageItem {
|
||||||
public class MergedMessageItem : Grid {
|
|
||||||
|
|
||||||
public Conversation conversation { get; set; }
|
private Label name_label = new Label("") { xalign=0, visible=true };
|
||||||
public Jid from { get; private set; }
|
private MessageTextView textview = new MessageTextView() { visible=true };
|
||||||
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;
|
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
|
||||||
private TextTag link_tag;
|
|
||||||
|
|
||||||
public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
|
public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
|
||||||
this.conversation = conversation;
|
base(stream_interactor, conversation, message);
|
||||||
this.from = message.from;
|
set_main_widget(textview);
|
||||||
this.initial_time = message.time;
|
set_title_widget(name_label);
|
||||||
this.stream_interactor = stream_interactor;
|
|
||||||
setup_tags();
|
|
||||||
add_message(message);
|
add_message(message);
|
||||||
|
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, false))\">$display_name</span>");
|
||||||
|
|
||||||
time_label.label = get_relative_time(initial_time.to_local());
|
textview.style_updated.connect(update_display_style);
|
||||||
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);
|
|
||||||
|
|
||||||
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() {
|
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);
|
string display_name = Util.get_message_display_name(stream_interactor, messages[0], conversation.account);
|
||||||
name_label.set_markup(@"<span foreground=\"#$(Util.get_name_hex_color(display_name, dark_theme))\">$display_name</span>");
|
name_label.set_markup(@"<span foreground=\"#$(Util.get_name_hex_color(display_name, Util.is_dark_theme(textview)))\">$display_name</span>");
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
112
main/src/ui/conversation_summary/message_item.vala
Normal file
112
main/src/ui/conversation_summary/message_item.vala
Normal file
|
@ -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<Message> messages = new ArrayList<Message>(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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
87
main/src/ui/conversation_summary/message_textview.vala
Normal file
87
main/src/ui/conversation_summary/message_textview.vala
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
main/src/ui/conversation_summary/slashme_item.vala
Normal file
43
main/src/ui/conversation_summary/slashme_item.vala
Normal file
|
@ -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)))";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,13 +11,13 @@ namespace Dino.Ui.ConversationSummary {
|
||||||
public class View : Box {
|
public class View : Box {
|
||||||
|
|
||||||
public Conversation? conversation { get; private set; }
|
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);
|
public HashMap<Entities.Message, ConversationItem> conversation_items = new HashMap<Entities.Message, ConversationItem>(Entities.Message.hash_func, Entities.Message.equals_func);
|
||||||
|
|
||||||
[GtkChild] private ScrolledWindow scrolled;
|
[GtkChild] private ScrolledWindow scrolled;
|
||||||
[GtkChild] private Box main;
|
[GtkChild] private Box main;
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private MergedMessageItem? last_message_item;
|
private ConversationItem? last_conversation_item;
|
||||||
private StatusItem typing_status;
|
private StatusItem typing_status;
|
||||||
private Entities.Message? earliest_message;
|
private Entities.Message? earliest_message;
|
||||||
double? was_value;
|
double? was_value;
|
||||||
|
@ -44,8 +44,9 @@ public class View : Box {
|
||||||
Idle.add(() => { on_show_received(show, jid, account); return false; });
|
Idle.add(() => { on_show_received(show, jid, account); return false; });
|
||||||
});
|
});
|
||||||
Timeout.add_seconds(60, () => {
|
Timeout.add_seconds(60, () => {
|
||||||
foreach (MergedMessageItem message_item in message_items.values) {
|
foreach (ConversationItem conversation_item in conversation_items.values) {
|
||||||
message_item.update();
|
MessageItem message_item = conversation_item as MessageItem;
|
||||||
|
if (message_item != null) message_item.update();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
@ -56,10 +57,10 @@ public class View : Box {
|
||||||
public void initialize_for_conversation(Conversation? conversation) {
|
public void initialize_for_conversation(Conversation? conversation) {
|
||||||
this.conversation = conversation;
|
this.conversation = conversation;
|
||||||
clear();
|
clear();
|
||||||
message_items.clear();
|
conversation_items.clear();
|
||||||
was_upper = null;
|
was_upper = null;
|
||||||
was_page_size = null;
|
was_page_size = null;
|
||||||
last_message_item = null;
|
last_conversation_item = null;
|
||||||
|
|
||||||
ArrayList<Object> objects = new ArrayList<Object>();
|
ArrayList<Object> objects = new ArrayList<Object>();
|
||||||
Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
|
Gee.List<Entities.Message>? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation);
|
||||||
|
@ -158,13 +159,11 @@ public class View : Box {
|
||||||
MergedMessageItem? current_item = null;
|
MergedMessageItem? current_item = null;
|
||||||
int items_added = 0;
|
int items_added = 0;
|
||||||
for (int i = 0; i < messages.size; i++) {
|
for (int i = 0; i < messages.size; i++) {
|
||||||
if (current_item != null && should_merge_message(current_item, messages[i])) {
|
if (current_item == null || !current_item.merge(messages[i])) {
|
||||||
current_item.add_message(messages[i]);
|
|
||||||
} else {
|
|
||||||
current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]);
|
current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]);
|
||||||
force_alloc_width(current_item, main.get_allocated_width());
|
force_alloc_width(current_item, main.get_allocated_width());
|
||||||
main.add(current_item);
|
main.add(current_item);
|
||||||
message_items[messages[i]] = current_item;
|
conversation_items[messages[i]] = current_item;
|
||||||
main.reorder_child(current_item, items_added);
|
main.reorder_child(current_item, items_added);
|
||||||
items_added++;
|
items_added++;
|
||||||
}
|
}
|
||||||
|
@ -176,35 +175,25 @@ public class View : Box {
|
||||||
|
|
||||||
private void show_message(Entities.Message message, Conversation conversation, bool animate = false) {
|
private void show_message(Entities.Message message, Conversation conversation, bool animate = false) {
|
||||||
if (this.conversation != null && this.conversation.equals(conversation)) {
|
if (this.conversation != null && this.conversation.equals(conversation)) {
|
||||||
if (should_merge_message(last_message_item, message)) {
|
if (last_conversation_item == null || !last_conversation_item.merge(message)) {
|
||||||
last_message_item.add_message(message);
|
ConversationItem conversation_item = ConversationItem.create_for_message(stream_interactor, conversation, message);
|
||||||
} else {
|
|
||||||
MergedMessageItem message_item = new MergedMessageItem(stream_interactor, conversation, message);
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
|
Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
|
||||||
revealer.add(message_item);
|
revealer.add(conversation_item);
|
||||||
force_alloc_width(revealer, main.get_allocated_width());
|
force_alloc_width(revealer, main.get_allocated_width());
|
||||||
main.add(revealer);
|
main.add(revealer);
|
||||||
revealer.set_reveal_child(true);
|
revealer.set_reveal_child(true);
|
||||||
} else {
|
} else {
|
||||||
force_alloc_width(message_item, main.get_allocated_width());
|
force_alloc_width(conversation_item, main.get_allocated_width());
|
||||||
main.add(message_item);
|
main.add(conversation_item);
|
||||||
}
|
}
|
||||||
last_message_item = message_item;
|
last_conversation_item = conversation_item;
|
||||||
}
|
}
|
||||||
message_items[message] = last_message_item;
|
conversation_items[message] = last_conversation_item;
|
||||||
update_chat_state();
|
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 &&
|
|
||||||
(message_item.messages.get(0).marked == Entities.Message.Marked.WONTSEND) == (message.marked == Entities.Message.Marked.WONTSEND);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void force_alloc_width(Widget widget, int width) {
|
private void force_alloc_width(Widget widget, int width) {
|
||||||
Allocation alloc = Allocation();
|
Allocation alloc = Allocation();
|
||||||
widget.get_preferred_width(out alloc.width, null);
|
widget.get_preferred_width(out alloc.width, null);
|
||||||
|
|
|
@ -105,6 +105,11 @@ public class Util : Object {
|
||||||
public static void force_error_color(Gtk.Widget widget, string selector = "*") {
|
public static void force_error_color(Gtk.Widget widget, string selector = "*") {
|
||||||
force_color(widget, "@error_color", selector);
|
force_color(widget, "@error_color", selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool is_dark_theme(Gtk.Widget widget) {
|
||||||
|
Gdk.RGBA bg = widget.get_style_context().get_background_color(StateFlags.NORMAL);
|
||||||
|
return (bg.red < 0.5 && bg.green < 0.5 && bg.blue < 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue