Просмотр исходного кода

wallet: chatview greatly simplify message buffer logic

darkfi 1 год назад
Родитель
Сommit
16b1bff413

+ 5 - 5
bin/darkwallet/insert_line.py

@@ -1,6 +1,6 @@
 #!/usr/bin/python
 from gui import *
-import time
+import os, time
 
 def send(timest, nick, msg):
     print(timest, nick, msg)
@@ -8,15 +8,15 @@ def send(timest, nick, msg):
 
     arg_data = bytearray()
     serial.write_u64(arg_data, timest)
-    arg_data += bytes(32)
+    arg_data += os.urandom(32)
     serial.encode_str(arg_data, nick)
     serial.encode_str(arg_data, msg)
 
     api.call_method(node_id, "insert_line", arg_data)
 
-for i in range(27):
+for i in range(200):
     name = f"bob-{i}"
-    send(1722944640000 + i*60000, "hhi12", f"hello {name}")
+    send(1732944640000 + i*60000, "hhi12", f"hello {name}")
     #time.sleep(0.4)
-    input("> ")
+    #input("> ")
 

+ 2 - 1
bin/darkwallet/src/app.rs

@@ -20,6 +20,7 @@ use async_recursion::async_recursion;
 use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone};
 use darkfi_serial::Encodable;
 use futures::{stream::FuturesUnordered, StreamExt};
+use sled_overlay::sled;
 use smol::Task;
 use std::{
     sync::{Arc, Mutex as SyncMutex},
@@ -721,7 +722,7 @@ impl App {
         node.set_property_f32(Role::App, "line_height", 30.).unwrap();
         node.set_property_f32(Role::App, "baseline", 20.).unwrap();
         node.set_property_u32(Role::App, "z_index", 1).unwrap();
-        node.set_property_bool(Role::App, "debug", true).unwrap();
+        //node.set_property_bool(Role::App, "debug", true).unwrap();
 
         let prop = node.get_property("timestamp_color").unwrap();
         prop.set_f32(Role::App, 0, 0.5).unwrap();

+ 1 - 0
bin/darkwallet/src/darkirc.rs

@@ -31,6 +31,7 @@ use darkfi::{
 use darkfi_serial::{
     async_trait, deserialize_async, serialize_async, Encodable, SerialDecodable, SerialEncodable,
 };
+use sled_overlay::sled;
 
 use crate::{scene::SceneGraphPtr2, ExecutorPtr};
 

+ 1 - 1
bin/darkwallet/src/text/mod.rs

@@ -40,7 +40,7 @@ pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
 //pub use old_atlas::{make_texture_atlas, RenderedAtlas};
 
 mod wrap;
-pub use wrap::wrap;
+pub use wrap::{glyph_str, wrap};
 
 // From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
 //

+ 38 - 84
bin/darkwallet/src/ui/chatview/mod.rs

@@ -25,6 +25,7 @@ use darkfi_serial::{
 };
 use miniquad::{KeyCode, KeyMods, TouchPhase};
 use rand::{rngs::OsRng, Rng};
+use sled_overlay::sled;
 use std::{
     hash::{DefaultHasher, Hash, Hasher},
     io::Cursor,
@@ -32,7 +33,7 @@ use std::{
 };
 
 mod page;
-use page::{FreedData, Message, Page, PageManager};
+use page::{FreedData, MessageBuffer};
 
 use crate::{
     gfx::{
@@ -107,7 +108,7 @@ pub struct ChatView {
     text_shaper: TextShaperPtr,
     tree: sled::Tree,
 
-    pages: AsyncMutex<page::PageManager>,
+    msgbuf: AsyncMutex<MessageBuffer>,
     dc_key: u64,
 
     /// Used for detecting when scrolling view
@@ -248,7 +249,7 @@ impl ChatView {
                 text_shaper: text_shaper.clone(),
                 tree,
 
-                pages: AsyncMutex::new(page::PageManager::new(render_api, text_shaper)),
+                msgbuf: AsyncMutex::new(MessageBuffer::new(render_api, text_shaper)),
                 dc_key: OsRng.gen(),
 
                 mouse_pos: SyncMutex::new(Point::from([0., 0.])),
@@ -276,10 +277,6 @@ impl ChatView {
                 parent_rect: SyncMutex::new(None),
             }
         });
-
-        let timer = std::time::Instant::now();
-        debug!(target: "ui::chatview", "populate() took {:?}", timer.elapsed());
-
         Pimpl::ChatView(self_)
     }
 
@@ -407,10 +404,6 @@ impl ChatView {
             return
         }
 
-        //debug!(target: "ui::chatview", "inside rect");
-        //let scroll = self.scroll.get() + wheel_y * 50.;
-        //self.scrollview(scroll).await;
-
         self.accel.fetch_add(wheel_y * self.mouse_scroll_start_accel.get(), Ordering::Relaxed);
         self.motion_cv.notify();
     }
@@ -520,10 +513,10 @@ impl ChatView {
         }
 
         // Add message to page
-        self.pages
+        self.msgbuf
             .lock()
             .await
-            .insert_line(self.font_size.get(), timest, message_id, nick.clone(), text.clone())
+            .insert_privmsg(self.font_size.get(), timest, message_id, nick.clone(), text.clone())
             .await;
 
         // This will refresh the view, so we just use this
@@ -578,13 +571,13 @@ impl ChatView {
         self.line_height.get() - self.baseline.get()
     }
 
-    /// Load extra pages
-    async fn preload_pages(&self, pages: &mut PageManager) -> usize {
+    /// Load extra msgs
+    async fn preload_msgs(&self, msgbuf: &mut MessageBuffer) -> usize {
         // Get the current earliest timestamp
-        let iter = match pages.last_timestamp() {
-            Some(last_timest) => {
+        let iter = match msgbuf.oldest_timestamp() {
+            Some(oldest_timest) => {
                 // iterate from there
-                let key = last_timest.to_be_bytes();
+                let key = oldest_timest.to_be_bytes();
                 debug!(target: "ui::chatview", "preloading from {key:?}");
                 let iter = self.tree.range(..key).rev();
                 iter
@@ -592,19 +585,16 @@ impl ChatView {
             None => self.tree.iter().rev(),
         };
 
-        self.load_n_pages(pages, iter, PRELOAD_PAGES).await
+        self.load_n_msgs(msgbuf, iter, PRELOAD_PAGES).await
     }
 
-    async fn load_n_pages<I: Iterator<Item = sled::Result<(sled::IVec, sled::IVec)>>>(
+    async fn load_n_msgs<I: Iterator<Item = sled::Result<(sled::IVec, sled::IVec)>>>(
         &self,
-        pages: &mut PageManager,
+        msgbuf: &mut MessageBuffer,
         iter: I,
         n: usize,
     ) -> usize {
-        let mut pages_len = 0;
-        let mut msgs = vec![];
-
-        let line_width = pages.line_width;
+        let mut msgs_len = 0;
 
         for entry in iter {
             let Ok((k, v)) = entry else { break };
@@ -615,50 +605,14 @@ impl ChatView {
             let chatmsg: ChatMsg = deserialize(&v).unwrap();
             debug!(target: "ui::chatview", "{timest:?} {chatmsg:?}");
 
-            let dt = Local.timestamp_millis_opt(timest as i64).unwrap();
-            let timestr = dt.format("%H:%M").to_string();
-
-            let text = format!("{} {} {}", timestr, chatmsg.nick, chatmsg.text);
-            let glyphs = self.text_shaper.shape(text, self.font_size.get()).await;
-
-            let msg = Message::new(
-                self.font_size.get(),
-                timest,
-                message_id,
-                chatmsg.nick,
-                chatmsg.text,
-                line_width,
-                &self.text_shaper,
-            )
-            .await;
-            msgs.push(msg);
-
-            if msgs.len() >= PAGE_SIZE {
-                debug!(target: "ui::chatview", "added new page. page_len={pages_len}");
+            msgbuf
+                .push_privmsg(self.font_size.get(), timest, message_id, chatmsg.nick, chatmsg.text)
+                .await;
 
-                let msgs = std::mem::take(&mut msgs);
-                let page = Page::new(msgs, &self.render_api).await;
-                pages.push(page);
-
-                pages_len += 1;
-                if pages_len >= n {
-                    break
-                }
-            }
-        }
-
-        // Any remaining messages added to a short page
-        if !msgs.is_empty() {
-            debug!(target: "ui::chatview", "added final page. page_len={pages_len}");
-
-            let page = Page::new(msgs, &self.render_api).await;
-            pages.push(page);
-            pages_len += 1;
+            msgs_len += 1;
         }
-
-        debug!(target: "ui::chatview", "populated {} pages", pages_len);
-
-        pages_len
+        debug!(target: "ui::chatview", "populated {} messages", msgs_len);
+        msgs_len
     }
 
     fn read_nick_colors(&self) -> Vec<Color> {
@@ -706,14 +660,14 @@ impl ChatView {
 
         let rect = read_rect(self.rect.clone()).expect("bad rect property");
 
-        let mut pages = self.pages.lock().await;
+        let mut msgbuf = self.msgbuf.lock().await;
 
-        if let Some(new_scroll) = self.adjust_scroll(&mut pages, scroll, rect.h).await {
+        if let Some(new_scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
             scroll = new_scroll;
         }
 
-        let (mut mesh_instrs, freed) = self.get_meshes(&mut pages, &rect).await;
-        drop(pages);
+        let (mut mesh_instrs, freed) = self.get_meshes(&mut msgbuf, &rect).await;
+        drop(msgbuf);
 
         let mut instrs = vec![DrawInstruction::ApplyViewport(rect)];
         instrs.append(&mut mesh_instrs);
@@ -739,7 +693,7 @@ impl ChatView {
     /// Returns None if the value is within range.
     async fn adjust_scroll(
         &self,
-        pages: &mut PageManager,
+        msgbuf: &mut MessageBuffer,
         mut scroll: f32,
         rect_h: f32,
     ) -> Option<f32> {
@@ -749,15 +703,15 @@ impl ChatView {
         // We still wish to preload pages to fill the screen, so we just adjust it up to 0.
         let nonneg_scroll = max(scroll, 0.);
 
-        let mut total_height = pages.calc_total_height(line_height, baseline);
+        let mut total_height = msgbuf.calc_total_height(line_height, baseline);
         // Load pages until we run out or we have enough
         while total_height < nonneg_scroll + rect_h {
             debug!(target: "ui::chatview", "set_adjusted_scroll() loading more pages");
 
-            let n_loaded_pages = self.preload_pages(pages).await;
+            let n_loaded_pages = self.preload_msgs(msgbuf).await;
 
             // We need this value after so first update it
-            total_height = pages.calc_total_height(line_height, baseline);
+            total_height = msgbuf.calc_total_height(line_height, baseline);
 
             // No more pages available to load
             if n_loaded_pages == 0 {
@@ -784,7 +738,7 @@ impl ChatView {
     /// Returns draw calls for drawing
     async fn get_meshes(
         &self,
-        pages: &mut PageManager,
+        msgbuf: &mut MessageBuffer,
         rect: &Rectangle,
     ) -> (Vec<DrawInstruction>, FreedData) {
         let scroll = self.scroll.get();
@@ -793,7 +747,7 @@ impl ChatView {
         let baseline = self.baseline.get();
         let debug_render = self.debug.get();
 
-        let total_height = pages.calc_total_height(line_height, baseline);
+        let total_height = msgbuf.calc_total_height(line_height, baseline);
         // If lines aren't enough to fill the available buffer then start from the top
         let start_pos = if total_height < rect.h { total_height } else { rect.h };
 
@@ -804,7 +758,7 @@ impl ChatView {
         let text_color = self.text_color.get();
         let nick_colors = self.read_nick_colors();
 
-        let meshes = pages
+        let meshes = msgbuf
             .gen_meshes(
                 rect,
                 scroll,
@@ -839,7 +793,7 @@ impl ChatView {
             current_height += height;
         }
 
-        let freed = std::mem::take(&mut pages.freed);
+        let freed = std::mem::take(&mut msgbuf.freed);
 
         (instrs, freed)
     }
@@ -851,16 +805,16 @@ impl ChatView {
 
         let rect = eval_rect(self.rect.clone(), parent_rect).expect("bad rect property");
 
-        let mut pages = self.pages.lock().await;
-        pages.adjust_line_width(rect.w);
+        let mut msgbuf = self.msgbuf.lock().await;
+        msgbuf.adjust_width(rect.w);
 
         let mut scroll = self.scroll.get();
-        if let Some(scroll) = self.adjust_scroll(&mut pages, scroll, rect.h).await {
+        if let Some(scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
             self.scroll.set(scroll);
         }
 
-        let (mut mesh_instrs, freed) = self.get_meshes(&mut pages, &rect).await;
-        drop(pages);
+        let (mut mesh_instrs, freed) = self.get_meshes(&mut msgbuf, &rect).await;
+        drop(msgbuf);
 
         let mut instrs = vec![DrawInstruction::ApplyViewport(rect)];
         instrs.append(&mut mesh_instrs);

+ 324 - 184
bin/darkwallet/src/ui/chatview/page.rs

@@ -34,7 +34,7 @@ use crate::{
     prop::{PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr, PropertyUint32, Role},
     pubsub::Subscription,
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId},
-    text::{self, Glyph, GlyphPositionIter, TextShaper, TextShaperPtr},
+    text::{self, glyph_str, Glyph, GlyphPositionIter, TextShaper, TextShaperPtr},
     util::enumerate_mut,
     ExecutorPtr,
 };
@@ -50,7 +50,7 @@ fn is_whitespace(s: &str) -> bool {
 }
 
 #[derive(Clone)]
-pub(super) struct Message {
+pub(super) struct PrivMessage {
     font_size: f32,
 
     timestamp: Timestamp,
@@ -60,9 +60,12 @@ pub(super) struct Message {
 
     unwrapped_glyphs: Vec<Glyph>,
     wrapped_lines: Vec<Vec<Glyph>>,
+
+    atlas: text::RenderedAtlas,
+    mesh_cache: Option<DrawMesh>,
 }
 
-impl Message {
+impl PrivMessage {
     pub(super) async fn new(
         font_size: f32,
 
@@ -72,150 +75,86 @@ impl Message {
         text: String,
 
         line_width: f32,
+
         text_shaper: &TextShaper,
-    ) -> Self {
+        render_api: &RenderApi,
+    ) -> Message {
         let dt = Local.timestamp_millis_opt(timestamp as i64).unwrap();
         let timestr = dt.format("%H:%M").to_string();
 
         let linetext = format!("{} {} {}", timestr, nick, text);
         let unwrapped_glyphs = text_shaper.shape(linetext, font_size).await;
 
-        let mut self_ =
-            Self { font_size, timestamp, id, nick, text, unwrapped_glyphs, wrapped_lines: vec![] };
-        self_.adjust_line_width(line_width);
-        self_
-    }
-
-    fn adjust_line_width(&mut self, line_width: f32) {
-        // Invalidate wrapped_glyphs and recalc
-        self.wrapped_lines = text::wrap(line_width, self.font_size, &self.unwrapped_glyphs);
-    }
-}
-
-enum PageVisibility {
-    Null,
-    Invisible,
-    EnterTop,
-    Visible,
-    ExitBottom,
-}
-
-pub(super) struct Page {
-    msgs: Vec<Message>,
-    atlas: text::RenderedAtlas,
-    mesh_cache: Option<DrawMesh>,
-    visible: PageVisibility,
-}
-
-impl Page {
-    pub(super) async fn new(msgs: Vec<Message>, render_api: &RenderApi) -> Self {
         let mut atlas = text::Atlas::new(render_api);
-        for msg in &msgs {
-            atlas.push(&msg.unwrapped_glyphs);
-        }
-        let atlas = atlas.make().await.expect("unable to make atlas!");
-
-        Self { msgs, atlas, mesh_cache: None, visible: PageVisibility::Null }
-    }
-
-    fn clear_mesh(&mut self) -> Option<DrawMesh> {
-        std::mem::replace(&mut self.mesh_cache, None)
-    }
+        atlas.push(&unwrapped_glyphs);
+        let atlas = atlas.make().await.expect("unable to make atlas");
 
-    async fn split(mut msgs: Vec<Message>, render_api: &RenderApi) -> Vec<Self> {
-        msgs.sort_unstable_by_key(|msg| msg.timestamp);
-        msgs.reverse();
-
-        let chunk_size = if msgs.len() > PAGE_SIZE {
-            // Round up so we don't get a weird page with a single item
-            msgs.len() / 2 + 1
-        } else {
-            PAGE_SIZE
+        let mut self_ = Self {
+            font_size,
+            timestamp,
+            id,
+            nick,
+            text,
+            unwrapped_glyphs,
+            wrapped_lines: vec![],
+            atlas,
+            mesh_cache: None,
         };
-
-        // Replace single page with N pages each with chunk_size messages
-        let mut new_pages = vec![];
-        for page_msgs in msgs.chunks(chunk_size).map(|m| m.to_vec()) {
-            debug!(target: "ui::chatview", "PAGE ==========================");
-            for msg in &page_msgs {
-                debug!(target: "ui::chatview", "{} {:?}", msg.timestamp, msg.text);
-            }
-            debug!(target: "ui::chatview", "===============================");
-
-            let new_page = Page::new(page_msgs, render_api).await;
-            new_pages.push(new_page);
-        }
-        assert!(!new_pages.is_empty());
-        assert!(new_pages.len() <= 2);
-        new_pages
+        self_.adjust_width(line_width);
+        Message::Priv(self_)
     }
 
-    fn wrapped_lines_count(&self) -> usize {
-        let mut count = 0;
-        for msg in &self.msgs {
-            count += msg.wrapped_lines.len();
-        }
-        count
+    fn height(&self, line_height: f32) -> f32 {
+        self.wrapped_lines.len() as f32 * line_height
     }
 
     async fn gen_mesh(
         &mut self,
-        render_api: &RenderApi,
         clip: &Rectangle,
-        font_size: f32,
         line_height: f32,
         baseline: f32,
         nick_colors: &[Color],
         timestamp_color: Color,
         text_color: Color,
         debug_render: bool,
+        render_api: &RenderApi,
     ) -> DrawMesh {
-        if let Some(mesh) = &self.mesh_cache {
-            return mesh.clone()
-        }
-
-        let mut line_idx = 0;
+        //debug!(target: "ui::chatview", "gen_mesh({})", glyph_str(&self.unwrapped_glyphs));
         let mut mesh = MeshBuilder::new();
 
-        for msg in &self.msgs {
-            let nick_color = select_nick_color(&msg.nick, nick_colors);
-
-            let last_idx = msg.wrapped_lines.len() - 1;
-            for (i, line) in msg.wrapped_lines.iter().rev().enumerate() {
-                let off_y = (line_idx + 1) as f32 * line_height;
-                let is_last_line = i == last_idx;
-
-                // debug draw baseline
-                if debug_render {
-                    let y = baseline - off_y;
-                    mesh.draw_filled_box(
-                        &Rectangle { x: 0., y: y - 1., w: clip.w, h: 1. },
-                        COLOR_BLUE,
-                    );
-                }
-
-                self.render_line(
-                    &mut mesh,
-                    line,
-                    off_y,
-                    is_last_line,
-                    font_size,
-                    baseline,
-                    timestamp_color,
-                    nick_color,
-                    text_color,
-                    debug_render,
-                );
+        let nick_color = select_nick_color(&self.nick, nick_colors);
 
-                line_idx += 1;
+        let last_idx = self.wrapped_lines.len() - 1;
+        for (i, line) in self.wrapped_lines.iter().rev().enumerate() {
+            let off_y = (i + 1) as f32 * line_height;
+            let is_last_line = i == last_idx;
+
+            // debug draw baseline
+            if debug_render {
+                let y = baseline - off_y;
+                mesh.draw_filled_box(&Rectangle { x: 0., y: y - 1., w: clip.w, h: 1. }, COLOR_BLUE);
             }
+
+            self.render_line(
+                &mut mesh,
+                line,
+                off_y,
+                is_last_line,
+                baseline,
+                timestamp_color,
+                nick_color,
+                text_color,
+                debug_render,
+            );
         }
 
-        // debug draw page outline
         if debug_render {
-            let height = line_idx as f32 * line_height;
-            let page_rect = Rectangle::from_array([0., -height, clip.w, height]);
-            mesh.draw_outline(&page_rect, COLOR_PINK, 1.);
+            let height = self.height(line_height);
+            mesh.draw_outline(
+                &Rectangle { x: 0., y: -height, w: clip.w, h: height },
+                COLOR_PINK,
+                1.,
+            );
         }
 
         let mesh = mesh.alloc(render_api).await.unwrap();
@@ -231,13 +170,13 @@ impl Page {
         line: &Vec<Glyph>,
         off_y: f32,
         is_last: bool,
-        font_size: f32,
         baseline: f32,
         timestamp_color: Color,
         nick_color: Color,
         text_color: Color,
         debug_render: bool,
     ) {
+        //debug!(target: "ui::chatview", "render_line({})", glyph_str(line));
         // Keep track of the 'section'
         // Section 0 is the timestamp
         // Section 1 is the nickname (colorized)
@@ -247,7 +186,7 @@ impl Page {
             section = 0;
         }
 
-        let glyph_pos_iter = GlyphPositionIter::new(font_size, line, baseline);
+        let glyph_pos_iter = GlyphPositionIter::new(self.font_size, line, baseline);
         for (mut glyph_rect, glyph) in glyph_pos_iter.zip(line.iter()) {
             let uv_rect = self.atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
             glyph_rect.y -= off_y;
@@ -269,6 +208,177 @@ impl Page {
             }
         }
     }
+
+    fn clear_mesh(&mut self) -> Option<DrawMesh> {
+        std::mem::replace(&mut self.mesh_cache, None)
+    }
+
+    fn adjust_width(&mut self, line_width: f32) {
+        // Invalidate wrapped_glyphs and recalc
+        self.wrapped_lines = text::wrap(line_width, self.font_size, &self.unwrapped_glyphs);
+    }
+}
+
+#[derive(Clone)]
+pub(super) struct DateMessage {
+    font_size: f32,
+    timestamp: Timestamp,
+    glyphs: Vec<Glyph>,
+
+    atlas: text::RenderedAtlas,
+    mesh_cache: Option<DrawMesh>,
+}
+
+impl DateMessage {
+    pub(super) async fn new(
+        font_size: f32,
+        timestamp: Timestamp,
+
+        text_shaper: &TextShaper,
+        render_api: &RenderApi,
+    ) -> Message {
+        let dt = Local.timestamp_millis_opt(timestamp as i64).unwrap();
+        let datestr = dt.format("%a %-d %b %Y").to_string();
+
+        let dt2 = dt.date_naive().and_hms_opt(0, 0, 0).unwrap();
+        assert_eq!(dt.date_naive(), dt2.date());
+        let timestamp = Local.from_local_datetime(&dt2).unwrap().timestamp_millis() as u64;
+
+        let glyphs = text_shaper.shape(datestr, font_size).await;
+
+        let mut atlas = text::Atlas::new(render_api);
+        atlas.push(&glyphs);
+        let atlas = atlas.make().await.expect("unable to make atlas");
+
+        Message::Date(Self { font_size, timestamp, glyphs, atlas, mesh_cache: None })
+    }
+
+    fn clear_mesh(&mut self) -> Option<DrawMesh> {
+        None
+    }
+
+    fn adjust_width(&mut self, line_width: f32) {
+        // Do nothing
+    }
+
+    async fn gen_mesh(
+        &mut self,
+        clip: &Rectangle,
+        line_height: f32,
+        baseline: f32,
+        nick_colors: &[Color],
+        timestamp_color: Color,
+        text_color: Color,
+        debug_render: bool,
+        render_api: &RenderApi,
+    ) -> DrawMesh {
+        let mut mesh = MeshBuilder::new();
+
+        let glyph_pos_iter = GlyphPositionIter::new(self.font_size, &self.glyphs, baseline);
+        for (mut glyph_rect, glyph) in glyph_pos_iter.zip(self.glyphs.iter()) {
+            let uv_rect = self.atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
+            glyph_rect.y -= line_height;
+            mesh.draw_box(&glyph_rect, timestamp_color, uv_rect);
+        }
+
+        if debug_render {
+            mesh.draw_outline(
+                &Rectangle { x: 0., y: -line_height, w: clip.w, h: line_height },
+                COLOR_PINK,
+                1.,
+            );
+        }
+
+        let mesh = mesh.alloc(render_api).await.unwrap();
+        let mesh = mesh.draw_with_texture(self.atlas.texture_id);
+        self.mesh_cache = Some(mesh.clone());
+
+        mesh
+    }
+}
+
+/// Easier than fucking around with traits nonsense
+enum Message {
+    Priv(PrivMessage),
+    Date(DateMessage),
+}
+
+impl Message {
+    fn timestamp(&self) -> u64 {
+        match self {
+            Self::Priv(m) => m.timestamp,
+            Self::Date(m) => m.timestamp,
+        }
+    }
+
+    fn height(&self, line_height: f32) -> f32 {
+        match self {
+            Self::Priv(m) => m.height(line_height),
+            Self::Date(_) => line_height,
+        }
+    }
+
+    fn clear_mesh(&mut self) -> Option<DrawMesh> {
+        match self {
+            Self::Priv(m) => m.clear_mesh(),
+            Self::Date(m) => m.clear_mesh(),
+        }
+    }
+
+    fn adjust_width(&mut self, line_width: f32) {
+        match self {
+            Self::Priv(m) => m.adjust_width(line_width),
+            Self::Date(_) => {}
+        }
+    }
+
+    async fn gen_mesh(
+        &mut self,
+        clip: &Rectangle,
+        line_height: f32,
+        baseline: f32,
+        nick_colors: &[Color],
+        timestamp_color: Color,
+        text_color: Color,
+        debug_render: bool,
+        render_api: &RenderApi,
+    ) -> DrawMesh {
+        match self {
+            Self::Priv(m) => {
+                m.gen_mesh(
+                    clip,
+                    line_height,
+                    baseline,
+                    nick_colors,
+                    timestamp_color,
+                    text_color,
+                    debug_render,
+                    render_api,
+                )
+                .await
+            }
+            Self::Date(m) => {
+                m.gen_mesh(
+                    clip,
+                    line_height,
+                    baseline,
+                    nick_colors,
+                    timestamp_color,
+                    text_color,
+                    debug_render,
+                    render_api,
+                )
+                .await
+            }
+        }
+    }
+
+    fn is_date(&self) -> bool {
+        match self {
+            Self::Priv(m) => false,
+            Self::Date(_) => true,
+        }
+    }
 }
 
 fn select_nick_color(nick: &str, nick_colors: &[Color]) -> Color {
@@ -295,35 +405,34 @@ impl FreedData {
     }
 }
 
-pub struct PageManager {
-    pages: Vec<Page>,
+pub struct MessageBuffer {
+    /// From most recent to older
+    msgs: Vec<Message>,
     pub(super) freed: FreedData,
     pub(super) line_width: f32,
     render_api: RenderApiPtr,
     text_shaper: TextShaperPtr,
 }
 
-impl PageManager {
+impl MessageBuffer {
     pub(super) fn new(render_api: RenderApiPtr, text_shaper: TextShaperPtr) -> Self {
-        Self { pages: vec![], freed: Default::default(), line_width: 0., render_api, text_shaper }
+        Self { msgs: vec![], freed: Default::default(), line_width: 0., render_api, text_shaper }
     }
 
-    pub(super) fn push(&mut self, page: Page) {
-        self.pages.push(page);
+    pub(super) fn push(&mut self, msg: Message) {
+        self.msgs.push(msg);
     }
 
     /// For scrolling we want to be able to adjust and measure without
     /// explicitly rendering since it may be off screen.
-    pub(super) fn adjust_line_width(&mut self, line_width: f32) {
+    pub(super) fn adjust_width(&mut self, line_width: f32) {
         if (line_width - self.line_width).abs() < f32::EPSILON {
             return;
         }
         self.line_width = line_width;
 
-        for page in &mut self.pages {
-            for msg in &mut page.msgs {
-                msg.adjust_line_width(line_width);
-            }
+        for msg in &mut self.msgs {
+            msg.adjust_width(line_width);
         }
 
         self.clear_meshes();
@@ -331,26 +440,24 @@ impl PageManager {
 
     /// Clear all meshes and caches. Returns data that needs to be freed.
     fn clear_meshes(&mut self) {
-        for page in &mut self.pages {
-            if let Some(mesh) = page.clear_mesh() {
+        for msg in &mut self.msgs {
+            if let Some(mesh) = msg.clear_mesh() {
                 self.freed.add_mesh(mesh);
             }
         }
     }
 
     pub(super) fn calc_total_height(&self, line_height: f32, baseline: f32) -> f32 {
-        let descent = line_height - baseline;
-        let mut height = descent;
+        let mut height = 0.;
 
-        for page in &self.pages {
-            let lines_count = page.wrapped_lines_count();
-            height += lines_count as f32 * line_height;
+        for msg in &self.msgs {
+            height += msg.height(line_height);
         }
 
         height
     }
 
-    pub(super) async fn insert_line(
+    pub(super) async fn insert_privmsg(
         &mut self,
         font_size: f32,
         timest: Timestamp,
@@ -358,7 +465,7 @@ impl PageManager {
         nick: String,
         text: String,
     ) {
-        let msg = Message::new(
+        let msg = PrivMessage::new(
             font_size,
             timest,
             message_id,
@@ -366,34 +473,33 @@ impl PageManager {
             text,
             self.line_width,
             &self.text_shaper,
+            &self.render_api,
         )
         .await;
 
-        // Now add message to page
+        if self.msgs.is_empty() {
+            let datemsg =
+                DateMessage::new(font_size, timest, &self.text_shaper, &self.render_api).await;
+            assert!(datemsg.timestamp() <= timest);
 
-        // Create our very first page
-        if self.pages.is_empty() {
-            let page = Page::new(vec![msg], &self.render_api).await;
-            self.pages.push(page);
-            return;
+            self.msgs.push(msg);
+            self.msgs.push(datemsg);
+            return
         }
 
         // We only add lines inside pages.
         // Calling the appropriate draw() function after should preload any missing pages.
         // When a line is before the first page, it will get preloaded as a new page.
-        let first_timest = self.pages.first().unwrap().msgs.last().unwrap().timestamp;
-        if timest < first_timest {
+        let oldest_timest = self.oldest_timestamp().unwrap();
+        if timest < oldest_timest {
             return;
         }
 
-        let mut idx = None;
-        for (i, page) in enumerate_mut(&mut self.pages) {
-            //let first_timest = page.msgs.last().unwrap().timestamp;
-            let last_timest = page.msgs.first().unwrap().timestamp;
+        // Timestamps go from most recent backwards
 
-            //debug!(target: "ui::chatview", "page {i} [{first_timest}, {last_timest}]");
-            if timest <= last_timest {
-                //debug!(target: "ui::chatview", "found page {i} [{first_timest}, {last_timest}]");
+        let mut idx = None;
+        for (i, msg) in enumerate_mut(&mut self.msgs) {
+            if timest >= msg.timestamp() {
                 idx = Some(i);
                 break
             }
@@ -407,29 +513,62 @@ impl PageManager {
             }
         };
 
-        let old_pages_len = self.pages.len();
-        // We now want to replace this page by 1 or 2 pages
-        // Split pages into 3 parts: head, replaced page and tail
-        let mut head = std::mem::replace(&mut self.pages, vec![]);
-        let mut drain_iter = head.drain(idx..);
-        // Drop the item at idx which will be replaced
-        let old_page = drain_iter.next().unwrap();
-        let mut tail: Vec<_> = drain_iter.collect();
-        assert_eq!(old_pages_len, head.len() + 1 + tail.len());
-
-        // Free texture and mesh before dropping page
-        if let Some(mesh) = old_page.mesh_cache {
-            self.freed.add_mesh(mesh);
-            self.freed.add_texture(old_page.atlas.texture_id);
+        self.insert_msg(msg, idx, font_size).await;
+    }
+
+    pub(super) async fn push_privmsg(
+        &mut self,
+        font_size: f32,
+        timest: Timestamp,
+        message_id: MessageId,
+        nick: String,
+        text: String,
+    ) {
+        let msg = PrivMessage::new(
+            font_size,
+            timest,
+            message_id,
+            nick,
+            text,
+            self.line_width,
+            &self.text_shaper,
+            &self.render_api,
+        )
+        .await;
+
+        if self.msgs.is_empty() {
+            let datemsg =
+                DateMessage::new(font_size, timest, &self.text_shaper, &self.render_api).await;
+            assert!(datemsg.timestamp() <= timest);
+
+            self.msgs.push(msg);
+            self.msgs.push(datemsg);
+            return
         }
 
-        let mut msgs = old_page.msgs;
-        msgs.push(msg);
-        let mut new_pages = Page::split(msgs, &self.render_api).await;
+        let idx = self.msgs.len() - 1;
+        self.insert_msg(msg, idx, font_size).await;
+    }
+
+    async fn insert_msg(&mut self, msg: Message, idx: usize, font_size: f32) {
+        let timest = msg.timestamp();
+        let msg_is_date = msg.is_date();
+
+        // Do we cross from a new day?
+        let prev_msg = &self.msgs[idx];
+        let prev_timest = prev_msg.timestamp();
+        let prev_is_date = prev_msg.is_date();
+        let prev_date = Local.timestamp_millis_opt(prev_timest as i64).unwrap().date_naive();
+        let curr_date = Local.timestamp_millis_opt(timest as i64).unwrap().date_naive();
 
-        self.pages.append(&mut head);
-        self.pages.append(&mut new_pages);
-        self.pages.append(&mut tail);
+        self.msgs.insert(idx, msg);
+
+        if !prev_is_date && !msg_is_date && prev_date != curr_date {
+            let datemsg =
+                DateMessage::new(font_size, timest, &self.text_shaper, &self.render_api).await;
+            assert!(datemsg.timestamp() <= timest);
+            self.msgs.insert(idx + 1, datemsg);
+        }
     }
 
     /// Generate caches and return meshes
@@ -449,27 +588,25 @@ impl PageManager {
 
         let descent = line_height - baseline;
         let mut current_height = descent;
-        for page in &mut self.pages {
+        for msg in &mut self.msgs {
             if current_height > scroll + rect.h {
                 break
             }
 
-            let mesh = page
+            let mesh = msg
                 .gen_mesh(
-                    &self.render_api,
                     rect,
-                    font_size,
                     line_height,
                     baseline,
                     nick_colors,
                     timestamp_color,
                     text_color,
                     debug_render,
+                    &self.render_api,
                 )
                 .await;
 
-            let lines_count = page.wrapped_lines_count();
-            let mesh_height = lines_count as f32 * line_height;
+            let mesh_height = msg.height(line_height);
 
             meshes.push((mesh_height, mesh));
 
@@ -479,10 +616,13 @@ impl PageManager {
         meshes
     }
 
-    pub(super) fn last_timestamp(&self) -> Option<Timestamp> {
-        let last_page_msgs = &self.pages.last()?.msgs;
-        // There should be no pages with 0 messages. We can unwrap here.
-        debug!(target: "ui::chatview", "last page has {} msgs", last_page_msgs.len());
-        Some(last_page_msgs.last().unwrap().timestamp)
+    pub(super) fn oldest_timestamp(&self) -> Option<Timestamp> {
+        let last_msg = &self.msgs.last()?;
+        Some(last_msg.timestamp())
+    }
+
+    pub(super) fn latest_timestamp(&self) -> Option<Timestamp> {
+        let first_msg = &self.msgs.first()?;
+        Some(first_msg.timestamp())
     }
 }