Skip to content

Commit 1c79c96

Browse files
committed
refactor(chat-history-model): Use sections as day dividers
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.
1 parent bd2b8cc commit 1c79c96

File tree

6 files changed

+133
-284
lines changed

6 files changed

+133
-284
lines changed

src/model/chat_history_item.rs

Lines changed: 0 additions & 92 deletions
This file was deleted.

src/model/chat_history_model.rs

Lines changed: 67 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use std::cell::Cell;
22
use std::cell::RefCell;
3-
use std::cmp::Ordering;
43
use std::collections::VecDeque;
54

65
use gio::prelude::*;
7-
use gio::subclass::prelude::*;
86
use glib::clone;
97
use gtk::gio;
108
use gtk::glib;
9+
use gtk::subclass::prelude::*;
1110
use once_cell::sync::Lazy;
1211
use thiserror::Error;
1312

@@ -28,14 +27,14 @@ mod imp {
2827
pub(crate) struct ChatHistoryModel {
2928
pub(super) chat: glib::WeakRef<model::Chat>,
3029
pub(super) is_loading: Cell<bool>,
31-
pub(super) list: RefCell<VecDeque<model::ChatHistoryItem>>,
30+
pub(super) list: RefCell<VecDeque<model::Message>>,
3231
}
3332

3433
#[glib::object_subclass]
3534
impl ObjectSubclass for ChatHistoryModel {
3635
const NAME: &'static str = "ChatHistoryModel";
3736
type Type = super::ChatHistoryModel;
38-
type Interfaces = (gio::ListModel,);
37+
type Interfaces = (gio::ListModel, gtk::SectionModel);
3938
}
4039

4140
impl ObjectImpl for ChatHistoryModel {
@@ -60,7 +59,7 @@ mod imp {
6059

6160
impl ListModelImpl for ChatHistoryModel {
6261
fn item_type(&self) -> glib::Type {
63-
model::ChatHistoryItem::static_type()
62+
model::Message::static_type()
6463
}
6564

6665
fn n_items(&self) -> u32 {
@@ -75,6 +74,26 @@ mod imp {
7574
.cloned()
7675
}
7776
}
77+
78+
impl SectionModelImpl for ChatHistoryModel {
79+
fn section(&self, position: u32) -> (u32, u32) {
80+
let list = &*self.list.borrow();
81+
let message = list.get(position as usize).unwrap();
82+
83+
let ymd = glib::DateTime::from_unix_local(message.date() as i64)
84+
.unwrap()
85+
.ymd();
86+
87+
(
88+
if position == 0 {
89+
0
90+
} else {
91+
find_section_border(list, ymd, position as i32 - 1, true)
92+
},
93+
find_section_border(list, ymd, position as i32 + 1, false),
94+
)
95+
}
96+
}
7897
}
7998

8099
glib::wrapper! {
@@ -111,9 +130,7 @@ impl ChatHistoryModel {
111130
let oldest_message_id = imp
112131
.list
113132
.borrow()
114-
.iter()
115-
.rev()
116-
.find_map(|item| item.message())
133+
.back()
117134
.map(|m| m.id())
118135
.unwrap_or_default();
119136

@@ -133,170 +150,73 @@ impl ChatHistoryModel {
133150
Ok(true)
134151
}
135152

136-
fn items_changed(&self, position: u32, removed: u32, added: u32) {
137-
let imp = self.imp();
138-
139-
// Insert day dividers where needed
140-
let added = {
141-
let position = position as usize;
142-
let added = added as usize;
143-
144-
let mut list = imp.list.borrow_mut();
145-
let mut previous_timestamp = if position + 1 < list.len() {
146-
list.get(position + 1)
147-
.and_then(|item| item.message_timestamp())
148-
} else {
149-
None
150-
};
151-
let mut dividers: Vec<(usize, model::ChatHistoryItem)> = vec![];
152-
153-
for (index, current) in list.range(position..position + added).enumerate().rev() {
154-
if let Some(current_timestamp) = current.message_timestamp() {
155-
if Some(current_timestamp.ymd()) != previous_timestamp.as_ref().map(|t| t.ymd())
156-
{
157-
let divider_pos = position + index + 1;
158-
dividers.push((
159-
divider_pos,
160-
model::ChatHistoryItem::for_day_divider(current_timestamp.clone()),
161-
));
162-
previous_timestamp = Some(current_timestamp);
163-
}
164-
}
165-
}
166-
167-
let dividers_len = dividers.len();
168-
for (position, item) in dividers {
169-
list.insert(position, item);
170-
}
171-
172-
(added + dividers_len) as u32
173-
};
174-
175-
// Check and remove no more needed day divider after removing messages
176-
let removed = {
177-
let mut removed = removed as usize;
178-
179-
if removed > 0 {
180-
let mut list = imp.list.borrow_mut();
181-
let position = position as usize;
182-
let item_before_removed = list.get(position);
183-
184-
if let Some(model::ChatHistoryItemType::DayDivider(_)) =
185-
item_before_removed.map(|i| i.type_())
186-
{
187-
let item_after_removed = if position > 0 {
188-
list.get(position - 1)
189-
} else {
190-
None
191-
};
192-
193-
match item_after_removed.map(|item| item.type_()) {
194-
None | Some(model::ChatHistoryItemType::DayDivider(_)) => {
195-
list.remove(position + removed);
196-
197-
removed += 1;
198-
}
199-
_ => {}
200-
}
201-
}
202-
}
203-
204-
removed as u32
205-
};
206-
207-
// Check and remove no more needed day divider after adding messages
208-
let (position, removed) = {
209-
let mut removed = removed;
210-
let mut position = position as usize;
211-
212-
if added > 0 && position > 0 {
213-
let mut list = imp.list.borrow_mut();
214-
let last_added_timestamp = list.get(position).unwrap().message_timestamp().unwrap();
215-
let next_item = list.get(position - 1);
216-
217-
if let Some(model::ChatHistoryItemType::DayDivider(date)) =
218-
next_item.map(|item| item.type_())
219-
{
220-
if date.ymd() == last_added_timestamp.ymd() {
221-
list.remove(position - 1);
222-
223-
removed += 1;
224-
position -= 1;
225-
}
226-
}
227-
}
228-
229-
(position as u32, removed)
230-
};
231-
232-
self.upcast_ref::<gio::ListModel>()
233-
.items_changed(position, removed, added);
234-
}
235-
236153
fn push_front(&self, message: model::Message) {
237-
self.imp()
238-
.list
239-
.borrow_mut()
240-
.push_front(model::ChatHistoryItem::for_message(message));
154+
self.imp().list.borrow_mut().push_front(message);
241155

242156
self.items_changed(0, 0, 1);
243157
}
244158

245159
fn append(&self, messages: Vec<model::Message>) {
246160
let imp = self.imp();
161+
247162
let added = messages.len();
248163

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

251166
for message in messages {
252-
imp.list
253-
.borrow_mut()
254-
.push_back(model::ChatHistoryItem::for_message(message));
167+
imp.list.borrow_mut().push_back(message);
255168
}
256169

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

261174
fn remove(&self, message: model::Message) {
262-
let imp = self.imp();
263-
264-
// Put this in a block, so that we only need to borrow the list once and the runtime
265-
// borrow checker does not panic in Self::items_changed when it borrows the list again.
266-
let index = {
267-
let mut list = imp.list.borrow_mut();
268-
269-
// The elements in this list are ordered. While the day dividers are ordered
270-
// only by their date time, the messages are additionally sorted by their id. We
271-
// can exploit this by applying a binary search.
272-
let index = list
273-
.binary_search_by(|m| match m.type_() {
274-
model::ChatHistoryItemType::Message(other_message) => {
275-
message.id().cmp(&other_message.id())
276-
}
277-
model::ChatHistoryItemType::DayDivider(date_time) => {
278-
let ordering = glib::DateTime::from_unix_utc(message.date() as i64)
279-
.unwrap()
280-
.cmp(date_time);
281-
if let Ordering::Equal = ordering {
282-
// We found the day divider of the message. Therefore, the message
283-
// must be among the following elements.
284-
Ordering::Greater
285-
} else {
286-
ordering
287-
}
288-
}
289-
})
290-
.unwrap();
175+
let mut list = self.imp().list.borrow_mut();
291176

177+
if let Ok(index) = list.binary_search_by(|m| message.id().cmp(&m.id())) {
292178
list.remove(index);
293-
index as u32
294-
};
295179

296-
self.items_changed(index, 1, 0);
180+
drop(list);
181+
self.items_changed(index as u32, 1, 0);
182+
}
297183
}
298184

299185
pub(crate) fn chat(&self) -> model::Chat {
300186
self.imp().chat.upgrade().unwrap()
301187
}
302188
}
189+
190+
fn find_section_border(
191+
messages: &VecDeque<model::Message>,
192+
ymd: (i32, i32, i32),
193+
position: i32,
194+
backwards: bool,
195+
) -> u32 {
196+
if position < 0 {
197+
return position as u32;
198+
}
199+
200+
match messages.get(position as usize) {
201+
None => position as u32,
202+
Some(message) => {
203+
let date_time = glib::DateTime::from_unix_local(message.date() as i64).unwrap();
204+
if date_time.ymd() == ymd {
205+
find_section_border(
206+
messages,
207+
ymd,
208+
if backwards {
209+
position - 1
210+
} else {
211+
position + 1
212+
},
213+
backwards,
214+
)
215+
} else if backwards {
216+
position as u32 + 1
217+
} else {
218+
position as u32
219+
}
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)