diff options
author | fiaxh <git@lightrise.org> | 2020-02-21 02:49:53 +0100 |
---|---|---|
committer | fiaxh <git@lightrise.org> | 2020-02-22 02:58:36 +0100 |
commit | 4ed6204fc2879c52fe88caa5711dea37cd4ae201 (patch) | |
tree | ba6f0f8227694c95e53645a701ac4b4111b8698c /main/src/ui/conversation_content_view | |
parent | 01698959feaa9005c8a5f3439478431ab5837792 (diff) | |
download | dino-4ed6204fc2879c52fe88caa5711dea37cd4ae201.tar.gz dino-4ed6204fc2879c52fe88caa5711dea37cd4ae201.zip |
Rename folders/files conversation_summary -> conversation_content_view
Diffstat (limited to 'main/src/ui/conversation_content_view')
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); + } +} + +} |