diff --git a/src/meson.build b/src/meson.build index 7ccb410..20b668f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,6 +37,7 @@ sources = [ 'transcript/layouts/bubble-layout.vala', 'transcript/layouts/chat-item-layout.vala', 'transcript/layouts/date-item-layout.vala', + 'transcript/layouts/sender-annotation-layout.vala', 'transcript/layouts/text-bubble-layout.vala', 'models/conversation.vala', diff --git a/src/transcript/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala index e4d123d..a256c1a 100644 --- a/src/transcript/layouts/bubble-layout.vala +++ b/src/transcript/layouts/bubble-layout.vala @@ -21,7 +21,8 @@ private struct BubbleLayoutConstants { private abstract class BubbleLayout : Object, ChatItemLayout { public bool from_me { get; set; } - + public float vertical_padding { get; set; } + protected float max_width; protected Widget parent; protected BubbleLayoutConstants constants; diff --git a/src/transcript/layouts/chat-item-layout.vala b/src/transcript/layouts/chat-item-layout.vala index 4a3c18e..e1f5a06 100644 --- a/src/transcript/layouts/chat-item-layout.vala +++ b/src/transcript/layouts/chat-item-layout.vala @@ -3,8 +3,10 @@ using Gtk; interface ChatItemLayout : Object { public abstract bool from_me { get; set; } + public abstract float vertical_padding { get; set; } public abstract float get_height(); public abstract float get_width(); + public abstract void draw(Snapshot snapshot); } diff --git a/src/transcript/layouts/date-item-layout.vala b/src/transcript/layouts/date-item-layout.vala index a87a714..4e18ee2 100644 --- a/src/transcript/layouts/date-item-layout.vala +++ b/src/transcript/layouts/date-item-layout.vala @@ -2,6 +2,7 @@ using Gtk; class DateItemLayout : Object, ChatItemLayout { public bool from_me { get; set; } + public float vertical_padding { get; set; } private Pango.Layout layout; private float max_width; diff --git a/src/transcript/layouts/sender-annotation-layout.vala b/src/transcript/layouts/sender-annotation-layout.vala new file mode 100644 index 0000000..b078d48 --- /dev/null +++ b/src/transcript/layouts/sender-annotation-layout.vala @@ -0,0 +1,56 @@ +using Gtk; + +private class SenderAnnotationLayout : Object, ChatItemLayout +{ + public string sender; + public float max_width; + public bool from_me { get; set; default = false; } + public float vertical_padding { get; set; default = 0.0f; } + + private Pango.Layout layout; + private BubbleLayoutConstants constants; + private const float padding = 10.0f; + + public SenderAnnotationLayout(string sender, float max_width, Widget parent) { + this.sender = sender; + this.max_width = max_width; + this.constants = BubbleLayoutConstants(parent.get_scale_factor()); + + layout = parent.create_pango_layout(sender); + var font_desc = Pango.FontDescription.from_string("Sans 8"); + layout.set_font_description(font_desc); + } + + public float get_height() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.height + padding; + } + + public float get_width() { + return max_width; + } + + public void draw(Snapshot snapshot) { + snapshot.save(); + + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + + snapshot.translate(Graphene.Point() { + x = constants.text_padding + constants.tail_width, + y = get_height() - logical_rect.height, + }); + + snapshot.append_layout(layout, Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 0.8f + }); + + snapshot.restore(); + } +} \ No newline at end of file diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index 30de42c..6d685f2 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -23,6 +23,7 @@ private class TextBubbleLayout : BubbleLayout var size = font_desc.get_size(); font_desc.set_size((int)(size)); layout.set_font_description(font_desc); + layout.set_wrap(Pango.WrapMode.WORD_CHAR); // Set max width layout.set_width((int)text_available_width * Pango.SCALE); diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index b1d362f..037e1f2 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -9,8 +9,16 @@ public class MessageListModel : Object, ListModel owned get { return _messages.read_only_view; } } + public bool is_group_chat { + get { + return participants.size > 2; + } + } + public string conversation_guid { get; private set; } + private SortedSet _messages; + private HashSet participants = new HashSet(); public MessageListModel(string conversation_guid) { _messages = new TreeSet((a, b) => { @@ -29,6 +37,7 @@ public class MessageListModel : Object, ListModel // Clear existing set uint old_count = _messages.size; _messages.clear(); + participants.clear(); // Notify of removal if (old_count > 0) { @@ -41,6 +50,8 @@ public class MessageListModel : Object, ListModel for (int i = 0; i < messages.length; i++) { var message = messages[i]; _messages.add(message); + participants.add(message.sender); + position++; } diff --git a/src/transcript/message-list-view.vala b/src/transcript/message-list-view.vala index e91f430..4b37cd2 100644 --- a/src/transcript/message-list-view.vala +++ b/src/transcript/message-list-view.vala @@ -42,6 +42,7 @@ public class TranscriptView : Adw.Bin } private void reload_messages() { + transcript_drawing_area.show_sender = _model.is_group_chat; transcript_drawing_area.set_messages(_model.messages); } } diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 0a88969..77add9b 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -3,10 +3,11 @@ using Gee; private class TranscriptDrawingArea : Widget { + public bool show_sender = true; + private SortedSet _messages = new TreeSet(); private ArrayList _chat_items = new ArrayList(); - private const float bubble_padding = 10.0f; private const float bubble_margin = 18.0f; public TranscriptDrawingArea() { @@ -32,7 +33,7 @@ private class TranscriptDrawingArea : Widget // compute total message layout height float total_height = 0.0f; _chat_items.foreach((chat_item) => { - total_height += chat_item.get_height() + bubble_padding; + total_height += chat_item.get_height() + chat_item.vertical_padding; return true; }); @@ -50,9 +51,12 @@ private class TranscriptDrawingArea : Widget } public override void snapshot(Snapshot snapshot) { - var container_width = get_width(); float y_offset = 0; - _chat_items.foreach((chat_item) => { + 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(); @@ -73,38 +77,45 @@ private class TranscriptDrawingArea : Widget chat_item.draw(snapshot); snapshot.restore(); - y_offset -= item_height + bubble_padding; - - return true; - }); + 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; - _chat_items.clear(); - - var reversed_messages = _messages - .order_by((a, b) => b.date.compare(a.date)); // reverse order - DateTime? last_date = null; - reversed_messages.foreach((message) => { - // Remember everything in here is backwards. - - if (last_date == null) { - last_date = message.date; - } else if (last_date.difference(message.date) > (TimeSpan.MINUTE * 25)) { - var date_item = new DateItemLayout(last_date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); - _chat_items.add(date_item); - last_date = message.date; + 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.MINUTE * 25)) { + 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); - _chat_items.add(text_bubble); + text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; + items.add(text_bubble); + + last_sender = message.sender; + last_date = date; return true; }); + + _chat_items.clear(); + _chat_items.add_all(items); queue_draw(); queue_resize();