From 6968821f9752e2cd99b0a9a1f01790a0e9c482be Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 09:08:51 +0100 Subject: [PATCH 01/22] cleanup --- src/backend/updates.rs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/backend/updates.rs b/src/backend/updates.rs index d1e244da..b6e822a8 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -109,22 +109,7 @@ fn post_upgrade() { } #[allow(clippy::all)] -fn sync_post_upgrade_fixtures() { - mutate(|state| { - for u in state.users.values_mut() { - // Clear feeds if they exceed 1000 chars in total - if u.feeds - .iter() - .flat_map(|feed| feed.iter()) - .map(|tag| tag.len()) - .sum::() - >= 1000 - { - u.feeds.clear(); - } - } - }) -} +fn sync_post_upgrade_fixtures() {} #[allow(clippy::all)] async fn async_post_upgrade_fixtures() {} From fdc56967eec4fbc948622ea644807fceeaaf8917 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 11:12:40 +0100 Subject: [PATCH 02/22] don't call backup if stat page neq 0 --- backup.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backup.sh b/backup.sh index b0e91723..e504405d 100755 --- a/backup.sh +++ b/backup.sh @@ -27,7 +27,9 @@ if [ "$CMD" == "restore" ]; then else echo "Running backup to $DIR..." git rev-parse HEAD > $DIR/commit.txt - dfx canister --network ic call taggr backup + if [ "$PAGE_START" -eq 0 ]; then + dfx canister --network ic call taggr backup + fi $BACKUP $DIR backup "6qfxa-ryaaa-aaaai-qbhsq-cai" $PAGE_START fi From 1b95acb44494dd98845b4e7a960f324adc9ea263 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 11:32:43 +0100 Subject: [PATCH 03/22] hide feauters older than 1y --- src/backend/env/features.rs | 53 ++++++++++++++++++++++--------------- src/backend/env/mod.rs | 1 + src/backend/queries.rs | 8 +----- src/backend/updates.rs | 4 +-- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/backend/env/features.rs b/src/backend/env/features.rs index 90cc3f4a..b4bce0ef 100644 --- a/src/backend/env/features.rs +++ b/src/backend/env/features.rs @@ -3,7 +3,10 @@ use std::collections::HashSet; use candid::Principal; use serde::{Deserialize, Serialize}; -use crate::mutate; +use crate::{ + env::{post::Meta, Time, YEAR}, + mutate, +}; use super::{ post::{Extension, Post, PostId}, @@ -18,47 +21,52 @@ pub struct Feature { pub supporters: HashSet, // 0: requested, 1: implemented pub status: u8, + #[serde(default)] + pub last_activity: Time, } /// Returns a list of all feature ids and current collective voting power of all supporters. pub fn features<'a>( state: &'a State, ids: &'a [PostId], -) -> Box + 'a> { - let count_support = move |(post_id, feature): (&PostId, Feature)| { - ( - *post_id, - feature - .supporters - .iter() - .map(|user_id| { - state - .users - .get(user_id) - .map(|user| user.total_balance()) - .unwrap_or_default() - }) - .sum::(), - feature, - ) + now: Time, +) -> Box), Token, Feature)> + 'a> { + let transform_feature = move |(post_id, feature): (&PostId, Feature)| { + if feature.last_activity + YEAR <= now { + return None; + } + let tokens = feature + .supporters + .iter() + .map(|user_id| { + state + .users + .get(user_id) + .map(|user| user.total_balance()) + .unwrap_or_default() + }) + .sum::(); + Post::get(state, post_id).map(|post| (post.with_meta(state), tokens, feature)) }; + if !ids.is_empty() { return Box::new( ids.iter() .filter_map(move |id| state.memory.features.get(id).map(|feature| (id, feature))) - .map(count_support), + .filter_map(transform_feature), ); } - Box::new(state.memory.features.iter().map(count_support)) + Box::new(state.memory.features.iter().filter_map(transform_feature)) } -pub fn toggle_feature_support(caller: Principal, post_id: PostId) -> Result<(), String> { +pub fn toggle_feature_support(caller: Principal, post_id: PostId, now: Time) -> Result<(), String> { mutate(|state| { let user_id = state.principal_to_user(caller).ok_or("no user found")?.id; let mut feature = state.memory.features.remove(&post_id)?; if feature.supporters.contains(&user_id) { feature.supporters.remove(&user_id); } else { + feature.last_activity = now; feature.supporters.insert(user_id); } state @@ -71,7 +79,7 @@ pub fn toggle_feature_support(caller: Principal, post_id: PostId) -> Result<(), }) } -pub fn create_feature(caller: Principal, post_id: PostId) -> Result<(), String> { +pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<(), String> { mutate(|state| { let user = state.principal_to_user(caller).ok_or("no user found")?; let user_name = user.name.clone(); @@ -97,6 +105,7 @@ pub fn create_feature(caller: Principal, post_id: PostId) -> Result<(), String> Feature { supporters: Default::default(), status: 0, + last_activity: now, }, ) .expect("couldn't persist feature"); diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index e66343fd..b14c3e7a 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -69,6 +69,7 @@ pub const MINUTE: u64 = 60 * SECOND; pub const HOUR: u64 = 60 * MINUTE; pub const DAY: u64 = 24 * HOUR; pub const WEEK: u64 = 7 * DAY; +pub const YEAR: u64 = 52 * WEEK; pub const MAX_USER_ID: UserId = 9_007_199_254_740_991; // Number.MAX_SAFE_INTEGER in JS diff --git a/src/backend/queries.rs b/src/backend/queries.rs index c34bdb07..58e8a648 100644 --- a/src/backend/queries.rs +++ b/src/backend/queries.rs @@ -48,13 +48,7 @@ fn auction() { fn features() { read(|state| { let ids: Vec = parse(&arg_data_raw()); - reply( - features::features(state, &ids) - .map(|(post_id, tokens, feature)| { - Post::get(state, &post_id).map(|post| (post.with_meta(state), tokens, feature)) - }) - .collect::>(), - ) + reply(features::features(state, &ids, time()).collect::>()) }); } diff --git a/src/backend/updates.rs b/src/backend/updates.rs index b6e822a8..489ae322 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -290,13 +290,13 @@ fn update_wallet_tokens() { #[export_name = "canister_update create_feature"] fn create_feature() { let post_id: PostId = parse(&arg_data_raw()); - reply(features::create_feature(read(caller), post_id)); + reply(features::create_feature(read(caller), post_id, time())); } #[export_name = "canister_update toggle_feature_support"] fn toggle_feature_support() { let post_id: PostId = parse(&arg_data_raw()); - reply(features::toggle_feature_support(read(caller), post_id)); + reply(features::toggle_feature_support(read(caller), post_id, time())); } #[export_name = "canister_update create_user"] From 9eb8ea89544edf7950861213683f6b3956cb6d38 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 12:24:11 +0100 Subject: [PATCH 04/22] post system message on realm creation --- src/backend/env/features.rs | 22 ++++++++++----------- src/backend/env/mod.rs | 8 ++++++++ src/backend/env/realms.rs | 39 ++++++++++++++++++++----------------- src/backend/env/reports.rs | 17 ++++++---------- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/backend/env/features.rs b/src/backend/env/features.rs index b4bce0ef..2a0383e5 100644 --- a/src/backend/env/features.rs +++ b/src/backend/env/features.rs @@ -84,14 +84,11 @@ pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<( let user = state.principal_to_user(caller).ok_or("no user found")?; let user_name = user.name.clone(); - if !Post::get(state, &post_id) - .map(|post| { - post.user == user.id && matches!(post.extension.as_ref(), Some(&Extension::Feature)) - }) - .unwrap_or_default() - { + let post = Post::get(state, &post_id).ok_or("post not found")?; + if post.user != user.id || !matches!(post.extension.as_ref(), Some(&Extension::Feature)) { return Err("no post with a feature found".into()); - }; + } + let realm = post.realm.clone(); if state.memory.features.get(&post_id).is_some() { return Err("feature already exists".into()); @@ -110,10 +107,13 @@ pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<( ) .expect("couldn't persist feature"); - state.logger.info(format!( - "@{} created a [new feature](#/post/{})", - user_name, post_id - )); + let _ = state.system_message( + format!( + "A [new feature](#/post/{}) was created by `@{}`", + post_id, user_name + ), + realm, + ); Ok(()) }) diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index b14c3e7a..a721ef83 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -890,6 +890,14 @@ impl State { Ok(()) } + pub fn system_message( + &mut self, + body: String, + realm: Option, + ) -> Result { + Post::create(self, body, Default::default(), id(), time(), None, realm, None) + } + pub fn create_invite( &mut self, principal: Principal, diff --git a/src/backend/env/realms.rs b/src/backend/env/realms.rs index 77187538..e731454c 100644 --- a/src/backend/env/realms.rs +++ b/src/backend/env/realms.rs @@ -148,10 +148,13 @@ pub fn create_realm( state.realms.insert(realm_id.clone(), realm); - state.logger.info(format!( - "@{} created realm [{1}](/#/realm/{1}) 🏰", - user_name, realm_id - )); + let _ = state.system_message( + format!( + "Realm [{}](/#/realm/{0}) was created by `@{}`", + realm_id, user_name + ), + Some(realm_id.clone()), + ); Ok(()) } @@ -535,7 +538,7 @@ pub(crate) mod tests { None, ) .unwrap(); - assert_eq!(state.realms.get(&name).unwrap().posts.len(), 1); + assert_eq!(state.realms.get(&name).unwrap().posts.len(), 2); assert_eq!( Post::get(state, &post_id).unwrap().realm, @@ -570,7 +573,7 @@ pub(crate) mod tests { None, None ), - Ok(2) + Ok(3) ); assert!(state.toggle_realm_membership(p0, name.clone())); @@ -587,10 +590,10 @@ pub(crate) mod tests { None, None ), - Ok(3) + Ok(4) ); - assert!(realm_posts(state, &name).contains(&2)); + assert!(realm_posts(state, &name).contains(&3)); // Create post without a realm @@ -660,7 +663,7 @@ pub(crate) mod tests { assert!(state.users.get(&_u1).unwrap().realms.contains(&name)); assert_eq!(state.realms.get(&realm_name).unwrap().num_members, 1); - assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 0); + assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 1); assert_eq!( Post::create( @@ -673,9 +676,9 @@ pub(crate) mod tests { Some(realm_name.clone()), None ), - Ok(6) + Ok(8) ); - assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 1); + assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 2); assert!(state .users @@ -689,7 +692,7 @@ pub(crate) mod tests { // Move the post to non-joined realm assert_eq!( Post::edit( - 6, + 8, "changed".to_string(), vec![], "".to_string(), @@ -702,12 +705,12 @@ pub(crate) mod tests { ); read(|state| { - assert_eq!(Post::get(state, &6).unwrap().realm, Some(realm_name)); - assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 1); + assert_eq!(Post::get(state, &8).unwrap().realm, Some(realm_name)); + assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 2); }); assert_eq!( Post::edit( - 6, + 8, "changed".to_string(), vec![], "".to_string(), @@ -720,10 +723,10 @@ pub(crate) mod tests { ); read(|state| { - assert_eq!(state.realms.get("NEW_REALM").unwrap().posts.len(), 0); - assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 2); + assert_eq!(state.realms.get("NEW_REALM").unwrap().posts.len(), 1); + assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 3); assert_eq!( - Post::get(state, &6).unwrap().realm, + Post::get(state, &8).unwrap().realm, Some("TAGGRDAO".to_string()) ); }); diff --git a/src/backend/env/reports.rs b/src/backend/env/reports.rs index 9b3ddcad..9b087e79 100644 --- a/src/backend/env/reports.rs +++ b/src/backend/env/reports.rs @@ -110,17 +110,12 @@ pub fn report( misbehaving_user.report = Some(report); let user_name = misbehaving_user.name.clone(); - let post_id = Post::create( - state, - format!("# New User Report âš ī¸\n\nUser @{user_name} was reported for misbehavior. Please review and discuss the report here.\n\n> {reason}"), - Default::default(), - super::id(), - time(), - None, - Some(CONFIG.stalwarts_realm.into()), - None, - ) - .expect("couldn't create report post"); + let post_id = state + .system_message( + format!("# New User Report âš ī¸\n\nUser @{user_name} was reported for misbehavior. Please review and discuss the report here.\n\n> {reason}"), + Some(CONFIG.stalwarts_realm.into()), + ) + .expect("couldn't create report post"); state.notify_with_predicate( &|u| u.stalwart && u.id != id, From e7ba4d15150173d8221fe5e810ce8c00352c425d Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 12:31:58 +0100 Subject: [PATCH 05/22] system messages --- src/backend/env/features.rs | 4 +- src/backend/env/mod.rs | 22 ++++--- src/backend/env/proposals.rs | 11 ++-- src/backend/env/realms.rs | 118 +++++++++++++++++------------------ src/backend/env/reports.rs | 2 +- 5 files changed, 81 insertions(+), 76 deletions(-) diff --git a/src/backend/env/features.rs b/src/backend/env/features.rs index 2a0383e5..39393efc 100644 --- a/src/backend/env/features.rs +++ b/src/backend/env/features.rs @@ -9,6 +9,7 @@ use crate::{ }; use super::{ + config::CONFIG, post::{Extension, Post, PostId}, token::Token, user::UserId, @@ -88,7 +89,6 @@ pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<( if post.user != user.id || !matches!(post.extension.as_ref(), Some(&Extension::Feature)) { return Err("no post with a feature found".into()); } - let realm = post.realm.clone(); if state.memory.features.get(&post_id).is_some() { return Err("feature already exists".into()); @@ -112,7 +112,7 @@ pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<( "A [new feature](#/post/{}) was created by `@{}`", post_id, user_name ), - realm, + CONFIG.dao_realm.into(), ); Ok(()) diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index a721ef83..75bb9608 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -854,7 +854,10 @@ impl State { self.users.insert(user.id, user); self.set_pfp(id, Default::default()) .expect("couldn't set default pfp"); - self.logger.info(format!("@{} joined Taggr! 🎉", name)); + let _ = self.system_message( + format!("`@{}` joined {}!", name, CONFIG.name), + CONFIG.dao_realm.into(), + ); Ok(id) } @@ -890,12 +893,17 @@ impl State { Ok(()) } - pub fn system_message( - &mut self, - body: String, - realm: Option, - ) -> Result { - Post::create(self, body, Default::default(), id(), time(), None, realm, None) + pub fn system_message(&mut self, body: String, realm: RealmId) -> Result { + Post::create( + self, + body, + Default::default(), + id(), + time(), + None, + Some(realm), + None, + ) } pub fn create_invite( diff --git a/src/backend/env/proposals.rs b/src/backend/env/proposals.rs index 5597ffa4..571928a3 100644 --- a/src/backend/env/proposals.rs +++ b/src/backend/env/proposals.rs @@ -441,10 +441,13 @@ pub fn create_proposal( Ok(()) }) .expect("couldn't mutate post"); - state.logger.info(format!( - "@{} submitted a new [proposal](#/post/{}).", - &proposer_name, post_id - )); + let _ = state.system_message( + format!( + "A new [proposal](#/post/{}) was submitted by `@{}`", + post_id, &proposer_name + ), + CONFIG.dao_realm.into(), + ); Ok(id) } diff --git a/src/backend/env/realms.rs b/src/backend/env/realms.rs index e731454c..43febeac 100644 --- a/src/backend/env/realms.rs +++ b/src/backend/env/realms.rs @@ -153,7 +153,7 @@ pub fn create_realm( "Realm [{}](/#/realm/{0}) was created by `@{}`", realm_id, user_name ), - Some(realm_id.clone()), + CONFIG.dao_realm.into(), ); Ok(()) @@ -409,7 +409,7 @@ pub(crate) mod tests { #[actix_rt::test] async fn test_realms() { - let (p1, realm_name) = mutate(|state| { + let (p1, realm_name, new_realm_post_id) = mutate(|state| { state.init(); let p0 = pr(0); @@ -527,7 +527,7 @@ pub(crate) mod tests { assert_eq!(state.realms.get(&name).unwrap().num_members, 1); // creating a post in a realm - let post_id = Post::create( + let realm_post_id = Post::create( state, "Realm post".to_string(), &[], @@ -538,16 +538,16 @@ pub(crate) mod tests { None, ) .unwrap(); - assert_eq!(state.realms.get(&name).unwrap().posts.len(), 2); + assert_eq!(state.realms.get(&name).unwrap().posts.len(), 1); assert_eq!( - Post::get(state, &post_id).unwrap().realm, + Post::get(state, &realm_post_id).unwrap().realm, Some(name.clone()) ); - assert!(realm_posts(state, &name).contains(&post_id)); + assert!(realm_posts(state, &name).contains(&realm_post_id)); // Posting without realm creates the post in the global realm - let post_id = Post::create( + let no_realm_post_id = Post::create( state, "Realm post".to_string(), &[], @@ -559,45 +559,41 @@ pub(crate) mod tests { ) .unwrap(); - assert_eq!(Post::get(state, &post_id).unwrap().realm, None,); + assert_eq!(Post::get(state, &no_realm_post_id).unwrap().realm, None,); // comments are possible even if user is not in the realm - assert_eq!( - Post::create( - state, - "comment".to_string(), - &[], - p0, - 0, - Some(0), - None, - None - ), - Ok(3) - ); + let _comment_id_1 = Post::create( + state, + "comment".to_string(), + &[], + p0, + 0, + Some(realm_post_id), + None, + None + ) + .unwrap(); assert!(state.toggle_realm_membership(p0, name.clone())); assert_eq!(state.realms.get(&name).unwrap().num_members, 2); - assert_eq!( - Post::create( - state, - "comment".to_string(), - &[], - p0, - 0, - Some(0), - None, - None - ), - Ok(4) - ); + let _comment_id_2 = Post::create( + state, + "comment".to_string(), + &[], + p0, + 0, + Some(realm_post_id), + None, + None + ) + .unwrap(); - assert!(realm_posts(state, &name).contains(&3)); + assert!(realm_posts(state, &name).contains(&realm_post_id)); // Create post without a realm - let post_id = Post::create( + let no_realm_post_id_2 = Post::create( state, "No realm post".to_string(), &[], @@ -608,19 +604,19 @@ pub(crate) mod tests { None, ) .unwrap(); - let comment_id = Post::create( + let comment_on_no_realm = Post::create( state, "comment".to_string(), &[], p0, 0, - Some(post_id), + Some(no_realm_post_id_2), None, None, ) .unwrap(); - assert_eq!(Post::get(state, &comment_id).unwrap().realm, None); + assert_eq!(Post::get(state, &comment_on_no_realm).unwrap().realm, None); // Creating post without entering the realm let realm_name = "NEW_REALM".to_string(); @@ -663,22 +659,20 @@ pub(crate) mod tests { assert!(state.users.get(&_u1).unwrap().realms.contains(&name)); assert_eq!(state.realms.get(&realm_name).unwrap().num_members, 1); - assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 1); + assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 0); - assert_eq!( - Post::create( - state, - "test".to_string(), - &[], - p1, - 0, - None, - Some(realm_name.clone()), - None - ), - Ok(8) - ); - assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 2); + let new_realm_post_id = Post::create( + state, + "test".to_string(), + &[], + p1, + 0, + None, + Some(realm_name.clone()), + None + ) + .unwrap(); + assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 1); assert!(state .users @@ -686,13 +680,13 @@ pub(crate) mod tests { .unwrap() .realms .contains(&"TAGGRDAO".to_string())); - (p1, realm_name) + (p1, realm_name, new_realm_post_id) }); // Move the post to non-joined realm assert_eq!( Post::edit( - 8, + new_realm_post_id, "changed".to_string(), vec![], "".to_string(), @@ -705,12 +699,12 @@ pub(crate) mod tests { ); read(|state| { - assert_eq!(Post::get(state, &8).unwrap().realm, Some(realm_name)); - assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 2); + assert_eq!(Post::get(state, &new_realm_post_id).unwrap().realm, Some(realm_name.clone())); + assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 1); }); assert_eq!( Post::edit( - 8, + new_realm_post_id, "changed".to_string(), vec![], "".to_string(), @@ -723,10 +717,10 @@ pub(crate) mod tests { ); read(|state| { - assert_eq!(state.realms.get("NEW_REALM").unwrap().posts.len(), 1); - assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 3); + assert_eq!(state.realms.get("NEW_REALM").unwrap().posts.len(), 0); + assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 2); assert_eq!( - Post::get(state, &8).unwrap().realm, + Post::get(state, &new_realm_post_id).unwrap().realm, Some("TAGGRDAO".to_string()) ); }); diff --git a/src/backend/env/reports.rs b/src/backend/env/reports.rs index 9b087e79..88027284 100644 --- a/src/backend/env/reports.rs +++ b/src/backend/env/reports.rs @@ -113,7 +113,7 @@ pub fn report( let post_id = state .system_message( format!("# New User Report âš ī¸\n\nUser @{user_name} was reported for misbehavior. Please review and discuss the report here.\n\n> {reason}"), - Some(CONFIG.stalwarts_realm.into()), + CONFIG.stalwarts_realm.into(), ) .expect("couldn't create report post"); From 1ad4d9c7bfb978d465df27e8a6f633ba8b69f362 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 12:48:18 +0100 Subject: [PATCH 06/22] simplify logs unify logs fixup --- e2e/test1.spec.ts | 3 - src/backend/env/mod.rs | 15 +++-- src/frontend/src/dashboard.tsx | 100 ++++++++++++--------------------- src/frontend/src/style.css | 44 +++++++++++++++ 4 files changed, 88 insertions(+), 74 deletions(-) diff --git a/e2e/test1.spec.ts b/e2e/test1.spec.ts index e0a1c81f..e1de6faa 100644 --- a/e2e/test1.spec.ts +++ b/e2e/test1.spec.ts @@ -251,8 +251,6 @@ test.describe("Upgrades & token transfer flow", () => { test("Verify recovery upgrade", async () => { await page.goto("/#/dashboard"); await waitForUILoading(page); - await page.getByRole("button", { name: "TECHNICAL" }).click(); - await waitForUILoading(page); await pollForCondition( async () => { @@ -354,7 +352,6 @@ test.describe("Upgrades & token transfer flow", () => { await page.reload(); await page.waitForURL(/dashboard/); await waitForUILoading(page); - await page.getByRole("button", { name: "TECHNICAL" }).click(); const count = await page .locator("p", { hasText: /Upgrade succeeded/ }) .count(); diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index 75bb9608..4b128e61 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -1629,12 +1629,15 @@ impl State { return; }; - state.logger.info(format!( - "@{} is the lucky receiver of `{}` ${} as a weekly random reward! 🎲", - winner_name, - CONFIG.random_reward_amount / base(), - CONFIG.token_symbol, - )); + let _ = state.system_message( + format!( + "`@{}` is the lucky receiver of `{}` ${} as a weekly random reward! 🎲", + winner_name, + CONFIG.random_reward_amount / base(), + CONFIG.token_symbol, + ), + CONFIG.dao_realm.into(), + ); state.minting_mode = true; crate::token::mint( state, diff --git a/src/frontend/src/dashboard.tsx b/src/frontend/src/dashboard.tsx index a59aba0c..d56b5c1c 100644 --- a/src/frontend/src/dashboard.tsx +++ b/src/frontend/src/dashboard.tsx @@ -7,7 +7,6 @@ import { icpCode, IcpAccountLink, USD_PER_XDR, - TabBar, } from "./common"; import { Content } from "./content"; import { @@ -35,7 +34,7 @@ import { UserList } from "./user_resolve"; const show = (val: number | BigInt, unit?: string, unit_position?: string) => ( {unit_position == "prefix" && unit} - {val.toLocaleString()} + {val?.toLocaleString() ?? "..."} {unit_position != "prefix" && unit} ); @@ -46,13 +45,8 @@ type Log = { message: string; }; -const TAB_KEY = "logs_tab"; - export const Dashboard = ({}) => { const [logs, setLogs] = React.useState([]); - const [tab, setTab] = React.useState( - localStorage.getItem(TAB_KEY) || "Social", - ); React.useEffect(() => { window.api.query("logs").then((logs) => { @@ -67,17 +61,6 @@ export const Dashboard = ({}) => { }); }, []); - const logSelector = ( - { - localStorage.setItem(TAB_KEY, newTab); - setTab(newTab); - }} - /> - ); - const { config, stats } = window.backendCache; return ( <> @@ -285,58 +268,45 @@ export const Dashboard = ({}) => {
- {logSelector} -
-
- - (tab == "Social" && level == "INFO") || - (tab == "Technical" && level != "INFO"), - ) - .map( - ({ timestamp, level, message }) => - `\`${shortDate( - new Date(Number(timestamp) / 1000000), - )}\`: ${level2icon(level)} ${message}`, - ) - .join("\n\n")} - /> -
+

LOGS

+ + + {logs.map(({ timestamp, level, message }, i) => { + const date = new Date(Number(timestamp) / 1000000); + const dateStr = new Intl.DateTimeFormat("default", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }).format(date); + const icon = + { + INFO: "â„šī¸", + DEBUG: "🤖", + WARN: "âš ī¸", + ERROR: "🔴", + CRITICAL: "đŸ’Ĩ", + }[level] || "❓"; + return ( + + + + + + ); + })} + +
+ {dateStr} + {icon} + +
); }; -const shortDate = (date: Date) => { - let options: any = { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }; - return new Intl.DateTimeFormat("default", options).format(date); -}; - -const level2icon = (level: string) => { - switch (level) { - case "INFO": - return ""; - case "DEBUG": - return ""; - case "WARN": - return "âš ī¸"; - case "ERROR": - return "🔴"; - case "CRITICAL": - return "đŸ’Ĩ"; - default: - return "❓"; - } -}; - const sizeMb = (size: number | BigInt) => ( {Math.ceil(Number(size) / 1024 / 1024).toLocaleString()} MB diff --git a/src/frontend/src/style.css b/src/frontend/src/style.css index 87ef4fb9..0bbc3a88 100644 --- a/src/frontend/src/style.css +++ b/src/frontend/src/style.css @@ -397,6 +397,46 @@ button { word-break: break-all; } +table.dashboard_logs { + width: 100%; + table-layout: auto; +} + +table.dashboard_logs td { + vertical-align: top; + width: 0; + padding: 0 0 0.5em 0; +} + +table.dashboard_logs td:first-child { + white-space: nowrap; +} + +table.dashboard_logs td:nth-child(2) { + width: 0; + padding-right: 0.5rem; +} + +table.dashboard_logs td:last-child { + width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; +} + +table.dashboard_logs p { + margin: 0; + padding: 0; +} + +table.dashboard_logs code { + word-break: break-all; +} + +table.dashboard_logs a { + word-break: break-all; +} + code { color: $code; } @@ -1055,6 +1095,10 @@ label svg { padding-right: 0; } + table.dashboard_logs { + font-size: small; + } + .fadein { animation: fadein 0.2s; } From 03ffaf3a1219cef6cf8cb70f0edba007251c74fb Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 13:06:25 +0100 Subject: [PATCH 07/22] clean up old logs --- src/backend/env/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index 4b128e61..1b2fe524 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -69,6 +69,7 @@ pub const MINUTE: u64 = 60 * SECOND; pub const HOUR: u64 = 60 * MINUTE; pub const DAY: u64 = 24 * HOUR; pub const WEEK: u64 = 7 * DAY; +pub const MONTH: u64 = 30 * DAY; pub const YEAR: u64 = 52 * WEEK; pub const MAX_USER_ID: UserId = 9_007_199_254_740_991; // Number.MAX_SAFE_INTEGER in JS @@ -286,6 +287,12 @@ impl Logger { }) .or_insert(vec![event]); } + + pub fn clean_up(&mut self, cutoff_time: u64) { + for events in self.events.values_mut() { + events.retain(|event| event.timestamp > cutoff_time); + } + } } #[derive(PartialEq)] @@ -1582,6 +1589,7 @@ impl State { } state.clean_up(now); + state.logger.clean_up(now - 6 * MONTH); // these burned credits go to the next week state.distribute_revenue_from_icp(auction_revenue); From e97c14fe8985775f0c7e9dab28e32f2bc4a64f79 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 14:20:07 +0100 Subject: [PATCH 08/22] don't mint rewards for system user --- src/backend/env/mod.rs | 8 +++++--- src/backend/env/post.rs | 29 +++++++++++++++-------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index 1b2fe524..a565d167 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -488,7 +488,8 @@ impl State { self.users .values() .filter(move |user| { - user.active_within(CONFIG.voting_power_activity_weeks, WEEK, time) + user.organic() + && user.active_within(CONFIG.voting_power_activity_weeks, WEEK, time) }) .map(move |user| (user.id, user.total_balance())), ) @@ -1225,8 +1226,8 @@ impl State { if rewards == 0 { continue; }; - // All normie rewards are burned. - if user.mode == Mode::Credits { + // Rewards are burned for system users and those who are in auto top-up mode. + if !user.organic() || user.mode == Mode::Credits { self.burned_cycles += rewards; } else { payouts.insert(user.id, rewards as Credits); @@ -1615,6 +1616,7 @@ impl State { let mut threshold = 0; for user in state.users.values_mut().filter(|user| { !user.controversial() + && user.organic() && user.active_within(1, WEEK, time()) && user.credits_burned() > 0 }) { diff --git a/src/backend/env/post.rs b/src/backend/env/post.rs index 50c19f88..ba5a495e 100644 --- a/src/backend/env/post.rs +++ b/src/backend/env/post.rs @@ -528,20 +528,21 @@ impl Post { }, }; if let Some(realm_id) = &realm { - if user.organic() && parent.is_none() && !user.realms.contains(realm_id) { - return Err(format!("not a member of the realm {}", realm_id)); - } - if let Some(realm) = state.realms.get(realm_id) { - let whitelist = &realm.whitelist; - if user.organic() - && (parent.is_some() && realm.comments_filtering || parent.is_none()) - && (!whitelist.is_empty() && !whitelist.contains(&user.id) - || whitelist.is_empty() && !user.get_filter().passes(&realm.filter)) - { - return Err(format!( - "{} realm is gated and you are not allowed to post to this realm", - realm_id - )); + if user.organic() { + if parent.is_none() && !user.realms.contains(realm_id) { + return Err(format!("not a member of the realm {}", realm_id)); + } + if let Some(realm) = state.realms.get(realm_id) { + let whitelist = &realm.whitelist; + if (parent.is_some() && realm.comments_filtering || parent.is_none()) + && (!whitelist.is_empty() && !whitelist.contains(&user.id) + || whitelist.is_empty() && !user.get_filter().passes(&realm.filter)) + { + return Err(format!( + "{} realm is gated and you are not allowed to post to this realm", + realm_id + )); + } } } } else if let Some(discussion_owner) = parent.and_then(|post_id| { From f34b03afdd72ad11b3a2486785ff0044c410e426 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 8 Nov 2025 14:24:14 +0100 Subject: [PATCH 09/22] tests stabilization --- e2e/helpers.ts | 28 +++++++++++++++++----------- e2e/test2.spec.ts | 26 +++++++++++++++----------- e2e/test3.spec.ts | 33 +++++++++++++++++++-------------- e2e/test4.spec.ts | 1 + src/frontend/src/index.tsx | 2 +- src/frontend/src/post.tsx | 2 +- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/e2e/helpers.ts b/e2e/helpers.ts index bd3c27d5..40ccb5df 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -258,18 +258,24 @@ export async function waitForTextContent( } = {}, ): Promise { const { timeout = 10000 } = options; - - await page.waitForFunction( - ({ sel, expected }) => { - const element = document.querySelector(sel); - if (!element) return false; - const text = element.textContent || ""; - return typeof expected === "string" - ? text.includes(expected) - : new RegExp(expected).test(text); + const maxAttempts = Math.ceil(timeout / 500); + + await pollForCondition( + async () => { + const element = page.locator(selector).first(); + const count = await element.count(); + if (count === 0) return false; + const text = await element.textContent(); + if (!text) return false; + return typeof expectedText === "string" + ? text.includes(expectedText) + : new RegExp(expectedText).test(text); + }, + { + maxAttempts, + interval: 500, + errorMessage: `Text content "${expectedText}" not found in selector "${selector}" within ${timeout}ms`, }, - { sel: selector, expected: expectedText.toString() }, - { timeout }, ); } diff --git a/e2e/test2.spec.ts b/e2e/test2.spec.ts index eac6bb92..2e2e4f7c 100644 --- a/e2e/test2.spec.ts +++ b/e2e/test2.spec.ts @@ -1,4 +1,4 @@ -import { waitForUILoading } from "./helpers"; +import { waitForUILoading, pollForCondition } from "./helpers"; import { test, expect, Page } from "@playwright/test"; import { resolve } from "node:path"; import { mkPwd, transferICP } from "./command"; @@ -179,17 +179,21 @@ test.describe("Regular users flow", () => { }, ); - await page.waitForFunction( - (oldBalance) => { - const elem = document.querySelector( - '[data-testid="icp-balance"]', - ); - if (!elem) return false; - const currentBalance = parseFloat(elem.textContent || "0"); - return currentBalance < oldBalance; + await pollForCondition( + async () => { + const elem = page.getByTestId("icp-balance"); + const count = await elem.count(); + if (count === 0) return false; + const text = await elem.textContent(); + if (!text) return false; + const currentBalance = parseFloat(text); + return currentBalance < icpBalance; + }, + { + maxAttempts: 20, + interval: 500, + errorMessage: `ICP balance did not decrease from ${icpBalance} within timeout`, }, - icpBalance, - { timeout: 10000 }, ); const newBalance = parseFloat( diff --git a/e2e/test3.spec.ts b/e2e/test3.spec.ts index 32be00e8..bcbffb52 100644 --- a/e2e/test3.spec.ts +++ b/e2e/test3.spec.ts @@ -1,4 +1,4 @@ -import { waitForUILoading, handleDialog } from "./helpers"; +import { waitForUILoading, handleDialog, pollForCondition } from "./helpers"; import { test, expect, Page } from "@playwright/test"; import { exec, mkPwd, transferICP } from "./command"; @@ -166,6 +166,7 @@ test.describe("Regular users flow, part two", () => { await page.getByTestId("toggle-user-section").click(); await expect(page.getByTestId("token-balance")).toHaveText("5"); await page.getByTestId("toggle-user-section").click(); + await waitForUILoading(page); const post = page .getByTestId("post-body") @@ -174,7 +175,7 @@ test.describe("Regular users flow, part two", () => { }) .last(); await expect(post).toBeVisible(); - const menuBTN = post.locator(`button[title="Menu"]`); + const menuBTN = post.getByTestId("post-info-toggle"); await expect(menuBTN).toBeVisible(); await menuBTN.click(); const postMenu = post.getByTestId("post-menu"); @@ -188,20 +189,24 @@ test.describe("Regular users flow, part two", () => { await handleDialog(page, /./, "", async () => { await popup.getByText("SEND").click(); }); - await waitForUILoading(page); - await page.goto("/"); - await waitForUILoading(page); - await page.getByTestId("toggle-user-section").click(); + await pollForCondition( + async () => { + await page.goto("/"); + await page.getByTestId("toggle-user-section").click(); - await page.waitForFunction( - () => { - const elem = document.querySelector( - '[data-testid="token-balance"]', - ); - return elem?.textContent === "4"; + const elem = page.getByTestId("token-balance"); + const count = await elem.count(); + if (count === 0) return false; + const text = await elem.textContent(); + return text === "4"; + }, + { + maxAttempts: 20, + interval: 500, + errorMessage: + "Token balance did not update to 4 within timeout", }, - { timeout: 10000 }, ); await expect(page.getByTestId("token-balance")).toHaveText("4"); @@ -215,7 +220,7 @@ test.describe("Regular users flow, part two", () => { }) .last(); await expect(post).toBeVisible(); - const menuBTN = post.locator(`button[title="Menu"]`); + const menuBTN = post.getByTestId("post-info-toggle"); await expect(menuBTN).toBeVisible(); await menuBTN.click(); const postMenu = post.getByTestId("post-menu"); diff --git a/e2e/test4.spec.ts b/e2e/test4.spec.ts index bd68c1ed..522e2304 100644 --- a/e2e/test4.spec.ts +++ b/e2e/test4.spec.ts @@ -36,6 +36,7 @@ test.describe("Report and transfer to user", () => { "e93e7f1cfa411dafa8debb4769c6cc1b7972434f1669083fd08d86d11c0c0722", 1, ); + await waitForUILoading(page); await page .getByRole("button", { name: "MINT CREDITS WITH ICP" }) .click(); diff --git a/src/frontend/src/index.tsx b/src/frontend/src/index.tsx index 1dbc6065..842a5570 100644 --- a/src/frontend/src/index.tsx +++ b/src/frontend/src/index.tsx @@ -108,7 +108,7 @@ const App = () => { window.lastActivity = new Date(); const auth = (content: React.ReactNode) => window.principalId ? content : ; - let [handler = "", param, param2] = parseHash(); + let [handler = "", param = "", param2 = ""] = parseHash(); let subtle = false; let inboxMode = false; diff --git a/src/frontend/src/post.tsx b/src/frontend/src/post.tsx index 21e5014a..843df7a7 100644 --- a/src/frontend/src/post.tsx +++ b/src/frontend/src/post.tsx @@ -285,7 +285,7 @@ export const PostView = ({ post.scrollIntoView({ behavior: "smooth" }); }} className={classNameArg} - data-testid="post-body" + data-testid={`post-body${repost ? "-repost" : ""}`} >
Date: Sun, 9 Nov 2025 12:38:03 +0100 Subject: [PATCH 10/22] fix native tokens --- src/frontend/src/token-select.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/frontend/src/token-select.tsx b/src/frontend/src/token-select.tsx index 4c2495f8..1ee11295 100755 --- a/src/frontend/src/token-select.tsx +++ b/src/frontend/src/token-select.tsx @@ -33,21 +33,12 @@ export const TokenSelect = ({ const loadData = () => { const canistersMap = new Map(canisters); - // Add ICP or Taggr - const mainTokens: Array<[string, Icrc1Canister]> = []; - const nativeCanister = canistersMap.get(CANISTER_ID); - if (nativeCanister) { - mainTokens.push([CANISTER_ID, nativeCanister]); - } - - const icpCanister = canistersMap.get(ICP_LEDGER); - if (icpCanister) { - mainTokens.push([ICP_LEDGER, icpCanister]); - } - const userTokens = window.user?.wallet_tokens || []; + const userTokenIds = window.user?.wallet_tokens || []; + userTokenIds.push(CANISTER_ID); + userTokenIds.push(ICP_LEDGER); setUserTokens( - userTokens + userTokenIds .filter((id) => canistersMap.has(id)) .map((canisterId) => [ canisterId, @@ -92,8 +83,7 @@ export const TokenSelect = ({ SELECT TOKEN )} - {userTokens.length > 0 && - renderOptions(userTokens, "Tipping Tokens")} + {renderOptions(userTokens, "Tipping Tokens")} ); }; From 6a225c8c4f3535d71bddfa4bb7c390d5a419f8c7 Mon Sep 17 00:00:00 2001 From: X Date: Sun, 9 Nov 2025 12:49:39 +0100 Subject: [PATCH 11/22] improve tipping UX --- src/frontend/src/post.tsx | 49 ++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/post.tsx b/src/frontend/src/post.tsx index 843df7a7..c8bc28d5 100644 --- a/src/frontend/src/post.tsx +++ b/src/frontend/src/post.tsx @@ -1336,6 +1336,7 @@ const TippingPopup = ({ React.useState(CANISTER_ID); const [tippingAmount, setTippingAmount] = React.useState(0.1); const [postAuthor, setPostAuthor] = React.useState(); + const [showConfirmation, setShowConfirmation] = React.useState(false); React.useEffect(() => { window.api @@ -1377,14 +1378,6 @@ const TippingPopup = ({ (tippingAmount * Math.pow(10, canister.decimals)).toFixed(0), ); if (!amount || isNaN(amount)) return; - if ( - !confirm( - `Transfer ${tippingAmount} ${canister.symbol} to @${ - post.meta.author_name - } as a tip?`, - ) - ) - return; if (!postAuthor) return showPopUp("error", "Could not load post author data."); @@ -1465,6 +1458,8 @@ const TippingPopup = ({ } }; + const canister = canistersMetaData[selectedTippingCanisterId]; + return (

@@ -1489,13 +1484,41 @@ const TippingPopup = ({ return; } setTippingAmount(amount); + setShowConfirmation(false); }} /> - finalizeTip(parentCallback || (() => {}))} - /> + {showConfirmation && canister && ( +

+ Transfer {tippingAmount} {canister.symbol} to + + as a tip? +
+ )} + {!showConfirmation ? ( + setShowConfirmation(true)} + /> + ) : ( +
+ setShowConfirmation(false)} + /> + + finalizeTip(parentCallback || (() => {})) + } + /> +
+ )}
); }; From 2172fed7204becc4c913668cf5181770de4aec48 Mon Sep 17 00:00:00 2001 From: X Date: Sun, 9 Nov 2025 13:00:10 +0100 Subject: [PATCH 12/22] remove old logs --- src/backend/updates.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/backend/updates.rs b/src/backend/updates.rs index 489ae322..82827b1b 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -109,7 +109,9 @@ fn post_upgrade() { } #[allow(clippy::all)] -fn sync_post_upgrade_fixtures() {} +fn sync_post_upgrade_fixtures() { + mutate(|state| state.logger.clean_up(time() - MONTH * 6)); +} #[allow(clippy::all)] async fn async_post_upgrade_fixtures() {} @@ -296,7 +298,11 @@ fn create_feature() { #[export_name = "canister_update toggle_feature_support"] fn toggle_feature_support() { let post_id: PostId = parse(&arg_data_raw()); - reply(features::toggle_feature_support(read(caller), post_id, time())); + reply(features::toggle_feature_support( + read(caller), + post_id, + time(), + )); } #[export_name = "canister_update create_user"] From e5eb669152aff24710f9d0b352f965f521eccb6b Mon Sep 17 00:00:00 2001 From: X Date: Sun, 9 Nov 2025 13:08:33 +0100 Subject: [PATCH 13/22] fix duplicate tokens --- src/frontend/src/post.tsx | 13 +++++++------ src/frontend/src/realms.tsx | 2 +- .../src/{token-select.tsx => token_select.tsx} | 13 +++++++------ 3 files changed, 15 insertions(+), 13 deletions(-) rename src/frontend/src/{token-select.tsx => token_select.tsx} (90%) diff --git a/src/frontend/src/post.tsx b/src/frontend/src/post.tsx index c8bc28d5..82654320 100644 --- a/src/frontend/src/post.tsx +++ b/src/frontend/src/post.tsx @@ -71,7 +71,7 @@ import { validUserId, } from "./user_resolve"; import { CANISTER_ID } from "./env"; -import { TokenSelect } from "./token-select"; +import { TokenSelect } from "./token_select"; export const PostView = ({ id, @@ -1480,16 +1480,17 @@ const TippingPopup = ({ value={tippingAmount} onChange={async (e) => { const amount = Number(e.target.value); - if (isNaN(amount) || amount <= 0) { - return; - } setTippingAmount(amount); setShowConfirmation(false); }} /> {showConfirmation && canister && ( -
- Transfer {tippingAmount} {canister.symbol} to +
+ Transfer{" "} + + {tippingAmount} ${canister.symbol} + {" "} + to void; selectedCanisterId?: string; }) => { - // State to store selected values const [selectedValue, setSelectedValue] = React.useState( selectedCanisterId || "", ); @@ -24,7 +23,6 @@ export const TokenSelect = ({ Array<[string, Icrc1Canister]> >([]); - // Handle change when options are selected/deselected const handleChange = (event: any) => { const value = (event.target as any).value || CANISTER_ID; setSelectedValue(value); @@ -34,11 +32,14 @@ export const TokenSelect = ({ const loadData = () => { const canistersMap = new Map(canisters); - const userTokenIds = window.user?.wallet_tokens || []; - userTokenIds.push(CANISTER_ID); - userTokenIds.push(ICP_LEDGER); + const userTokenIds = [ + ...(window.user?.wallet_tokens || []), + CANISTER_ID, + ICP_LEDGER, + ]; + const uniqueTokenIds = [...new Set(userTokenIds)]; setUserTokens( - userTokenIds + uniqueTokenIds .filter((id) => canistersMap.has(id)) .map((canisterId) => [ canisterId, From f8df11e436297842e4094427fb89c8c5b595f33a Mon Sep 17 00:00:00 2001 From: X Date: Sun, 9 Nov 2025 13:25:22 +0100 Subject: [PATCH 14/22] fix entering the amount --- src/frontend/src/post.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/post.tsx b/src/frontend/src/post.tsx index 82654320..e1ac6a5a 100644 --- a/src/frontend/src/post.tsx +++ b/src/frontend/src/post.tsx @@ -1334,7 +1334,7 @@ const TippingPopup = ({ }) => { const [selectedTippingCanisterId, setSelectedTippingCanisterId] = React.useState(CANISTER_ID); - const [tippingAmount, setTippingAmount] = React.useState(0.1); + const [tippingAmount, setTippingAmount] = React.useState("0.1"); const [postAuthor, setPostAuthor] = React.useState(); const [showConfirmation, setShowConfirmation] = React.useState(false); @@ -1355,10 +1355,8 @@ const TippingPopup = ({ ); } setTippingAmount( - Number( - (canister.fee / Math.pow(10, canister.decimals)).toFixed( - canister.decimals, - ), + (canister.fee / Math.pow(10, canister.decimals)).toFixed( + canister.decimals, ), ); }; @@ -1374,10 +1372,11 @@ const TippingPopup = ({ ); } + const numericAmount = Number(tippingAmount); + if (!numericAmount || isNaN(numericAmount)) return; const amount = Number( - (tippingAmount * Math.pow(10, canister.decimals)).toFixed(0), + (numericAmount * Math.pow(10, canister.decimals)).toFixed(0), ); - if (!amount || isNaN(amount)) return; if (!postAuthor) return showPopUp("error", "Could not load post author data."); @@ -1479,8 +1478,7 @@ const TippingPopup = ({ className="bottom_spaced" value={tippingAmount} onChange={async (e) => { - const amount = Number(e.target.value); - setTippingAmount(amount); + setTippingAmount(e.target.value); setShowConfirmation(false); }} /> @@ -1488,11 +1486,13 @@ const TippingPopup = ({
Transfer{" "} - {tippingAmount} ${canister.symbol} + {Number(tippingAmount).toLocaleString()}{" "} + {canister.symbol} {" "} to as a tip? @@ -1509,7 +1509,9 @@ const TippingPopup = ({ setShowConfirmation(false)} + onClick={async () => { + if (parentCallback) parentCallback(); + }} /> Date: Wed, 12 Nov 2025 08:44:54 +0100 Subject: [PATCH 15/22] cargo fmt --- src/backend/env/realms.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/backend/env/realms.rs b/src/backend/env/realms.rs index 43febeac..fee79d92 100644 --- a/src/backend/env/realms.rs +++ b/src/backend/env/realms.rs @@ -570,7 +570,7 @@ pub(crate) mod tests { 0, Some(realm_post_id), None, - None + None, ) .unwrap(); @@ -585,7 +585,7 @@ pub(crate) mod tests { 0, Some(realm_post_id), None, - None + None, ) .unwrap(); @@ -669,7 +669,7 @@ pub(crate) mod tests { 0, None, Some(realm_name.clone()), - None + None, ) .unwrap(); assert_eq!(state.realms.get(&realm_name).unwrap().posts.len(), 1); @@ -699,7 +699,10 @@ pub(crate) mod tests { ); read(|state| { - assert_eq!(Post::get(state, &new_realm_post_id).unwrap().realm, Some(realm_name.clone())); + assert_eq!( + Post::get(state, &new_realm_post_id).unwrap().realm, + Some(realm_name.clone()) + ); assert_eq!(state.realms.get("TAGGRDAO").unwrap().posts.len(), 1); }); assert_eq!( From dac91214c9dabe274042e543bbed26b999dac83d Mon Sep 17 00:00:00 2001 From: X Date: Wed, 12 Nov 2025 08:59:24 +0100 Subject: [PATCH 16/22] simplify dashboard changes --- src/frontend/src/dashboard.tsx | 42 ++++++++++++++++++++-------------- src/frontend/src/style.css | 1 + 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/frontend/src/dashboard.tsx b/src/frontend/src/dashboard.tsx index d56b5c1c..59afb4a3 100644 --- a/src/frontend/src/dashboard.tsx +++ b/src/frontend/src/dashboard.tsx @@ -273,27 +273,12 @@ export const Dashboard = ({}) => { {logs.map(({ timestamp, level, message }, i) => { const date = new Date(Number(timestamp) / 1000000); - const dateStr = new Intl.DateTimeFormat("default", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }).format(date); - const icon = - { - INFO: "â„šī¸", - DEBUG: "🤖", - WARN: "âš ī¸", - ERROR: "🔴", - CRITICAL: "đŸ’Ĩ", - }[level] || "❓"; return ( - {dateStr} + {shortDate(date)} - {icon} + {level2icon(level)} @@ -307,6 +292,29 @@ export const Dashboard = ({}) => { ); }; +const shortDate = (date: Date) => { + let options: any = { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }; + return new Intl.DateTimeFormat("default", options).format(date); +}; + +const level2icon = (level: string) => { + return ( + { + INFO: "â„šī¸", + DEBUG: "🤖", + WARN: "âš ī¸", + ERROR: "🔴", + CRITICAL: "đŸ’Ĩ", + }[level] || "❓" + ); +}; + const sizeMb = (size: number | BigInt) => ( {Math.ceil(Number(size) / 1024 / 1024).toLocaleString()} MB diff --git a/src/frontend/src/style.css b/src/frontend/src/style.css index 0bbc3a88..fa170721 100644 --- a/src/frontend/src/style.css +++ b/src/frontend/src/style.css @@ -415,6 +415,7 @@ table.dashboard_logs td:first-child { table.dashboard_logs td:nth-child(2) { width: 0; padding-right: 0.5rem; + padding-left: 0.5rem; } table.dashboard_logs td:last-child { From a56194e82b791496052e15fa1f5452789fbd9432 Mon Sep 17 00:00:00 2001 From: X Date: Sun, 16 Nov 2025 11:37:14 +0100 Subject: [PATCH 17/22] test fix --- e2e/test3.spec.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/e2e/test3.spec.ts b/e2e/test3.spec.ts index bcbffb52..019b3f95 100644 --- a/e2e/test3.spec.ts +++ b/e2e/test3.spec.ts @@ -186,9 +186,12 @@ test.describe("Regular users flow, part two", () => { await expect(popup).toHaveText(/Tip john with.*/); await popup.locator("input").fill("1"); - await handleDialog(page, /./, "", async () => { - await popup.getByText("SEND").click(); - }); + // Click SEND to show confirmation + await popup.getByText("SEND").click(); + + // Wait for confirmation UI to appear and click CONFIRM + await expect(popup.getByText("CONFIRM")).toBeVisible(); + await popup.getByText("CONFIRM").click(); await pollForCondition( async () => { @@ -230,11 +233,13 @@ test.describe("Regular users flow, part two", () => { await expect(popup).toBeVisible(); await popup.locator("input").fill("1"); - page.once("dialog", async (dialog) => { - await dialog.dismiss(); - }); + // Click SEND to show confirmation await popup.getByText("SEND").click(); + // Wait for confirmation UI to appear and click CANCEL + await expect(popup.getByText("CANCEL")).toBeVisible(); + await popup.getByText("CANCEL").click(); + await page.goto("/"); await waitForUILoading(page); await page.getByTestId("toggle-user-section").click(); From a7ffaa9133e8a5f7b728674d0074d63037c59c4f Mon Sep 17 00:00:00 2001 From: X Date: Sun, 16 Nov 2025 11:37:26 +0100 Subject: [PATCH 18/22] better landing page --- src/frontend/src/authentication.tsx | 83 +++++++++++++++-------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/frontend/src/authentication.tsx b/src/frontend/src/authentication.tsx index 60964453..aa23b422 100644 --- a/src/frontend/src/authentication.tsx +++ b/src/frontend/src/authentication.tsx @@ -16,6 +16,46 @@ import { DELEGATION_PRINCIPAL } from "./delegation"; import { instantiateApi } from "."; export const authMethods = [ + { + icon: , + label: "Invite", + description: + "If you have received an invite from someone, use this connection method.", + login: async () => { + const code = prompt("Enter your invite code:")?.toLowerCase(); + if (!(await window.api.query("check_invite", code))) { + showPopUp("error", "Invalid invite"); + return; + } + location.href = `#/welcome/${code}`; + return <>; + }, + }, + { + icon: , + label: "Internet Identity", + description: + "Passkey-based decentralized authentication service hosted on IC.", + login: async (signUp?: boolean) => { + if ( + (location.href.includes(".raw") || + location.href.includes("share.")) && + confirm( + "You're using an uncertified, insecure frontend. Do you want to be re-routed to the certified one?", + ) + ) { + location.href = location.href.replace(".raw", ""); + return null; + } + window.authClient.login({ + onSuccess: () => finalize(signUp), + identityProvider: II_URL, + maxTimeToLive: BigInt(30 * 24 * 3600000000000), + derivationOrigin: window.location.origin, + }); + return null; + }, + }, { icon: , label: "Seed Phrase", @@ -58,49 +98,10 @@ export const authMethods = [ /> ), }, - { - icon: , - label: "Invite", - description: - "If you have received an invite from someone, use this connection method.", - login: async () => { - const code = prompt("Enter your invite code:")?.toLowerCase(); - if (!(await window.api.query("check_invite", code))) { - showPopUp("error", "Invalid invite"); - return; - } - location.href = `#/welcome/${code}`; - return <>; - }, - }, - { - icon: , - label: "Internet Identity", - description: - "Passkey-based decentralized authentication service hosted on IC.", - login: async (signUp?: boolean) => { - if ( - (location.href.includes(".raw") || - location.href.includes("share.")) && - confirm( - "You're using an uncertified, insecure frontend. Do you want to be re-routed to the certified one?", - ) - ) { - location.href = location.href.replace(".raw", ""); - return null; - } - window.authClient.login({ - onSuccess: () => finalize(signUp), - identityProvider: II_URL, - maxTimeToLive: BigInt(30 * 24 * 3600000000000), - derivationOrigin: window.location.origin, - }); - return null; - }, - }, { icon: , label: "Internet Identity (Legacy)", + deprecated: true, description: "Decentralized authentication service hosted on IC and based on biometric devices.", login: async (signUp?: boolean) => { @@ -183,7 +184,7 @@ export const LoginMasks = ({ {methods.map((method) => (
Date: Sun, 16 Nov 2025 17:08:40 +0100 Subject: [PATCH 19/22] new invite page --- src/frontend/src/authentication.tsx | 105 ++++++++++++++-------------- src/frontend/src/header.tsx | 10 +-- src/frontend/src/index.tsx | 18 ++--- src/frontend/src/invite_page.tsx | 67 ++++++++++++++++++ src/frontend/src/style.css | 7 ++ 5 files changed, 142 insertions(+), 65 deletions(-) create mode 100644 src/frontend/src/invite_page.tsx diff --git a/src/frontend/src/authentication.tsx b/src/frontend/src/authentication.tsx index aa23b422..f355a3f6 100644 --- a/src/frontend/src/authentication.tsx +++ b/src/frontend/src/authentication.tsx @@ -14,6 +14,7 @@ import { II_URL } from "./env"; import { Ed25519KeyIdentity } from "@dfinity/identity"; import { DELEGATION_PRINCIPAL } from "./delegation"; import { instantiateApi } from "."; +import { InviteInfo } from "./invite_page"; export const authMethods = [ { @@ -157,60 +158,60 @@ export const LoginMasks = ({ : authMethods; return ( -
-

{signUp ? "Sign-up" : "Sign-in"}

- {mask ? ( - mask - ) : ( - <> - {invite || signUp ? ( -

- {invite && ( - - Welcome! You were invited to{" "} - {window.backendCache.config.name}! - - )} - - Select one of the available authentication - methods to create your user account. - -

- ) : ( -

- Choose your authentication method. -

- )} - {methods.map((method) => ( -
- + {invite && } +
+

{signUp ? "Sign-up" : "Sign-in"}

+ {mask ? ( + mask + ) : ( + <> + {invite || signUp ? ( +

+ {!invite && ( + + Select one of the available + authentication methods to create your + user account. + + )} +

+ ) : ( +

+ Choose your authentication method. +

+ )} + {methods.map((method) => ( +
{ - let mask = await method.login(signUp); - if (mask) { - setMask(mask); + className={`left_spaced right_spaced bottom_spaced ${method.deprecated ? "inactive" : ""}`} + > + { + let mask = await method.login(signUp); + if (mask) { + setMask(mask); + } + }} + label={ + <> + {method.icon} {method.label} + } - }} - label={ - <> - {method.icon} {method.label} - - } - /> - {signUp && ( -

- {method.description} -

- )}{" "} -
- ))} - - )} + /> + {signUp && ( +

+ {method.description} +

+ )}{" "} +
+ ))} + + )} +
); }; diff --git a/src/frontend/src/header.tsx b/src/frontend/src/header.tsx index 8bd7d91e..3f244c3b 100644 --- a/src/frontend/src/header.tsx +++ b/src/frontend/src/header.tsx @@ -30,14 +30,16 @@ import { connect } from "./authentication"; let interval: any = null; export const Header = ({ - subtle, + style, route, inboxMode, }: { - subtle?: boolean; + style?: string; route: string; inboxMode: boolean; }) => { + if (style === "hidden") return null; + const user = window.user; const [showUserSection, toggleUserSection] = React.useState(false); const [showRealms, toggleRealms] = React.useState(false); @@ -97,7 +99,7 @@ export const Header = ({ data-testid="home-page-link" >
- {!subtle && user && ( + {!style && user && ( <> )} - {!window.user && !subtle && ( + {!window.user && !style && ( <>
); }; - -const TippingPopup = ({ - parentCallback, - post, - allowedTippingCanisterIds, - canistersMetaData, - externalTips, - setExternalTips, - callback, -}: { - parentCallback?: () => void; - post: Post; - allowedTippingCanisterIds: string[]; - canistersMetaData: Record; - externalTips: PostTip[]; - setExternalTips: React.Dispatch>; - callback: () => Promise; -}) => { - const [selectedTippingCanisterId, setSelectedTippingCanisterId] = - React.useState(CANISTER_ID); - const [tippingAmount, setTippingAmount] = React.useState("0.1"); - const [postAuthor, setPostAuthor] = React.useState(); - const [showConfirmation, setShowConfirmation] = React.useState(false); - - React.useEffect(() => { - window.api - .query("user", domain(), [post.user.toString()]) - .then(setPostAuthor); - }, []); - - const onTokenSelectionChange = (canisterId: string) => { - setSelectedTippingCanisterId(canisterId); - - const canister = canistersMetaData[canisterId]; - if (!canister) { - return showPopUp( - "error", - `Could not find canister data for ${canisterId}`, - ); - } - setTippingAmount( - (canister.fee / Math.pow(10, canister.decimals)).toFixed( - canister.decimals, - ), - ); - }; - - const finalizeTip = async (popUpCallback: () => void) => { - try { - const canisterId = selectedTippingCanisterId; - const canister = canistersMetaData[canisterId]; - if (!canister) { - return showPopUp( - "error", - `Could not find canister data for ${canisterId}`, - ); - } - - const numericAmount = Number(tippingAmount); - if (!numericAmount || isNaN(numericAmount)) return; - const amount = Number( - (numericAmount * Math.pow(10, canister.decimals)).toFixed(0), - ); - - if (!postAuthor) - return showPopUp("error", "Could not load post author data."); - - const { token_symbol } = window.backendCache.config; - - // Native token tipping - if (canister.symbol === token_symbol) { - let response = await window.api.call( - "tip", - post.id, - amount, - ); - if ("Err" in response) { - throw new Error(response.Err); - } else await callback(); - - popUpCallback(); - - return; - } - - // ICRC-1 token tipping - let transId = await window.api.icrc_transfer( - Principal.fromText(canisterId), - Principal.fromText(postAuthor.principal), - amount, - canister.fee, - numberToUint8Array(post.id), - ); - - if (Number.isNaN(transId as number)) { - throw new Error( - transId.toString() || "Something went wrong with transfer!", - ); - } - - const optimisticPostTip: PostTip = { - amount, - canister_id: canisterId, - index: Number(transId), - sender_id: window.user.id, - }; - setExternalTips([...externalTips, optimisticPostTip]); - - popUpCallback(); - - let addTipResponse = await window.api.call<{ - Ok: PostTip; - Err: string; - }>( - "add_external_icrc_transaction", - canisterId, - Number(transId), - post.id, - ); - if ("Err" in (addTipResponse || {}) || !addTipResponse) { - setExternalTips( - externalTips.filter( - ({ canister_id, index }) => - index !== optimisticPostTip.index || - canisterId !== canister_id, - ), - ); - throw new Error( - addTipResponse?.Err || "Could not add tip to post.", - ); - } - - setExternalTips([ - ...externalTips.filter( - ({ index }) => index !== Number(transId), - ), - addTipResponse.Ok, - ]); - } catch (e: any) { - return showPopUp("error", e?.message || e); - } - }; - - const canister = canistersMetaData[selectedTippingCanisterId]; - - return ( -
-

- Tip {post.meta.author_name} - with - [ - canisterId, - canistersMetaData[canisterId], - ])} - onSelectionChange={onTokenSelectionChange} - selectedCanisterId={selectedTippingCanisterId} - /> -

- { - setTippingAmount(e.target.value); - setShowConfirmation(false); - }} - /> - {showConfirmation && canister && ( -
- Transfer{" "} - - {Number(tippingAmount).toLocaleString()}{" "} - {canister.symbol} - {" "} - to - - as a tip? -
- )} - {!showConfirmation ? ( - setShowConfirmation(true)} - /> - ) : ( -
- { - if (parentCallback) parentCallback(); - }} - /> - - finalizeTip(parentCallback || (() => {})) - } - /> -
- )} -
- ); -}; diff --git a/src/frontend/src/tipping.tsx b/src/frontend/src/tipping.tsx new file mode 100644 index 00000000..d3c9ff01 --- /dev/null +++ b/src/frontend/src/tipping.tsx @@ -0,0 +1,223 @@ +import { Principal } from "@dfinity/principal"; +import * as React from "react"; +import { TokenSelect } from "./token_select"; +import { UserLink } from "./user_resolve"; +import { + showPopUp, + domain, + numberToUint8Array, + ButtonWithLoading, +} from "./common"; +import { Icrc1Canister, Post, PostTip, User } from "./types"; +import { CANISTER_ID } from "./env"; + +export const TippingPopup = ({ + parentCallback, + post, + allowedTippingCanisterIds, + canistersMetaData, + externalTips, + setExternalTips, + callback, +}: { + parentCallback?: () => void; + post: Post; + allowedTippingCanisterIds: string[]; + canistersMetaData: Record; + externalTips: PostTip[]; + setExternalTips: React.Dispatch>; + callback: () => Promise; +}) => { + const [selectedTippingCanisterId, setSelectedTippingCanisterId] = + React.useState(CANISTER_ID); + const [tippingAmount, setTippingAmount] = React.useState("0.1"); + const [postAuthor, setPostAuthor] = React.useState(); + const [showConfirmation, setShowConfirmation] = React.useState(false); + + React.useEffect(() => { + window.api + .query("user", domain(), [post.user.toString()]) + .then(setPostAuthor); + }, []); + + const onTokenSelectionChange = (canisterId: string) => { + setSelectedTippingCanisterId(canisterId); + + const canister = canistersMetaData[canisterId]; + if (!canister) { + return showPopUp( + "error", + `Could not find canister data for ${canisterId}`, + ); + } + setTippingAmount( + (canister.fee / Math.pow(10, canister.decimals)).toFixed( + canister.decimals, + ), + ); + }; + + const finalizeTip = async (popUpCallback: () => void) => { + try { + const canisterId = selectedTippingCanisterId; + const canister = canistersMetaData[canisterId]; + if (!canister) { + return showPopUp( + "error", + `Could not find canister data for ${canisterId}`, + ); + } + + const numericAmount = Number(tippingAmount); + if (!numericAmount || isNaN(numericAmount)) return; + const amount = Number( + (numericAmount * Math.pow(10, canister.decimals)).toFixed(0), + ); + + if (!postAuthor) + return showPopUp("error", "Could not load post author data."); + + const { token_symbol } = window.backendCache.config; + + // Native token tipping + if (canister.symbol === token_symbol) { + let response = await window.api.call( + "tip", + post.id, + amount, + ); + if ("Err" in response) { + throw new Error(response.Err); + } else await callback(); + + popUpCallback(); + + return; + } + + // ICRC-1 token tipping + let transId = await window.api.icrc_transfer( + Principal.fromText(canisterId), + Principal.fromText(postAuthor.principal), + amount, + canister.fee, + numberToUint8Array(post.id), + ); + + if (Number.isNaN(transId as number)) { + throw new Error( + transId.toString() || "Something went wrong with transfer!", + ); + } + + const optimisticPostTip: PostTip = { + amount, + canister_id: canisterId, + index: Number(transId), + sender_id: window.user.id, + }; + setExternalTips([...externalTips, optimisticPostTip]); + + popUpCallback(); + + let addTipResponse = await window.api.call<{ + Ok: PostTip; + Err: string; + }>( + "add_external_icrc_transaction", + canisterId, + Number(transId), + post.id, + ); + if ("Err" in (addTipResponse || {}) || !addTipResponse) { + setExternalTips( + externalTips.filter( + ({ canister_id, index }) => + index !== optimisticPostTip.index || + canisterId !== canister_id, + ), + ); + throw new Error( + addTipResponse?.Err || "Could not add tip to post.", + ); + } + + setExternalTips([ + ...externalTips.filter( + ({ index }) => index !== Number(transId), + ), + addTipResponse.Ok, + ]); + } catch (e: any) { + return showPopUp("error", e?.message || e); + } + }; + + const canister = canistersMetaData[selectedTippingCanisterId]; + + return ( +
+

+ Tip {post.meta.author_name} + with + [ + canisterId, + canistersMetaData[canisterId], + ])} + onSelectionChange={onTokenSelectionChange} + selectedCanisterId={selectedTippingCanisterId} + /> +

+ { + setTippingAmount(e.target.value); + setShowConfirmation(false); + }} + /> + {showConfirmation && canister && ( +
+ Transfer{" "} + + {Number(tippingAmount).toLocaleString()}{" "} + {canister.symbol} + {" "} + to + + as a tip? +
+ )} + {!showConfirmation ? ( + setShowConfirmation(true)} + /> + ) : ( +
+ { + if (parentCallback) parentCallback(); + }} + /> + + finalizeTip(parentCallback || (() => {})) + } + /> +
+ )} +
+ ); +};