Skip to main content

hashiverse_client_wasm/
hashiverse_client_wasm.rs

1use crate::wasm_client_storage::WasmClientStorage;
2use crate::wasm_key_locker::WasmKeyLockerManager;
3use crate::wasm_transport::WasmTransportFactory;
4use crate::wasm_try;
5use hashiverse_lib::client::args::Args;
6use hashiverse_lib::client::hashiverse_client::HashiverseClient;
7use hashiverse_lib::client::key_locker::key_locker::KeyLockerManager;
8use hashiverse_lib::tools::buckets::{BucketLocation, BucketType};
9use hashiverse_lib::tools::time::TimeMillis;
10use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
11use hashiverse_lib::tools::pow_generator::pow_generator::PowGenerator;
12use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
13use hashiverse_lib::tools::runtime_services::RuntimeServices;
14use hashiverse_lib::tools::types::Id;
15use log::warn;
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use anyhow::anyhow;
19use tsify::Tsify;
20use wasm_bindgen::prelude::*;
21use wasm_bindgen::JsValue;
22use bytes::Bytes;
23use hashiverse_lib::protocol::posting::encoded_post::EncodedPostV1;
24
25#[wasm_bindgen]
26/// Provides a simplified dispatch interface for [HashiverseClient] to the browser.
27pub struct HashiverseClientWasm {
28    logged_in: bool,
29    hashiverse_client: HashiverseClient,
30}
31
32#[wasm_bindgen]
33impl HashiverseClientWasm {
34    async fn create_from_xxx(logged_in: bool, key_locker: Arc<dyn hashiverse_lib::client::key_locker::key_locker::KeyLocker>) -> anyhow::Result<Self> {
35        let time_provider: Arc<dyn hashiverse_lib::tools::time_provider::time_provider::TimeProvider> = Arc::new(RealTimeProvider::default());
36        let transport_factory: Arc<dyn hashiverse_lib::transport::transport::TransportFactory> = Arc::new(WasmTransportFactory::default());
37        let client_storage = WasmClientStorage::new().await?;
38        let pow_generator: Arc<dyn PowGenerator> = match crate::get_wasm_parallel_pow_generator() {
39            Some(g) => g as Arc<dyn PowGenerator>,
40            None => {
41                warn!("No native PoW generator available, falling back to SingleThreadedPowGenerator");
42                Arc::new(SingleThreadedPowGenerator::new())
43            }
44        };
45        let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
46        let hashiverse_client = HashiverseClient::new(runtime_services, client_storage, key_locker, Args::new()).await?;
47        Ok(Self { logged_in, hashiverse_client })
48    }
49
50    #[wasm_bindgen]
51    pub async fn create_from_keyphrase(key_phrase: String) -> Result<Self, JsValue> {
52        wasm_try!({
53            let logged_in = !key_phrase.is_empty();
54            let key_locker_manager = WasmKeyLockerManager::new().await?;
55            let key_locker = key_locker_manager.create(key_phrase).await?;
56            Self::create_from_xxx(logged_in, key_locker).await?
57        })
58    }
59
60    #[wasm_bindgen]
61    pub async fn create_from_stored_key(client_id_hex: String) -> Result<Self, JsValue> {
62        wasm_try!({
63            let key_locker_manager = WasmKeyLockerManager::new().await?;
64            let key_locker = key_locker_manager.switch(client_id_hex).await?;
65            Self::create_from_xxx(true, key_locker).await?
66        })
67    }
68
69    #[wasm_bindgen]
70    pub fn logged_in(&self) -> bool {
71        self.logged_in
72    }
73
74    #[wasm_bindgen]
75    pub async fn list_stored_key_ids_v1(&self) -> Result<Vec<String>, JsValue> {
76        wasm_try!({
77            let key_locker_manager = WasmKeyLockerManager::new().await?;
78            key_locker_manager.list().await?
79        })
80    }
81
82    #[wasm_bindgen]
83    pub async fn delete_stored_key_v1(&self, key_public: String) -> Result<(), JsValue> {
84        wasm_try!({
85            let key_locker_manager = WasmKeyLockerManager::new().await?;
86            key_locker_manager.delete(key_public).await?;
87        })
88    }
89
90    #[wasm_bindgen]
91    pub async fn delete_all_stored_keys_v1(&self) -> Result<(), JsValue> {
92        wasm_try!({
93            let key_locker_manager = WasmKeyLockerManager::new().await?;
94            key_locker_manager.reset().await?;
95        })
96    }
97
98    #[wasm_bindgen]
99    pub fn get_client_id(&self) -> String {
100        self.hashiverse_client.client_id().id_hex()
101    }
102
103    #[wasm_bindgen]
104    pub async fn client_storage_reset(&self) -> Result<(), JsValue> {
105        wasm_try!({
106            self.hashiverse_client.client_storage_reset().await?;
107        })
108    }
109
110    #[wasm_bindgen]
111    pub async fn post_v1(&self, post: &str) -> Result<Post, JsValue> {
112        self.post_v2(post, true).await
113    }
114
115    /// Submit a post. With `wait_for_all_submissions == false` this returns as soon as the post
116    /// secures its first User-bucket commit, finishing the remaining redundancy and secondary
117    /// buckets on a background task (visible via the PoW busy indicator).
118    #[wasm_bindgen]
119    pub async fn post_v2(&self, post: &str, wait_for_all_submissions: bool) -> Result<Post, JsValue> {
120        wasm_try!({
121            let (commit_tokens, (encoded_post, raw_bytes)) = self.hashiverse_client.submit_post_with_wait(post, wait_for_all_submissions).await?;
122            let bucket_location = &commit_tokens[0].bucket_location;
123            let client_id = encoded_post.header.client_id()?;
124            let encoded_post_header_hex = hex::encode(EncodedPostV1::bytes_without_body(raw_bytes)?);
125            Post {
126                post_id: encoded_post.post_id.to_hex_str(),
127                time_millis: encoded_post.header.time_millis.0,
128                client_id: client_id.id_hex(),
129                bucket_location: bucket_location.to_html_attr(),
130                post: encoded_post.post,
131                encoded_post_header_hex,
132                healed: false,
133            }
134        })
135    }
136
137    fn meta_post_manager(&self) -> &hashiverse_lib::client::meta_post::meta_post_manager::MetaPostManager {
138        self.hashiverse_client.meta_post_manager()
139    }
140
141    pub async fn set_bio(&self, nickname: String, status: String, selfie: String, avatar: String) -> Result<(), JsValue> {
142        wasm_try!({
143            self.meta_post_manager().set_bio(nickname, status, selfie, avatar).await?;
144        })
145    }
146
147    #[wasm_bindgen]
148    pub async fn submit_feedback_v1(&self, bucket_location: String, post_id: String, feedback_type: u8) -> Result<(), JsValue> {
149        wasm_try!({
150            let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
151            let post_id = Id::from_hex_str(&post_id)?;
152            self.hashiverse_client.submit_feedback(bucket_location, post_id, feedback_type).await?;
153        })
154    }
155
156    #[wasm_bindgen]
157    /// Gets a specific post
158    pub async fn get_post_v1(&self, bucket_location: String, post_id: String) -> Result<Post, JsValue> {
159        wasm_try!({
160            let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
161            let post_id = Id::from_hex_str(&post_id)?;
162            let (bucket_location, post, raw_bytes, healed) = self.hashiverse_client.get_post(bucket_location, &post_id).await?;
163            let client_id = post.header.client_id()?;
164            let encoded_post_header_hex = hex::encode(EncodedPostV1::bytes_without_body(raw_bytes)?);
165            Post {
166                post_id: post.post_id.to_hex_str(),
167                time_millis: post.header.time_millis.0,
168                client_id: client_id.id_hex(),
169                bucket_location: bucket_location.to_html_attr(),
170                post: post.post,
171                encoded_post_header_hex,
172                healed,
173            }
174        })
175    }
176
177    #[wasm_bindgen]
178    /// Gets all the feedbacks for a specific post
179    ///
180    /// The resulting vector has 256 entries - one per feedback_type that have been mapped to the statistical number of clicks a feedback button has received.
181    pub async fn get_post_feedbacks_v1(&self, bucket_location: String, post_id: String) -> Result<Vec<u32>, JsValue> {
182        wasm_try!({
183            let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
184            let post_id = Id::from_hex_str(&post_id)?;
185            let post_feedbacks = self.hashiverse_client.get_post_feedbacks(bucket_location, post_id).await?;
186            post_feedbacks.iter().map(|&feedback| feedback.min(u32::MAX as u64) as u32).collect()
187        })
188    }
189
190    #[wasm_bindgen]
191    pub async fn get_bio(&self, id: String) -> Result<Bio, JsValue> {
192        wasm_try!({
193            let meta_post_public = self.meta_post_manager().get_meta_post_public(Id::from_hex_str(&id)?).await?;
194            match meta_post_public {
195                Some(meta_post_public) => Bio {
196                    client_id: id,
197                    nickname: meta_post_public.nickname.value.unwrap_or_default(),
198                    status: meta_post_public.status.value.unwrap_or_default(),
199                    selfie: meta_post_public.selfie.value.unwrap_or_default(),
200                    avatar: meta_post_public.avatar.value.unwrap_or_default(),
201                },
202                None => Bio {
203                    client_id: id,
204                    nickname: "".to_string(),
205                    status: "".to_string(),
206                    selfie: "".to_string(),
207                    avatar: "".to_string(),
208                },
209            }
210        })
211    }
212
213    #[wasm_bindgen]
214    pub async fn get_all_bios(&self) -> Result<Vec<Bio>, JsValue> {
215        wasm_try!({
216            let meta_post_publics = self.meta_post_manager().get_all_meta_post_publics().await?;
217            meta_post_publics.into_iter()
218                .map(|(client_id, meta_post_public)| Bio {
219                    client_id,
220                    nickname: meta_post_public.nickname.value.unwrap_or_default(),
221                    status: meta_post_public.status.value.unwrap_or_default(),
222                    selfie: meta_post_public.selfie.value.unwrap_or_default(),
223                    avatar: meta_post_public.avatar.value.unwrap_or_default(),
224                })
225                .collect()
226        })
227    }
228
229    #[wasm_bindgen]
230    pub async fn get_all_known_peers_v1(&self) -> Result<Vec<PeerInfoV1>, JsValue> {
231        wasm_try!({
232            self.hashiverse_client.get_all_known_peers().await
233                .into_iter()
234                .map(|peer| PeerInfoV1 {
235                    peer_id_hex: peer.id.to_hex_str(),
236                    address: peer.address,
237                    version: peer.version,
238                    timestamp_millis: peer.timestamp.0,
239                    pow_initial: peer.pow_initial.pow.0,
240                    pow_current_day: peer.pow_current_day.pow.0,
241                    pow_current_month: peer.pow_current_month.pow.0,
242                })
243                .collect::<Vec<_>>()
244        })
245    }
246
247    #[wasm_bindgen]
248    pub async fn get_peer_stats_v1(&self, peer_id_hex: String) -> Result<JsValue, JsValue> {
249        wasm_try!({
250            let peer_id = Id::from_hex_str(&peer_id_hex)?;
251            let doc = self.hashiverse_client.fetch_peer_stats(&peer_id).await?;
252            let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
253            doc.serialize(&serializer).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?
254        })
255    }
256
257    #[wasm_bindgen]
258    pub async fn get_active_pow_jobs_v1(&self) -> Result<Vec<PowJobStatusV1>, JsValue> {
259        wasm_try!({
260            self.hashiverse_client.active_pow_jobs()
261                .into_iter()
262                .map(|job| PowJobStatusV1 {
263                    label: job.label,
264                    pow_min: job.pow_min.0,
265                    best_pow_so_far: job.best_pow_so_far.0,
266                })
267                .collect::<Vec<_>>()
268        })
269    }
270
271    /// Whether there is background PoW work happening now, or within the last `within_millis`.
272    /// Polled by the GUI to show/hide the "busy" indicator.
273    #[wasm_bindgen]
274    pub async fn is_pow_busy_v1(&self, within_millis: u32) -> Result<bool, JsValue> {
275        wasm_try!({ self.hashiverse_client.is_pow_busy(within_millis as i64) })
276    }
277
278    fn post_process_timeline_posts(&self, encoded_posts: Vec<(BucketLocation, EncodedPostV1, Bytes, bool)>, oldest_processed_time_millis: TimeMillis) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
279        let response = SingleTimelineGetMoreV1Response {
280            oldest_processed_time_millis: if oldest_processed_time_millis == TimeMillis::MAX { None } else { Some(oldest_processed_time_millis.0) },
281            posts: encoded_posts
282                .into_iter()
283                .filter_map(|(bucket_location, post, raw_bytes, healed)| {
284                    let client_id = match post.header.client_id() {
285                        Ok(client_id) => client_id,
286                        Err(e) => {
287                            warn!("Skipping post with bad client_id in header: {}", e);
288                            return None;
289                        }
290                    };
291                    let encoded_post_header_hex = match EncodedPostV1::bytes_without_body(raw_bytes) {
292                        Ok(header_bytes) => hex::encode(header_bytes),
293                        Err(e) => {
294                            warn!("Skipping post with bad header bytes: {}", e);
295                            return None;
296                        }
297                    };
298                    Some(Post {
299                        post_id: post.post_id.to_hex_str(),
300                        time_millis: post.header.time_millis.0,
301                        client_id: client_id.id_hex(),
302                        bucket_location: bucket_location.to_html_attr(),
303                        post: post.post,
304                        encoded_post_header_hex,
305                        healed,
306                    })
307                })
308                .collect(),
309        };
310
311        Ok(response)
312    }
313
314
315    #[wasm_bindgen]
316    pub async fn single_timeline_reset(&self) -> Result<(), JsValue> {
317        wasm_try!({
318            self.hashiverse_client.single_timeline_reset().await?;
319        })
320    }
321
322    async fn single_timeline_get_more(&self, bucket_type: BucketType, base_id: &Id) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
323        let (encoded_posts, oldest_processed_time_millis) = self.hashiverse_client.single_timeline_get_more(bucket_type, base_id).await?;
324        self.post_process_timeline_posts(encoded_posts, oldest_processed_time_millis)
325    }
326
327    #[wasm_bindgen]
328    pub async fn single_timeline_get_more_me_v1(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
329        wasm_try!({
330            let id = self.hashiverse_client.client_id().id;
331            self.single_timeline_get_more(BucketType::User, &id).await?
332        })
333    }
334
335    #[wasm_bindgen]
336    pub async fn single_timeline_get_more_me_mentioned_v1(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
337        wasm_try!({
338            let id = self.hashiverse_client.client_id().id;
339            self.single_timeline_get_more(BucketType::Mention, &id).await?
340        })
341    }
342
343    #[wasm_bindgen]
344    pub async fn single_timeline_get_more_hashtag_v1(&self, hashtag: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
345        wasm_try!({
346            let id = Id::from_hashtag_str(&hashtag)?;
347            self.single_timeline_get_more(BucketType::Hashtag, &id).await?
348        })
349    }
350
351    #[wasm_bindgen]
352    pub async fn single_timeline_get_more_user_v1(&self, client_id_hex: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
353        wasm_try!({
354            let id = Id::from_hex_str(&client_id_hex)?;
355            self.single_timeline_get_more(BucketType::User, &id).await?
356        })
357    }
358
359    #[wasm_bindgen]
360    pub async fn single_timeline_get_more_user_mentioned_v1(&self, client_id_hex: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
361        wasm_try!({
362            let id = Id::from_hex_str(&client_id_hex)?;
363            self.single_timeline_get_more(BucketType::Mention, &id).await?
364        })
365    }
366
367    #[wasm_bindgen]
368    pub async fn single_timeline_get_more_reply_to_post_v1(&self, post_id: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
369        wasm_try!({
370            let id = Id::from_hex_str(&post_id)?;
371            self.single_timeline_get_more(BucketType::ReplyToPost, &id).await?
372        })
373    }
374
375    #[wasm_bindgen]
376    pub async fn single_timeline_get_more_sequel_v1(&self, post_id: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
377        wasm_try!({
378            let id = Id::from_hex_str(&post_id)?;
379            self.single_timeline_get_more(BucketType::Sequel, &id).await?
380        })
381    }
382
383    #[wasm_bindgen]
384    pub async fn multiple_timeline_reset(&self) -> Result<(), JsValue> {
385        wasm_try!({
386            self.hashiverse_client.multiple_timeline_reset().await?;
387        })
388    }
389
390    async fn multiple_timeline_get_more(&self, bucket_type: BucketType, base_ids: &Vec<Id>) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
391        let (encoded_posts, oldest_processed_time_millis) = self.hashiverse_client.multiple_timeline_get_more(bucket_type, base_ids).await?;
392        self.post_process_timeline_posts(encoded_posts, oldest_processed_time_millis)
393    }
394
395    #[wasm_bindgen]
396    pub async fn multiple_timeline_get_more_followed_users(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
397        wasm_try!({
398            let ids = self.meta_post_manager().get_followed_client_ids().await?;
399            self.multiple_timeline_get_more(BucketType::User, &ids).await?
400        })
401    }
402
403    #[wasm_bindgen]
404    pub async fn get_followed_client_ids_v1(&self) -> Result<Vec<String>, JsValue> {
405        wasm_try!({
406            let ids = self.meta_post_manager().get_followed_client_ids().await?;
407            ids.into_iter().map(|id| id.to_hex_str()).collect()
408        })
409    }
410
411    #[wasm_bindgen]
412    pub async fn set_followed_client_ids_v1(&self, client_ids: JsValue) -> Result<(), JsValue> {
413        wasm_try!({
414            let client_id_strs: Vec<String> = serde_wasm_bindgen::from_value(client_ids).map_err(|e| anyhow!("serde_wasm_bindgen::from_value error: {}", e))?;
415            let ids = client_id_strs.iter().map(|s| Id::from_hex_str(s)).collect::<anyhow::Result<Vec<_>>>()?;
416            self.meta_post_manager().set_followed_client_ids(ids).await?;
417        })
418    }
419
420    #[wasm_bindgen]
421    pub async fn set_followed_client_id_v1(&self, client_id: String, is_followed: bool) -> Result<(), JsValue> {
422        wasm_try!({
423            let id = Id::from_hex_str(&client_id)?;
424            self.meta_post_manager().set_followed_client_id(id, is_followed).await?;
425        })
426    }
427
428    #[wasm_bindgen]
429    pub async fn multiple_timeline_get_more_followed_hashtags(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
430        wasm_try!({
431            let hashtags = self.meta_post_manager().get_followed_hashtags().await?;
432            let ids = hashtags.iter().map(|h| Id::from_hashtag_str(h)).collect::<anyhow::Result<Vec<_>>>()?;
433            self.multiple_timeline_get_more(BucketType::Hashtag, &ids).await?
434        })
435    }
436
437    #[wasm_bindgen]
438    pub async fn get_followed_hashtags_v1(&self) -> Result<Vec<String>, JsValue> {
439        wasm_try!({
440            self.meta_post_manager().get_followed_hashtags().await?
441        })
442    }
443
444    #[wasm_bindgen]
445    pub async fn set_followed_hashtags_v1(&self, hashtags: JsValue) -> Result<(), JsValue> {
446        wasm_try!({
447            let hashtags: Vec<String> = serde_wasm_bindgen::from_value(hashtags).map_err(|e| anyhow!("serde_wasm_bindgen::from_value error: {}", e))?;
448            self.meta_post_manager().set_followed_hashtags(hashtags).await?;
449        })
450    }
451
452    #[wasm_bindgen]
453    pub async fn set_followed_hashtag_v1(&self, hashtag: String, is_followed: bool) -> Result<(), JsValue> {
454        wasm_try!({
455            self.meta_post_manager().set_followed_hashtag(hashtag, is_followed).await?;
456        })
457    }
458
459    // ------------------------------------------------------------------
460    // MetaPostV1 — unified config publish
461    // ------------------------------------------------------------------
462
463    #[wasm_bindgen]
464    pub async fn submit_meta_post_v1(&self) -> Result<(), JsValue> {
465        wasm_try!({
466            self.hashiverse_client.submit_meta_post().await?;
467        })
468    }
469
470    #[wasm_bindgen]
471    pub async fn ensure_meta_post_in_current_bucket_v1(&self) -> Result<(), JsValue> {
472        wasm_try!({
473            self.hashiverse_client.ensure_meta_post_in_current_bucket().await?;
474        })
475    }
476
477    // ------------------------------------------------------------------
478    // Content thresholds
479    // ------------------------------------------------------------------
480
481    #[wasm_bindgen]
482    pub async fn get_content_thresholds_v1(&self) -> Result<JsValue, JsValue> {
483        wasm_try!({
484            let thresholds = self.meta_post_manager().get_content_thresholds().await?;
485            // serde_wasm_bindgen serializes HashMaps as JS Map (not plain object) and rejects
486            // non-string keys with serialize_maps_as_objects.  Convert to String keys so the
487            // result is a plain JS object matching TS Record<number, number>.
488            let thresholds_js: std::collections::HashMap<String, u32> = thresholds.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
489            let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
490            thresholds_js.serialize(&serializer).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?
491        })
492    }
493
494    #[wasm_bindgen]
495    pub async fn set_content_thresholds_v1(&self, thresholds: JsValue) -> Result<(), JsValue> {
496        wasm_try!({
497            let thresholds_str: std::collections::HashMap<String, u32> = serde_wasm_bindgen::from_value(thresholds).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?;
498            let thresholds: std::collections::HashMap<u8, u32> = thresholds_str.into_iter()
499                .map(|(k, v)| Ok((k.parse::<u8>().map_err(|e| anyhow!("invalid feedback_type key: {}", e))?, v)))
500                .collect::<anyhow::Result<_>>()?;
501            self.meta_post_manager().set_content_thresholds(thresholds).await?;
502        })
503    }
504
505    // ------------------------------------------------------------------
506    // Skip warnings for followed
507    // ------------------------------------------------------------------
508
509    #[wasm_bindgen]
510    pub async fn get_skip_warnings_for_followed_v1(&self) -> Result<bool, JsValue> {
511        wasm_try!({
512            self.meta_post_manager().get_skip_warnings_for_followed().await?
513        })
514    }
515
516    #[wasm_bindgen]
517    pub async fn set_skip_warnings_for_followed_v1(&self, value: bool) -> Result<(), JsValue> {
518        wasm_try!({
519            self.meta_post_manager().set_skip_warnings_for_followed(value).await?;
520        })
521    }
522
523    #[wasm_bindgen]
524    pub async fn fetch_url_preview_v1(&self, url: String) -> Result<UrlPreview, JsValue> {
525        wasm_try!({
526            let preview = self.hashiverse_client.fetch_url_preview(&url).await?;
527            UrlPreview {
528                url: preview.url,
529                title: preview.title,
530                description: preview.description,
531                image_url: preview.image_url,
532            }
533        })
534    }
535
536    #[wasm_bindgen]
537    pub async fn fetch_trending_hashtags_v1(&self, limit: u16) -> Result<TrendingHashtagsFetchResponse, JsValue> {
538        wasm_try!({
539            let response = self.hashiverse_client.fetch_trending_hashtags(limit).await?;
540            TrendingHashtagsFetchResponse {
541                trending_hashtags: response.trending_hashtags.into_iter().map(|entry| TrendingHashtag {
542                    hashtag: entry.hashtag,
543                    count: entry.count,
544                }).collect(),
545            }
546        })
547    }
548}
549
550#[derive(Tsify, Serialize, Deserialize)]
551#[tsify(into_wasm_abi)]
552pub struct SingleTimelineGetMoreV1Response {
553    pub posts: Vec<Post>,
554    pub oldest_processed_time_millis: Option<i64>,
555}
556
557#[derive(Tsify, Serialize, Deserialize)]
558#[tsify(into_wasm_abi)]
559pub struct Post {
560    pub post_id: String,
561    pub time_millis: i64,
562    pub client_id: String,
563    pub bucket_location: String,
564    pub post: String,
565    pub encoded_post_header_hex: String, // contains the hex-encoded EncodedPost without the post body
566    pub healed: bool, // true if the bundle header marks this post as healed (re-uploaded after loss); the displayed time may be inaccurate
567}
568
569#[derive(Tsify, Serialize, Deserialize)]
570#[tsify(into_wasm_abi)]
571pub struct Bio {
572    pub client_id: String,
573    pub nickname: String,
574    pub status: String,
575    pub selfie: String,
576    pub avatar: String,
577}
578
579#[derive(Tsify, Serialize, Deserialize)]
580#[tsify(into_wasm_abi)]
581pub struct UrlPreview {
582    pub url: String,
583    pub title: String,
584    pub description: String,
585    pub image_url: String,
586}
587
588#[derive(Tsify, Serialize, Deserialize)]
589#[tsify(into_wasm_abi)]
590pub struct TrendingHashtag {
591    pub hashtag: String,
592    pub count: u64,
593}
594
595#[derive(Tsify, Serialize, Deserialize)]
596#[tsify(into_wasm_abi)]
597pub struct TrendingHashtagsFetchResponse {
598    pub trending_hashtags: Vec<TrendingHashtag>,
599}
600
601#[derive(Tsify, Serialize, Deserialize)]
602#[tsify(into_wasm_abi)]
603pub struct PeerInfoV1 {
604    pub peer_id_hex: String,
605    pub address: String,
606    pub version: String,
607    pub timestamp_millis: i64,
608    pub pow_initial: u8,
609    pub pow_current_day: u8,
610    pub pow_current_month: u8,
611}
612
613#[derive(Tsify, Serialize, Deserialize)]
614#[tsify(into_wasm_abi)]
615pub struct PowJobStatusV1 {
616    pub label: String,
617    pub pow_min: u8,
618    pub best_pow_so_far: u8,
619}