Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chat-history: Add basic message grouping #475

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ gettext-rs = { version = "0.7", features = ["gettext-system"] }
gtk = { version = "0.6", package = "gtk4", features = ["v4_8", "blueprint"] }
image = { version = "0.24", default-features = false, features = ["webp"] }
indexmap = "1.9"
itertools = "0.10.5"
locale_config = "0.3"
log = "0.4"
once_cell = "1.17"
Expand Down
79 changes: 75 additions & 4 deletions src/session/content/chat_history_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,25 @@ pub(crate) enum ChatHistoryItemType {
DayDivider(DateTime),
}

#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, glib::Enum)]
#[enum_type(name = "MessageStyle")]
pub(crate) enum MessageStyle {
#[default]
Single,
First,
Last,
Center,
}

mod imp {
use super::*;
use once_cell::sync::{Lazy, OnceCell};
use std::cell::Cell;

#[derive(Debug, Default)]
pub(crate) struct ChatHistoryItem {
pub(super) type_: OnceCell<ChatHistoryItemType>,
pub(super) style: Cell<MessageStyle>,
}

#[glib::object_subclass]
Expand All @@ -30,20 +42,33 @@ mod imp {
impl ObjectImpl for ChatHistoryItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecBoxed::builder::<ChatHistoryItemType>("type")
.write_only()
.construct_only()
.build()]
vec![
glib::ParamSpecBoxed::builder::<ChatHistoryItemType>("type")
.write_only()
.construct_only()
.build(),
glib::ParamSpecEnum::builder::<MessageStyle>("style").build(),
]
});
PROPERTIES.as_ref()
}

fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"style" => self.style.get().into(),
_ => unimplemented!(),
}
}

fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"type" => {
let type_ = value.get::<ChatHistoryItemType>().unwrap();
self.type_.set(type_).unwrap();
}
"style" => {
self.style.set(value.get().unwrap());
}
_ => unimplemented!(),
}
}
Expand All @@ -69,6 +94,52 @@ impl ChatHistoryItem {
self.imp().type_.get().unwrap()
}

pub(crate) fn style(&self) -> MessageStyle {
self.imp().style.get()
}

pub(crate) fn set_style(&self, style: MessageStyle) {
self.imp().style.set(style);
}

pub(crate) fn is_groupable(&self) -> bool {
if let Some(message) = self.message() {
use tdlib::enums::MessageContent::*;
matches!(
message.content().0,
MessageText(_)
| MessageAnimation(_)
| MessageAudio(_)
| MessageDocument(_)
| MessagePhoto(_)
| MessageSticker(_)
| MessageVideo(_)
| MessageVideoNote(_)
| MessageVoiceNote(_)
| MessageLocation(_)
| MessageVenue(_)
| MessageContact(_)
| MessageAnimatedEmoji(_)
| MessageDice(_)
| MessageGame(_)
| MessagePoll(_)
| MessageInvoice(_)
| MessageCall(_)
| MessageUnsupported
)
} else {
false
}
}

pub(crate) fn group_key(&self) -> Option<(bool, i64)> {
if self.is_groupable() {
self.message().map(|m| (m.is_outgoing(), m.sender().id()))
} else {
None
}
}

pub(crate) fn message(&self) -> Option<&Message> {
if let ChatHistoryItemType::Message(message) = self.type_() {
Some(message)
Expand Down
65 changes: 64 additions & 1 deletion src/session/content/chat_history_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use glib::clone;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use itertools::Itertools;
use std::cmp::Ordering;
use thiserror::Error;

use crate::session::content::{ChatHistoryItem, ChatHistoryItemType};
use crate::session::content::{ChatHistoryItem, ChatHistoryItemType, MessageStyle};
use crate::tdlib::{Chat, Message};

#[derive(Error, Debug)]
Expand Down Expand Up @@ -228,6 +229,68 @@ impl ChatHistoryModel {
(position as u32, removed)
};

// Set message styles
{
let list = imp.list.borrow_mut();
let range = list.range((position as usize)..(position as usize + added as usize));
for (_, mut group) in &range.group_by(|item| item.group_key()) {
use MessageStyle::*;
if let Some(last) = group.next() {
if let Some(first) = group
.map(|m| {
m.set_style(Center);
m
})
.last()
{
last.set_style(Last);
first.set_style(First);
} else {
last.set_style(Single);
}
}
}

// Check corner cases
fn refresh_grouping_at_connection(first: &ChatHistoryItem, second: &ChatHistoryItem) {
if first.group_key() == second.group_key() {
use MessageStyle::*;

let new_style = match first.style() {
Single => First,
Last => Center,
other => other,
};

first.set_style(new_style);

let new_style = match second.style() {
Single => Last,
First => Center,
other => other,
};

second.set_style(new_style);
}
}

if position > 0 {
if let Some(after_last) = list.get(position as usize - 1) {
if let Some(last) = list.get(position as usize) {
refresh_grouping_at_connection(last, after_last);
}
}
}

if position + added + 1 < list.len() as u32 {
if let Some(before_first) = list.get((position + added) as usize + 1) {
if let Some(first) = list.get((position + added) as usize) {
refresh_grouping_at_connection(before_first, first);
}
}
}
}

self.upcast_ref::<gio::ListModel>()
.items_changed(position, removed, added);
}
Expand Down
28 changes: 27 additions & 1 deletion src/session/content/message_row/bubble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use gtk::{glib, CompositeTemplate};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use crate::session::content::message_row::{MessageIndicators, MessageLabel};
use crate::session::content::message_row::{MessageIndicators, MessageLabel, MessageRow};
use crate::session::content::MessageStyle;
use crate::tdlib::{Chat, ChatType, Message, MessageSender, SponsoredMessage};

const MAX_WIDTH: i32 = 400;
Expand Down Expand Up @@ -115,6 +116,20 @@ mod imp {
}

impl WidgetImpl for MessageBubble {
fn map(&self) {
self.parent_map();
if let Some(style) = self.style() {
use MessageStyle::*;
let label_is_visible = match style {
Single | First => true,
Last | Center => false,
};
if self.sender_color_class.borrow().is_some() {
self.sender_label.set_visible(label_is_visible);
}
}
}

fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
// Limit the widget width
if orientation == gtk::Orientation::Horizontal {
Expand All @@ -141,6 +156,17 @@ mod imp {
gtk::SizeRequestMode::HeightForWidth
}
}

impl MessageBubble {
fn style(&self) -> Option<MessageStyle> {
self.obj()
.parent()?
.parent()?
.downcast::<MessageRow>()
.ok()?
.message_style()
}
}
}

glib::wrapper! {
Expand Down
34 changes: 33 additions & 1 deletion src/session/content/message_row/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use gtk::{gio, glib, CompositeTemplate};
use tdlib::enums::{MessageContent, StickerFormat};

use crate::components::Avatar;
use crate::session::content::{ChatHistoryItem, ChatHistoryRow, MessageStyle};
use crate::tdlib::{Chat, ChatType, Message, MessageForwardOrigin, MessageSender};
use crate::utils::spawn;

Expand Down Expand Up @@ -124,7 +125,27 @@ mod imp {
}
}

impl WidgetImpl for MessageRow {}
impl WidgetImpl for MessageRow {
fn map(&self) {
self.parent_map();
let obj = self.obj();
if let Some(style) = obj.message_style() {
use MessageStyle::*;
let avatar_is_visible = match style {
Single | Last => true,
First | Center => false,
};
if let Some(avatar) = self.avatar.borrow().as_ref() {
avatar.set_visible(avatar_is_visible);
if !avatar_is_visible {
obj.set_margin_start(AVATAR_SIZE + 6);
} else {
obj.set_margin_start(0);
}
}
}
}
}
}

glib::wrapper! {
Expand Down Expand Up @@ -202,6 +223,17 @@ impl MessageRow {
self.imp().message.borrow().clone().unwrap()
}

pub(crate) fn message_style(&self) -> Option<MessageStyle> {
let item = self
.parent()?
.downcast::<ChatHistoryRow>()
.ok()?
.item()?
.downcast::<ChatHistoryItem>()
.ok()?;
Some(item.style())
}

pub(crate) fn set_message(&self, message: glib::Object) {
let imp = self.imp();

Expand Down
2 changes: 1 addition & 1 deletion src/session/content/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod send_photo_dialog;

use self::chat_action_bar::ChatActionBar;
use self::chat_history::ChatHistory;
use self::chat_history_item::{ChatHistoryItem, ChatHistoryItemType};
use self::chat_history_item::{ChatHistoryItem, ChatHistoryItemType, MessageStyle};
use self::chat_history_model::{ChatHistoryError, ChatHistoryModel};
use self::chat_history_row::ChatHistoryRow;
use self::chat_info_window::ChatInfoWindow;
Expand Down