Private
Public Access
1
0

Adds incoming bubble animations

This commit is contained in:
2025-06-17 00:47:03 -07:00
parent c70ae00d5b
commit 54ca001892
8 changed files with 141 additions and 6 deletions

View File

@@ -51,5 +51,6 @@ executable('kordophone',
resources, resources,
dependencies : dependencies, dependencies : dependencies,
vala_args: ['--pkg', 'posix'], vala_args: ['--pkg', 'posix'],
link_args: ['-lm'],
install : true install : true
) )

View File

@@ -1,6 +1,7 @@
using GLib; using GLib;
using Gee;
public class Message : Object public class Message : Object, Comparable<Message>, Hashable<Message>
{ {
public string guid { get; set; default = ""; } public string guid { get; set; default = ""; }
public string text { get; set; default = ""; } public string text { get; set; default = ""; }
@@ -9,6 +10,8 @@ public class Message : Object
public Attachment[] attachments { get; set; default = {}; } public Attachment[] attachments { get; set; default = {}; }
public bool should_animate = false;
public bool from_me { public bool from_me {
get { get {
// Hm, this may have been accidental. // Hm, this may have been accidental.
@@ -52,4 +55,20 @@ public class Message : Object
this.attachments = attachments.to_array(); this.attachments = attachments.to_array();
} }
public int compare_to(Message other) {
if (guid == other.guid) {
return 0;
}
return 1;
}
public bool equal_to(Message other) {
return guid == other.guid;
}
public uint hash() {
return guid.hash();
}
} }

View File

@@ -27,6 +27,7 @@ private abstract class BubbleLayout : Object, ChatItemLayout
{ {
public bool from_me { get; set; } public bool from_me { get; set; }
public float vertical_padding { get; set; } public float vertical_padding { get; set; }
public string id { get; set; }
protected float max_width; protected float max_width;
protected Widget parent; protected Widget parent;

View File

@@ -4,6 +4,7 @@ interface ChatItemLayout : Object
{ {
public abstract bool from_me { get; set; } public abstract bool from_me { get; set; }
public abstract float vertical_padding { get; set; } public abstract float vertical_padding { get; set; }
public abstract string id { get; set; }
public abstract float get_height(); public abstract float get_height();
public abstract float get_width(); public abstract float get_width();

View File

@@ -3,6 +3,7 @@ using Gtk;
class DateItemLayout : Object, ChatItemLayout { class DateItemLayout : Object, ChatItemLayout {
public bool from_me { get; set; } public bool from_me { get; set; }
public float vertical_padding { get; set; } public float vertical_padding { get; set; }
public string id { get; set; }
private Pango.Layout layout; private Pango.Layout layout;
private float max_width; private float max_width;

View File

@@ -6,6 +6,7 @@ private class SenderAnnotationLayout : Object, ChatItemLayout
public float max_width; public float max_width;
public bool from_me { get; set; default = false; } public bool from_me { get; set; default = false; }
public float vertical_padding { get; set; default = 0.0f; } public float vertical_padding { get; set; default = 0.0f; }
public string id { get; set; }
private Pango.Layout layout; private Pango.Layout layout;
private BubbleLayoutConstants constants; private BubbleLayoutConstants constants;

View File

@@ -63,7 +63,12 @@ public class MessageListModel : Object, ListModel
} }
public void load_messages() { public void load_messages() {
var previous_messages = new HashSet<Message>();
previous_messages.add_all(_messages);
try { try {
bool first_load = _messages.size == 0;
Message[] messages = Repository.get_instance().get_messages(conversation_guid); Message[] messages = Repository.get_instance().get_messages(conversation_guid);
// Clear existing set // Clear existing set
@@ -81,9 +86,14 @@ public class MessageListModel : Object, ListModel
for (int i = 0; i < messages.length; i++) { for (int i = 0; i < messages.length; i++) {
var message = messages[i]; var message = messages[i];
_messages.add(message);
participants.add(message.sender); participants.add(message.sender);
if (!first_load && !previous_messages.contains(message)) {
// This is a new message according to the UI, schedule an animation for it.
message.should_animate = true;
}
_messages.add(message);
position++; position++;
} }

View File

@@ -1,5 +1,6 @@
using Gtk; using Gtk;
using Gee; using Gee;
using Gdk;
private class TranscriptDrawingArea : Widget private class TranscriptDrawingArea : Widget
{ {
@@ -22,6 +23,8 @@ private class TranscriptDrawingArea : Widget
private const float bubble_margin = 18.0f; private const float bubble_margin = 18.0f;
private const bool debug_viewport = false; private const bool debug_viewport = false;
private uint? _tick_callback_id = null;
private HashMap<string, ChatItemAnimation> _animations = new HashMap<string, ChatItemAnimation>();
public TranscriptDrawingArea() { public TranscriptDrawingArea() {
add_css_class("transcript-drawing-area"); add_css_class("transcript-drawing-area");
@@ -63,7 +66,7 @@ private class TranscriptDrawingArea : Widget
recompute_message_layouts(); recompute_message_layouts();
} }
public override void snapshot(Snapshot snapshot) { public override void snapshot(Gtk.Snapshot snapshot) {
const int viewport_y_padding = 50; const int viewport_y_padding = 50;
var viewport_rect = Graphene.Rect() { var viewport_rect = Graphene.Rect() {
origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) }, origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) },
@@ -104,9 +107,22 @@ private class TranscriptDrawingArea : Widget
}; };
// Skip drawing if this item is not in the viewport // Skip drawing if this item is not in the viewport
float height_offset = 0.0f;
if (viewport_rect.intersection(rect, null)) { if (viewport_rect.intersection(rect, null)) {
snapshot.save(); snapshot.save();
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;
}
// Translate to the correct position // Translate to the correct position
snapshot.translate(origin); snapshot.translate(origin);
@@ -117,11 +133,58 @@ private class TranscriptDrawingArea : Widget
snapshot.translate(Graphene.Point() { x = 0, y = -item_height }); snapshot.translate(Graphene.Point() { x = 0, y = -item_height });
chat_item.draw(snapshot); chat_item.draw(snapshot);
if (pushed_opacity) {
snapshot.pop();
}
snapshot.restore(); snapshot.restore();
} }
y_offset += item_height + chat_item.vertical_padding; y_offset += item_height + chat_item.vertical_padding + height_offset;
} }
animation_tick();
}
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;
}
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);
} }
private void recompute_message_layouts() { private void recompute_message_layouts() {
@@ -147,8 +210,14 @@ private class TranscriptDrawingArea : Widget
} }
// Text Bubble // Text Bubble
var animate = message.should_animate;
if (message.text.length > 0 && !message.is_attachment_marker) { if (message.text.length > 0 && !message.is_attachment_marker) {
var text_bubble = new TextBubbleLayout(message, this, max_width); var text_bubble = new TextBubbleLayout(message, this, max_width);
text_bubble.id = @"text-$(message.guid)";
if (animate) {
start_animation(text_bubble.id);
}
text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f; text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f;
items.add(text_bubble); items.add(text_bubble);
} }
@@ -164,6 +233,11 @@ private class TranscriptDrawingArea : Widget
} }
var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size);
image_layout.id = @"image-$(attachment.guid)";
if (animate) {
start_animation(image_layout.id);
}
image_layout.is_downloaded = attachment.preview_downloaded; image_layout.is_downloaded = attachment.preview_downloaded;
items.add(image_layout); items.add(image_layout);
@@ -191,3 +265,30 @@ private class TranscriptDrawingArea : Widget
queue_resize(); queue_resize();
} }
} }
private class ChatItemAnimation
{
public double progress = 0.0;
private int64 start_time = 0;
private double duration = 0.0;
private Gdk.FrameClock frame_clock = null;
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() {
progress = ease_out_quart((frame_clock.get_frame_time() - start_time) / (duration * 1000000.0));
}
private static double ease_out_quart(double t) {
return 1.0 - Math.pow(1.0 - t, 4);
}
}