Adds incoming bubble animations
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -14,7 +15,7 @@ class DateItemLayout : Object, ChatItemLayout {
|
|||||||
layout.set_font_description(Pango.FontDescription.from_string("Sans 9"));
|
layout.set_font_description(Pango.FontDescription.from_string("Sans 9"));
|
||||||
layout.set_alignment(Pango.Alignment.CENTER);
|
layout.set_alignment(Pango.Alignment.CENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float get_height() {
|
public float get_height() {
|
||||||
Pango.Rectangle ink_rect, logical_rect;
|
Pango.Rectangle ink_rect, logical_rect;
|
||||||
layout.get_pixel_extents(out ink_rect, out logical_rect);
|
layout.get_pixel_extents(out ink_rect, out logical_rect);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,17 +133,64 @@ 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() {
|
||||||
var container_width = get_width();
|
var container_width = get_width();
|
||||||
float max_width = container_width * 0.90f;
|
float max_width = container_width * 0.90f;
|
||||||
|
|
||||||
DateTime? last_date = null;
|
DateTime? last_date = null;
|
||||||
string? last_sender = null;
|
string? last_sender = null;
|
||||||
ArrayList<ChatItemLayout> items = new ArrayList<ChatItemLayout>();
|
ArrayList<ChatItemLayout> items = new ArrayList<ChatItemLayout>();
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user