using Gtk; using Gee; private class TranscriptDrawingArea : Widget { public bool show_sender = true; private ArrayList _messages = new ArrayList(); private ArrayList _chat_items = new ArrayList(); private const float bubble_margin = 18.0f; public TranscriptDrawingArea() { add_css_class("transcript-drawing-area"); } public void set_messages(ArrayList messages) { _messages = messages; recompute_message_layouts(); } public override SizeRequestMode get_request_mode() { return SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { if (orientation == Orientation.HORIZONTAL) { // Horizontal, so we take up the full width provided minimum = 0; natural = for_size; } else { // compute total message layout height float total_height = 0.0f; _chat_items.foreach((chat_item) => { total_height += chat_item.get_height() + chat_item.vertical_padding; return true; }); minimum = (int)total_height; natural = (int)total_height; } minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate(int width, int height, int baseline) { base.size_allocate(width, height, baseline); recompute_message_layouts(); } public override void snapshot(Snapshot snapshot) { float y_offset = 0; var container_width = get_width(); // Draw each item in reverse order, since the messages are in reverse order for (int i = _chat_items.size - 1; i >= 0; i--) { var chat_item = _chat_items[i]; var item_width = chat_item.get_width(); var item_height = chat_item.get_height(); snapshot.save(); // Flip the y-axis, since our parent is upside down (so newest messages are at the bottom) snapshot.scale(1.0f, -1.0f); // Translate to the correct position snapshot.translate(Graphene.Point() { x = (chat_item.from_me ? (container_width - item_width - bubble_margin) : bubble_margin), y = y_offset }); // Undo the y-axis flip, origin is top left snapshot.translate(Graphene.Point() { x = 0, y = -item_height }); chat_item.draw(snapshot); snapshot.restore(); y_offset -= item_height + chat_item.vertical_padding; } } private void recompute_message_layouts() { var container_width = get_width(); float max_width = container_width * 0.90f; DateTime? last_date = null; string? last_sender = null; ArrayList items = new ArrayList(); _messages.foreach((message) => { // Date Annotation DateTime date = message.date; if (last_date != null && date.difference(last_date) > (TimeSpan.HOUR * 1)) { var date_item = new DateItemLayout(date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); items.add(date_item); last_date = date; } // Sender Annotation if (show_sender && !message.from_me && last_sender != message.sender) { var sender_bubble = new SenderAnnotationLayout(message.sender, max_width, this); items.add(sender_bubble); } // Text Bubble var text_bubble = new TextBubbleLayout(message, this, max_width); text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; items.add(text_bubble); // Check for attachments. For each one, add an image layout bubble foreach (var attachment in message.attachments) { if (attachment.metadata != null) { var image_size = Graphene.Size() { width = attachment.metadata.attribution_info.width, height = attachment.metadata.attribution_info.height }; var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); image_layout.is_downloaded = attachment.preview_downloaded; items.add(image_layout); // If the attachment isn't downloaded, queue a download since we are going to be showing it here. // TODO: Probably would be better if we only did this for stuff in the viewport. if (!attachment.preview_downloaded) { try { Repository.get_instance().download_attachment(attachment.guid, true); } catch (GLib.Error e) { warning("Wasn't able to message daemon about queuing attachment download: %s", e.message); } } } } last_sender = message.sender; last_date = date; return true; }); _chat_items.clear(); _chat_items.add_all(items); queue_draw(); queue_resize(); } }