aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/conversation_content_view
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/ui/conversation_content_view')
-rw-r--r--main/src/ui/conversation_content_view/chat_state_populator.vala126
-rw-r--r--main/src/ui/conversation_content_view/content_item_widget_factory.vala114
-rw-r--r--main/src/ui/conversation_content_view/content_populator.vala111
-rw-r--r--main/src/ui/conversation_content_view/conversation_item_skeleton.vala221
-rw-r--r--main/src/ui/conversation_content_view/conversation_view.vala374
-rw-r--r--main/src/ui/conversation_content_view/date_separator_populator.vala105
-rw-r--r--main/src/ui/conversation_content_view/file_widget.vala338
-rw-r--r--main/src/ui/conversation_content_view/message_item.vala0
-rw-r--r--main/src/ui/conversation_content_view/subscription_notification.vala55
9 files changed, 1444 insertions, 0 deletions
diff --git a/main/src/ui/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala
new file mode 100644
index 00000000..54b41b7d
--- /dev/null
+++ b/main/src/ui/conversation_content_view/chat_state_populator.vala
@@ -0,0 +1,126 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui.ConversationSummary {
+
+class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
+
+ public string id { get { return "chat_state"; } }
+
+ private StreamInteractor? stream_interactor;
+ private Conversation? current_conversation;
+ private Plugins.ConversationItemCollection? item_collection;
+
+ private MetaChatStateItem? meta_item;
+
+ public ChatStatePopulator(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((conversation, state) => {
+ if (current_conversation != null && current_conversation.equals(conversation)) {
+ update_chat_state();
+ }
+ });
+ stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
+ if (conversation.equals(current_conversation)) {
+ update_chat_state();
+ }
+ });
+ }
+
+ public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) {
+ current_conversation = conversation;
+ this.item_collection = item_collection;
+ this.meta_item = null;
+
+ update_chat_state();
+ }
+
+ public void close(Conversation conversation) { }
+
+ public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
+
+ private void update_chat_state() {
+ Gee.List<Jid>? typing_jids = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_typing_jids(current_conversation);
+
+ if (meta_item != null && typing_jids == null) {
+ // Remove state (stoped typing)
+ item_collection.remove_item(meta_item);
+ meta_item = null;
+ } else if (meta_item != null && typing_jids != null) {
+ // Update state (other people typing in MUC)
+ meta_item.set_new(typing_jids);
+ } else if (typing_jids != null) {
+ // New state (started typing)
+ meta_item = new MetaChatStateItem(stream_interactor, current_conversation, typing_jids);
+ item_collection.insert_item(meta_item);
+ }
+ }
+}
+
+private class MetaChatStateItem : Plugins.MetaConversationItem {
+ public override bool dim { get; set; default=true; }
+ public override DateTime sort_time { get; set; default=new DateTime.now_utc().add_years(10); }
+
+ public override bool can_merge { get; set; default=false; }
+ public override bool requires_avatar { get; set; default=false; }
+ public override bool requires_header { get; set; default=false; }
+
+ private StreamInteractor stream_interactor;
+ private Conversation conversation;
+ private Gee.List<Jid> jids = new ArrayList<Jid>();
+ private Label label;
+ private AvatarImage image;
+
+ public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Gee.List<Jid> jids) {
+ this.stream_interactor = stream_interactor;
+ this.conversation = conversation;
+ this.jids = jids;
+ }
+
+ public override Object? get_widget(Plugins.WidgetType widget_type) {
+ label = new Label("") { xalign=0, vexpand=true, visible=true };
+ label.get_style_context().add_class("dim-label");
+ image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true };
+
+ Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true };
+ image_content_box.add(image);
+ image_content_box.add(label);
+
+ update();
+ return image_content_box;
+ }
+
+ public void set_new(Gee.List<Jid> jids) {
+ this.jids = jids;
+ update();
+ }
+
+ private void update() {
+ if (image == null || label == null) return;
+
+ image.set_conversation_participants(stream_interactor, conversation, jids.to_array());
+
+ Gee.List<string> display_names = new ArrayList<string>();
+ foreach (Jid jid in jids) {
+ display_names.add(Util.get_participant_display_name(stream_interactor, conversation, jid));
+ }
+ string new_text = "";
+ if (jids.size > 3) {
+ new_text = _("%s, %s and %i others").printf(display_names[0], display_names[1], jids.size - 2);
+ } else if (jids.size == 3) {
+ new_text = _("%s, %s and %s are typing…").printf(display_names[0], display_names[1], display_names[2]);
+ } else if (jids.size == 2) {
+ new_text =_("%s and %s are typing…").printf(display_names[0], display_names[1]);
+ } else {
+ new_text = "%s is typing…".printf(display_names[0]);
+ }
+
+ label.label = new_text;
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/content_item_widget_factory.vala b/main/src/ui/conversation_content_view/content_item_widget_factory.vala
new file mode 100644
index 00000000..54283e75
--- /dev/null
+++ b/main/src/ui/conversation_content_view/content_item_widget_factory.vala
@@ -0,0 +1,114 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Pango;
+using Xmpp;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class ContentItemWidgetFactory : Object {
+
+ private StreamInteractor stream_interactor;
+ private HashMap<string, WidgetGenerator> generators = new HashMap<string, WidgetGenerator>();
+
+ public ContentItemWidgetFactory(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ generators[MessageItem.TYPE] = new MessageItemWidgetGenerator(stream_interactor);
+ generators[FileItem.TYPE] = new FileItemWidgetGenerator(stream_interactor);
+ }
+
+ public Widget? get_widget(ContentItem item) {
+ WidgetGenerator? generator = generators[item.type_];
+ if (generator != null) {
+ return (Widget?) generator.get_widget(item);
+ }
+ return null;
+ }
+
+ public void register_widget_generator(WidgetGenerator generator) {
+ generators[generator.handles_type] = generator;
+ }
+}
+
+public interface WidgetGenerator : Object {
+ public abstract string handles_type { get; set; }
+ public abstract Object get_widget(ContentItem item);
+}
+
+public class MessageItemWidgetGenerator : WidgetGenerator, Object {
+
+ public string handles_type { get; set; default=FileItem.TYPE; }
+
+ private StreamInteractor stream_interactor;
+
+ public MessageItemWidgetGenerator(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public Object get_widget(ContentItem item) {
+ MessageItem message_item = item as MessageItem;
+ Conversation conversation = message_item.conversation;
+ Message message = message_item.message;
+
+ Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true };
+ string markup_text = message.body;
+ if (markup_text.length > 10000) {
+ markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]";
+ }
+ if (message_item.message.body.has_prefix("/me")) {
+ markup_text = markup_text.substring(3);
+ }
+
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ markup_text = Util.parse_add_markup(markup_text, conversation.nickname, true, true);
+ } else {
+ markup_text = Util.parse_add_markup(markup_text, null, true, true);
+ }
+
+ if (message_item.message.body.has_prefix("/me")) {
+ string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from);
+ update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text);
+ label.realize.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text));
+ label.style_updated.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text));
+ }
+
+ int only_emoji_count = Util.get_only_emoji_count(markup_text);
+ if (only_emoji_count != -1) {
+ string size_str = only_emoji_count < 5 ? "xx-large" : "large";
+ markup_text = @"<span size=\'$size_str\'>" + markup_text + "</span>";
+ }
+
+ label.label = markup_text;
+ return label;
+ }
+
+ public static void update_me_style(StreamInteractor stream_interactor, Jid jid, string display_name, Account account, Label label, string action_text) {
+ string color = Util.get_name_hex_color(stream_interactor, account, jid, Util.is_dark_theme(label));
+ label.label = @"<span color=\"#$(color)\">$(Markup.escape_text(display_name))</span>" + action_text;
+ }
+}
+
+public class FileItemWidgetGenerator : WidgetGenerator, Object {
+
+ public StreamInteractor stream_interactor;
+ public string handles_type { get; set; default=FileItem.TYPE; }
+
+ private const int MAX_HEIGHT = 300;
+ private const int MAX_WIDTH = 600;
+
+ public FileItemWidgetGenerator(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public Object get_widget(ContentItem item) {
+ FileItem file_item = item as FileItem;
+ FileTransfer transfer = file_item.file_transfer;
+
+ return new FileWidget(stream_interactor, transfer) { visible=true };
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/content_populator.vala b/main/src/ui/conversation_content_view/content_populator.vala
new file mode 100644
index 00000000..e8eee06c
--- /dev/null
+++ b/main/src/ui/conversation_content_view/content_populator.vala
@@ -0,0 +1,111 @@
+using Gee;
+using Gtk;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class ContentProvider : ContentItemCollection, Object {
+
+ private StreamInteractor stream_interactor;
+ private ContentItemWidgetFactory widget_factory;
+ private Conversation? current_conversation;
+ private Plugins.ConversationItemCollection? item_collection;
+
+ public ContentProvider(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ this.widget_factory = new ContentItemWidgetFactory(stream_interactor);
+ }
+
+ public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) {
+ if (current_conversation != null) {
+ stream_interactor.get_module(ContentItemStore.IDENTITY).uninit(current_conversation, this);
+ }
+ current_conversation = conversation;
+ this.item_collection = item_collection;
+ stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this);
+ }
+
+ public void insert_item(ContentItem item) {
+ item_collection.insert_item(new ContentMetaItem(item, widget_factory));
+ }
+
+ public void remove_item(ContentItem item) { }
+
+
+ public Gee.List<ContentMetaItem> populate_latest(Conversation conversation, int n) {
+ Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_n_latest(conversation, n);
+ Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
+ foreach (ContentItem item in items) {
+ ret.add(new ContentMetaItem(item, widget_factory));
+ }
+ return ret;
+ }
+
+ public Gee.List<ContentMetaItem> populate_before(Conversation conversation, ContentItem before_item, int n) {
+ Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
+ Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n);
+ foreach (ContentItem item in items) {
+ ret.add(new ContentMetaItem(item, widget_factory));
+ }
+ return ret;
+ }
+
+ public Gee.List<ContentMetaItem> populate_after(Conversation conversation, ContentItem after_item, int n) {
+ Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>();
+ Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n);
+ foreach (ContentItem item in items) {
+ ret.add(new ContentMetaItem(item, widget_factory));
+ }
+ return ret;
+ }
+
+ public ContentMetaItem get_content_meta_item(ContentItem content_item) {
+ return new ContentMetaItem(content_item, widget_factory);
+ }
+}
+
+public class ContentMetaItem : Plugins.MetaConversationItem {
+ public override Jid? jid { get; set; }
+ public override DateTime sort_time { get; set; }
+ public override DateTime? display_time { get; set; }
+ public override Encryption encryption { get; set; }
+
+ public ContentItem content_item;
+ private ContentItemWidgetFactory widget_factory;
+
+ public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) {
+ this.jid = content_item.jid;
+ this.sort_time = content_item.sort_time;
+ this.seccondary_sort_indicator = (long) content_item.display_time.to_unix();
+ this.tertiary_sort_indicator = content_item.id;
+ this.display_time = content_item.display_time;
+ this.encryption = content_item.encryption;
+ this.mark = content_item.mark;
+
+ WeakRef weak_item = WeakRef(content_item);
+ content_item.notify["mark"].connect(() => {
+ ContentItem? ci = weak_item.get() as ContentItem;
+ if (ci == null) return;
+ this.mark = ci.mark;
+ });
+
+ this.can_merge = true;
+ this.requires_avatar = true;
+ this.requires_header = true;
+
+ this.content_item = content_item;
+ this.widget_factory = widget_factory;
+ }
+
+ public override bool can_merge { get; set; default=true; }
+ public override bool requires_avatar { get; set; default=true; }
+ public override bool requires_header { get; set; default=true; }
+
+ public override Object? get_widget(Plugins.WidgetType type) {
+ return widget_factory.get_widget(content_item);
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala
new file mode 100644
index 00000000..dbba2276
--- /dev/null
+++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala
@@ -0,0 +1,221 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Markup;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class ConversationItemSkeleton : EventBox {
+
+ private AvatarImage image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true, allow_gray = false };
+
+ public bool show_skeleton { get; set; }
+ public bool last_group_item { get; set; }
+
+ public StreamInteractor stream_interactor;
+ public Conversation conversation { get; set; }
+ public Plugins.MetaConversationItem item;
+
+ private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true };
+ private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true };
+ private ItemMetaDataHeader metadata_header;
+
+ public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) {
+ this.stream_interactor = stream_interactor;
+ this.conversation = conversation;
+ this.item = item;
+ this.get_style_context().add_class("message-box");
+
+ if (item.requires_avatar) {
+ image.set_conversation_participant(stream_interactor, conversation, item.jid);
+ image_content_box.add(image);
+ }
+ if (item.requires_header) {
+ metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true };
+ header_content_box.add(metadata_header);
+ }
+
+ Widget? widget = item.get_widget(Plugins.WidgetType.GTK) as Widget;
+ if (widget != null) {
+ widget.valign = Align.END;
+ header_content_box.add(widget);
+ }
+
+ image_content_box.add(header_content_box);
+ this.add(image_content_box);
+
+ if (item.get_type().is_a(typeof(ContentMetaItem))) {
+ this.motion_notify_event.connect((event) => {
+ this.set_state_flags(StateFlags.PRELIGHT, false);
+ return false;
+ });
+ this.enter_notify_event.connect((event) => {
+ this.set_state_flags(StateFlags.PRELIGHT, false);
+ return false;
+ });
+ this.leave_notify_event.connect((event) => {
+ this.unset_state_flags(StateFlags.PRELIGHT);
+ return false;
+ });
+ }
+
+ this.notify["show-skeleton"].connect(update_margin);
+ this.notify["last-group-item"].connect(update_margin);
+
+ this.show_skeleton = true;
+ this.last_group_item = true;
+ update_margin();
+ this.notify["show-skeleton"].connect(update_margin);
+ }
+
+ public void update_time() {
+ if (metadata_header != null) {
+ metadata_header.update_time();
+ }
+ }
+
+ public void update_margin() {
+ image.visible = this.show_skeleton;
+ if (metadata_header != null) {
+ metadata_header.visible = this.show_skeleton;
+ }
+ image_content_box.margin_start = this.show_skeleton ? 15 : 58;
+ image_content_box.margin_end = 15;
+
+ if (this.show_skeleton && this.last_group_item) {
+ image_content_box.margin_top = 8;
+ image_content_box.margin_bottom = 8;
+ } else {
+ image_content_box.margin_top = 4;
+ image_content_box.margin_bottom = 4;
+ }
+ }
+}
+
+[GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/item_metadata_header.ui")]
+public class ItemMetaDataHeader : Box {
+ [GtkChild] public Label name_label;
+ [GtkChild] public Label dot_label;
+ [GtkChild] public Label time_label;
+ [GtkChild] public Image encryption_image;
+ [GtkChild] public Image received_image;
+
+ public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12);
+
+ private StreamInteractor stream_interactor;
+ private Conversation conversation;
+ private Plugins.MetaConversationItem item;
+ private ArrayList<Plugins.MetaConversationItem> items = new ArrayList<Plugins.MetaConversationItem>();
+
+ public ItemMetaDataHeader(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) {
+ this.stream_interactor = stream_interactor;
+ this.conversation = conversation;
+ this.item = item;
+ items.add(item);
+
+ update_name_label();
+ name_label.style_updated.connect(update_name_label);
+ if (item.encryption != Encryption.NONE) {
+ encryption_image.visible = true;
+ encryption_image.set_from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER);
+ }
+ update_time();
+
+ item.notify["mark"].connect_after(update_received_mark);
+ update_received_mark();
+ }
+
+ public void update_time() {
+ if (item.display_time != null) {
+ time_label.label = get_relative_time(item.display_time.to_local()).to_string();
+ }
+ }
+
+ private void update_name_label() {
+ string display_name = Markup.escape_text(Util.get_participant_display_name(stream_interactor, conversation, item.jid));
+ string color = Util.get_name_hex_color(stream_interactor, conversation.account, item.jid, Util.is_dark_theme(name_label));
+ name_label.label = @"<span foreground=\"#$color\">$display_name</span>";
+ }
+
+ private void update_received_mark() {
+ bool all_received = true;
+ bool all_read = true;
+ bool all_sent = true;
+ foreach (Plugins.MetaConversationItem item in items) {
+ if (item.mark == Message.Marked.WONTSEND) {
+ received_image.visible = true;
+ received_image.set_from_icon_name("dialog-warning-symbolic", ICON_SIZE_HEADER);
+ Util.force_error_color(received_image);
+ Util.force_error_color(encryption_image);
+ Util.force_error_color(time_label);
+ string error_text = _("Unable to send message");
+ received_image.tooltip_text = error_text;
+ encryption_image.tooltip_text = error_text;
+ time_label.tooltip_text = error_text;
+ return;
+ } else if (item.mark != Message.Marked.READ) {
+ all_read = false;
+ if (item.mark != Message.Marked.RECEIVED) {
+ all_received = false;
+ if (item.mark == Message.Marked.UNSENT) {
+ all_sent = false;
+ }
+ }
+ }
+ }
+ if (all_read) {
+ received_image.visible = true;
+ received_image.set_from_icon_name("dino-double-tick-symbolic", ICON_SIZE_HEADER);
+ } else if (all_received) {
+ received_image.visible = true;
+ received_image.set_from_icon_name("dino-tick-symbolic", ICON_SIZE_HEADER);
+ } else if (!all_sent) {
+ received_image.visible = true;
+ received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER);
+ } else if (received_image.visible) {
+ received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER);
+
+ }
+ }
+
+ public static string format_time(DateTime datetime, string format_24h, string format_12h) {
+ string format = Util.is_24h_format() ? format_24h : format_12h;
+ if (!get_charset(null)) {
+ // No UTF-8 support, use simple colon for time instead
+ format = format.replace("∶", ":");
+ }
+ return datetime.format(format);
+ }
+
+ public static string get_relative_time(DateTime datetime) {
+ DateTime now = new DateTime.now_local();
+ TimeSpan timespan = now.difference(datetime);
+ if (timespan > 365 * TimeSpan.DAY) {
+ return format_time(datetime,
+ /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H∶%M"),
+ /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l∶%M %p"));
+ } else if (timespan > 7 * TimeSpan.DAY) {
+ return format_time(datetime,
+ /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H∶%M"),
+ /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l∶%M %p"));
+ } else if (datetime.get_day_of_month() != now.get_day_of_month()) {
+ return format_time(datetime,
+ /* xgettext:no-c-format */ /* Day of week and time in 24h format (w/o seconds) */ _("%a, %H∶%M"),
+ /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */_("%a, %l∶%M %p"));
+ } else if (timespan > 9 * TimeSpan.MINUTE) {
+ return format_time(datetime,
+ /* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H∶%M"),
+ /* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l∶%M %p"));
+ } else if (timespan > TimeSpan.MINUTE) {
+ ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE);
+ /* xgettext:this is the beginning of a sentence. */
+ return n("%i min ago", "%i mins ago", mins).printf(mins);
+ } else {
+ return _("Just now");
+ }
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala
new file mode 100644
index 00000000..6c286fc0
--- /dev/null
+++ b/main/src/ui/conversation_content_view/conversation_view.vala
@@ -0,0 +1,374 @@
+using Gee;
+using Gtk;
+using Pango;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+[GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/view.ui")]
+public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins.NotificationCollection {
+
+ public Conversation? conversation { get; private set; }
+
+ [GtkChild] public ScrolledWindow scrolled;
+ [GtkChild] private Revealer notification_revealer;
+ [GtkChild] private Box notifications;
+ [GtkChild] private Box main;
+ [GtkChild] private Stack stack;
+
+ private StreamInteractor stream_interactor;
+ private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
+ private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
+ private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
+ private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
+ private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
+ private ContentProvider content_populator;
+ private SubscriptionNotitication subscription_notification;
+
+ private double? was_value;
+ private double? was_upper;
+ private double? was_page_size;
+
+ private Mutex reloading_mutex = Mutex();
+ private bool animate = false;
+ private bool firstLoad = true;
+ private bool at_current_content = true;
+ private bool reload_messages = true;
+
+ public ConversationView init(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
+ scrolled.vadjustment.notify["value"].connect(on_value_notify);
+
+ content_populator = new ContentProvider(stream_interactor);
+ subscription_notification = new SubscriptionNotitication(stream_interactor);
+
+ add_meta_notification.connect(on_add_meta_notification);
+ remove_meta_notification.connect(on_remove_meta_notification);
+
+ Application app = GLib.Application.get_default() as Application;
+ app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor));
+ app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor));
+
+ Timeout.add_seconds(60, () => {
+ foreach (ConversationItemSkeleton item_skeleton in item_skeletons) {
+ item_skeleton.update_time();
+ }
+ return true;
+ });
+ return this;
+ }
+
+ public void initialize_for_conversation(Conversation? conversation) {
+ // Workaround for rendering issues
+ if (firstLoad) {
+ main.visible = false;
+ Idle.add(() => {
+ main.visible=true;
+ return false;
+ });
+ firstLoad = false;
+ }
+ stack.set_visible_child_name("void");
+ clear();
+ initialize_for_conversation_(conversation);
+ display_latest();
+ stack.set_visible_child_name("main");
+ }
+
+ public void initialize_around_message(Conversation conversation, ContentItem content_item) {
+ stack.set_visible_child_name("void");
+ clear();
+ initialize_for_conversation_(conversation);
+ Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
+ foreach (ContentMetaItem item in before_items) {
+ do_insert_item(item);
+ }
+ ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item);
+ meta_item.can_merge = false;
+ Widget w = insert_new(meta_item);
+ content_items.add(meta_item);
+ meta_items.add(meta_item);
+
+ Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, 40);
+ foreach (ContentMetaItem item in after_items) {
+ do_insert_item(item);
+ }
+ if (after_items.size == 40) {
+ at_current_content = false;
+ }
+
+ // Compute where to jump to for centered message, jump, highlight.
+ reload_messages = false;
+ Timeout.add(700, () => {
+ int h = 0, i = 0;
+ bool @break = false;
+ main.@foreach((widget) => {
+ if (widget == w || @break) {
+ @break = true;
+ return;
+ }
+ h += widget.get_allocated_height();
+ i++;
+ });
+ scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
+ w.get_style_context().add_class("highlight-once");
+ reload_messages = true;
+ stack.set_visible_child_name("main");
+ return false;
+ });
+ }
+
+ private void initialize_for_conversation_(Conversation? conversation) {
+ // Deinitialize old conversation
+ Dino.Application app = Dino.Application.get_default();
+ if (this.conversation != null) {
+ foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
+ populator.close(conversation);
+ }
+ foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
+ populator.close(conversation);
+ }
+ }
+
+ // Clear data structures
+ clear_notifications();
+ this.conversation = conversation;
+
+ // Init for new conversation
+ foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
+ populator.init(conversation, this, Plugins.WidgetType.GTK);
+ }
+ content_populator.init(this, conversation, Plugins.WidgetType.GTK);
+ subscription_notification.init(conversation, this);
+
+ animate = false;
+ Timeout.add(20, () => { animate = true; return false; });
+ }
+
+ private void display_latest() {
+ Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40);
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ Application app = GLib.Application.get_default() as Application;
+ foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
+ populator.init(conversation, this, Plugins.WidgetType.GTK);
+ }
+ Idle.add(() => { on_value_notify(); return false; });
+ }
+
+ public void insert_item(Plugins.MetaConversationItem item) {
+ if (meta_items.size > 0) {
+ bool after_last = meta_items.last().sort_time.compare(item.sort_time) <= 0;
+ bool within_range = meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0;
+ bool accept = within_range || (at_current_content && after_last);
+ if (!accept) {
+ return;
+ }
+ }
+ do_insert_item(item);
+ }
+
+ public void do_insert_item(Plugins.MetaConversationItem item) {
+ lock (meta_items) {
+ insert_new(item);
+ if (item as ContentMetaItem != null) {
+ content_items.add(item);
+ }
+ meta_items.add(item);
+ }
+
+ inserted_item(item);
+ }
+
+ private void remove_item(Plugins.MetaConversationItem item) {
+ ConversationItemSkeleton? skeleton = item_item_skeletons[item];
+ if (skeleton != null) {
+ widgets[item].destroy();
+ widgets.unset(item);
+ skeleton.destroy();
+ item_skeletons.remove(skeleton);
+ item_item_skeletons.unset(item);
+
+ content_items.remove(item);
+ meta_items.remove(item);
+ }
+
+ removed_item(item);
+ }
+
+ public void on_add_meta_notification(Plugins.MetaConversationNotification notification) {
+ Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK);
+ if (widget != null) {
+ add_notification(widget);
+ }
+ }
+
+ public void on_remove_meta_notification(Plugins.MetaConversationNotification notification){
+ Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK);
+ if (widget != null) {
+ remove_notification(widget);
+ }
+ }
+
+ public void add_notification(Widget widget) {
+ notifications.add(widget);
+ Timeout.add(20, () => {
+ notification_revealer.transition_duration = 200;
+ notification_revealer.reveal_child = true;
+ return false;
+ });
+ }
+
+ public void remove_notification(Widget widget) {
+ notification_revealer.reveal_child = false;
+ widget.destroy();
+ }
+
+ private Widget insert_new(Plugins.MetaConversationItem item) {
+ Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
+
+ // Fill datastructure
+ ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item) { visible=true };
+ item_item_skeletons[item] = item_skeleton;
+ int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
+ item_skeletons.insert(index, item_skeleton);
+
+ // Insert widget
+ Widget insert = item_skeleton;
+ if (animate) {
+ Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
+ revealer.add(item_skeleton);
+ insert = revealer;
+ main.add(insert);
+ revealer.reveal_child = true;
+ } else {
+ main.add(insert);
+ }
+ widgets[item] = insert;
+ main.reorder_child(insert, index);
+
+ if (lower_item != null) {
+ if (can_merge(item, lower_item)) {
+ ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item];
+ item_skeleton.show_skeleton = false;
+ lower_skeleton.last_group_item = false;
+ }
+ }
+
+ Plugins.MetaConversationItem? upper_item = meta_items.higher(item);
+ if (upper_item != null) {
+ if (!can_merge(upper_item, item)) {
+ ConversationItemSkeleton upper_skeleton = item_item_skeletons[upper_item];
+ upper_skeleton.show_skeleton = true;
+ }
+ }
+
+ // If an item from the past was added, add everything between that item and the (post-)first present item
+ if (index == 0) {
+ Dino.Application app = Dino.Application.get_default();
+ if (item_skeletons.size == 1) {
+ foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
+ populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc());
+ }
+ } else {
+ foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
+ populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time);
+ }
+ }
+ }
+ return insert;
+ }
+
+ private bool can_merge(Plugins.MetaConversationItem upper_item /*more recent, displayed below*/, Plugins.MetaConversationItem lower_item /*less recent, displayed above*/) {
+ return upper_item.display_time != null && lower_item.display_time != null &&
+ upper_item.display_time.difference(lower_item.display_time) < TimeSpan.MINUTE &&
+ upper_item.jid.equals(lower_item.jid) &&
+ upper_item.encryption == lower_item.encryption &&
+ (upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND);
+ }
+
+ private void on_upper_notify() {
+ if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
+ if (at_current_content) {
+ 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;
+ was_value = scrolled.vadjustment.value;
+ reloading_mutex.trylock();
+ reloading_mutex.unlock();
+ }
+
+ private void on_value_notify() {
+ if (scrolled.vadjustment.value < 400) {
+ load_earlier_messages();
+ } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) {
+ load_later_messages();
+ }
+ }
+
+ private void load_earlier_messages() {
+ was_value = scrolled.vadjustment.value;
+ if (!reloading_mutex.trylock()) return;
+ if (meta_items.size > 0) {
+ Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20);
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ } else {
+ reloading_mutex.unlock();
+ }
+ }
+
+ private void load_later_messages() {
+ if (!reloading_mutex.trylock()) return;
+ if (meta_items.size > 0 && !at_current_content) {
+ Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20);
+ if (items.size == 0) {
+ at_current_content = true;
+ }
+ foreach (ContentMetaItem item in items) {
+ do_insert_item(item);
+ }
+ } else {
+ reloading_mutex.unlock();
+ }
+ }
+
+ private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
+ int cmp1 = a.sort_time.compare(b.sort_time);
+ if (cmp1 == 0) {
+ double cmp2 = a.seccondary_sort_indicator - b.seccondary_sort_indicator;
+ if (cmp2 == 0) {
+ return (int) (a.tertiary_sort_indicator - b.tertiary_sort_indicator);
+ }
+ return (int) cmp2;
+ }
+ return cmp1;
+ }
+
+ private void clear() {
+ was_upper = null;
+ was_page_size = null;
+ content_items.clear();
+ meta_items.clear();
+ item_skeletons.clear();
+ item_item_skeletons.clear();
+ widgets.clear();
+ main.@foreach((widget) => { widget.destroy(); });
+ }
+
+ private void clear_notifications() {
+ notifications.@foreach((widget) => { widget.destroy(); });
+ notification_revealer.transition_duration = 0;
+ notification_revealer.set_reveal_child(false);
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/date_separator_populator.vala b/main/src/ui/conversation_content_view/date_separator_populator.vala
new file mode 100644
index 00000000..3ddb0d9a
--- /dev/null
+++ b/main/src/ui/conversation_content_view/date_separator_populator.vala
@@ -0,0 +1,105 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui.ConversationSummary {
+
+class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object {
+
+ public string id { get { return "date_separator"; } }
+
+ private StreamInteractor stream_interactor;
+ private Conversation? current_conversation;
+ private Plugins.ConversationItemCollection? item_collection;
+ private Gee.TreeSet<DateTime> insert_times;
+
+
+ public DateSeparatorPopulator(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) {
+ current_conversation = conversation;
+ this.item_collection = item_collection;
+ item_collection.inserted_item.connect(on_inserted_item);
+ this.insert_times = new TreeSet<DateTime>((a, b) => {
+ return a.compare(b);
+ });
+ }
+
+ public void close(Conversation conversation) {
+ item_collection.inserted_item.disconnect(on_inserted_item);
+ }
+
+ public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { }
+
+ private void on_inserted_item(Plugins.MetaConversationItem item) {
+ if (!(item is ContentMetaItem)) return;
+
+ DateTime time = item.sort_time.to_local();
+ DateTime msg_date = new DateTime.local(time.get_year(), time.get_month(), time.get_day_of_month(), 0, 0, 0);
+ if (!insert_times.contains(msg_date)) {
+ if (insert_times.lower(msg_date) != null) {
+ item_collection.insert_item(new MetaDateItem(msg_date.to_utc()));
+ } else if (insert_times.size > 0) {
+ item_collection.insert_item(new MetaDateItem(insert_times.first().to_utc()));
+ }
+ insert_times.add(msg_date);
+ }
+ }
+}
+
+public class MetaDateItem : Plugins.MetaConversationItem {
+ public override DateTime sort_time { get; set; }
+
+ public override bool can_merge { get; set; default=false; }
+ public override bool requires_avatar { get; set; default=false; }
+ public override bool requires_header { get; set; default=false; }
+
+ private DateTime date;
+
+ public MetaDateItem(DateTime date) {
+ this.date = date;
+ this.sort_time = date;
+ }
+
+ public override Object? get_widget(Plugins.WidgetType widget_type) {
+ Box box = new Box(Orientation.HORIZONTAL, 10) { width_request=300, halign=Align.CENTER, visible=true };
+ box.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true });
+ string date_str = get_relative_time(date);
+ Label label = new Label(@"<span size='small'>$date_str</span>") { use_markup=true, halign=Align.CENTER, hexpand=false, visible=true };
+ label.get_style_context().add_class("dim-label");
+ box.add(label);
+ box.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true });
+ return box;
+ }
+
+ private static string get_relative_time(DateTime time) {
+ DateTime time_local = time.to_local();
+ DateTime now_local = new DateTime.now_local();
+ if (time_local.get_year() == now_local.get_year() &&
+ time_local.get_month() == now_local.get_month() &&
+ time_local.get_day_of_month() == now_local.get_day_of_month()) {
+ return _("Today");
+ }
+ DateTime now_local_minus = now_local.add_days(-1);
+ if (time_local.get_year() == now_local_minus.get_year() &&
+ time_local.get_month() == now_local_minus.get_month() &&
+ time_local.get_day_of_month() == now_local_minus.get_day_of_month()) {
+ return _("Yesterday");
+ }
+ if (time_local.get_year() != now_local.get_year()) {
+ return time_local.format("%x");
+ }
+ TimeSpan timespan = now_local.difference(time_local);
+ if (timespan < 7 * TimeSpan.DAY) {
+ return time_local.format(_("%a, %b %d"));
+ } else {
+ return time_local.format(_("%b %d"));
+ }
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala
new file mode 100644
index 00000000..dd28b385
--- /dev/null
+++ b/main/src/ui/conversation_content_view/file_widget.vala
@@ -0,0 +1,338 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Pango;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class FileWidget : Box {
+
+ enum State {
+ IMAGE,
+ DEFAULT
+ }
+
+ private const int MAX_HEIGHT = 300;
+ private const int MAX_WIDTH = 600;
+
+ private StreamInteractor stream_interactor;
+ private FileTransfer file_transfer;
+ private State state;
+
+ // default box
+ private Box main_box;
+ private Image content_type_image;
+ private Image download_image;
+ private Spinner spinner;
+ private Label mime_label;
+ private Stack image_stack;
+
+ private Widget content;
+
+ private bool pointer_inside = false;
+
+ public FileWidget(StreamInteractor stream_interactor, FileTransfer file_transfer) {
+ this.stream_interactor = stream_interactor;
+ this.file_transfer = file_transfer;
+
+ load_widget.begin();
+ }
+
+ private async void load_widget() {
+ if (show_image()) {
+ content = yield get_image_widget(file_transfer);
+ if (content != null) {
+ this.state = State.IMAGE;
+ this.add(content);
+ return;
+ }
+ }
+ content = get_default_widget(file_transfer);
+ this.state = State.DEFAULT;
+ this.add(content);
+ }
+
+ private async Widget? get_image_widget(FileTransfer file_transfer) {
+ // Load and prepare image in tread
+ Thread<Image?> thread = new Thread<Image?> (null, () => {
+ Image image = new Image() { halign=Align.START, visible = true };
+
+ Gdk.Pixbuf pixbuf;
+ try {
+ pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path());
+ } catch (Error error) {
+ warning("Can't load picture %s - %s", file_transfer.get_file().get_path(), error.message);
+ Idle.add(get_image_widget.callback);
+ return null;
+ }
+
+ pixbuf = pixbuf.apply_embedded_orientation();
+
+ int max_scaled_height = MAX_HEIGHT * image.scale_factor;
+ if (pixbuf.height > max_scaled_height) {
+ pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR);
+ }
+ int max_scaled_width = MAX_WIDTH * image.scale_factor;
+ if (pixbuf.width > max_scaled_width) {
+ pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR);
+ }
+ pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor());
+ Util.image_set_from_scaled_pixbuf(image, pixbuf);
+
+ Idle.add(get_image_widget.callback);
+ return image;
+ });
+ yield;
+ Image image = thread.join();
+ if (image == null) return null;
+
+ Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }");
+
+ Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_content_view/image_toolbar.ui");
+ Widget toolbar = builder.get_object("main") as Widget;
+ Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)");
+ Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }");
+
+ Label url_label = builder.get_object("url_label") as Label;
+ Util.force_color(url_label, "#eee");
+
+ if (file_transfer.file_name != null && file_transfer.file_name != "") {
+ string caption = file_transfer.file_name;
+ url_label.label = caption;
+ } else {
+ url_label.visible = false;
+ }
+
+ Image open_image = builder.get_object("open_image") as Image;
+ Util.force_css(open_image, "*:not(:hover) { color: #eee; }");
+ Button open_button = builder.get_object("open_button") as Button;
+ Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }");
+ open_button.clicked.connect(() => {
+ try{
+ AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
+ } catch (Error err) {
+ info("Could not to open file://%s: %s", file_transfer.get_file().get_path(), err.message);
+ }
+ });
+
+ Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true };
+ toolbar_revealer.add(toolbar);
+
+ Grid grid = new Grid() { visible=true };
+ grid.attach(toolbar_revealer, 0, 0, 1, 1);
+ grid.attach(image, 0, 0, 1, 1);
+
+ EventBox event_box = new EventBox() { margin_top=5, halign=Align.START, visible=true };
+ event_box.events = EventMask.POINTER_MOTION_MASK;
+ event_box.add(grid);
+ event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; });
+ event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; });
+
+ return event_box;
+ }
+
+ private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) {
+ Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height));
+ Gdk.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 Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height);
+ }
+
+ private Widget get_default_widget(FileTransfer file_transfer) {
+ string icon_name = get_file_icon_name(file_transfer.mime_type);
+
+ main_box = new Box(Orientation.HORIZONTAL, 10) { halign=Align.FILL, hexpand=true, visible=true };
+ content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { opacity=0.5, visible=true };
+ download_image = new Image.from_icon_name("dino-file-download-symbolic", IconSize.DND) { opacity=0.7, visible=true };
+ spinner = new Spinner() { visible=true };
+
+ EventBox stack_event_box = new EventBox() { visible=true };
+ image_stack = new Stack() { transition_type = StackTransitionType.CROSSFADE, transition_duration=50, valign=Align.CENTER, visible=true };
+ image_stack.add_named(download_image, "download_image");
+ image_stack.add_named(spinner, "spinner");
+ image_stack.add_named(content_type_image, "content_type_image");
+ stack_event_box.add(image_stack);
+
+ main_box.add(stack_event_box);
+
+ Box right_box = new Box(Orientation.VERTICAL, 0) { hexpand=true, visible=true };
+ Label name_label = new Label(file_transfer.file_name) { ellipsize=EllipsizeMode.MIDDLE, max_width_chars=1, hexpand=true, xalign=0, yalign=0, visible=true};
+ right_box.add(name_label);
+
+ EventBox mime_label_event_box = new EventBox() { visible=true };
+ mime_label = new Label("") { use_markup=true, xalign=0, yalign=1, visible=true};
+
+ mime_label_event_box.add(mime_label);
+ mime_label.get_style_context().add_class("dim-label");
+
+ right_box.add(mime_label_event_box);
+ main_box.add(right_box);
+
+ EventBox event_box = new EventBox() { margin_top=5, width_request=500, halign=Align.START, visible=true };
+ event_box.get_style_context().add_class("file-box-outer");
+ event_box.add(main_box);
+ main_box.get_style_context().add_class("file-box");
+
+ event_box.enter_notify_event.connect((event) => {
+ pointer_inside = true;
+ Timeout.add(20, () => {
+ if (pointer_inside) {
+ event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2));
+ content_type_image.opacity = 0.7;
+ if (file_transfer.state == FileTransfer.State.NOT_STARTED) {
+ image_stack.set_visible_child_name("download_image");
+ }
+ }
+ return false;
+ });
+ return false;
+ });
+ stack_event_box.enter_notify_event.connect((event) => { pointer_inside = true; return false; });
+ mime_label_event_box.enter_notify_event.connect((event) => { pointer_inside = true; return false; });
+ mime_label.enter_notify_event.connect((event) => { pointer_inside = true; return false; });
+ event_box.leave_notify_event.connect((event) => {
+ pointer_inside = false;
+ Timeout.add(20, () => {
+ if (!pointer_inside) {
+ event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM));
+ content_type_image.opacity = 0.5;
+ if (file_transfer.state == FileTransfer.State.NOT_STARTED) {
+ image_stack.set_visible_child_name("content_type_image");
+ }
+ }
+ return false;
+ });
+ return false;
+ });
+ stack_event_box.leave_notify_event.connect((event) => { pointer_inside = true; return false; });
+ mime_label_event_box.leave_notify_event.connect((event) => { pointer_inside = true; return false; });
+ mime_label.leave_notify_event.connect((event) => { pointer_inside = true; return false; });
+ event_box.button_release_event.connect((event_button) => {
+ switch (file_transfer.state) {
+ case FileTransfer.State.COMPLETE:
+ if (event_button.button == 1) {
+ try{
+ AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
+ } catch (Error err) {
+ print("Tried to open " + file_transfer.get_file().get_path());
+ }
+ }
+ break;
+ case FileTransfer.State.NOT_STARTED:
+ stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer);
+ break;
+ }
+ return false;
+ });
+
+ main_box.events = EventMask.POINTER_MOTION_MASK;
+ content_type_image.events = EventMask.POINTER_MOTION_MASK;
+ download_image.events = EventMask.POINTER_MOTION_MASK;
+ spinner.events = EventMask.POINTER_MOTION_MASK;
+ image_stack.events = EventMask.POINTER_MOTION_MASK;
+ right_box.events = EventMask.POINTER_MOTION_MASK;
+ name_label.events = EventMask.POINTER_MOTION_MASK;
+ mime_label.events = EventMask.POINTER_MOTION_MASK;
+ event_box.events = EventMask.POINTER_MOTION_MASK;
+ mime_label.events = EventMask.POINTER_MOTION_MASK;
+ mime_label_event_box.events = EventMask.POINTER_MOTION_MASK;
+
+ file_transfer.notify["path"].connect(update_file_info);
+ file_transfer.notify["state"].connect(update_file_info);
+ file_transfer.notify["mime-type"].connect(update_file_info);
+ update_file_info.begin();
+
+ return event_box;
+ }
+
+ private async void update_file_info() {
+ if (file_transfer.state == FileTransfer.State.COMPLETE && show_image() && state != State.IMAGE) {
+ this.remove(content);
+ this.add(yield get_image_widget(file_transfer));
+ state = State.IMAGE;
+ }
+
+ spinner.active = false; // A hidden spinning spinner still uses CPU. Deactivate asap
+
+ string? mime_description = file_transfer.mime_type != null ? ContentType.get_description(file_transfer.mime_type) : null;
+
+ switch (file_transfer.state) {
+ case FileTransfer.State.COMPLETE:
+ mime_label.label = "<span size='small'>" + mime_description + "</span>";
+ image_stack.set_visible_child_name("content_type_image");
+ break;
+ case FileTransfer.State.IN_PROGRESS:
+ mime_label.label = "<span size='small'>" + _("Downloading %s…").printf(get_size_string(file_transfer.size)) + "</span>";
+ spinner.active = true;
+ image_stack.set_visible_child_name("spinner");
+ break;
+ case FileTransfer.State.NOT_STARTED:
+ if (mime_description != null) {
+ mime_label.label = "<span size='small'>" + _("%s offered: %s").printf(mime_description, get_size_string(file_transfer.size)) + "</span>";
+ } else if (file_transfer.size != -1) {
+ mime_label.label = "<span size='small'>" + _("File offered: %s").printf(get_size_string(file_transfer.size)) + "</span>";
+ } else {
+ mime_label.label = "<span size='small'>" + _("File offered") + "</span>";
+ }
+ image_stack.set_visible_child_name("content_type_image");
+ break;
+ case FileTransfer.State.FAILED:
+ mime_label.label = "<span size='small' foreground=\"#f44336\">" + _("File transfer failed") + "</span>";
+ image_stack.set_visible_child_name("content_type_image");
+ break;
+ }
+ }
+
+ private static string get_file_icon_name(string? mime_type) {
+ if (mime_type == null) return "dino-file-symbolic";
+
+ string generic_icon_name = ContentType.get_generic_icon_name(mime_type) ?? "";
+ switch (generic_icon_name) {
+ case "audio-x-generic": return "dino-file-music-symbolic";
+ case "image-x-generic": return "dino-file-image-symbolic";
+ case "text-x-generic": return "dino-file-document-symbolic";
+ case "text-x-generic-template": return "dino-file-document-symbolic";
+ case "video-x-generic": return "dino-file-video-symbolic";
+ case "x-office-document": return "dino-file-document-symbolic";
+ case "x-office-spreadsheet": return "dino-file-table-symbolic";
+ default: return "dino-file-symbolic";
+ }
+ }
+
+ private static string get_size_string(int size) {
+ if (size < 1024) {
+ return @"$(size) B";
+ } else if (size < 1000 * 1000) {
+ return @"$(size / 1000) kB";
+ } else if (size < 1000 * 1000 * 1000) {
+ return @"$(size / 1000 / 1000) MB";
+ } else {
+ return @"$(size / 1000 / 1000 / 1000) GB";
+ }
+ }
+
+ private bool show_image() {
+ if (file_transfer.mime_type == null || file_transfer.state != FileTransfer.State.COMPLETE) return false;
+
+ foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) {
+ foreach (string mime_type in pixbuf_format.get_mime_types()) {
+ if (mime_type == file_transfer.mime_type) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/message_item.vala b/main/src/ui/conversation_content_view/message_item.vala
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/main/src/ui/conversation_content_view/message_item.vala
diff --git a/main/src/ui/conversation_content_view/subscription_notification.vala b/main/src/ui/conversation_content_view/subscription_notification.vala
new file mode 100644
index 00000000..d493ff78
--- /dev/null
+++ b/main/src/ui/conversation_content_view/subscription_notification.vala
@@ -0,0 +1,55 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class SubscriptionNotitication : Object {
+
+ private StreamInteractor stream_interactor;
+ private Conversation conversation;
+ private ConversationView conversation_view;
+
+ public SubscriptionNotitication(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+
+ stream_interactor.get_module(PresenceManager.IDENTITY).received_subscription_request.connect((jid, account) => {
+ Conversation relevant_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.CHAT);
+ stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(relevant_conversation);
+ if (conversation != null && account.equals(conversation.account) && jid.equals(conversation.counterpart)) {
+ show_notification();
+ }
+ });
+ }
+
+ public void init(Conversation conversation, ConversationView conversation_view) {
+ this.conversation = conversation;
+ this.conversation_view = conversation_view;
+
+ if (stream_interactor.get_module(PresenceManager.IDENTITY).exists_subscription_request(conversation.account, conversation.counterpart)) {
+ show_notification();
+ }
+ }
+
+ private void show_notification() {
+ Box box = new Box(Orientation.HORIZONTAL, 5) { visible=true };
+ Button accept_button = new Button() { label=_("Accept"), visible=true };
+ Button deny_button = new Button() { label=_("Deny"), visible=true };
+ GLib.Application app = GLib.Application.get_default();
+ accept_button.clicked.connect(() => {
+ app.activate_action("accept-subscription", conversation.id);
+ conversation_view.remove_notification(box);
+ });
+ deny_button.clicked.connect(() => {
+ app.activate_action("deny-subscription", conversation.id);
+ conversation_view.remove_notification(box);
+ });
+ box.add(new Label(_("This contact would like to add you to their contact list")) { margin_end=10, visible=true });
+ box.add(accept_button);
+ box.add(deny_button);
+ conversation_view.add_notification(box);
+ }
+}
+
+}