reorg: message-list -> transcript
This commit is contained in:
195
src/transcript/layouts/bubble-layout.vala
Normal file
195
src/transcript/layouts/bubble-layout.vala
Normal file
@@ -0,0 +1,195 @@
|
||||
using Gtk;
|
||||
|
||||
private struct BubbleLayoutConstants {
|
||||
public float tail_width;
|
||||
public float tail_curve_offset;
|
||||
public float tail_side_offset;
|
||||
public float tail_bottom_padding;
|
||||
public float corner_radius;
|
||||
public float text_padding;
|
||||
|
||||
public BubbleLayoutConstants(float scale_factor) {
|
||||
tail_width = 15.0f / scale_factor;
|
||||
tail_curve_offset = 2.5f / scale_factor;
|
||||
tail_side_offset = 0.0f / scale_factor;
|
||||
tail_bottom_padding = 4.0f / scale_factor;
|
||||
corner_radius = 24.0f / scale_factor;
|
||||
text_padding = 18.0f / scale_factor;
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class BubbleLayout : Object, ChatItemLayout
|
||||
{
|
||||
public bool from_me { get; set; }
|
||||
|
||||
protected float max_width;
|
||||
protected Widget parent;
|
||||
protected BubbleLayoutConstants constants;
|
||||
|
||||
protected BubbleLayout(Widget parent, float max_width) {
|
||||
this.max_width = max_width;
|
||||
this.parent = parent;
|
||||
this.constants = BubbleLayoutConstants(parent.get_scale_factor());
|
||||
}
|
||||
|
||||
private Gdk.RGBA background_color {
|
||||
get {
|
||||
return from_me ? parent.get_color() : Gdk.RGBA() {
|
||||
red = 1.0f,
|
||||
green = 1.0f,
|
||||
blue = 1.0f,
|
||||
alpha = 0.08f
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public abstract float get_height();
|
||||
|
||||
public abstract float get_width();
|
||||
|
||||
public void draw(Snapshot snapshot) {
|
||||
with_bubble_clip(snapshot, (snapshot) => {
|
||||
draw_background(snapshot);
|
||||
draw_content(snapshot);
|
||||
});
|
||||
}
|
||||
|
||||
public abstract void draw_content(Snapshot snapshot);
|
||||
|
||||
private void draw_background(Snapshot snapshot)
|
||||
{
|
||||
var width = get_width();
|
||||
var height = get_height();
|
||||
var color = background_color;
|
||||
var bounds = Graphene.Rect().init(0, 0, width, height);
|
||||
|
||||
const float gradient_darkening = 0.67f;
|
||||
var start_point = Graphene.Point() { x = 0, y = 0 };
|
||||
var end_point = Graphene.Point() { x = 0, y = height };
|
||||
var stops = new Gsk.ColorStop[] {
|
||||
Gsk.ColorStop() { offset = 0.0f, color = color },
|
||||
Gsk.ColorStop() { offset = 1.0f, color = Gdk.RGBA () {
|
||||
red = color.red * gradient_darkening,
|
||||
green = color.green * gradient_darkening,
|
||||
blue = color.blue * gradient_darkening,
|
||||
alpha = color.alpha
|
||||
} },
|
||||
};
|
||||
|
||||
snapshot.append_linear_gradient(bounds, start_point, end_point, stops);
|
||||
}
|
||||
|
||||
protected void with_bubble_clip(Snapshot snapshot, Func<Snapshot> func) {
|
||||
var width = get_width();
|
||||
var height = get_height();
|
||||
|
||||
if (width > 10 && height > 10) {
|
||||
var path = create_bubble_path(width, height, from_me);
|
||||
snapshot.push_fill(path, Gsk.FillRule.WINDING);
|
||||
func(snapshot);
|
||||
snapshot.pop();
|
||||
} else {
|
||||
func(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
private Gsk.Path create_bubble_path(float width, float height, bool tail_on_right = false)
|
||||
{
|
||||
var builder = new Gsk.PathBuilder();
|
||||
|
||||
float bubble_width = width - constants.tail_width;
|
||||
float bubble_height = height - constants.tail_bottom_padding;
|
||||
|
||||
// Base position adjustments based on tail position
|
||||
float x = tail_on_right ? 0.0f : constants.tail_width;
|
||||
float y = 0.0f;
|
||||
|
||||
// Calculate tail direction multiplier (-1 for left, 1 for right)
|
||||
float dir = tail_on_right ? 1.0f : -1.0f;
|
||||
|
||||
// Calculate tail side positions based on direction
|
||||
float tail_side_x = tail_on_right ? (x + bubble_width) : x;
|
||||
|
||||
// Start at top corner opposite to the tail
|
||||
builder.move_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y);
|
||||
|
||||
// Top edge
|
||||
builder.line_to(tail_on_right ? (x + bubble_width - constants.corner_radius) : (x + constants.corner_radius), y);
|
||||
|
||||
// Top corner on tail side
|
||||
if (tail_on_right) {
|
||||
builder.html_arc_to(x + bubble_width, y,
|
||||
x + bubble_width, y + constants.corner_radius,
|
||||
constants.corner_radius);
|
||||
} else {
|
||||
builder.html_arc_to(x, y,
|
||||
x, y + constants.corner_radius,
|
||||
constants.corner_radius);
|
||||
}
|
||||
|
||||
// Side edge on tail side
|
||||
builder.line_to(tail_side_x, y + bubble_height - constants.corner_radius);
|
||||
|
||||
// Corner with tail
|
||||
float tail_tip_x = tail_side_x + (dir * (constants.tail_width - constants.tail_curve_offset));
|
||||
float tail_tip_y = y + bubble_height;
|
||||
|
||||
// Control points for the bezier curve
|
||||
float ctrl_point1_x = tail_side_x + (dir * constants.tail_side_offset);
|
||||
float ctrl_point1_y = y + bubble_height - constants.corner_radius/3;
|
||||
|
||||
float ctrl_point2_x = tail_side_x + (dir * (constants.tail_width / 2.0f));
|
||||
float ctrl_point2_y = y + bubble_height - 2;
|
||||
|
||||
// Point where the tail meets the bottom edge
|
||||
float tail_base_x = tail_side_x - (dir * constants.corner_radius/2);
|
||||
float tail_base_y = y + bubble_height;
|
||||
|
||||
// Draw the corner with tail using bezier curves
|
||||
builder.cubic_to(ctrl_point1_x, ctrl_point1_y,
|
||||
ctrl_point2_x, ctrl_point2_y,
|
||||
tail_tip_x, tail_tip_y);
|
||||
|
||||
builder.cubic_to(tail_tip_x - (dir * constants.tail_curve_offset), tail_tip_y + constants.tail_curve_offset,
|
||||
tail_base_x + (dir * constants.tail_width), tail_base_y,
|
||||
tail_base_x, tail_base_y);
|
||||
|
||||
// Bottom edge
|
||||
builder.line_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y + bubble_height);
|
||||
|
||||
// Bottom corner opposite to tail
|
||||
if (tail_on_right) {
|
||||
builder.html_arc_to(x, y + bubble_height,
|
||||
x, y + bubble_height - constants.corner_radius,
|
||||
constants.corner_radius);
|
||||
} else {
|
||||
builder.html_arc_to(x + bubble_width, y + bubble_height,
|
||||
x + bubble_width, y + bubble_height - constants.corner_radius,
|
||||
constants.corner_radius);
|
||||
}
|
||||
|
||||
// Side edge opposite to tail
|
||||
if (tail_on_right) {
|
||||
builder.line_to(x, y + constants.corner_radius);
|
||||
|
||||
// Top corner to close path
|
||||
builder.html_arc_to(x, y,
|
||||
x + constants.corner_radius, y,
|
||||
constants.corner_radius);
|
||||
} else {
|
||||
builder.line_to(x + bubble_width, y + constants.corner_radius);
|
||||
|
||||
// Top corner to close path
|
||||
builder.html_arc_to(x + bubble_width, y,
|
||||
x + bubble_width - constants.corner_radius, y,
|
||||
constants.corner_radius);
|
||||
}
|
||||
|
||||
// Close the path
|
||||
builder.close();
|
||||
|
||||
return builder.to_path();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
src/transcript/layouts/chat-item-layout.vala
Normal file
10
src/transcript/layouts/chat-item-layout.vala
Normal file
@@ -0,0 +1,10 @@
|
||||
using Gtk;
|
||||
|
||||
interface ChatItemLayout : Object
|
||||
{
|
||||
public abstract bool from_me { get; set; }
|
||||
|
||||
public abstract float get_height();
|
||||
public abstract float get_width();
|
||||
public abstract void draw(Snapshot snapshot);
|
||||
}
|
||||
48
src/transcript/layouts/date-item-layout.vala
Normal file
48
src/transcript/layouts/date-item-layout.vala
Normal file
@@ -0,0 +1,48 @@
|
||||
using Gtk;
|
||||
|
||||
class DateItemLayout : Object, ChatItemLayout {
|
||||
public bool from_me { get; set; }
|
||||
|
||||
private Pango.Layout layout;
|
||||
private float max_width;
|
||||
|
||||
public DateItemLayout(string date_str, Widget parent, float max_width) {
|
||||
this.max_width = max_width;
|
||||
|
||||
layout = parent.create_pango_layout(date_str);
|
||||
layout.set_font_description(Pango.FontDescription.from_string("Sans 9"));
|
||||
layout.set_alignment(Pango.Alignment.CENTER);
|
||||
}
|
||||
|
||||
public float get_height() {
|
||||
Pango.Rectangle ink_rect, logical_rect;
|
||||
layout.get_pixel_extents(out ink_rect, out logical_rect);
|
||||
|
||||
return logical_rect.height + 50.0f;
|
||||
}
|
||||
|
||||
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 = (max_width - logical_rect.width) / 2,
|
||||
y = (get_height() - logical_rect.height) / 2
|
||||
});
|
||||
|
||||
snapshot.append_layout(layout, Gdk.RGBA() {
|
||||
red = 1.0f,
|
||||
green = 1.0f,
|
||||
blue = 1.0f,
|
||||
alpha = 0.5f
|
||||
});
|
||||
|
||||
snapshot.restore();
|
||||
}
|
||||
}
|
||||
96
src/transcript/layouts/text-bubble-layout.vala
Normal file
96
src/transcript/layouts/text-bubble-layout.vala
Normal file
@@ -0,0 +1,96 @@
|
||||
using Gtk;
|
||||
|
||||
private class TextBubbleLayout : BubbleLayout
|
||||
{
|
||||
public Message message;
|
||||
private Pango.Layout layout;
|
||||
|
||||
public TextBubbleLayout(Message message, Widget parent, float max_width) {
|
||||
base(parent, max_width);
|
||||
|
||||
this.from_me = message.from_me;
|
||||
this.message = message;
|
||||
|
||||
layout = parent.create_pango_layout(message.text);
|
||||
|
||||
// Get the system font settings
|
||||
var settings = Gtk.Settings.get_default();
|
||||
var font_name = settings.gtk_font_name;
|
||||
|
||||
// Create font description from system font
|
||||
var font_desc = Pango.FontDescription.from_string(font_name);
|
||||
|
||||
var size = font_desc.get_size();
|
||||
font_desc.set_size((int)(size));
|
||||
layout.set_font_description(font_desc);
|
||||
|
||||
// Set max width
|
||||
layout.set_width((int)text_available_width * Pango.SCALE);
|
||||
}
|
||||
|
||||
private float text_available_width {
|
||||
get {
|
||||
return max_width - text_x_offset - constants.text_padding;
|
||||
}
|
||||
}
|
||||
|
||||
private float text_x_offset {
|
||||
get {
|
||||
return from_me ? constants.text_padding : constants.tail_width + constants.text_padding;
|
||||
}
|
||||
}
|
||||
|
||||
private float text_x_padding {
|
||||
get {
|
||||
// Opposite of text_x_offset
|
||||
return from_me ? constants.tail_width + constants.text_padding : constants.text_padding;
|
||||
}
|
||||
}
|
||||
|
||||
private Gdk.RGBA background_color {
|
||||
get {
|
||||
return from_me ? parent.get_color() : Gdk.RGBA() {
|
||||
red = 1.0f,
|
||||
green = 1.0f,
|
||||
blue = 1.0f,
|
||||
alpha = 0.08f
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override float get_height() {
|
||||
Pango.Rectangle ink_rect, logical_rect;
|
||||
layout.get_pixel_extents(out ink_rect, out logical_rect);
|
||||
|
||||
return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding;
|
||||
}
|
||||
|
||||
public override float get_width() {
|
||||
Pango.Rectangle ink_rect, logical_rect;
|
||||
layout.get_pixel_extents(out ink_rect, out logical_rect);
|
||||
|
||||
return logical_rect.width + text_x_offset + text_x_padding;
|
||||
}
|
||||
|
||||
public override void draw_content(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 = text_x_offset,
|
||||
y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2
|
||||
});
|
||||
|
||||
snapshot.append_layout(layout, Gdk.RGBA() {
|
||||
red = 1.0f,
|
||||
green = 1.0f,
|
||||
blue = 1.0f,
|
||||
alpha = 1.0f
|
||||
});
|
||||
|
||||
snapshot.restore();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
76
src/transcript/message-list-model.vala
Normal file
76
src/transcript/message-list-model.vala
Normal file
@@ -0,0 +1,76 @@
|
||||
using GLib;
|
||||
using Gee;
|
||||
|
||||
public class MessageListModel : Object, ListModel
|
||||
{
|
||||
public signal void messages_changed();
|
||||
|
||||
public SortedSet<Message> messages {
|
||||
owned get { return _messages.read_only_view; }
|
||||
}
|
||||
|
||||
public string conversation_guid { get; private set; }
|
||||
private SortedSet<Message> _messages;
|
||||
|
||||
public MessageListModel(string conversation_guid) {
|
||||
_messages = new TreeSet<Message>((a, b) => {
|
||||
// Sort by date in descending order (newest first)
|
||||
return a.date.compare(b.date);
|
||||
});
|
||||
|
||||
Repository.get_instance().messages_updated.connect(got_messages_updated);
|
||||
this.conversation_guid = conversation_guid;
|
||||
}
|
||||
|
||||
public void load_messages() {
|
||||
try {
|
||||
Message[] messages = Repository.get_instance().get_messages(conversation_guid);
|
||||
|
||||
// Clear existing set
|
||||
uint old_count = _messages.size;
|
||||
_messages.clear();
|
||||
|
||||
// Notify of removal
|
||||
if (old_count > 0) {
|
||||
items_changed(0, old_count, 0);
|
||||
}
|
||||
|
||||
// Process each conversation
|
||||
uint position = 0;
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
var message = messages[i];
|
||||
_messages.add(message);
|
||||
position++;
|
||||
}
|
||||
|
||||
// Notify of additions
|
||||
if (position > 0) {
|
||||
items_changed(0, 0, position);
|
||||
}
|
||||
} catch (Error e) {
|
||||
warning("Failed to load messages: %s", e.message);
|
||||
}
|
||||
|
||||
messages_changed();
|
||||
}
|
||||
|
||||
private void got_messages_updated(string conversation_guid) {
|
||||
if (conversation_guid == this.conversation_guid) {
|
||||
load_messages();
|
||||
}
|
||||
}
|
||||
|
||||
// ListModel implementation
|
||||
public Type get_item_type() {
|
||||
return typeof(Message);
|
||||
}
|
||||
|
||||
public uint get_n_items() {
|
||||
return _messages.size;
|
||||
}
|
||||
|
||||
public Object? get_item(uint position) {
|
||||
return _messages.to_array()[position];
|
||||
}
|
||||
}
|
||||
47
src/transcript/message-list-view.vala
Normal file
47
src/transcript/message-list-view.vala
Normal file
@@ -0,0 +1,47 @@
|
||||
using Adw;
|
||||
using Gtk;
|
||||
using Gee;
|
||||
|
||||
public class TranscriptView : Adw.Bin
|
||||
{
|
||||
public MessageListModel? model {
|
||||
get {
|
||||
return _model;
|
||||
}
|
||||
set {
|
||||
_model = value;
|
||||
|
||||
if (model != null) {
|
||||
model.messages_changed.connect(reload_messages);
|
||||
model.load_messages();
|
||||
} else {
|
||||
transcript_drawing_area.set_messages(new TreeSet<Message>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MessageListModel? _model = null;
|
||||
private Adw.ToolbarView container;
|
||||
|
||||
private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea();
|
||||
private ScrolledWindow scrolled_window = new ScrolledWindow();
|
||||
|
||||
public TranscriptView(MessageListModel? model = null) {
|
||||
this.model = model;
|
||||
|
||||
container = new Adw.ToolbarView();
|
||||
set_child(container);
|
||||
|
||||
scrolled_window.set_child(transcript_drawing_area);
|
||||
scrolled_window.add_css_class("message-list-scroller");
|
||||
container.set_content(scrolled_window);
|
||||
|
||||
var header_bar = new Adw.HeaderBar();
|
||||
header_bar.set_title_widget(new Label("Messages"));
|
||||
container.add_top_bar(header_bar);
|
||||
}
|
||||
|
||||
private void reload_messages() {
|
||||
transcript_drawing_area.set_messages(_model.messages);
|
||||
}
|
||||
}
|
||||
57
src/transcript/transcript-container-view.vala
Normal file
57
src/transcript/transcript-container-view.vala
Normal file
@@ -0,0 +1,57 @@
|
||||
using Gtk;
|
||||
using Adw;
|
||||
|
||||
class TranscriptContainerView : Adw.Bin {
|
||||
public TranscriptView transcript_view;
|
||||
public Entry message_entry;
|
||||
public signal void on_send(string message);
|
||||
|
||||
private Box container;
|
||||
private Button send_button;
|
||||
|
||||
public TranscriptContainerView () {
|
||||
container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
|
||||
set_child (container);
|
||||
|
||||
// Create message list view
|
||||
transcript_view = new TranscriptView();
|
||||
transcript_view.set_vexpand(true);
|
||||
container.append(transcript_view);
|
||||
|
||||
// Create bottom box for input
|
||||
var input_box = new Box(Orientation.HORIZONTAL, 6);
|
||||
input_box.add_css_class("message-input-box");
|
||||
input_box.set_valign(Align.END);
|
||||
input_box.set_spacing(6);
|
||||
container.append(input_box);
|
||||
|
||||
// Create message entry
|
||||
message_entry = new Entry();
|
||||
message_entry.add_css_class("message-input-entry");
|
||||
message_entry.set_placeholder_text("Type a message...");
|
||||
message_entry.set_hexpand(true);
|
||||
message_entry.changed.connect(on_text_changed);
|
||||
message_entry.activate.connect(on_request_send);
|
||||
input_box.append(message_entry);
|
||||
|
||||
// Create send button
|
||||
send_button = new Button();
|
||||
send_button.set_label("Send");
|
||||
send_button.set_sensitive(false);
|
||||
send_button.add_css_class("suggested-action");
|
||||
send_button.clicked.connect(on_request_send);
|
||||
input_box.append(send_button);
|
||||
}
|
||||
|
||||
private void on_text_changed() {
|
||||
send_button.set_sensitive(message_entry.text.length > 0);
|
||||
}
|
||||
|
||||
private void on_request_send() {
|
||||
if (message_entry.text.length > 0) {
|
||||
on_send(message_entry.text);
|
||||
message_entry.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
src/transcript/transcript-drawing-area.vala
Normal file
112
src/transcript/transcript-drawing-area.vala
Normal file
@@ -0,0 +1,112 @@
|
||||
using Gtk;
|
||||
using Gee;
|
||||
|
||||
private class TranscriptDrawingArea : Widget
|
||||
{
|
||||
private SortedSet<Message> _messages = new TreeSet<Message>();
|
||||
private ArrayList<ChatItemLayout> _chat_items = new ArrayList<ChatItemLayout>();
|
||||
|
||||
private const float bubble_padding = 10.0f;
|
||||
private const float bubble_margin = 18.0f;
|
||||
|
||||
public TranscriptDrawingArea() {
|
||||
add_css_class("transcript-drawing-area");
|
||||
}
|
||||
|
||||
public void set_messages(SortedSet<Message> 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() + bubble_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) {
|
||||
var container_width = get_width();
|
||||
float y_offset = 0;
|
||||
_chat_items.foreach((chat_item) => {
|
||||
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 + bubble_padding;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var text_bubble = new TextBubbleLayout(message, this, max_width);
|
||||
_chat_items.add(text_bubble);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
queue_draw();
|
||||
queue_resize();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user