1use crate::context::XmtpSharedContext;
2use crate::groups::MlsGroup;
3use crate::messages::decoded_message::DecodedMessage;
4use crate::messages::enrichment::{
5 EnrichMessageError, EnrichedStoredMessage, enrich_messages, enrich_messages_with_stored,
6};
7use xmtp_db::DbQuery;
8use xmtp_db::group_message::{ContentType as DbContentType, MsgQueryArgs};
9use xmtp_db::prelude::QueryGroupMessage;
10
11mod page;
12pub use page::{EnrichedHistoryPage, EnrichedRecoveryPage};
13
14impl<Context> MlsGroup<Context>
15where
16 Context: XmtpSharedContext,
17{
18 #[xmtp_common::mls_span]
19 pub fn find_messages_v2(
20 &self,
21 query: &MsgQueryArgs,
22 ) -> Result<Vec<DecodedMessage>, EnrichMessageError> {
23 let conn = self.context.db();
24 self.find_messages_v2_with_conn(query, conn)
25 }
26
27 #[xmtp_common::mls_span]
28 pub fn find_messages_v2_with_stored(
29 &self,
30 query: &MsgQueryArgs,
31 ) -> Result<Vec<EnrichedStoredMessage>, EnrichMessageError> {
32 let conn = self.context.db();
33 use xmtp_db::delivery::QueryDelivery;
34 let rows = conn
35 .app_visible_message_rows(
36 &self.group_id,
37 &filter_out_hidden_message_types_from_query(query),
38 )
39 .map_err(EnrichMessageError::Storage)?;
40 let cursors: std::collections::HashMap<_, _> = rows
41 .iter()
42 .map(|row| (row.stored.id.clone(), row.cursor))
43 .collect();
44 let mut enriched = enrich_messages_with_stored(
45 conn,
46 &self.group_id,
47 rows.into_iter().map(|row| row.stored).collect(),
48 )?;
49 for message in &mut enriched {
50 message.delivery_cursor = cursors.get(&message.stored.id).copied().flatten();
51 }
52 Ok(enriched)
53 }
54
55 #[xmtp_common::mls_span]
56 pub fn find_messages_v2_with_conn<C>(
57 &self,
58 query: &MsgQueryArgs,
59 conn: C,
60 ) -> Result<Vec<DecodedMessage>, EnrichMessageError>
61 where
62 C: QueryGroupMessage + DbQuery,
63 {
64 let initial_messages = conn.get_group_messages(
65 &self.group_id,
66 &filter_out_hidden_message_types_from_query(query),
67 )?;
68
69 enrich_messages(conn, &self.group_id, initial_messages)
70 }
71}
72
73fn filter_out_hidden_message_types_from_query(query: &MsgQueryArgs) -> MsgQueryArgs {
74 let mut new_query = query.clone();
75 let hidden_message_types = vec![
76 DbContentType::Reaction,
77 DbContentType::ReadReceipt,
78 DbContentType::DeleteMessage,
79 ];
80
81 let excluded_content_types = match &query.exclude_content_types {
82 Some(excluded) => {
83 let mut content_types = excluded.clone();
84 content_types.extend(
85 hidden_message_types
86 .into_iter()
87 .filter(|t| !excluded.contains(t)),
88 );
89 content_types
90 }
91 None => hidden_message_types,
92 };
93 new_query.exclude_content_types = Some(excluded_content_types);
94 new_query
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::builder::ClientBuilder;
101 use crate::groups::QueryableContentFields;
102 use crate::messages::decoded_message::{ContentDecodeFailureKind, MessageBody};
103 use crate::prelude::*;
104 use hex::ToHexExt;
105 use xmtp_common::time::now_ns;
106 use xmtp_content_types::ContentCodec;
107 use xmtp_content_types::reaction::ReactionCodec;
108 use xmtp_content_types::test_utils::TestContentGenerator;
109 use xmtp_content_types::text::TextCodec;
110 use xmtp_cryptography::utils::generate_local_wallet;
111 use xmtp_db::Store;
112 use xmtp_db::group_message::{
113 ContentType as DbContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage,
114 };
115 use xmtp_proto::types::GroupId;
116 use xmtp_proto::xmtp::mls::message_contents::content_types::ReactionAction;
117 use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
118
119 async fn setup_test_group() -> (MlsGroup<impl XmtpSharedContext>, impl XmtpSharedContext) {
120 let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
121 let group = client.create_group(None, Default::default()).unwrap();
122
123 (group, client.context.clone())
124 }
125
126 fn create_test_message(
127 group_id: &GroupId,
128 message_id: Vec<u8>,
129 encoded_content: EncodedContent,
130 sent_at_ns: i64,
131 ) -> StoredGroupMessage {
132 let content_bytes = xmtp_content_types::encoded_content_to_bytes(encoded_content.clone());
133 let queryable_fields =
134 QueryableContentFields::try_from(encoded_content).unwrap_or_default();
135
136 StoredGroupMessage {
137 id: message_id,
138 group_id: *group_id,
139 decrypted_message_bytes: content_bytes,
140 sent_at_ns,
141 kind: GroupMessageKind::Application,
142 sender_installation_id: vec![1, 2, 3],
143 sender_inbox_id: "sender".into(),
144 delivery_status: DeliveryStatus::Published,
145 content_type: queryable_fields.content_type,
146 version_major: queryable_fields.version_major,
147 version_minor: queryable_fields.version_minor,
148 authority_id: queryable_fields.authority_id,
149 reference_id: queryable_fields.reference_id,
150 sequence_id: 0,
151 envelope_hash: None,
152 expiry_ns: None,
153 expiry_unknown: false,
154 deleted_by_inbox_id: None,
155 expire_at_ns: None,
156 inserted_at_ns: 0,
157 should_push: true,
158 idempotency_key: sent_at_ns.to_string(),
159 }
160 }
161
162 fn create_test_message_raw(
164 group_id: &GroupId,
165 message_id: Vec<u8>,
166 content: Vec<u8>,
167 sent_at_ns: i64,
168 content_type_id: Option<ContentTypeId>,
169 reference_id: Option<Vec<u8>>,
170 ) -> StoredGroupMessage {
171 let queryable_fields = content_type_id
172 .as_ref()
173 .map(|ct| {
174 (
175 DbContentType::from_identifier(&ct.authority_id, &ct.type_id, ct.version_major),
176 ct.version_major as i32,
177 ct.version_minor as i32,
178 ct.authority_id.clone(),
179 )
180 })
181 .unwrap_or((DbContentType::Text, 1, 0, "xmtp.org".to_string()));
182
183 StoredGroupMessage {
184 id: message_id,
185 group_id: *group_id,
186 decrypted_message_bytes: content,
187 sent_at_ns,
188 kind: GroupMessageKind::Application,
189 sender_installation_id: vec![1, 2, 3],
190 sender_inbox_id: "sender".into(),
191 delivery_status: DeliveryStatus::Published,
192 content_type: queryable_fields.0,
193 version_major: queryable_fields.1,
194 version_minor: queryable_fields.2,
195 authority_id: queryable_fields.3,
196 reference_id,
197 sequence_id: 0,
198 envelope_hash: None,
199 expiry_ns: None,
200 expiry_unknown: false,
201 deleted_by_inbox_id: None,
202 expire_at_ns: None,
203 inserted_at_ns: 0,
204 should_push: true,
205 idempotency_key: sent_at_ns.to_string(),
206 }
207 }
208
209 fn create_and_store_message<S>(conn: &S, group_id: &GroupId, n: u8, content: EncodedContent)
211 where
212 StoredGroupMessage: Store<S>,
213 {
214 let sent_at_ns = now_ns() + i64::from(n) * 1000;
215 create_test_message(group_id, vec![n; 32], content, sent_at_ns)
216 .store(conn)
217 .unwrap();
218 }
219
220 fn ref_to(n: u8) -> String {
222 hex::encode([n; 32])
223 }
224
225 fn ids(messages: &[DecodedMessage]) -> Vec<u8> {
227 messages
228 .iter()
229 .map(|message| message.metadata.id[0])
230 .collect()
231 }
232
233 fn emojis(message: &DecodedMessage) -> Vec<&str> {
234 message
235 .reactions
236 .iter()
237 .filter_map(|reaction| match &reaction.content {
238 MessageBody::Reaction(reaction) => Some(reaction.content.as_str()),
239 _ => None,
240 })
241 .collect()
242 }
243
244 #[xmtp_common::test(unwrap_try = true)]
247 async fn test_exclude_content_types_with_custom_exclusions() {
248 let (group, context) = setup_test_group().await;
249 let conn = context.db();
250 create_and_store_message(
251 &conn,
252 &group.group_id,
253 1,
254 TestContentGenerator::text_content("Hello World"),
255 );
256
257 let mut transcript = create_test_message(
259 &group.group_id,
260 vec![2; 32],
261 TestContentGenerator::group_updated_content(vec!["inbox1".to_string()]),
262 now_ns() + 2000,
263 );
264 transcript.kind = GroupMessageKind::MembershipChange;
265 transcript.store(&conn)?;
266
267 let messages = group.find_messages_v2(&MsgQueryArgs::default())?;
268 assert_eq!(ids(&messages), [1, 2]);
269
270 let query = MsgQueryArgs {
272 exclude_content_types: Some(vec![DbContentType::Text]),
273 ..Default::default()
274 };
275 let messages = group.find_messages_v2(&query)?;
276 assert_eq!(ids(&messages), [2]);
277 assert!(
278 matches!(messages[0].content, MessageBody::GroupUpdated(_)),
279 "{:?}",
280 messages[0].content
281 );
282 }
283
284 #[xmtp_common::test(unwrap_try = true)]
287 async fn reactions_attach_to_their_target_and_hidden_types_are_filtered() {
288 let (group, context) = setup_test_group().await;
289 let conn = context.db();
290 let put = |n, content| create_and_store_message(&conn, &group.group_id, n, content);
291
292 put(1, TestContentGenerator::text_content("Hello World"));
293 let reaction = |emoji| {
294 TestContentGenerator::reaction_content(&ref_to(1), emoji, ReactionAction::Added)
295 };
296 put(2, reaction("👍"));
297 put(3, reaction("❤️"));
298 create_test_message_raw(
299 &group.group_id,
300 vec![4; 32],
301 b"invalid reaction bytes".to_vec(),
302 now_ns() + 4000,
303 Some(ReactionCodec::content_type()),
304 Some(vec![1; 32]),
305 )
306 .store(&conn)?;
307 put(5, TestContentGenerator::read_receipt_content());
308 put(6, TestContentGenerator::delete_message_content(&ref_to(1)));
309 put(7, TestContentGenerator::text_content("Second message"));
310
311 let messages = group.find_messages_v2(&MsgQueryArgs::default())?;
312 assert_eq!(ids(&messages), [1, 7], "hidden types are filtered");
313 assert!(
314 matches!(&messages[0].content, MessageBody::Text(text) if text.content == "Hello World")
315 );
316 assert!(
317 matches!(&messages[1].content, MessageBody::Text(text) if text.content == "Second message")
318 );
319 assert_eq!(emojis(&messages[0]), ["👍", "❤️"]);
320 assert_eq!(messages[0].reactions.len(), 2, "malformed reaction");
321 assert!(messages[1].reactions.is_empty());
322 }
323
324 #[xmtp_common::test(unwrap_try = true)]
329 async fn history_reaction_summary_is_bounded_per_message() {
330 const REACTION_COUNT: i64 = 256;
331 const SUMMARY: usize = 50;
332
333 let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
334 let group = client.create_group(None, Default::default())?;
335 let conn = client.context.db();
336 let base = now_ns();
337 let busy_parent = vec![0xa5; 32];
338 let quiet_parent = vec![0xb6; 32];
339 for parent in [&busy_parent, &quiet_parent] {
340 create_test_message(
341 &group.group_id,
342 parent.clone(),
343 TestContentGenerator::text_content("target"),
344 base,
345 )
346 .store(&conn)?;
347 }
348 let reaction = |parent: &[u8], id: Vec<u8>, offset: i64| {
349 create_test_message(
350 &group.group_id,
351 id,
352 TestContentGenerator::reaction_content(
353 &parent.encode_hex(),
354 "👍",
355 ReactionAction::Added,
356 ),
357 base + offset,
358 )
359 };
360 for offset in 1..=REACTION_COUNT {
362 reaction(&busy_parent, offset.to_be_bytes().to_vec(), offset).store(&conn)?;
363 }
364 let mut expired = reaction(&busy_parent, vec![0xee; 8], REACTION_COUNT + 1);
366 expired.expire_at_ns = Some(now_ns() - 1);
367 expired.store(&conn)?;
368 for offset in 1..=3 {
369 reaction(&quiet_parent, vec![0xb6, offset as u8], offset).store(&conn)?;
370 }
371
372 let assert_summary = |message: &DecodedMessage| {
373 let offsets: Vec<_> = message
374 .reactions
375 .iter()
376 .map(|reaction| reaction.metadata.sent_at_ns - base)
377 .collect();
378 if message.metadata.id == busy_parent {
379 let latest: Vec<_> =
380 (REACTION_COUNT - SUMMARY as i64 + 1..=REACTION_COUNT).collect();
381 assert_eq!(offsets, latest);
382 assert_eq!(message.num_reaction_messages, REACTION_COUNT as usize);
383 } else {
384 assert_eq!(offsets, [1, 2, 3]);
385 assert_eq!(message.num_reaction_messages, 3);
386 }
387 };
388
389 let history = group.find_messages_v2(&MsgQueryArgs::default())?;
390 assert_eq!(history.len(), 2);
391 history.iter().for_each(assert_summary);
392 for parent in [&busy_parent, &quiet_parent] {
393 assert_summary(&client.message_v2(parent.clone())?);
394 }
395 let sdk_history = group.find_messages_v2_with_stored(&MsgQueryArgs::default())?;
396 assert_eq!(sdk_history.len(), 2);
397 sdk_history
398 .iter()
399 .for_each(|message| assert_summary(&message.decoded));
400 }
401
402 #[xmtp_common::test(unwrap_try = true)]
405 async fn replies_resolve_one_level_of_parent() {
406 let (group, context) = setup_test_group().await;
407 let conn = context.db();
408 let put = |n, content| create_and_store_message(&conn, &group.group_id, n, content);
409 let reply_to = |reference: &str| {
410 let content = b"reply".to_vec();
411 TestContentGenerator::reply_content(reference, TextCodec::content_type(), content)
412 };
413
414 put(1, TestContentGenerator::text_content("Original"));
415 put(2, reply_to(&ref_to(1)));
416 put(3, reply_to(&ref_to(2)));
417 put(4, reply_to(&ref_to(99)));
418 put(5, reply_to("not-valid-hex!@#"));
419
420 let messages = group.find_messages_v2(&MsgQueryArgs::default())?;
421 assert_eq!(ids(&messages), [1, 2, 3, 4, 5]);
422 let reply = |n: usize| match &messages[n - 1].content {
423 MessageBody::Reply(reply) => reply,
424 other => panic!("message {n}: expected reply, got {other:?}"),
425 };
426 for (n, reference, parent) in [
427 (2, ref_to(1), Some(1)),
428 (3, ref_to(2), Some(2)),
429 (4, ref_to(99), None),
430 (5, "not-valid-hex!@#".into(), None),
431 ] {
432 let in_reply_to = reply(n).in_reply_to.as_ref();
433 assert_eq!(reply(n).reference_id, reference, "reply {n}");
434 assert_eq!(in_reply_to.map(|p| p.metadata.id[0]), parent, "reply {n}");
435 }
436 let parent = |n| reply(n).in_reply_to.as_deref().expect("parent");
437 assert!(
438 matches!(&parent(2).content, MessageBody::Text(text) if text.content == "Original")
439 );
440 assert!(
441 matches!(&parent(3).content, MessageBody::Reply(inner) if inner.in_reply_to.is_none()),
442 "parents are not resolved recursively"
443 );
444 }
445
446 #[xmtp_common::test(unwrap_try = true)]
450 async fn stitched_deletions_apply_in_their_own_group() {
451 use xmtp_common::Generate;
452 use xmtp_db::ConnectionExt;
453 use xmtp_db::diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
454 use xmtp_db::group::{ConversationType, QueryGroup};
455 use xmtp_db::schema::groups::dsl;
456
457 let (group, context) = setup_test_group().await;
458 let conn = context.db();
459 let home = group.group_id;
460 conn.raw_query(|sqlite| {
461 xmtp_db::diesel::update(dsl::groups.find(home))
462 .set((
463 dsl::conversation_type.eq(ConversationType::Dm),
464 dsl::dm_id.eq("dm:sender:peer"),
465 ))
466 .execute(sqlite)
467 })?;
468 let mut sibling = conn.find_group(&home)??;
469 sibling.id = GroupId::generate();
470 sibling.store(&conn)?;
471 let sibling = sibling.id;
472 let put = |group_id, n, content| create_and_store_message(&conn, &group_id, n, content);
473 let reply_to = |n| {
474 let content = b"reply".to_vec();
475 TestContentGenerator::reply_content(&ref_to(n), TextCodec::content_type(), content)
476 };
477 put(sibling, 1, TestContentGenerator::text_content("deleted"));
478 put(sibling, 2, TestContentGenerator::text_content("kept"));
479 put(home, 3, reply_to(1));
480 put(home, 4, reply_to(2));
481 for (group_id, n, target) in [(sibling, 5, 1), (home, 6, 2)] {
483 let deletion = TestContentGenerator::delete_message_content(&ref_to(target));
484 put(group_id, n, deletion);
485 let mut row = conn.get_group_message(vec![n; 32])??;
486 row.reference_id = Some(vec![target; 32]);
487 crate::groups::deletion::apply_deletion(
488 &conn,
489 &crate::groups::deletion::DiscardEvents,
490 &row,
491 crate::groups::deletion::Source::Topic {
492 super_admin: false,
493 cause: xmtp_events::DeletionCause::Deleted,
494 },
495 )?;
496 }
497
498 let messages = group.find_messages_v2_with_stored(&MsgQueryArgs::default())?;
499 let messages: Vec<_> = messages.into_iter().map(|m| m.decoded).collect();
500 assert_eq!(ids(&messages), [1, 2, 3, 4]);
501 let parent = |n: usize| match &messages[n - 1].content {
502 MessageBody::Reply(reply) => reply.in_reply_to.as_deref().expect("parent"),
503 other => panic!("message {n}: expected reply, got {other:?}"),
504 };
505 let text = |message: &DecodedMessage| match &message.content {
507 MessageBody::Text(text) => Some(text.content.clone()),
508 MessageBody::DeletedMessage { .. } => None,
509 other => panic!("expected text or a deletion, got {other:?}"),
510 };
511 assert_eq!(text(&messages[0]), None, "target 1");
512 assert_eq!(text(parent(3)), None, "parent of 3");
513 assert_eq!(text(&messages[1]), Some("kept".into()), "target 2");
514 assert_eq!(text(parent(4)), Some("kept".into()), "parent of 4");
515 }
516
517 #[xmtp_common::test(unwrap_try = true)]
519 async fn test_find_messages_undecodable_messages() {
520 let (group, context) = setup_test_group().await;
521 let conn = context.db();
522 let put = |n, content| create_and_store_message(&conn, &group.group_id, n, content);
523
524 put(1, TestContentGenerator::text_content("Valid message"));
525 let malformed = vec![0xff, 0x00, 0x80];
528 create_test_message_raw(
529 &group.group_id,
530 vec![2; 32],
531 malformed.clone(),
532 now_ns() + 2000,
533 None,
534 None,
535 )
536 .store(&conn)?;
537 put(3, TestContentGenerator::invalid_content());
539
540 let messages = group.find_messages_v2(&MsgQueryArgs::default())?;
541 assert_eq!(ids(&messages), [1, 2, 3]);
542 assert!(
543 matches!(&messages[0].content, MessageBody::Text(text) if text.content == "Valid message")
544 );
545
546 assert_eq!(messages[1].metadata.content_type, None);
547 let MessageBody::Undecodable(undecodable) = &messages[1].content else {
548 panic!(
549 "expected undecodable content, got {:?}",
550 messages[1].content
551 );
552 };
553 assert_eq!(undecodable.raw_bytes, malformed);
554 assert_eq!(
555 undecodable.failure.kind,
556 ContentDecodeFailureKind::MalformedEnvelope
557 );
558
559 let MessageBody::Custom(content) = &messages[2].content else {
560 panic!("expected custom content for unknown type message");
561 };
562 assert_eq!(
563 content.encoded.fallback,
564 Some("Invalid message".to_string())
565 );
566 assert_eq!(
567 messages[2].fallback_text,
568 Some("Invalid message".to_string())
569 );
570 }
571
572 #[xmtp_common::test(unwrap_try = true)]
576 async fn malformed_rows_are_kept_on_history_lookup_and_parent_paths() {
577 let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
578 let group = client.create_group(None, Default::default())?;
579 let conn = client.context.db();
580 let put = |n, content| create_and_store_message(&conn, &group.group_id, n, content);
581
582 let malformed = vec![0xff, 0x00, 0x80];
583 let malformed_id = vec![7; 32];
584 create_test_message_raw(
585 &group.group_id,
586 malformed_id.clone(),
587 malformed.clone(),
588 now_ns(),
589 None,
590 None,
591 )
592 .store(&conn)?;
593 let reply = TestContentGenerator::reply_content(
594 &ref_to(7),
595 TextCodec::content_type(),
596 b"replying to bytes".to_vec(),
597 );
598 put(8, reply);
599 put(9, TestContentGenerator::text_content("later"));
600
601 let messages = group.find_messages_v2(&MsgQueryArgs::default())?;
602 assert_eq!(ids(&messages), [7, 8, 9]);
603 let is_kept = |message: &DecodedMessage| {
604 matches!(&message.content, MessageBody::Undecodable(undecodable)
605 if undecodable.raw_bytes == malformed
606 && undecodable.failure.kind == ContentDecodeFailureKind::MalformedEnvelope)
607 };
608 assert!(is_kept(&messages[0]), "history: {:?}", messages[0].content);
609 assert!(matches!(&messages[2].content, MessageBody::Text(text) if text.content == "later"));
610
611 let MessageBody::Reply(reply) = &messages[1].content else {
612 panic!("expected reply, got {:?}", messages[1].content);
613 };
614 let parent = reply.in_reply_to.as_ref().expect("reply parent");
615 assert_eq!(parent.metadata.id, malformed_id);
616 assert!(is_kept(parent), "parent: {:?}", parent.content);
617
618 let looked_up = client.message_v2(malformed_id.clone())?;
619 assert_eq!(looked_up.metadata.id, malformed_id);
620 assert!(is_kept(&looked_up), "lookup: {:?}", looked_up.content);
621 }
622
623 #[xmtp_common::test(unwrap_try = true)]
625 async fn unlimited_history_read_spans_the_bind_limit() {
626 use xmtp_db::ConnectionExt;
627 use xmtp_db::diesel::RunQueryDsl;
628 use xmtp_db::diesel::sql_types::{BigInt, Binary, Integer};
629 const ROWS: i64 = 33_000;
630
631 let (group, context) = setup_test_group().await;
632 let conn = context.db();
633 let text =
634 xmtp_content_types::encoded_content_to_bytes(TestContentGenerator::text_content("row"));
635 conn.raw_query(|sqlite| {
636 xmtp_db::diesel::sql_query(
637 "WITH RECURSIVE n(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM n WHERE x<?) \
638 INSERT INTO group_messages (id,group_id,decrypted_message_bytes,sent_at_ns,kind,\
639 sender_installation_id,sender_inbox_id,delivery_status,content_type,version_major,\
640 version_minor,authority_id,sequence_id,idempotency_key) \
641 SELECT CAST(printf('%032d',x) AS BLOB),?,?,x,?,x'01','sender',?,?,1,0,\
642 'xmtp.org',0,CAST(x AS TEXT) FROM n",
643 )
644 .bind::<BigInt, _>(ROWS)
645 .bind::<Binary, _>(group.group_id.as_ref())
646 .bind::<Binary, _>(&text)
647 .bind::<Integer, _>(GroupMessageKind::Application as i32)
648 .bind::<Integer, _>(DeliveryStatus::Published as i32)
649 .bind::<Integer, _>(DbContentType::Text as i32)
650 .execute(sqlite)
651 })?;
652 let newest = format!("{ROWS:032}").into_bytes();
653 create_test_message(
654 &group.group_id,
655 vec![0xee; 32],
656 TestContentGenerator::reaction_content(
657 &newest.encode_hex(),
658 "👍",
659 ReactionAction::Added,
660 ),
661 ROWS + 1,
662 )
663 .store(&conn)?;
664
665 let ascending: Vec<i64> = (1..=ROWS).collect();
666 let stored = group.find_messages_v2_with_stored(&MsgQueryArgs::default())?;
667 let sent_at: Vec<_> = stored.iter().map(|m| m.stored.sent_at_ns).collect();
668 assert_eq!(sent_at, ascending);
669 assert_eq!(emojis(&stored.last()?.decoded), ["👍"]);
670 let decoded = group.find_messages_v2(&MsgQueryArgs::default())?;
671 let sent_at: Vec<_> = decoded.iter().map(|m| m.metadata.sent_at_ns).collect();
672 assert_eq!(sent_at, ascending);
673 assert_eq!(emojis(decoded.last()?), ["👍"]);
674 }
675}