1use crate::{
2 AccountSnapshot, AppState, ChatKind, ChatMessageKind, ChatMessageSnapshot, ChatThreadSnapshot,
3 CurrentChatSnapshot, DeliveryState, DeviceAuthorizationState, GroupDetailsSnapshot,
4 GroupMemberSnapshot, MessageDeliveryTraceSnapshot, MessageReactionSnapshot, MessageReactor,
5 MessageSearchHit, PreferencesSnapshot, Router, Screen, SearchResultSnapshot,
6};
7
8const MAX_FIXTURE_THREADS: u32 = 2_000;
9const MAX_FIXTURE_MESSAGES: u32 = 10_000;
10const BASE_TIME_SECS: u64 = 1_800_000_000;
11
12#[uniffi::export]
16pub fn build_large_test_app_state(
17 direct_chat_count: u32,
18 group_chat_count: u32,
19 messages_in_current_chat: u32,
20) -> AppState {
21 let direct_chat_count = direct_chat_count.min(MAX_FIXTURE_THREADS);
22 let group_chat_count = group_chat_count.min(MAX_FIXTURE_THREADS);
23 let messages_in_current_chat = messages_in_current_chat.min(MAX_FIXTURE_MESSAGES);
24
25 let mut chat_list =
26 Vec::with_capacity((direct_chat_count as usize) + (group_chat_count as usize));
27 for index in 0..direct_chat_count {
28 chat_list.push(fixture_thread(ChatKind::Direct, index));
29 }
30 for index in 0..group_chat_count {
31 chat_list.push(fixture_thread(ChatKind::Group, index));
32 }
33
34 let current_thread = chat_list
35 .first()
36 .cloned()
37 .unwrap_or_else(|| fixture_thread(ChatKind::Direct, 0));
38 let messages = (0..messages_in_current_chat)
39 .map(|index| fixture_message(¤t_thread.chat_id, index))
40 .collect();
41 let current_chat = CurrentChatSnapshot {
42 chat_id: current_thread.chat_id.clone(),
43 kind: current_thread.kind.clone(),
44 display_name: current_thread.display_name.clone(),
45 nickname: current_thread.nickname.clone(),
46 profile_name: current_thread.profile_name.clone(),
47 subtitle: current_thread.subtitle.clone(),
48 picture_url: current_thread.picture_url.clone(),
49 about: current_thread.about.clone(),
50 group_id: match ¤t_thread.kind {
51 ChatKind::Direct => None,
52 ChatKind::Group => Some(current_thread.chat_id.clone()),
53 },
54 member_count: current_thread.member_count,
55 message_ttl_seconds: Some(86_400),
56 is_muted: current_thread.is_muted,
57 participants: Vec::new(),
58 messages,
59 typing_indicators: Vec::new(),
60 draft: current_thread.draft.clone(),
61 is_request: current_thread.is_request,
62 };
63
64 AppState {
65 rev: 1,
66 router: Router {
67 default_screen: Screen::ChatList,
68 screen_stack: vec![
69 Screen::ChatList,
70 Screen::Chat {
71 chat_id: current_thread.chat_id.clone(),
72 },
73 ],
74 },
75 account: Some(fixture_account()),
76 device_roster: None,
77 busy: Default::default(),
78 chat_list,
79 current_chat: Some(current_chat),
80 group_details: Some(fixture_group_details(group_chat_count.max(1))),
81 public_invite: None,
82 link_device: None,
83 network_status: None,
84 mobile_push: Default::default(),
85 preferences: PreferencesSnapshot {
86 send_typing_indicators: true,
87 nearby_bluetooth_enabled: true,
88 nearby_lan_enabled: true,
89 ..PreferencesSnapshot::default()
90 },
91 toast: None,
92 }
93}
94
95#[uniffi::export]
99pub fn build_large_test_search_result(
100 query: String,
101 contact_count: u32,
102 group_count: u32,
103 message_count: u32,
104) -> SearchResultSnapshot {
105 let contact_count = contact_count.min(MAX_FIXTURE_THREADS);
106 let group_count = group_count.min(MAX_FIXTURE_THREADS);
107 let message_count = message_count.min(MAX_FIXTURE_MESSAGES);
108 let query = if query.trim().is_empty() {
109 "needle".to_string()
110 } else {
111 query
112 };
113
114 SearchResultSnapshot {
115 query: query.clone(),
116 scope_chat_id: None,
117 contacts: (0..contact_count)
118 .map(|index| {
119 let mut thread = fixture_thread(ChatKind::Direct, index);
120 thread.display_name = format!("{} Contact {:04}", title_token(&query), index + 1);
121 thread.last_message_preview =
122 Some(format!("{query} appears in contact preview {}", index + 1));
123 thread
124 })
125 .collect(),
126 groups: (0..group_count)
127 .map(|index| {
128 let mut thread = fixture_thread(ChatKind::Group, index);
129 thread.display_name = format!("{} Group {:04}", title_token(&query), index + 1);
130 thread.last_message_preview =
131 Some(format!("{query} appears in group preview {}", index + 1));
132 thread
133 })
134 .collect(),
135 messages: (0..message_count)
136 .map(|index| fixture_search_hit(&query, index))
137 .collect(),
138 shortcut: None,
139 }
140}
141
142fn fixture_account() -> AccountSnapshot {
143 AccountSnapshot {
144 public_key_hex: fixture_hex(1),
145 npub: "npub1fixtureowner".to_string(),
146 display_name: "Fixture User".to_string(),
147 picture_url: None,
148 about: None,
149 device_public_key_hex: fixture_hex(2),
150 device_npub: "npub1fixturedevice".to_string(),
151 has_owner_signing_authority: true,
152 authorization_state: DeviceAuthorizationState::Authorized,
153 }
154}
155
156fn fixture_thread(kind: ChatKind, index: u32) -> ChatThreadSnapshot {
157 let is_group = matches!(kind, ChatKind::Group);
158 let prefix = if is_group { "group" } else { "direct" };
159 let display_prefix = if is_group { "Project Group" } else { "Contact" };
160 let member_count = if is_group {
161 3 + u64::from(index % 12)
162 } else {
163 2
164 };
165
166 ChatThreadSnapshot {
167 chat_id: format!("{prefix}-{:04}", index + 1),
168 kind,
169 display_name: format!("{display_prefix} {:04}", index + 1),
170 nickname: None,
171 profile_name: None,
172 subtitle: Some(if is_group {
173 format!("{member_count} people")
174 } else {
175 format!("user {}", index + 1)
176 }),
177 picture_url: None,
178 about: None,
179 member_count,
180 last_message_preview: Some(format!(
181 "Fixture preview {} with searchable token needle",
182 index + 1
183 )),
184 last_message_at_secs: Some(BASE_TIME_SECS.saturating_sub(u64::from(index) * 60)),
185 last_message_is_outgoing: Some(index % 3 == 0),
186 last_message_delivery: Some(match index % 5 {
187 0 => DeliveryState::Seen,
188 1 => DeliveryState::Sent,
189 2 => DeliveryState::Received,
190 3 => DeliveryState::Pending,
191 _ => DeliveryState::Failed,
192 }),
193 unread_count: u64::from(index % 4),
194 is_typing: index % 17 == 0,
195 is_muted: index % 19 == 0,
196 is_pinned: index < 3,
197 draft: if index % 23 == 0 {
198 format!("draft {}", index + 1)
199 } else {
200 String::new()
201 },
202 is_request: false,
203 }
204}
205
206fn fixture_message(chat_id: &str, index: u32) -> ChatMessageSnapshot {
207 let outgoing = index % 2 == 0;
208 ChatMessageSnapshot {
209 id: format!("{chat_id}-message-{:05}", index + 1),
210 chat_id: chat_id.to_string(),
211 kind: if index % 29 == 0 {
212 ChatMessageKind::System
213 } else {
214 ChatMessageKind::User
215 },
216 author: if outgoing {
217 fixture_hex(1)
218 } else {
219 fixture_hex(10_000 + index)
220 },
221 author_owner_pubkey_hex: None,
222 author_picture_url: None,
223 body: format!(
224 "Fixture message {:05} for render and search stress with needle token",
225 index + 1
226 ),
227 attachments: Vec::new(),
228 reactions: fixture_reactions(index),
229 reactors: fixture_reactors(index),
230 is_outgoing: outgoing,
231 created_at_secs: BASE_TIME_SECS + u64::from(index),
232 expires_at_secs: if index % 31 == 0 {
233 Some(BASE_TIME_SECS + u64::from(index) + 86_400)
234 } else {
235 None
236 },
237 delivery: if outgoing {
238 DeliveryState::Seen
239 } else {
240 DeliveryState::Received
241 },
242 recipient_deliveries: Vec::new(),
243 delivery_trace: MessageDeliveryTraceSnapshot::default(),
244 source_event_id: Some(fixture_hex(50_000 + index)),
245 }
246}
247
248fn fixture_reactions(index: u32) -> Vec<MessageReactionSnapshot> {
249 if index % 4 != 0 {
250 return Vec::new();
251 }
252 vec![
253 MessageReactionSnapshot {
254 emoji: "\u{1f44d}".to_string(),
255 count: 1 + u64::from(index % 5),
256 reacted_by_me: index % 8 == 0,
257 },
258 MessageReactionSnapshot {
259 emoji: "\u{2764}\u{fe0f}".to_string(),
260 count: 1,
261 reacted_by_me: false,
262 },
263 ]
264}
265
266fn fixture_reactors(index: u32) -> Vec<MessageReactor> {
267 if index % 4 != 0 {
268 return Vec::new();
269 }
270 vec![MessageReactor {
271 author: fixture_hex(20_000 + index),
272 display_name: String::new(),
273 picture_url: None,
274 emoji: "\u{1f44d}".to_string(),
275 }]
276}
277
278fn fixture_search_hit(query: &str, index: u32) -> MessageSearchHit {
279 let kind = if index % 5 == 0 {
280 ChatKind::Group
281 } else {
282 ChatKind::Direct
283 };
284 let chat_id = match &kind {
285 ChatKind::Direct => format!("direct-{:04}", (index % 500) + 1),
286 ChatKind::Group => format!("group-{:04}", (index % 200) + 1),
287 };
288 MessageSearchHit {
289 chat_id,
290 message_id: format!("search-message-{:05}", index + 1),
291 chat_display_name: match &kind {
292 ChatKind::Direct => format!("Contact {:04}", (index % 500) + 1),
293 ChatKind::Group => format!("Project Group {:04}", (index % 200) + 1),
294 },
295 chat_picture_url: None,
296 chat_kind: kind,
297 author_pubkey: fixture_hex(30_000 + index),
298 body: format!("{query} search fixture message body {:05}", index + 1),
299 is_outgoing: index % 2 == 0,
300 created_at_secs: BASE_TIME_SECS.saturating_sub(u64::from(index) * 30),
301 }
302}
303
304fn fixture_group_details(group_count: u32) -> GroupDetailsSnapshot {
305 let member_count = group_count.min(32).max(3);
306 GroupDetailsSnapshot {
307 group_id: "group-0001".to_string(),
308 name: "Project Group 0001".to_string(),
309 picture_url: None,
310 about: None,
311 created_by_display_name: "Fixture User".to_string(),
312 created_by_npub: "npub1fixtureowner".to_string(),
313 can_manage: true,
314 is_muted: false,
315 revision: 1,
316 members: (0..member_count)
317 .map(|index| GroupMemberSnapshot {
318 owner_pubkey_hex: fixture_hex(40_000 + index),
319 display_name: format!("Member {:02}", index + 1),
320 npub: format!("npub1fixturemember{:02}", index + 1),
321 picture_url: None,
322 is_admin: index < 2,
323 is_creator: index == 0,
324 is_local_owner: index == 0,
325 })
326 .collect(),
327 }
328}
329
330fn fixture_hex(seed: u32) -> String {
331 format!("{seed:064x}")
332}
333
334fn title_token(query: &str) -> String {
335 let trimmed = query.trim();
336 let mut chars = trimmed.chars();
337 match chars.next() {
338 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
339 None => "Needle".to_string(),
340 }
341}