2025-04-30 19:12:00 -07:00
|
|
|
using Gtk;
|
|
|
|
|
using Gee;
|
2025-06-17 00:47:03 -07:00
|
|
|
using Gdk;
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-05-03 22:47:56 -07:00
|
|
|
private class TranscriptDrawingArea : Widget
|
2025-04-30 19:12:00 -07:00
|
|
|
{
|
2025-05-03 23:19:15 -07:00
|
|
|
public bool show_sender = true;
|
2025-06-16 20:09:56 -07:00
|
|
|
public Adjustment? viewport {
|
|
|
|
|
get {
|
|
|
|
|
return _viewport;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set {
|
|
|
|
|
_viewport = value;
|
|
|
|
|
queue_draw();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-03 23:19:15 -07:00
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
public signal void on_text_bubble_hover(VisibleLayout? text_bubble);
|
|
|
|
|
public signal void on_text_bubble_click(VisibleLayout? text_bubble);
|
|
|
|
|
public signal void on_image_bubble_activate(string attachment_guid);
|
2025-06-18 16:50:14 -07:00
|
|
|
|
2025-05-14 17:37:23 -07:00
|
|
|
private ArrayList<Message> _messages = new ArrayList<Message>();
|
2025-05-03 22:12:26 -07:00
|
|
|
private ArrayList<ChatItemLayout> _chat_items = new ArrayList<ChatItemLayout>();
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
private Adjustment? _viewport = null;
|
2025-04-30 19:12:00 -07:00
|
|
|
private const float bubble_margin = 18.0f;
|
|
|
|
|
|
2025-06-18 01:36:29 -07:00
|
|
|
private GestureClick _click_gesture = new GestureClick();
|
|
|
|
|
private Gdk.Rectangle? _click_bounding_box = null;
|
|
|
|
|
|
2025-06-18 15:32:37 -07:00
|
|
|
private EventControllerMotion _motion_controller = new EventControllerMotion();
|
2025-06-18 18:07:59 -07:00
|
|
|
private ArrayList<VisibleLayout?> _visible_text_layouts = new ArrayList<VisibleLayout?>();
|
2025-06-18 15:32:37 -07:00
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
private const bool debug_viewport = false;
|
2025-06-17 00:47:03 -07:00
|
|
|
private uint? _tick_callback_id = null;
|
|
|
|
|
private HashMap<string, ChatItemAnimation> _animations = new HashMap<string, ChatItemAnimation>();
|
2025-06-16 20:09:56 -07:00
|
|
|
|
2025-05-03 22:47:56 -07:00
|
|
|
public TranscriptDrawingArea() {
|
|
|
|
|
add_css_class("transcript-drawing-area");
|
2025-06-18 01:36:29 -07:00
|
|
|
|
|
|
|
|
weak TranscriptDrawingArea self = this;
|
|
|
|
|
|
2025-06-18 17:01:01 -07:00
|
|
|
_click_gesture.button = 0;
|
2025-06-18 18:07:59 -07:00
|
|
|
_click_gesture.pressed.connect((n_press, x, y) => {
|
|
|
|
|
self.on_click(self._click_gesture.get_current_button(), n_press);
|
2025-06-18 01:36:29 -07:00
|
|
|
});
|
|
|
|
|
add_controller(_click_gesture);
|
|
|
|
|
|
2025-06-18 15:32:37 -07:00
|
|
|
_motion_controller.motion.connect((x, y) => {
|
|
|
|
|
if (self == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only print motion events if window is active/focused
|
|
|
|
|
var window = self.get_native() as Gtk.Window;
|
|
|
|
|
if (window == null || !window.is_active) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.on_mouse_motion(x, y);
|
|
|
|
|
});
|
|
|
|
|
_motion_controller.set_propagation_phase(PropagationPhase.CAPTURE);
|
|
|
|
|
add_controller(_motion_controller);
|
|
|
|
|
|
2025-06-18 01:36:29 -07:00
|
|
|
SimpleAction copy_action = new SimpleAction("copy", null);
|
|
|
|
|
copy_action.activate.connect(() => {
|
|
|
|
|
if (_click_bounding_box != null) {
|
|
|
|
|
copy_message_at(_click_bounding_box);
|
|
|
|
|
} else {
|
|
|
|
|
GLib.warning("Failed to get bounding box for right click");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
SimpleActionGroup action_group = new SimpleActionGroup();
|
|
|
|
|
action_group.add_action(copy_action);
|
|
|
|
|
|
|
|
|
|
insert_action_group("transcript", action_group);
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-14 17:37:23 -07:00
|
|
|
public void set_messages(ArrayList<Message> messages) {
|
2025-04-30 19:12:00 -07:00
|
|
|
_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;
|
2025-05-03 22:12:26 -07:00
|
|
|
_chat_items.foreach((chat_item) => {
|
2025-05-03 23:19:15 -07:00
|
|
|
total_height += chat_item.get_height() + chat_item.vertical_padding;
|
2025-04-30 19:12:00 -07:00
|
|
|
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();
|
|
|
|
|
}
|
2025-06-16 20:09:56 -07:00
|
|
|
|
2025-06-17 00:47:03 -07:00
|
|
|
public override void snapshot(Gtk.Snapshot snapshot) {
|
2025-06-16 20:09:56 -07:00
|
|
|
const int viewport_y_padding = 50;
|
|
|
|
|
var viewport_rect = Graphene.Rect() {
|
|
|
|
|
origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) },
|
|
|
|
|
size = Graphene.Size() { width = get_width(), height = (int)viewport.page_size + (viewport_y_padding * 2) }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (debug_viewport) {
|
|
|
|
|
// Draw viewport outline for debugging
|
|
|
|
|
var color = Gdk.RGBA() { red = 1.0f, green = 0, blue = 0, alpha = 1.0f };
|
|
|
|
|
snapshot.append_border(
|
|
|
|
|
Gsk.RoundedRect().init_from_rect(viewport_rect, 0),
|
|
|
|
|
{ 1, 1, 1, 1 },
|
|
|
|
|
{ color, color, color, color }
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-05-03 23:19:15 -07:00
|
|
|
|
|
|
|
|
// Draw each item in reverse order, since the messages are in reverse order
|
2025-06-16 20:09:56 -07:00
|
|
|
float y_offset = 0;
|
|
|
|
|
int container_width = get_width();
|
2025-06-18 16:50:14 -07:00
|
|
|
_visible_text_layouts.clear();
|
2025-05-03 23:19:15 -07:00
|
|
|
for (int i = _chat_items.size - 1; i >= 0; i--) {
|
|
|
|
|
var chat_item = _chat_items[i];
|
2025-05-03 22:41:51 -07:00
|
|
|
var item_width = chat_item.get_width();
|
|
|
|
|
var item_height = chat_item.get_height();
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
var origin = Graphene.Point() {
|
2025-05-03 22:41:51 -07:00
|
|
|
x = (chat_item.from_me ? (container_width - item_width - bubble_margin) : bubble_margin),
|
2025-04-30 19:12:00 -07:00
|
|
|
y = y_offset
|
2025-06-16 20:09:56 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var size = Graphene.Size() {
|
|
|
|
|
width = item_width,
|
|
|
|
|
height = item_height
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var rect = Graphene.Rect() {
|
|
|
|
|
origin = origin,
|
|
|
|
|
size = size
|
|
|
|
|
};
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
// Skip drawing if this item is not in the viewport
|
2025-06-17 00:47:03 -07:00
|
|
|
float height_offset = 0.0f;
|
2025-06-16 20:09:56 -07:00
|
|
|
if (viewport_rect.intersection(rect, null)) {
|
2025-06-18 18:07:59 -07:00
|
|
|
if (chat_item is BubbleLayout) {
|
|
|
|
|
_visible_text_layouts.add(VisibleLayout(chat_item as BubbleLayout, rect));
|
2025-06-18 16:50:14 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
snapshot.save();
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-06-17 00:47:03 -07:00
|
|
|
var pushed_opacity = false;
|
|
|
|
|
if (_animations.has_key(chat_item.id)) {
|
|
|
|
|
var animation = _animations[chat_item.id];
|
|
|
|
|
|
|
|
|
|
var item_height_offset = (float) (-item_height * (1.0 - animation.progress));
|
|
|
|
|
snapshot.translate(Graphene.Point() { x = 0, y = item_height_offset });
|
|
|
|
|
height_offset = item_height_offset;
|
|
|
|
|
|
|
|
|
|
snapshot.push_opacity(animation.progress);
|
|
|
|
|
pushed_opacity = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
// Translate to the correct position
|
|
|
|
|
snapshot.translate(origin);
|
|
|
|
|
|
|
|
|
|
// Flip the y-axis, since our parent is upside down (so newest messages are at the bottom)
|
|
|
|
|
snapshot.scale(1.0f, -1.0f);
|
|
|
|
|
|
|
|
|
|
// Undo the y-axis flip, origin is top left
|
|
|
|
|
snapshot.translate(Graphene.Point() { x = 0, y = -item_height });
|
|
|
|
|
|
|
|
|
|
chat_item.draw(snapshot);
|
2025-06-17 00:47:03 -07:00
|
|
|
|
|
|
|
|
if (pushed_opacity) {
|
|
|
|
|
snapshot.pop();
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 20:09:56 -07:00
|
|
|
snapshot.restore();
|
|
|
|
|
}
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-06-17 00:47:03 -07:00
|
|
|
y_offset += item_height + chat_item.vertical_padding + height_offset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animation_tick();
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
private VisibleLayout? get_visible_layout_at(double x, double y) {
|
|
|
|
|
var point = Graphene.Point() { x = (float)x, y = (float)y };
|
2025-06-18 16:50:14 -07:00
|
|
|
foreach (var layout in _visible_text_layouts) {
|
2025-06-18 18:07:59 -07:00
|
|
|
if (layout.rect.contains_point(point)) {
|
2025-06-18 16:50:14 -07:00
|
|
|
return layout;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
private VisibleLayout? get_text_bubble_at(double x, double y) {
|
|
|
|
|
var layout = get_visible_layout_at(x, y);
|
|
|
|
|
if (layout != null && layout.bubble is TextBubbleLayout) {
|
|
|
|
|
return layout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 15:32:37 -07:00
|
|
|
private void on_mouse_motion(double x, double y) {
|
2025-06-18 18:07:59 -07:00
|
|
|
VisibleLayout? hovered_text_bubble = get_text_bubble_at(x, y);
|
2025-06-18 16:50:14 -07:00
|
|
|
on_text_bubble_hover(hovered_text_bubble);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
private void on_click(uint button, int n_press) {
|
2025-06-18 16:50:14 -07:00
|
|
|
if (button == Gdk.BUTTON_SECONDARY) {
|
|
|
|
|
on_right_click();
|
|
|
|
|
} else if (button == Gdk.BUTTON_PRIMARY) {
|
|
|
|
|
on_left_click();
|
2025-06-18 18:07:59 -07:00
|
|
|
|
|
|
|
|
if (n_press == 2) {
|
|
|
|
|
on_double_click();
|
|
|
|
|
}
|
2025-06-18 16:50:14 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void on_left_click() {
|
|
|
|
|
Gdk.Rectangle? bounding_box = null;
|
|
|
|
|
if (_click_gesture.get_bounding_box(out bounding_box)) {
|
|
|
|
|
var text_bubble = get_text_bubble_at(bounding_box.x, bounding_box.y);
|
|
|
|
|
on_text_bubble_click(text_bubble);
|
|
|
|
|
}
|
2025-06-18 15:32:37 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
private void on_double_click() {
|
|
|
|
|
Gdk.Rectangle? bounding_box = null;
|
|
|
|
|
if (_click_gesture.get_bounding_box(out bounding_box)) {
|
|
|
|
|
var double_clicked_bubble = get_visible_layout_at(bounding_box.x, bounding_box.y);
|
|
|
|
|
if (double_clicked_bubble != null && double_clicked_bubble.bubble is ImageBubbleLayout) {
|
|
|
|
|
var image_bubble = double_clicked_bubble.bubble as ImageBubbleLayout;
|
|
|
|
|
on_image_bubble_activate(image_bubble.attachment_guid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 01:36:29 -07:00
|
|
|
private void on_right_click() {
|
|
|
|
|
var menu_model = new Menu();
|
|
|
|
|
menu_model.append("Copy", "transcript.copy");
|
|
|
|
|
|
|
|
|
|
Gdk.Rectangle? bounding_box = null;
|
|
|
|
|
if (_click_gesture.get_bounding_box(out bounding_box)) {
|
|
|
|
|
_click_bounding_box = bounding_box;
|
|
|
|
|
|
|
|
|
|
var menu = new PopoverMenu.from_model(menu_model);
|
|
|
|
|
menu.set_position(PositionType.TOP);
|
|
|
|
|
menu.pointing_to = bounding_box;
|
|
|
|
|
menu.set_parent(this);
|
|
|
|
|
menu.popup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void copy_message_at(Gdk.Rectangle bounding_box) {
|
|
|
|
|
double y_offset = 0.0;
|
|
|
|
|
for (int i = _chat_items.size - 1; i >= 0; i--) {
|
|
|
|
|
var chat_item = _chat_items[i];
|
|
|
|
|
y_offset += chat_item.get_height() + chat_item.vertical_padding;
|
|
|
|
|
|
|
|
|
|
if (y_offset > bounding_box.y) {
|
2025-06-18 01:49:33 -07:00
|
|
|
chat_item.copy(get_clipboard());
|
2025-06-18 01:36:29 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 00:47:03 -07:00
|
|
|
private bool animation_tick() {
|
|
|
|
|
HashSet<string> animations_to_remove = new HashSet<string>();
|
|
|
|
|
_animations.foreach(entry => {
|
|
|
|
|
var animation = entry.value;
|
|
|
|
|
animation.tick_animation();
|
|
|
|
|
|
|
|
|
|
if ((animation.progress - 1.0).abs() < 0.000001) {
|
|
|
|
|
animations_to_remove.add(entry.key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
foreach (var key in animations_to_remove) {
|
|
|
|
|
_animations.unset(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_animations.size > 0) {
|
|
|
|
|
queue_draw();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _animations.size > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool on_frame_clock_tick(Gtk.Widget widget, Gdk.FrameClock frame_clock) {
|
|
|
|
|
return animation_tick();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void start_animation(string id) {
|
|
|
|
|
if (_animations.has_key(id)) {
|
|
|
|
|
return;
|
2025-05-03 23:19:15 -07:00
|
|
|
}
|
2025-06-17 00:47:03 -07:00
|
|
|
|
|
|
|
|
var animation = new ChatItemAnimation(get_frame_clock());
|
|
|
|
|
animation.start_animation(0.18f);
|
|
|
|
|
_animations.set(id, animation);
|
|
|
|
|
|
|
|
|
|
_tick_callback_id = add_tick_callback(on_frame_clock_tick);
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void recompute_message_layouts() {
|
|
|
|
|
var container_width = get_width();
|
|
|
|
|
float max_width = container_width * 0.90f;
|
2025-06-17 00:47:03 -07:00
|
|
|
|
2025-05-03 22:41:51 -07:00
|
|
|
DateTime? last_date = null;
|
2025-05-03 23:19:15 -07:00
|
|
|
string? last_sender = null;
|
|
|
|
|
ArrayList<ChatItemLayout> items = new ArrayList<ChatItemLayout>();
|
|
|
|
|
_messages.foreach((message) => {
|
|
|
|
|
// Date Annotation
|
|
|
|
|
DateTime date = message.date;
|
2025-05-04 00:17:50 -07:00
|
|
|
if (last_date != null && date.difference(last_date) > (TimeSpan.HOUR * 1)) {
|
2025-05-03 23:19:15 -07:00
|
|
|
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;
|
2025-06-26 18:48:52 -07:00
|
|
|
last_sender = null;
|
2025-05-03 23:19:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-05-03 22:41:51 -07:00
|
|
|
}
|
2025-05-03 22:12:26 -07:00
|
|
|
|
2025-05-03 23:19:15 -07:00
|
|
|
// Text Bubble
|
2025-06-17 00:47:03 -07:00
|
|
|
var animate = message.should_animate;
|
2025-06-12 20:35:56 -07:00
|
|
|
if (message.text.length > 0 && !message.is_attachment_marker) {
|
|
|
|
|
var text_bubble = new TextBubbleLayout(message, this, max_width);
|
2025-06-17 00:47:03 -07:00
|
|
|
text_bubble.id = @"text-$(message.guid)";
|
|
|
|
|
if (animate) {
|
|
|
|
|
start_animation(text_bubble.id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-14 00:14:58 -07:00
|
|
|
text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f;
|
2025-06-12 20:35:56 -07:00
|
|
|
items.add(text_bubble);
|
|
|
|
|
}
|
2025-05-03 23:19:15 -07:00
|
|
|
|
2025-06-06 20:03:02 -07:00
|
|
|
// Check for attachments. For each one, add an image layout bubble
|
|
|
|
|
foreach (var attachment in message.attachments) {
|
2025-06-12 17:54:09 -07:00
|
|
|
Graphene.Size? image_size = null;
|
2025-06-06 20:03:02 -07:00
|
|
|
if (attachment.metadata != null) {
|
2025-06-12 17:54:09 -07:00
|
|
|
image_size = Graphene.Size() {
|
2025-06-06 20:03:02 -07:00
|
|
|
width = attachment.metadata.attribution_info.width,
|
|
|
|
|
height = attachment.metadata.attribution_info.height
|
2025-06-12 17:54:09 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size);
|
2025-06-17 00:47:03 -07:00
|
|
|
image_layout.id = @"image-$(attachment.guid)";
|
2025-06-18 18:07:59 -07:00
|
|
|
image_layout.attachment_guid = attachment.guid;
|
|
|
|
|
|
2025-06-17 00:47:03 -07:00
|
|
|
if (animate) {
|
|
|
|
|
start_animation(image_layout.id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 17:54:09 -07:00
|
|
|
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);
|
2025-06-06 20:03:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-03 23:19:15 -07:00
|
|
|
last_sender = message.sender;
|
|
|
|
|
last_date = date;
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-05-03 22:12:26 -07:00
|
|
|
return true;
|
|
|
|
|
});
|
2025-05-03 23:19:15 -07:00
|
|
|
|
|
|
|
|
_chat_items.clear();
|
|
|
|
|
_chat_items.add_all(items);
|
2025-05-03 22:12:26 -07:00
|
|
|
|
2025-04-30 19:12:00 -07:00
|
|
|
queue_draw();
|
2025-04-30 19:50:36 -07:00
|
|
|
queue_resize();
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
2025-05-04 00:17:50 -07:00
|
|
|
}
|
2025-06-17 00:47:03 -07:00
|
|
|
|
|
|
|
|
private class ChatItemAnimation
|
|
|
|
|
{
|
|
|
|
|
public double progress = 0.0;
|
|
|
|
|
private int64 start_time = 0;
|
|
|
|
|
private double duration = 0.0;
|
|
|
|
|
private Gdk.FrameClock frame_clock = null;
|
|
|
|
|
|
2025-06-17 00:53:37 -07:00
|
|
|
delegate double EaseFunction(double t);
|
|
|
|
|
private EaseFunction ease_function = ease_out_quart;
|
|
|
|
|
|
2025-06-17 00:47:03 -07:00
|
|
|
public ChatItemAnimation(Gdk.FrameClock frame_clock) {
|
|
|
|
|
this.frame_clock = frame_clock;
|
|
|
|
|
progress = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void start_animation(double duration) {
|
|
|
|
|
this.duration = duration;
|
|
|
|
|
this.progress = 0.0;
|
|
|
|
|
this.start_time = frame_clock.get_frame_time();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void tick_animation() {
|
2025-06-17 00:53:37 -07:00
|
|
|
double t = (frame_clock.get_frame_time() - start_time) / (duration * 1000000.0);
|
|
|
|
|
t = double.min(1.0, double.max(0.0, t));
|
|
|
|
|
progress = ease_function(t);
|
2025-06-17 00:47:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double ease_out_quart(double t) {
|
|
|
|
|
return 1.0 - Math.pow(1.0 - t, 4);
|
|
|
|
|
}
|
2025-06-18 16:50:14 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
public struct VisibleLayout {
|
|
|
|
|
public weak BubbleLayout bubble;
|
2025-06-18 16:50:14 -07:00
|
|
|
public Graphene.Rect rect;
|
|
|
|
|
|
2025-06-18 18:07:59 -07:00
|
|
|
public VisibleLayout(BubbleLayout bubble, Graphene.Rect rect) {
|
|
|
|
|
this.bubble = bubble;
|
2025-06-18 16:50:14 -07:00
|
|
|
this.rect = rect;
|
|
|
|
|
}
|
2025-06-17 00:47:03 -07:00
|
|
|
}
|