Skip to content

Commit

Permalink
refactor(chat-history-model): Use sections as day dividers
Browse files Browse the repository at this point in the history
Using sections simplifies our code a lot. It still needs changes applied
to the reversed list patch, otherwise sections are shifted forward by
one position.
  • Loading branch information
marhkb committed Oct 3, 2023
1 parent bd2b8cc commit c56c78f
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 289 deletions.
92 changes: 0 additions & 92 deletions src/model/chat_history_item.rs

This file was deleted.

219 changes: 67 additions & 152 deletions src/model/chat_history_model.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::cell::Cell;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::VecDeque;

use gio::prelude::*;
use gio::subclass::prelude::*;
use glib::clone;
use gtk::gio;
use gtk::glib;
use gtk::subclass::prelude::*;
use once_cell::sync::Lazy;
use thiserror::Error;

Expand All @@ -28,14 +27,14 @@ mod imp {
pub(crate) struct ChatHistoryModel {
pub(super) chat: glib::WeakRef<model::Chat>,
pub(super) is_loading: Cell<bool>,
pub(super) list: RefCell<VecDeque<model::ChatHistoryItem>>,
pub(super) list: RefCell<VecDeque<model::Message>>,
}

#[glib::object_subclass]
impl ObjectSubclass for ChatHistoryModel {
const NAME: &'static str = "ChatHistoryModel";
type Type = super::ChatHistoryModel;
type Interfaces = (gio::ListModel,);
type Interfaces = (gio::ListModel, gtk::SectionModel);
}

impl ObjectImpl for ChatHistoryModel {
Expand All @@ -60,7 +59,7 @@ mod imp {

impl ListModelImpl for ChatHistoryModel {
fn item_type(&self) -> glib::Type {
model::ChatHistoryItem::static_type()
model::Message::static_type()
}

fn n_items(&self) -> u32 {
Expand All @@ -75,6 +74,26 @@ mod imp {
.cloned()
}
}

impl SectionModelImpl for ChatHistoryModel {
fn section(&self, position: u32) -> (u32, u32) {
let list = &*self.list.borrow();
let message = list.get(position as usize).unwrap();

let ymd = glib::DateTime::from_unix_local(message.date() as i64)
.unwrap()
.ymd();

(
if position == 0 {
0
} else {
find_section_border(list, ymd, position as i32 - 1, true)
},
find_section_border(list, ymd, position as i32 + 1, false),
)
}
}
}

glib::wrapper! {
Expand Down Expand Up @@ -108,14 +127,7 @@ impl ChatHistoryModel {
return Err(ChatHistoryError::AlreadyLoading);
}

let oldest_message_id = imp
.list
.borrow()
.iter()
.rev()
.find_map(|item| item.message())
.map(|m| m.id())
.unwrap_or_default();
let oldest_message_id = imp.list.borrow().back().map(|m| m.id()).unwrap_or_default();

imp.is_loading.set(true);

Expand All @@ -133,170 +145,73 @@ impl ChatHistoryModel {
Ok(true)
}

fn items_changed(&self, position: u32, removed: u32, added: u32) {
let imp = self.imp();

// Insert day dividers where needed
let added = {
let position = position as usize;
let added = added as usize;

let mut list = imp.list.borrow_mut();
let mut previous_timestamp = if position + 1 < list.len() {
list.get(position + 1)
.and_then(|item| item.message_timestamp())
} else {
None
};
let mut dividers: Vec<(usize, model::ChatHistoryItem)> = vec![];

for (index, current) in list.range(position..position + added).enumerate().rev() {
if let Some(current_timestamp) = current.message_timestamp() {
if Some(current_timestamp.ymd()) != previous_timestamp.as_ref().map(|t| t.ymd())
{
let divider_pos = position + index + 1;
dividers.push((
divider_pos,
model::ChatHistoryItem::for_day_divider(current_timestamp.clone()),
));
previous_timestamp = Some(current_timestamp);
}
}
}

let dividers_len = dividers.len();
for (position, item) in dividers {
list.insert(position, item);
}

(added + dividers_len) as u32
};

// Check and remove no more needed day divider after removing messages
let removed = {
let mut removed = removed as usize;

if removed > 0 {
let mut list = imp.list.borrow_mut();
let position = position as usize;
let item_before_removed = list.get(position);

if let Some(model::ChatHistoryItemType::DayDivider(_)) =
item_before_removed.map(|i| i.type_())
{
let item_after_removed = if position > 0 {
list.get(position - 1)
} else {
None
};

match item_after_removed.map(|item| item.type_()) {
None | Some(model::ChatHistoryItemType::DayDivider(_)) => {
list.remove(position + removed);

removed += 1;
}
_ => {}
}
}
}

removed as u32
};

// Check and remove no more needed day divider after adding messages
let (position, removed) = {
let mut removed = removed;
let mut position = position as usize;

if added > 0 && position > 0 {
let mut list = imp.list.borrow_mut();
let last_added_timestamp = list.get(position).unwrap().message_timestamp().unwrap();
let next_item = list.get(position - 1);

if let Some(model::ChatHistoryItemType::DayDivider(date)) =
next_item.map(|item| item.type_())
{
if date.ymd() == last_added_timestamp.ymd() {
list.remove(position - 1);

removed += 1;
position -= 1;
}
}
}

(position as u32, removed)
};

self.upcast_ref::<gio::ListModel>()
.items_changed(position, removed, added);
}

fn push_front(&self, message: model::Message) {
self.imp()
.list
.borrow_mut()
.push_front(model::ChatHistoryItem::for_message(message));
self.imp().list.borrow_mut().push_front(message);

self.items_changed(0, 0, 1);
}

fn append(&self, messages: Vec<model::Message>) {
let imp = self.imp();

let added = messages.len();

imp.list.borrow_mut().reserve(added);

for message in messages {
imp.list
.borrow_mut()
.push_back(model::ChatHistoryItem::for_message(message));
imp.list.borrow_mut().push_back(message);
}

let index = imp.list.borrow().len() - added;
self.items_changed(index as u32, 0, added as u32);
}

fn remove(&self, message: model::Message) {
let imp = self.imp();

// Put this in a block, so that we only need to borrow the list once and the runtime
// borrow checker does not panic in Self::items_changed when it borrows the list again.
let index = {
let mut list = imp.list.borrow_mut();

// The elements in this list are ordered. While the day dividers are ordered
// only by their date time, the messages are additionally sorted by their id. We
// can exploit this by applying a binary search.
let index = list
.binary_search_by(|m| match m.type_() {
model::ChatHistoryItemType::Message(other_message) => {
message.id().cmp(&other_message.id())
}
model::ChatHistoryItemType::DayDivider(date_time) => {
let ordering = glib::DateTime::from_unix_utc(message.date() as i64)
.unwrap()
.cmp(date_time);
if let Ordering::Equal = ordering {
// We found the day divider of the message. Therefore, the message
// must be among the following elements.
Ordering::Greater
} else {
ordering
}
}
})
.unwrap();
let mut list = self.imp().list.borrow_mut();

if let Ok(index) = list.binary_search_by(|m| message.id().cmp(&m.id())) {
list.remove(index);
index as u32
};

self.items_changed(index, 1, 0);
drop(list);
self.items_changed(index as u32, 1, 0);
}
}

pub(crate) fn chat(&self) -> model::Chat {
self.imp().chat.upgrade().unwrap()
}
}

fn find_section_border(
messages: &VecDeque<model::Message>,
ymd: (i32, i32, i32),
position: i32,
backwards: bool,
) -> u32 {
if position < 0 {
return position as u32;
}

match messages.get(position as usize) {
None => position as u32,
Some(message) => {
let date_time = glib::DateTime::from_unix_local(message.date() as i64).unwrap();
if date_time.ymd() == ymd {
find_section_border(
messages,
ymd,
if backwards {
position - 1
} else {
position + 1
},
backwards,
)
} else if backwards {
position as u32 + 1
} else {
position as u32
}
}
}
}
Loading

0 comments on commit c56c78f

Please sign in to comment.