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 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/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/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..019b3f95 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"); @@ -185,23 +186,30 @@ 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(); - }); - await waitForUILoading(page); + // Click SEND to show confirmation + await popup.getByText("SEND").click(); - await page.goto("/"); - await waitForUILoading(page); - await page.getByTestId("toggle-user-section").click(); + // Wait for confirmation UI to appear and click CONFIRM + await expect(popup.getByText("CONFIRM")).toBeVisible(); + await popup.getByText("CONFIRM").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 +223,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"); @@ -225,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(); 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/backend/env/features.rs b/src/backend/env/features.rs index 90cc3f4a..39393efc 100644 --- a/src/backend/env/features.rs +++ b/src/backend/env/features.rs @@ -3,9 +3,13 @@ use std::collections::HashSet; use candid::Principal; use serde::{Deserialize, Serialize}; -use crate::mutate; +use crate::{ + env::{post::Meta, Time, YEAR}, + mutate, +}; use super::{ + config::CONFIG, post::{Extension, Post, PostId}, token::Token, user::UserId, @@ -18,47 +22,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,19 +80,15 @@ 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(); - 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()); - }; + } if state.memory.features.get(&post_id).is_some() { return Err("feature already exists".into()); @@ -97,14 +102,18 @@ 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"); - 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 + ), + CONFIG.dao_realm.into(), + ); Ok(()) }) diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index e66343fd..a565d167 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -69,6 +69,8 @@ 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 @@ -285,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)] @@ -480,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())), ) @@ -853,7 +862,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) } @@ -889,6 +901,19 @@ impl State { Ok(()) } + 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( &mut self, principal: Principal, @@ -1201,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); @@ -1565,6 +1590,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); @@ -1590,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 }) { @@ -1612,12 +1639,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/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| { 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 77187538..fee79d92 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 + ), + CONFIG.dao_realm.into(), + ); Ok(()) } @@ -406,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); @@ -524,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,13 +541,13 @@ pub(crate) mod tests { 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(), &[], @@ -556,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(2) - ); + 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(3) - ); + 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(&2)); + 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(), &[], @@ -605,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(); @@ -662,19 +661,17 @@ pub(crate) mod tests { assert_eq!(state.realms.get(&realm_name).unwrap().num_members, 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(6) - ); + 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 @@ -683,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( - 6, + new_realm_post_id, "changed".to_string(), vec![], "".to_string(), @@ -702,12 +699,15 @@ pub(crate) mod tests { ); read(|state| { - assert_eq!(Post::get(state, &6).unwrap().realm, Some(realm_name)); + 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( - 6, + new_realm_post_id, "changed".to_string(), vec![], "".to_string(), @@ -723,7 +723,7 @@ pub(crate) mod tests { 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, &6).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 9b3ddcad..88027284 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}"), + CONFIG.stalwarts_realm.into(), + ) + .expect("couldn't create report post"); state.notify_with_predicate( &|u| u.stalwart && u.id != id, 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 d1e244da..5b49d4e5 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -111,19 +111,41 @@ fn post_upgrade() { #[allow(clippy::all)] fn sync_post_upgrade_fixtures() { mutate(|state| { + // Update last_activity for features + let ids = state + .memory + .features + .iter() + .map(|(id, _)| id) + .cloned() + .collect::>(); + for id in ids.into_iter() { + let mut feature = state.memory.features.remove(&id).unwrap(); + let post = Post::get(state, &id).expect("post not found for feature"); + feature.last_activity = post.timestamp(); + state + .memory + .features + .insert(id, feature) + .expect("couldn't re-insert feature"); + } + + // Cleanup old logs + state.logger.clean_up(time() - MONTH * 6); + + // Clear feeds if they exceed 200 chars in total 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 + >= 200 { u.feeds.clear(); } } - }) + }); } #[allow(clippy::all)] @@ -305,13 +327,17 @@ 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"] diff --git a/src/frontend/src/authentication.tsx b/src/frontend/src/authentication.tsx index 60964453..f355a3f6 100644 --- a/src/frontend/src/authentication.tsx +++ b/src/frontend/src/authentication.tsx @@ -14,8 +14,49 @@ 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 = [ + { + 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 +99,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) => { @@ -156,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/dashboard.tsx b/src/frontend/src/dashboard.tsx index a59aba0c..ec1db9e5 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,25 +268,25 @@ 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); + return ( + + + + + + ); + })} + +
+ {shortDate(date)} + {level2icon(level)} + +
); @@ -323,9 +306,9 @@ const shortDate = (date: Date) => { const level2icon = (level: string) => { switch (level) { case "INFO": - return ""; + return "â„šī¸"; case "DEBUG": - return ""; + return "🤖"; case "WARN": return "âš ī¸"; case "ERROR": 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 && ( <>