diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index 7bd892efa9..f57a695c3c 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Add support for expiring access gates ([#6401](https://github.com/open-chat-labs/open-chat/pull/6401)) +- Add support for a message activity feed ([#6539](https://github.com/open-chat-labs/open-chat/pull/6539)) + ## [[2.0.1374](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1374-user)] - 2024-10-07 ### Added @@ -24,7 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Refactor transfers to differentiate between transfers that failed due to c2c error vs transfer error ([#6500](https://github.com/open-chat-labs/open-chat/pull/6500)) - Refactor ICPSwap and Sonic swap clients ([#6505](https://github.com/open-chat-labs/open-chat/pull/6505)) - Award more daily CHIT when on 100 or 365 day streaks ([#6522](https://github.com/open-chat-labs/open-chat/pull/6522)) -- Add support for expiring access gates ([#6401](https://github.com/open-chat-labs/open-chat/pull/6401)) ### Fixed diff --git a/backend/canisters/user/api/can.did b/backend/canisters/user/api/can.did index fa3ec917b3..f8137dc8f6 100644 --- a/backend/canisters/user/api/can.did +++ b/backend/canisters/user/api/can.did @@ -355,6 +355,14 @@ type MarkAchievementsSeenResponse = variant { Success; }; +type MarkMessageActivityFeedReadArgs = record { + read_up_to : TimestampMillis; +}; + +type MarkMessageActivityFeedReadResponse = variant { + Success; +}; + type MarkReadArgs = record { messages_read : vec ChatMessagesRead; community_messages_read : vec CommunityMessagesRead; @@ -918,6 +926,7 @@ type InitialStateResponse = variant { is_unique_person : bool; wallet_config: WalletConfig; referrals : vec Referral; + message_activity_summary : MessageActivitySummary; }; }; @@ -981,10 +990,17 @@ type UpdatesResponse = variant { is_unique_person : opt bool; wallet_config: opt WalletConfig; referrals : vec Referral; + message_activity_summary : opt MessageActivitySummary; }; SuccessNoUpdates; }; +type MessageActivitySummary = record { + read_up_to : TimestampMillis; + latest_event : TimestampMillis; + unread_count : nat32; +}; + type DirectChatsUpdates = record { added : vec DirectChatSummary; updated : vec DirectChatSummaryUpdates; @@ -1248,6 +1264,38 @@ type Referral = record { status : ReferralStatus; }; +type MessageActivityFeedArgs = record { + since : TimestampMillis; + max : nat32; +}; + +type MessageActivityFeedResponse = variant { + Success: record { + events : vec MessageActivityEvent; + total : nat32; + }; +}; + +type MessageActivityEvent = record { + chat : Chat; + thread_root_message_index : opt MessageIndex; + message_index : MessageIndex; + activity : MessageActivity; + timestamp : TimestampMillis; + user_id : UserId; +}; + +type MessageActivity = variant { + Mention; + Reaction; + QuoteReply; + ThreadReply; + Tip; + Crypto; + PollVote; + P2PSwapAccepted; +}; + service : { send_message_v2 : (SendMessageV2Args) -> (SendMessageResponse); edit_message_v2 : (EditMessageV2Args) -> (EditMessageResponse); @@ -1257,6 +1305,7 @@ service : { remove_reaction : (RemoveReactionArgs) -> (RemoveReactionResponse); tip_message : (TipMessageArgs) -> (TipMessageResponse); mark_achievements_seen : (MarkAchievementsSeenArgs) -> (MarkAchievementsSeenResponse); + mark_message_activity_feed_read : (MarkMessageActivityFeedReadArgs) -> (MarkMessageActivityFeedReadResponse); mark_read : (MarkReadArgs) -> (MarkReadResponse); block_user : (BlockUserArgs) -> (BlockUserResponse); unblock_user : (UnblockUserArgs) -> (UnblockUserResponse); @@ -1314,6 +1363,7 @@ service : { token_swap_status : (TokenSwapStatusArgs) -> (TokenSwapStatusResponse) query; local_user_index : (EmptyArgs) -> (LocalUserIndexResponse) query; chit_events : (ChitEventsArgs) -> (ChitEventsResponse) query; + message_activity_feed : (MessageActivityFeedArgs) -> (MessageActivityFeedResponse) query; cached_btc_address : (EmptyArgs) -> (CachedBtcAddressResponse) query; btc_address : (EmptyArgs) -> (BtcAddressResponse); diff --git a/backend/canisters/user/api/src/lib.rs b/backend/canisters/user/api/src/lib.rs index 6f000920d4..f2458082ef 100644 --- a/backend/canisters/user/api/src/lib.rs +++ b/backend/canisters/user/api/src/lib.rs @@ -356,8 +356,59 @@ pub struct Referrals { pub referrals: Vec, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct ExternalAchievementAwarded { pub name: String, pub chit_reward: u32, } + +#[ts_export(user)] +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct MessageActivityEvent { + pub chat: Chat, + pub thread_root_message_index: Option, + pub message_index: MessageIndex, + pub activity: MessageActivity, + pub timestamp: TimestampMillis, + pub user_id: UserId, +} + +impl MessageActivityEvent { + pub fn matches(&self, event: &MessageActivityEvent) -> bool { + self.chat == event.chat + && self.thread_root_message_index == event.thread_root_message_index + && self.message_index == event.message_index + && self.activity == event.activity + } +} + +#[ts_export(user)] +#[derive(CandidType, Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] +pub enum MessageActivity { + Mention, + Reaction, + QuoteReply, + ThreadReply, + Tip, + Crypto, + PollVote, + P2PSwapAccepted, +} + +#[ts_export(user)] +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct MessageActivitySummary { + pub read_up_to: TimestampMillis, + pub latest_event: TimestampMillis, + pub unread_count: u32, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum CommunityCanisterEvent { + MessageActivity(MessageActivityEvent), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum GroupCanisterEvent { + MessageActivity(MessageActivityEvent), +} diff --git a/backend/canisters/user/api/src/main.rs b/backend/canisters/user/api/src/main.rs index 7e439adc77..0fb258cba8 100644 --- a/backend/canisters/user/api/src/main.rs +++ b/backend/canisters/user/api/src/main.rs @@ -4,6 +4,7 @@ use ts_export::generate_ts_method; #[allow(deprecated)] fn main() { + generate_candid_method!(user, message_activity_feed, query); generate_candid_method!(user, bio, query); generate_candid_method!(user, cached_btc_address, query); generate_candid_method!(user, chit_events, query); @@ -46,6 +47,7 @@ fn main() { generate_candid_method!(user, leave_group, update); generate_candid_method!(user, manage_favourite_chats, update); generate_candid_method!(user, mark_achievements_seen, update); + generate_candid_method!(user, mark_message_activity_feed_read, update); generate_candid_method!(user, mark_read, update); generate_candid_method!(user, mute_notifications, update); generate_candid_method!(user, pin_chat_v2, update); diff --git a/backend/canisters/user/api/src/queries/initial_state.rs b/backend/canisters/user/api/src/queries/initial_state.rs index 2d593daf29..3ddd2a1806 100644 --- a/backend/canisters/user/api/src/queries/initial_state.rs +++ b/backend/canisters/user/api/src/queries/initial_state.rs @@ -1,4 +1,4 @@ -use crate::{Referral, WalletConfig}; +use crate::{MessageActivitySummary, Referral, WalletConfig}; use candid::CandidType; use serde::{Deserialize, Serialize}; use ts_export::ts_export; @@ -38,6 +38,7 @@ pub struct SuccessResult { pub is_unique_person: bool, pub wallet_config: WalletConfig, pub referrals: Vec, + pub message_activity_summary: MessageActivitySummary, } #[ts_export(user, initial_state)] diff --git a/backend/canisters/user/api/src/queries/message_activity_feed.rs b/backend/canisters/user/api/src/queries/message_activity_feed.rs new file mode 100644 index 0000000000..62678093e1 --- /dev/null +++ b/backend/canisters/user/api/src/queries/message_activity_feed.rs @@ -0,0 +1,26 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use ts_export::ts_export; +use types::TimestampMillis; + +use crate::MessageActivityEvent; + +#[ts_export(user, activity_feed)] +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub since: TimestampMillis, + pub max: u32, +} + +#[ts_export(user, activity_feed)] +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success(SuccessResult), +} + +#[ts_export(user, activity_feed)] +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct SuccessResult { + pub events: Vec, + pub total: u32, +} diff --git a/backend/canisters/user/api/src/queries/mod.rs b/backend/canisters/user/api/src/queries/mod.rs index 9980c2622e..c7e7873409 100644 --- a/backend/canisters/user/api/src/queries/mod.rs +++ b/backend/canisters/user/api/src/queries/mod.rs @@ -11,6 +11,7 @@ pub mod events_window; pub mod hot_group_exclusions; pub mod initial_state; pub mod local_user_index; +pub mod message_activity_feed; pub mod messages_by_message_index; pub mod public_profile; pub mod saved_crypto_accounts; diff --git a/backend/canisters/user/api/src/queries/updates.rs b/backend/canisters/user/api/src/queries/updates.rs index 5eb09e23ce..241f97cf72 100644 --- a/backend/canisters/user/api/src/queries/updates.rs +++ b/backend/canisters/user/api/src/queries/updates.rs @@ -1,4 +1,4 @@ -use crate::{Referral, WalletConfig}; +use crate::{MessageActivitySummary, Referral, WalletConfig}; use candid::CandidType; use serde::{Deserialize, Serialize}; use ts_export::ts_export; @@ -48,6 +48,7 @@ pub struct SuccessResult { pub is_unique_person: Option, pub wallet_config: Option, pub referrals: Vec, + pub message_activity_summary: Option, } #[ts_export(user, updates)] diff --git a/backend/canisters/user/api/src/updates/c2c_notify_community_canister_events.rs b/backend/canisters/user/api/src/updates/c2c_notify_community_canister_events.rs new file mode 100644 index 0000000000..93df963d01 --- /dev/null +++ b/backend/canisters/user/api/src/updates/c2c_notify_community_canister_events.rs @@ -0,0 +1,13 @@ +use crate::CommunityCanisterEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Args { + pub events: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + Success, + Blocked, +} diff --git a/backend/canisters/user/api/src/updates/c2c_notify_group_canister_events.rs b/backend/canisters/user/api/src/updates/c2c_notify_group_canister_events.rs new file mode 100644 index 0000000000..9f66470254 --- /dev/null +++ b/backend/canisters/user/api/src/updates/c2c_notify_group_canister_events.rs @@ -0,0 +1,12 @@ +use crate::GroupCanisterEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Args { + pub events: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + Success, +} diff --git a/backend/canisters/user/api/src/updates/mark_message_activity_feed_read.rs b/backend/canisters/user/api/src/updates/mark_message_activity_feed_read.rs new file mode 100644 index 0000000000..8aec3ddd17 --- /dev/null +++ b/backend/canisters/user/api/src/updates/mark_message_activity_feed_read.rs @@ -0,0 +1,16 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use ts_export::ts_export; +use types::TimestampMillis; + +#[ts_export(user, mark_read)] +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub read_up_to: TimestampMillis, +} + +#[ts_export(user, mark_read)] +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success, +} diff --git a/backend/canisters/user/api/src/updates/mod.rs b/backend/canisters/user/api/src/updates/mod.rs index b5590fc52d..49723c7066 100644 --- a/backend/canisters/user/api/src/updates/mod.rs +++ b/backend/canisters/user/api/src/updates/mod.rs @@ -12,8 +12,10 @@ pub mod c2c_handle_bot_messages; pub mod c2c_mark_community_updated_for_user; pub mod c2c_mark_group_updated_for_user; pub mod c2c_notify_achievement; +pub mod c2c_notify_community_canister_events; pub mod c2c_notify_community_deleted; pub mod c2c_notify_events; +pub mod c2c_notify_group_canister_events; pub mod c2c_notify_group_deleted; pub mod c2c_notify_user_canister_events; pub mod c2c_remove_from_community; @@ -38,6 +40,7 @@ pub mod leave_community; pub mod leave_group; pub mod manage_favourite_chats; pub mod mark_achievements_seen; +pub mod mark_message_activity_feed_read; pub mod mark_read; pub mod mute_notifications; pub mod pin_chat_v2; diff --git a/backend/canisters/user/impl/src/guards.rs b/backend/canisters/user/impl/src/guards.rs index a13202d9d5..25f5708889 100644 --- a/backend/canisters/user/impl/src/guards.rs +++ b/backend/canisters/user/impl/src/guards.rs @@ -51,11 +51,27 @@ pub fn caller_is_escrow_canister() -> Result<(), String> { pub fn caller_is_known_group_or_community_canister() -> Result<(), String> { if read_state(|state| state.is_caller_known_group_canister() || state.is_caller_known_community_canister()) { Ok(()) + } else { + Err("Caller is not a known group or community canister".to_owned()) + } +} + +pub fn caller_is_known_group_canister() -> Result<(), String> { + if read_state(|state| state.is_caller_known_group_canister()) { + Ok(()) } else { Err("Caller is not a known group canister".to_owned()) } } +pub fn caller_is_known_community_canister() -> Result<(), String> { + if read_state(|state| state.is_caller_known_community_canister()) { + Ok(()) + } else { + Err("Caller is not a known community canister".to_owned()) + } +} + pub fn caller_is_video_call_operator() -> Result<(), String> { if read_state(|state| state.is_caller_video_call_operator()) { Ok(()) diff --git a/backend/canisters/user/impl/src/lib.rs b/backend/canisters/user/impl/src/lib.rs index caaa36b168..da232a233c 100644 --- a/backend/canisters/user/impl/src/lib.rs +++ b/backend/canisters/user/impl/src/lib.rs @@ -18,6 +18,7 @@ use fire_and_forget_handler::FireAndForgetHandler; use model::chit::ChitEarnedEvents; use model::contacts::Contacts; use model::favourite_chats::FavouriteChats; +use model::message_activity_events::MessageActivityEvents; use model::referrals::Referrals; use model::streak::Streak; use notifications_canister::c2c_push_notification; @@ -243,17 +244,15 @@ struct Data { pub chit_events: ChitEarnedEvents, pub streak: Streak, pub achievements: HashSet, - #[serde(default)] pub external_achievements: HashSet, pub achievements_last_seen: TimestampMillis, pub unique_person_proof: Option, - #[serde(default)] pub wallet_config: Timestamped, pub rng_seed: [u8; 32], - #[serde(default)] pub referred_by: Option, - #[serde(default)] pub referrals: Referrals, + #[serde(default)] + pub message_activity_events: MessageActivityEvents, } impl Data { @@ -321,6 +320,7 @@ impl Data { wallet_config: Timestamped::default(), referred_by, referrals: Referrals::default(), + message_activity_events: MessageActivityEvents::default(), } } diff --git a/backend/canisters/user/impl/src/model/message_activity_events.rs b/backend/canisters/user/impl/src/model/message_activity_events.rs new file mode 100644 index 0000000000..8485ed9078 --- /dev/null +++ b/backend/canisters/user/impl/src/model/message_activity_events.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use types::TimestampMillis; +use user_canister::{MessageActivityEvent, MessageActivitySummary}; + +#[derive(Serialize, Deserialize, Default)] +pub struct MessageActivityEvents { + events: VecDeque, + read_up_to: TimestampMillis, + last_updated: TimestampMillis, +} + +impl MessageActivityEvents { + const MAX_EVENTS: usize = 1000; + + pub fn push(&mut self, event: MessageActivityEvent, now: TimestampMillis) { + if let Some(matching_index) = self.events.iter().position(|e| event.matches(e)) { + // Remove any matching event action + self.events.remove(matching_index); + } else if self.events.len() > MessageActivityEvents::MAX_EVENTS - 1 { + // Keep no more than MAX_EVENTS + self.events.pop_back(); + } + + // Ensure events are ordered by timestamp - in general the event will be inserted at the front + if let Some(index) = self.events.iter().position(|e| event.timestamp >= e.timestamp) { + self.events.insert(index, event); + } else { + self.events.push_back(event); + } + + self.last_updated = now; + } + + pub fn mark_read_up_to(&mut self, read_up_to: TimestampMillis, now: TimestampMillis) { + self.read_up_to = read_up_to; + self.last_updated = now; + } + + pub fn summary(&self) -> MessageActivitySummary { + let unread_count = self.events.iter().take_while(|e| e.timestamp > self.read_up_to).count() as u32; + + MessageActivitySummary { + read_up_to: self.read_up_to, + unread_count, + latest_event: self.events.front().map_or(0, |e| e.timestamp), + } + } + + pub fn latest_events(&self, since: TimestampMillis, max: u32) -> Vec { + self.events + .iter() + .take(max as usize) + .take_while(|e| e.timestamp > since) + .cloned() + .collect() + } + + pub fn len(&self) -> u32 { + self.events.len() as u32 + } + + pub fn last_updated(&self) -> TimestampMillis { + self.last_updated + } +} diff --git a/backend/canisters/user/impl/src/model/mod.rs b/backend/canisters/user/impl/src/model/mod.rs index 4189ec7c94..0aa67346a9 100644 --- a/backend/canisters/user/impl/src/model/mod.rs +++ b/backend/canisters/user/impl/src/model/mod.rs @@ -8,6 +8,7 @@ pub mod favourite_chats; pub mod group_chat; pub mod group_chats; pub mod hot_group_exclusions; +pub mod message_activity_events; pub mod p2p_swaps; pub mod pin_number; pub mod referrals; diff --git a/backend/canisters/user/impl/src/queries/initial_state.rs b/backend/canisters/user/impl/src/queries/initial_state.rs index ed037e2bf2..4d7738eb42 100644 --- a/backend/canisters/user/impl/src/queries/initial_state.rs +++ b/backend/canisters/user/impl/src/queries/initial_state.rs @@ -56,5 +56,6 @@ fn initial_state_impl(state: &RuntimeState) -> Response { is_unique_person: state.data.unique_person_proof.is_some(), wallet_config: state.data.wallet_config.value.clone(), referrals: state.data.referrals.list(), + message_activity_summary: state.data.message_activity_events.summary(), }) } diff --git a/backend/canisters/user/impl/src/queries/message_activity_feed.rs b/backend/canisters/user/impl/src/queries/message_activity_feed.rs new file mode 100644 index 0000000000..71fa65aa75 --- /dev/null +++ b/backend/canisters/user/impl/src/queries/message_activity_feed.rs @@ -0,0 +1,16 @@ +use crate::read_state; +use crate::{guards::caller_is_owner, RuntimeState}; +use canister_api_macros::query; +use user_canister::message_activity_feed::{Response::*, *}; + +#[query(guard = "caller_is_owner", candid = true, msgpack = true)] +fn message_activity_feed(args: Args) -> Response { + read_state(|state| message_activity_feed_impl(args, state)) +} + +fn message_activity_feed_impl(args: Args, state: &RuntimeState) -> Response { + let events = state.data.message_activity_events.latest_events(args.since, args.max); + let total = state.data.message_activity_events.len(); + + Success(SuccessResult { events, total }) +} diff --git a/backend/canisters/user/impl/src/queries/mod.rs b/backend/canisters/user/impl/src/queries/mod.rs index a181ea0651..9e110e0898 100644 --- a/backend/canisters/user/impl/src/queries/mod.rs +++ b/backend/canisters/user/impl/src/queries/mod.rs @@ -15,6 +15,7 @@ pub mod hot_group_exclusions; pub mod http_request; pub mod initial_state; pub mod local_user_index; +pub mod message_activity_feed; pub mod messages_by_message_index; pub mod public_profile; pub mod saved_crypto_accounts; diff --git a/backend/canisters/user/impl/src/queries/updates.rs b/backend/canisters/user/impl/src/queries/updates.rs index 89dcbdb3d7..7f597bfedd 100644 --- a/backend/canisters/user/impl/src/queries/updates.rs +++ b/backend/canisters/user/impl/src/queries/updates.rs @@ -60,7 +60,8 @@ fn updates_impl(updates_since: TimestampMillis, state: &RuntimeState) -> Respons || state.data.favourite_chats.any_updated(updates_since) || state.data.communities.any_updated(updates_since) || state.data.chit_events.last_updated() > updates_since - || state.data.achievements_last_seen > updates_since; + || state.data.achievements_last_seen > updates_since + || state.data.message_activity_events.last_updated() > updates_since; // Short circuit prior to calling `ic0.time()` so that caching works effectively if !has_any_updates { @@ -151,6 +152,8 @@ fn updates_impl(updates_since: TimestampMillis, state: &RuntimeState) -> Respons let next_daily_claim = if state.data.streak.can_claim(now) { today(now) } else { tomorrow(now) }; let streak_ends = state.data.streak.ends(); let is_unique_person = is_unique_person_updated.then_some(true); + let activity_feed = + (state.data.message_activity_events.last_updated() > updates_since).then(|| state.data.message_activity_events.summary()); Success(SuccessResult { timestamp: now, @@ -174,5 +177,6 @@ fn updates_impl(updates_since: TimestampMillis, state: &RuntimeState) -> Respons is_unique_person, wallet_config, referrals, + message_activity_summary: activity_feed, }) } diff --git a/backend/canisters/user/impl/src/updates/c2c_notify_community_canister_events.rs b/backend/canisters/user/impl/src/updates/c2c_notify_community_canister_events.rs new file mode 100644 index 0000000000..3f2abf252c --- /dev/null +++ b/backend/canisters/user/impl/src/updates/c2c_notify_community_canister_events.rs @@ -0,0 +1,29 @@ +use crate::guards::caller_is_known_community_canister; +use crate::{mutate_state, run_regular_jobs, RuntimeState}; +use canister_api_macros::update; +use canister_tracing_macros::trace; +use user_canister::c2c_notify_community_canister_events::{Response::*, *}; +use user_canister::CommunityCanisterEvent; + +#[update(guard = "caller_is_known_community_canister", msgpack = true)] +#[trace] +async fn c2c_notify_community_canister_events(args: Args) -> Response { + run_regular_jobs(); + + mutate_state(|state| c2c_notify_community_canister_events_impl(args, state)) +} + +fn c2c_notify_community_canister_events_impl(args: Args, state: &mut RuntimeState) -> Response { + for event in args.events { + process_event(event, state); + } + Success +} + +fn process_event(event: CommunityCanisterEvent, state: &mut RuntimeState) { + let now = state.env.now(); + + match event { + CommunityCanisterEvent::MessageActivity(event) => state.data.message_activity_events.push(event, now), + } +} diff --git a/backend/canisters/user/impl/src/updates/c2c_notify_group_canister_events.rs b/backend/canisters/user/impl/src/updates/c2c_notify_group_canister_events.rs new file mode 100644 index 0000000000..5313f33b85 --- /dev/null +++ b/backend/canisters/user/impl/src/updates/c2c_notify_group_canister_events.rs @@ -0,0 +1,29 @@ +use crate::guards::caller_is_known_group_canister; +use crate::{mutate_state, run_regular_jobs, RuntimeState}; +use canister_api_macros::update; +use canister_tracing_macros::trace; +use user_canister::c2c_notify_group_canister_events::{Response::*, *}; +use user_canister::GroupCanisterEvent; + +#[update(guard = "caller_is_known_group_canister", msgpack = true)] +#[trace] +async fn c2c_notify_group_canister_events(args: Args) -> Response { + run_regular_jobs(); + + mutate_state(|state| c2c_notify_group_canister_events_impl(args, state)) +} + +fn c2c_notify_group_canister_events_impl(args: Args, state: &mut RuntimeState) -> Response { + for event in args.events { + process_event(event, state); + } + Success +} + +fn process_event(event: GroupCanisterEvent, state: &mut RuntimeState) { + let now = state.env.now(); + + match event { + GroupCanisterEvent::MessageActivity(event) => state.data.message_activity_events.push(event, now), + } +} diff --git a/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs b/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs index f433aa1137..4b77852cc4 100644 --- a/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs +++ b/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs @@ -1,4 +1,3 @@ -use crate::model::direct_chat::DirectChat; use crate::timer_job_types::{HardDeleteMessageContentJob, TimerJob}; use crate::updates::c2c_send_messages::{get_sender_status, handle_message_impl, verify_user, HandleMessageArgs}; use crate::updates::start_video_call::handle_start_video_call; @@ -12,11 +11,13 @@ use chat_events::{ use event_store_producer_cdk_runtime::CdkRuntime; use ledger_utils::format_crypto_amount_with_symbol; use types::{ - Achievement, ChitEarned, ChitEarnedReason, DirectMessageTipped, DirectReactionAddedNotification, EventIndex, Notification, - UserId, UserType, VideoCallPresence, + Achievement, Chat, ChitEarned, ChitEarnedReason, DirectMessageTipped, DirectReactionAddedNotification, EventIndex, + Notification, P2PSwapStatus, UserId, UserType, VideoCallPresence, }; use user_canister::c2c_notify_user_canister_events::{Response::*, *}; -use user_canister::{SendMessagesArgs, ToggleReactionArgs, UserCanisterEvent}; +use user_canister::{ + MessageActivity, MessageActivityEvent, P2PSwapStatusChange, SendMessagesArgs, ToggleReactionArgs, UserCanisterEvent, +}; use utils::time::{HOUR_IN_MS, MINUTE_IN_MS}; #[update(msgpack = true)] @@ -88,9 +89,7 @@ fn process_event(event: UserCanisterEvent, caller_user_id: UserId, state: &mut R } } UserCanisterEvent::P2PSwapStatusChange(c) => { - if let Some(chat) = state.data.direct_chats.get_mut(&caller_user_id.into()) { - chat.events.set_p2p_swap_status(None, c.message_id, c.status, now); - } + p2p_swap_change_status(*c, caller_user_id, state); } UserCanisterEvent::JoinVideoCall(c) => { if let Some(chat) = state.data.direct_chats.get_mut(&caller_user_id.into()) { @@ -272,56 +271,79 @@ fn toggle_reaction(args: ToggleReactionArgs, caller_user_id: UserId, state: &mut if matches!( chat.events.add_reaction::(add_remove_reaction_args, None), AddRemoveReactionResult::Success(_) - ) && !state.data.suspended.value - { - if let Some((recipient, notification)) = build_notification(args, chat) { - state.push_notification(recipient, notification); + ) { + if let Some(message_event) = chat + .events + .main_events_reader() + .message_event_internal(args.message_id.into()) + { + if !state.data.suspended.value && !args.username.is_empty() && !chat.notifications_muted.value { + let notification = Notification::DirectReactionAdded(DirectReactionAddedNotification { + them: chat.them, + thread_root_message_index, + message_index: message_event.event.message_index, + message_event_index: message_event.index, + username: args.username, + display_name: args.display_name, + reaction: args.reaction, + user_avatar_id: args.user_avatar_id, + }); + + state.push_notification(message_event.event.sender, notification); + } + + state.data.message_activity_events.push( + MessageActivityEvent { + chat: Chat::Direct(caller_user_id.into()), + thread_root_message_index, + message_index: message_event.event.message_index, + activity: MessageActivity::Reaction, + timestamp: now, + user_id: caller_user_id, + }, + now, + ); } - } - state.data.award_achievement_and_notify(Achievement::HadMessageReactedTo, now); + state.data.award_achievement_and_notify(Achievement::HadMessageReactedTo, now); + } } else { chat.events.remove_reaction(add_remove_reaction_args); } } } -fn build_notification( - ToggleReactionArgs { - thread_root_message_id, - message_id, - reaction, - username, - display_name, - user_avatar_id, - .. - }: ToggleReactionArgs, - chat: &DirectChat, -) -> Option<(UserId, Notification)> { - if username.is_empty() || chat.notifications_muted.value { - return None; - } +fn p2p_swap_change_status(args: P2PSwapStatusChange, caller_user_id: UserId, state: &mut RuntimeState) { + let Some(chat) = state.data.direct_chats.get_mut(&caller_user_id.into()) else { + return; + }; - let thread_root_message_index = thread_root_message_id.map(|id| chat.main_message_id_to_index(id)); - let message_event = chat - .events - .events_reader(EventIndex::default(), thread_root_message_index) - .and_then(|reader| reader.message_event(message_id.into(), None)) - .filter(|m| m.event.sender != chat.them)?; - - Some(( - message_event.event.sender, - Notification::DirectReactionAdded(DirectReactionAddedNotification { - them: chat.them, - thread_root_message_index, - message_index: message_event.event.message_index, - message_event_index: message_event.index, - username, - display_name, - reaction, - user_avatar_id, - }), - )) + let now = state.env.now(); + let completed = matches!(args.status, P2PSwapStatus::Completed(_)); + + chat.events.set_p2p_swap_status(None, args.message_id, args.status, now); + + if completed { + if let Some(message_event) = chat + .events + .main_events_reader() + .message_event_internal(args.message_id.into()) + { + let thread_root_message_index = args.thread_root_message_id.map(|id| chat.main_message_id_to_index(id)); + + state.data.message_activity_events.push( + MessageActivityEvent { + chat: Chat::Direct(caller_user_id.into()), + thread_root_message_index, + message_index: message_event.event.message_index, + activity: MessageActivity::P2PSwapAccepted, + timestamp: now, + user_id: caller_user_id, + }, + now, + ); + } + } } fn tip_message(args: user_canister::TipMessageArgs, caller_user_id: UserId, state: &mut RuntimeState) { @@ -362,6 +384,18 @@ fn tip_message(args: user_canister::TipMessageArgs, caller_user_id: UserId, stat user_avatar_id: args.user_avatar_id, }); state.push_notification(my_user_id, notification); + + state.data.message_activity_events.push( + MessageActivityEvent { + chat: Chat::Direct(caller_user_id.into()), + thread_root_message_index, + message_index: event.event.message_index, + activity: MessageActivity::Tip, + timestamp: now, + user_id: caller_user_id, + }, + now, + ); } state.data.award_achievement_and_notify(Achievement::HadMessageTipped, now); diff --git a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs index d7ac222b50..e9233a41c6 100644 --- a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs +++ b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs @@ -5,10 +5,10 @@ use chat_events::{MessageContentInternal, PushMessageArgs, Reader, ReplyContextI use ic_cdk::update; use rand::Rng; use types::{ - CanisterId, DirectMessageNotification, EventWrapper, Message, MessageId, MessageIndex, Notification, TimestampMillis, User, - UserId, UserType, + CanisterId, Chat, DirectMessageNotification, EventWrapper, Message, MessageContent, MessageId, MessageIndex, Notification, + TimestampMillis, User, UserId, UserType, }; -use user_canister::C2CReplyContext; +use user_canister::{C2CReplyContext, MessageActivity, MessageActivityEvent}; #[update] #[trace] @@ -156,12 +156,13 @@ pub(crate) fn handle_message_impl(args: HandleMessageArgs, state: &mut RuntimeSt args.push_message_sent_event.then_some(&mut state.data.event_store_client), ); + let content = &message_event.event.content; + if args.sender_user_type.is_bot() { chat.mark_read_up_to(message_event.event.message_index, false, args.now); } if !args.mute_notification && !chat.notifications_muted.value && !state.data.suspended.value { - let content = &message_event.event.content; let notification = Notification::DirectMessage(DirectMessageNotification { sender: args.sender, thread_root_message_index, @@ -180,6 +181,20 @@ pub(crate) fn handle_message_impl(args: HandleMessageArgs, state: &mut RuntimeSt state.push_notification(recipient, notification); } + if matches!(content, MessageContent::Crypto(_)) { + state.data.message_activity_events.push( + MessageActivityEvent { + chat: Chat::Direct(chat_id), + thread_root_message_index, + message_index: message_event.event.message_index, + activity: MessageActivity::Crypto, + timestamp: args.now, + user_id: args.sender, + }, + args.now, + ); + } + register_timer_jobs( chat_id, thread_root_message_index, diff --git a/backend/canisters/user/impl/src/updates/mark_message_activity_feed_read.rs b/backend/canisters/user/impl/src/updates/mark_message_activity_feed_read.rs new file mode 100644 index 0000000000..1e5a4ce02a --- /dev/null +++ b/backend/canisters/user/impl/src/updates/mark_message_activity_feed_read.rs @@ -0,0 +1,19 @@ +use crate::guards::caller_is_owner; +use crate::{mutate_state, run_regular_jobs, RuntimeState}; +use canister_api_macros::update; +use canister_tracing_macros::trace; +use user_canister::mark_message_activity_feed_read::{Response::*, *}; + +#[update(guard = "caller_is_owner", candid = true, msgpack = true)] +#[trace] +fn mark_message_activity_feed_read(args: Args) -> Response { + run_regular_jobs(); + + mutate_state(|state| mark_message_activity_feed_read_impl(args, state)) +} + +fn mark_message_activity_feed_read_impl(args: Args, state: &mut RuntimeState) -> Response { + let now = state.env.now(); + state.data.message_activity_events.mark_read_up_to(args.read_up_to, now); + Success +} diff --git a/backend/canisters/user/impl/src/updates/mod.rs b/backend/canisters/user/impl/src/updates/mod.rs index 3cca0a5960..347b8e77db 100644 --- a/backend/canisters/user/impl/src/updates/mod.rs +++ b/backend/canisters/user/impl/src/updates/mod.rs @@ -11,8 +11,10 @@ pub mod c2c_grant_super_admin; pub mod c2c_mark_community_updated_for_user; pub mod c2c_mark_group_updated_for_user; pub mod c2c_notify_achievement; +pub mod c2c_notify_community_canister_events; pub mod c2c_notify_community_deleted; pub mod c2c_notify_events; +pub mod c2c_notify_group_canister_events; pub mod c2c_notify_group_deleted; pub mod c2c_notify_p2p_swap_status_change; pub mod c2c_notify_user_canister_events; @@ -39,6 +41,7 @@ pub mod leave_community; pub mod leave_group; pub mod manage_favourite_chats; pub mod mark_achievements_seen; +pub mod mark_message_activity_feed_read; pub mod mark_read; pub mod mute_notifications; pub mod pin_chat_v2; diff --git a/tsBindings/user/UserMessageActivitySummary.ts b/tsBindings/user/UserMessageActivitySummary.ts new file mode 100644 index 0000000000..cb01e6c683 --- /dev/null +++ b/tsBindings/user/UserMessageActivitySummary.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UserMessageActivitySummary = { read_up_to: bigint, latest_event: bigint, unread_count: number, }; diff --git a/tsBindings/user/initialState/UserInitialStateSuccessResult.ts b/tsBindings/user/initialState/UserInitialStateSuccessResult.ts index fd10b850bf..3142e35098 100644 --- a/tsBindings/user/initialState/UserInitialStateSuccessResult.ts +++ b/tsBindings/user/initialState/UserInitialStateSuccessResult.ts @@ -7,7 +7,8 @@ import type { UserInitialStateCommunitiesInitial } from "./UserInitialStateCommu import type { UserInitialStateDirectChatsInitial } from "./UserInitialStateDirectChatsInitial"; import type { UserInitialStateFavouriteChatsInitial } from "./UserInitialStateFavouriteChatsInitial"; import type { UserInitialStateGroupChatsInitial } from "./UserInitialStateGroupChatsInitial"; +import type { UserMessageActivitySummary } from "../UserMessageActivitySummary"; import type { UserReferral } from "../UserReferral"; import type { UserWalletConfig } from "../UserWalletConfig"; -export type UserInitialStateSuccessResult = { timestamp: bigint, direct_chats: UserInitialStateDirectChatsInitial, group_chats: UserInitialStateGroupChatsInitial, favourite_chats: UserInitialStateFavouriteChatsInitial, communities: UserInitialStateCommunitiesInitial, avatar_id?: bigint, blocked_users: Array, suspended: boolean, pin_number_settings?: PinNumberSettings, local_user_index_canister_id: TSBytes, achievements: Array, achievements_last_seen: bigint, total_chit_earned: number, chit_balance: number, streak: number, streak_ends: bigint, next_daily_claim: bigint, is_unique_person: boolean, wallet_config: UserWalletConfig, referrals: Array, }; +export type UserInitialStateSuccessResult = { timestamp: bigint, direct_chats: UserInitialStateDirectChatsInitial, group_chats: UserInitialStateGroupChatsInitial, favourite_chats: UserInitialStateFavouriteChatsInitial, communities: UserInitialStateCommunitiesInitial, avatar_id?: bigint, blocked_users: Array, suspended: boolean, pin_number_settings?: PinNumberSettings, local_user_index_canister_id: TSBytes, achievements: Array, achievements_last_seen: bigint, total_chit_earned: number, chit_balance: number, streak: number, streak_ends: bigint, next_daily_claim: bigint, is_unique_person: boolean, wallet_config: UserWalletConfig, referrals: Array, message_activity_summary: UserMessageActivitySummary, }; diff --git a/tsBindings/user/sendMessageWithTransferToChannel/UserSendMessageWithTransferToChannelResponse.ts b/tsBindings/user/sendMessageWithTransferToChannel/UserSendMessageWithTransferToChannelResponse.ts index 91f195277f..46ba08daf1 100644 --- a/tsBindings/user/sendMessageWithTransferToChannel/UserSendMessageWithTransferToChannelResponse.ts +++ b/tsBindings/user/sendMessageWithTransferToChannel/UserSendMessageWithTransferToChannelResponse.ts @@ -3,4 +3,4 @@ import type { CompletedCryptoTransaction } from "../../shared/CompletedCryptoTra import type { Cryptocurrency } from "../../shared/Cryptocurrency"; import type { UserSendMessageWithTransferToChannelSuccessResult } from "./UserSendMessageWithTransferToChannelSuccessResult"; -export type UserSendMessageWithTransferToChannelResponse = { "Success": UserSendMessageWithTransferToChannelSuccessResult } | { "TextTooLong": number } | "RecipientBlocked" | { "UserNotInCommunity": CompletedCryptoTransaction | null } | { "UserNotInChannel": CompletedCryptoTransaction } | { "ChannelNotFound": CompletedCryptoTransaction } | { "CryptocurrencyNotSupported": Cryptocurrency } | { "InvalidRequest": string } | { "TransferFailed": string } | "TransferCannotBeZero" | "TransferCannotBeToSelf" | { "P2PSwapSetUpFailed": string } | "UserSuspended" | "UserLapsed" | "CommunityFrozen" | "RulesNotAccepted" | "CommunityRulesNotAccepted" | { "Retrying": [string, CompletedCryptoTransaction] } | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint }; +export type UserSendMessageWithTransferToChannelResponse = { "Success": UserSendMessageWithTransferToChannelSuccessResult } | { "TextTooLong": number } | "RecipientBlocked" | { "UserNotInCommunity": CompletedCryptoTransaction | null } | { "UserNotInChannel": CompletedCryptoTransaction } | { "ChannelNotFound": CompletedCryptoTransaction } | { "CryptocurrencyNotSupported": Cryptocurrency } | { "InvalidRequest": string } | { "TransferFailed": string } | "TransferCannotBeZero" | "TransferCannotBeToSelf" | { "P2PSwapSetUpFailed": string } | "UserSuspended" | "UserLapsed" | "CommunityFrozen" | "RulesNotAccepted" | "CommunityRulesNotAccepted" | { "Retrying": [string, CompletedCryptoTransaction] } | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint } | { "InternalError": string }; diff --git a/tsBindings/user/sendMessageWithTransferToGroup/UserSendMessageWithTransferToGroupResponse.ts b/tsBindings/user/sendMessageWithTransferToGroup/UserSendMessageWithTransferToGroupResponse.ts index 88c23045ef..40a587995e 100644 --- a/tsBindings/user/sendMessageWithTransferToGroup/UserSendMessageWithTransferToGroupResponse.ts +++ b/tsBindings/user/sendMessageWithTransferToGroup/UserSendMessageWithTransferToGroupResponse.ts @@ -3,4 +3,4 @@ import type { CompletedCryptoTransaction } from "../../shared/CompletedCryptoTra import type { Cryptocurrency } from "../../shared/Cryptocurrency"; import type { UserSendMessageWithTransferToGroupSuccessResult } from "./UserSendMessageWithTransferToGroupSuccessResult"; -export type UserSendMessageWithTransferToGroupResponse = { "Success": UserSendMessageWithTransferToGroupSuccessResult } | { "TextTooLong": number } | "RecipientBlocked" | { "CallerNotInGroup": CompletedCryptoTransaction | null } | { "CryptocurrencyNotSupported": Cryptocurrency } | { "InvalidRequest": string } | { "TransferFailed": string } | "TransferCannotBeZero" | "TransferCannotBeToSelf" | { "P2PSwapSetUpFailed": string } | "UserSuspended" | "UserLapsed" | "ChatFrozen" | "RulesNotAccepted" | { "Retrying": [string, CompletedCryptoTransaction] } | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint }; +export type UserSendMessageWithTransferToGroupResponse = { "Success": UserSendMessageWithTransferToGroupSuccessResult } | { "TextTooLong": number } | "RecipientBlocked" | { "CallerNotInGroup": CompletedCryptoTransaction | null } | { "CryptocurrencyNotSupported": Cryptocurrency } | { "InvalidRequest": string } | { "TransferFailed": string } | "TransferCannotBeZero" | "TransferCannotBeToSelf" | { "P2PSwapSetUpFailed": string } | "UserSuspended" | "UserLapsed" | "ChatFrozen" | "RulesNotAccepted" | { "Retrying": [string, CompletedCryptoTransaction] } | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint } | { "InternalError": string }; diff --git a/tsBindings/user/tipMessage/UserTipMessageResponse.ts b/tsBindings/user/tipMessage/UserTipMessageResponse.ts index 545dc04d28..8f960804ad 100644 --- a/tsBindings/user/tipMessage/UserTipMessageResponse.ts +++ b/tsBindings/user/tipMessage/UserTipMessageResponse.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CompletedCryptoTransaction } from "../../shared/CompletedCryptoTransaction"; -export type UserTipMessageResponse = "Success" | "ChatNotFound" | "MessageNotFound" | "CannotTipSelf" | "NotAuthorized" | "TransferCannotBeZero" | "TransferNotToMessageSender" | { "TransferFailed": string } | "ChatFrozen" | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint } | "UserSuspended" | "UserLapsed" | { "Retrying": string } | { "InternalError": [string, CompletedCryptoTransaction] }; +export type UserTipMessageResponse = "Success" | "ChatNotFound" | "MessageNotFound" | "CannotTipSelf" | "NotAuthorized" | "TransferCannotBeZero" | "TransferNotToMessageSender" | { "TransferFailed": string } | "ChatFrozen" | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint } | "UserSuspended" | "UserLapsed" | { "Retrying": string } | { "InternalError": string }; diff --git a/tsBindings/user/tokenSwapStatus/UserTokenSwapStatusTokenSwapStatus.ts b/tsBindings/user/tokenSwapStatus/UserTokenSwapStatusTokenSwapStatus.ts index 84044b357a..16f65397e0 100644 --- a/tsBindings/user/tokenSwapStatus/UserTokenSwapStatusTokenSwapStatus.ts +++ b/tsBindings/user/tokenSwapStatus/UserTokenSwapStatusTokenSwapStatus.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type UserTokenSwapStatusTokenSwapStatus = { started: bigint, deposit_account: { Ok : null } | { Err : string } | null, transfer: { Ok : bigint } | { Err : string } | null, notify_dex: { Ok : null } | { Err : string } | null, amount_swapped: { Ok : { Ok : bigint } | { Err : string } } | { Err : string } | null, withdraw_from_dex: { Ok : bigint } | { Err : string } | null, success?: boolean, }; +export type UserTokenSwapStatusTokenSwapStatus = { started: bigint, icrc2: boolean, auto_withdrawals: boolean, deposit_account: { Ok : null } | { Err : string } | null, transfer: { Ok : bigint } | { Err : string } | null, transfer_or_approval: { Ok : bigint } | { Err : string } | null, notify_dex: { Ok : null } | { Err : string } | null, amount_swapped: { Ok : { Ok : bigint } | { Err : string } } | { Err : string } | null, withdraw_from_dex: { Ok : bigint } | { Err : string } | null, success?: boolean, }; diff --git a/tsBindings/user/tokenSwaps/UserTokenSwapsTokenSwap.ts b/tsBindings/user/tokenSwaps/UserTokenSwapsTokenSwap.ts index 339e6e8032..d5b2183d98 100644 --- a/tsBindings/user/tokenSwaps/UserTokenSwapsTokenSwap.ts +++ b/tsBindings/user/tokenSwaps/UserTokenSwapsTokenSwap.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { UserSwapTokensArgs } from "../swapTokens/UserSwapTokensArgs"; -export type UserTokenSwapsTokenSwap = { args: UserSwapTokensArgs, started: bigint, transfer?: { Ok : bigint } | { Err : string }, notified_dex?: { Ok : null } | { Err : string }, amount_swapped?: { Ok : { Ok : bigint } | { Err : string } } | { Err : string }, withdrawn_from_dex?: { Ok : bigint } | { Err : string }, success?: boolean, }; +export type UserTokenSwapsTokenSwap = { args: UserSwapTokensArgs, started: bigint, icrc2: boolean, transfer_or_approval?: { Ok : bigint } | { Err : string }, notified_dex?: { Ok : null } | { Err : string }, amount_swapped?: { Ok : { Ok : bigint } | { Err : string } } | { Err : string }, withdrawn_from_dex?: { Ok : bigint } | { Err : string }, success?: boolean, }; diff --git a/tsBindings/user/updates/UserUpdatesSuccessResult.ts b/tsBindings/user/updates/UserUpdatesSuccessResult.ts index a2ac0d257c..65dd0297d0 100644 --- a/tsBindings/user/updates/UserUpdatesSuccessResult.ts +++ b/tsBindings/user/updates/UserUpdatesSuccessResult.ts @@ -4,6 +4,7 @@ import type { OptionUpdatePinNumberSettings } from "../../shared/OptionUpdatePin import type { OptionUpdateString } from "../../shared/OptionUpdateString"; import type { OptionUpdateU128 } from "../../shared/OptionUpdateU128"; import type { UserId } from "../../shared/UserId"; +import type { UserMessageActivitySummary } from "../UserMessageActivitySummary"; import type { UserReferral } from "../UserReferral"; import type { UserUpdatesCommunitiesUpdates } from "./UserUpdatesCommunitiesUpdates"; import type { UserUpdatesDirectChatsUpdates } from "./UserUpdatesDirectChatsUpdates"; @@ -11,4 +12,4 @@ import type { UserUpdatesFavouriteChatsUpdates } from "./UserUpdatesFavouriteCha import type { UserUpdatesGroupChatsUpdates } from "./UserUpdatesGroupChatsUpdates"; import type { UserWalletConfig } from "../UserWalletConfig"; -export type UserUpdatesSuccessResult = { timestamp: bigint, username?: string, display_name: OptionUpdateString, direct_chats: UserUpdatesDirectChatsUpdates, group_chats: UserUpdatesGroupChatsUpdates, favourite_chats: UserUpdatesFavouriteChatsUpdates, communities: UserUpdatesCommunitiesUpdates, avatar_id: OptionUpdateU128, blocked_users?: Array, suspended?: boolean, pin_number_settings: OptionUpdatePinNumberSettings, achievements: Array, achievements_last_seen?: bigint, total_chit_earned: number, chit_balance: number, streak: number, streak_ends: bigint, next_daily_claim: bigint, is_unique_person?: boolean, wallet_config?: UserWalletConfig, referrals: Array, }; +export type UserUpdatesSuccessResult = { timestamp: bigint, username?: string, display_name: OptionUpdateString, direct_chats: UserUpdatesDirectChatsUpdates, group_chats: UserUpdatesGroupChatsUpdates, favourite_chats: UserUpdatesFavouriteChatsUpdates, communities: UserUpdatesCommunitiesUpdates, avatar_id: OptionUpdateU128, blocked_users?: Array, suspended?: boolean, pin_number_settings: OptionUpdatePinNumberSettings, achievements: Array, achievements_last_seen?: bigint, total_chit_earned: number, chit_balance: number, streak: number, streak_ends: bigint, next_daily_claim: bigint, is_unique_person?: boolean, wallet_config?: UserWalletConfig, referrals: Array, message_activity_summary?: UserMessageActivitySummary, }; diff --git a/tsBindings/user/withdrawCrypto/UserWithdrawCryptoResponse.ts b/tsBindings/user/withdrawCrypto/UserWithdrawCryptoResponse.ts index fe38c5f66d..f0b86cab13 100644 --- a/tsBindings/user/withdrawCrypto/UserWithdrawCryptoResponse.ts +++ b/tsBindings/user/withdrawCrypto/UserWithdrawCryptoResponse.ts @@ -2,4 +2,4 @@ import type { CompletedCryptoTransaction } from "../../shared/CompletedCryptoTransaction"; import type { FailedCryptoTransaction } from "../../shared/FailedCryptoTransaction"; -export type UserWithdrawCryptoResponse = { "Success": CompletedCryptoTransaction } | { "TransactionFailed": FailedCryptoTransaction } | "CurrencyNotSupported" | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint }; +export type UserWithdrawCryptoResponse = { "Success": CompletedCryptoTransaction } | { "TransactionFailed": FailedCryptoTransaction } | "CurrencyNotSupported" | "PinRequired" | { "PinIncorrect": bigint } | { "TooManyFailedPinAttempts": bigint } | { "InternalError": string };