diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 0310383aee..9cff598073 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Implement tipping messages ([#4420](https://github.com/open-chat-labs/open-chat/pull/4420)) +- Implement notifications for message tips ([#4427](https://github.com/open-chat-labs/open-chat/pull/4427)) + ### Changed - Disable mentions for messages sent by the ProposalsBot ([#4424](https://github.com/open-chat-labs/open-chat/pull/4424)) diff --git a/backend/canisters/community/api/src/updates/c2c_tip_message.rs b/backend/canisters/community/api/src/updates/c2c_tip_message.rs index 3ce8600541..cc1a226284 100644 --- a/backend/canisters/community/api/src/updates/c2c_tip_message.rs +++ b/backend/canisters/community/api/src/updates/c2c_tip_message.rs @@ -4,11 +4,13 @@ use types::{ChannelId, CompletedCryptoTransaction, MessageId, MessageIndex, User #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { - pub message_sender: UserId, + pub recipient: UserId, pub channel_id: ChannelId, pub thread_root_message_index: Option, pub message_id: MessageId, pub transfer: CompletedCryptoTransaction, + pub username: String, + pub display_name: Option, } #[derive(CandidType, Serialize, Deserialize, Debug)] @@ -16,7 +18,7 @@ pub enum Response { Success, MessageNotFound, CannotTipSelf, - MessageSenderMismatch, + RecipientMismatch, NotAuthorized, CommunityFrozen, UserNotInCommunity, diff --git a/backend/canisters/community/impl/src/updates/c2c_tip_message.rs b/backend/canisters/community/impl/src/updates/c2c_tip_message.rs index 901e208c10..6b50789fc0 100644 --- a/backend/canisters/community/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/community/impl/src/updates/c2c_tip_message.rs @@ -2,8 +2,11 @@ use crate::activity_notifications::handle_activity_notification; use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; +use chat_events::Reader; use community_canister::c2c_tip_message::{Response::*, *}; use group_chat_core::TipMessageResult; +use ledger_utils::format_crypto_amount_with_symbol; +use types::{ChannelMessageTipped, EventIndex, Notification}; #[update_msgpack] #[trace] @@ -26,22 +29,50 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { if let Some(channel) = state.data.channels.get_mut(&args.channel_id) { let now = state.env.now(); + let token = args.transfer.token(); + let amount = args.transfer.units(); + match channel.chat.tip_message( user_id, - args.message_sender, + args.recipient, args.thread_root_message_index, args.message_id, args.transfer, now, ) { TipMessageResult::Success => { - // TODO push notification + if let Some((message_index, message_event_index)) = channel + .chat + .events + .events_reader(EventIndex::default(), args.thread_root_message_index, now) + .and_then(|r| { + r.message_event_internal(args.message_id.into()) + .map(|e| (e.event.message_index, e.index)) + }) + { + let notification = Notification::ChannelMessageTipped(ChannelMessageTipped { + community_id: state.env.canister_id().into(), + channel_id: channel.id, + thread_root_message_index: args.thread_root_message_index, + message_index, + message_event_index, + community_name: state.data.name.clone(), + channel_name: channel.chat.name.clone(), + tipped_by: user_id, + tipped_by_name: args.username, + tipped_by_display_name: args.display_name, + tip: format_crypto_amount_with_symbol(amount, token.decimals().unwrap_or(8), token.token_symbol()), + community_avatar_id: state.data.avatar.as_ref().map(|a| a.id), + channel_avatar_id: channel.chat.avatar.as_ref().map(|a| a.id), + }); + state.push_notification(vec![args.recipient], notification); + } handle_activity_notification(state); Success } TipMessageResult::MessageNotFound => MessageNotFound, TipMessageResult::CannotTipSelf => CannotTipSelf, - TipMessageResult::MessageSenderMismatch => MessageSenderMismatch, + TipMessageResult::RecipientMismatch => RecipientMismatch, TipMessageResult::UserNotInGroup => ChannelNotFound, TipMessageResult::NotAuthorized => NotAuthorized, TipMessageResult::UserSuspended => UserSuspended, diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 3a619c6afa..6c6ef5a13c 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -5,7 +5,7 @@ use crate::swap_client::SwapClient; use crate::{mutate_state, RuntimeState}; use exchange_bot_canister::ExchangeId; use lazy_static::lazy_static; -use ledger_utils::format_crypto_amount; +use ledger_utils::format_crypto_amount_with_symbol; use rand::Rng; use regex_lite::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; @@ -118,9 +118,8 @@ impl QuoteCommand { pub fn build_message_text(&self) -> String { let mut text = format!( - "Quotes ({} {} to {}):", - format_crypto_amount(self.amount, self.input_token.decimals), - self.input_token.token.token_symbol(), + "Quotes ({} to {}):", + format_crypto_amount_with_symbol(self.amount, self.input_token.decimals, self.input_token.token.token_symbol()), self.output_token.token.token_symbol() ); for (exchange_id, status) in self.results.iter() { diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs index fde32da302..35512c9aa7 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs @@ -1,5 +1,5 @@ use crate::commands::CommandSubTaskResult; -use ledger_utils::{convert_to_subaccount, format_crypto_amount}; +use ledger_utils::{convert_to_subaccount, format_crypto_amount_with_symbol}; use types::icrc1::Account; use types::{CanisterId, TokenInfo, UserId}; @@ -18,11 +18,7 @@ pub(crate) async fn check_user_balance( .map(|a| u128::try_from(a.0).unwrap()) { Ok(amount) => { - let text = format!( - "{} {}", - format_crypto_amount(amount, token.decimals), - token.token.token_symbol() - ); + let text = format_crypto_amount_with_symbol(amount, token.decimals, token.token.token_symbol()); CommandSubTaskResult::Complete(amount, Some(text)) } Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs index f26d581da3..d8f687237b 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs @@ -3,7 +3,7 @@ use crate::swap_client::SwapClient; use exchange_bot_canister::ExchangeId; use futures::stream::FuturesUnordered; use futures::StreamExt; -use ledger_utils::format_crypto_amount; +use ledger_utils::format_crypto_amount_with_symbol; use std::future::ready; pub(crate) async fn get_quotes)>( @@ -30,11 +30,7 @@ async fn get_quote(client: Box, amount: u128) -> (ExchangeId, Co let result = match response { Ok(amount_out) => { let output_token = client.output_token(); - let text = format!( - "{} {}", - format_crypto_amount(amount_out, output_token.decimals), - output_token.token.token_symbol() - ); + let text = format_crypto_amount_with_symbol(amount_out, output_token.decimals, output_token.token.token_symbol()); CommandSubTaskResult::Complete(amount_out, Some(text)) } Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 8fa1e8ccc7..017130307a 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -8,7 +8,7 @@ use candid::Principal; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use exchange_bot_canister::handle_direct_message::*; -use ledger_utils::format_crypto_amount; +use ledger_utils::format_crypto_amount_with_symbol; use local_user_index_canister_c2c_client::LookupUserError; use types::{BotMessage, MessageContent, MessageContentInitial, UserId}; @@ -28,10 +28,10 @@ fn handle_direct_message_impl(message: MessageContent, state: &mut RuntimeState) if let MessageContent::Crypto(c) = &message { let token = c.transfer.token(); - response_messages.push(convert_to_message(format!( - "{} {} received", - format_crypto_amount(c.transfer.units(), token.decimals().unwrap_or(8)), - token.token_symbol() + response_messages.push(convert_to_message(format_crypto_amount_with_symbol( + c.transfer.units(), + token.decimals().unwrap_or(8), + token.token_symbol(), ))); } diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index 32ed7be122..13cb2a24e9 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Implement tipping messages ([#4420](https://github.com/open-chat-labs/open-chat/pull/4420)) +- Implement notifications for message tips ([#4427](https://github.com/open-chat-labs/open-chat/pull/4427)) + ### Changed - Disable mentions for messages sent by the ProposalsBot ([#4424](https://github.com/open-chat-labs/open-chat/pull/4424)) diff --git a/backend/canisters/group/api/src/updates/c2c_tip_message.rs b/backend/canisters/group/api/src/updates/c2c_tip_message.rs index 29ea0333d3..382357905b 100644 --- a/backend/canisters/group/api/src/updates/c2c_tip_message.rs +++ b/backend/canisters/group/api/src/updates/c2c_tip_message.rs @@ -4,10 +4,12 @@ use types::{CompletedCryptoTransaction, MessageId, MessageIndex, UserId}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { - pub message_sender: UserId, + pub recipient: UserId, pub thread_root_message_index: Option, pub message_id: MessageId, pub transfer: CompletedCryptoTransaction, + pub username: String, + pub display_name: Option, } #[derive(CandidType, Serialize, Deserialize, Debug)] @@ -15,7 +17,7 @@ pub enum Response { Success, MessageNotFound, CannotTipSelf, - MessageSenderMismatch, + RecipientMismatch, NotAuthorized, GroupFrozen, UserNotInGroup, diff --git a/backend/canisters/group/impl/src/updates/c2c_tip_message.rs b/backend/canisters/group/impl/src/updates/c2c_tip_message.rs index 39e08bbf68..a84f732a6c 100644 --- a/backend/canisters/group/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/group/impl/src/updates/c2c_tip_message.rs @@ -2,8 +2,11 @@ use crate::activity_notifications::handle_activity_notification; use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; +use chat_events::Reader; use group_canister::c2c_tip_message::{Response::*, *}; use group_chat_core::TipMessageResult; +use ledger_utils::format_crypto_amount_with_symbol; +use types::{EventIndex, GroupMessageTipped, Notification}; #[update_msgpack] #[trace] @@ -20,22 +23,50 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { let user_id = state.env.caller().into(); let now = state.env.now(); + let token = args.transfer.token(); + let amount = args.transfer.units(); + match state.data.chat.tip_message( user_id, - args.message_sender, + args.recipient, args.thread_root_message_index, args.message_id, args.transfer, now, ) { TipMessageResult::Success => { - // TODO push notification + if let Some((message_index, message_event_index)) = state + .data + .chat + .events + .events_reader(EventIndex::default(), args.thread_root_message_index, now) + .and_then(|r| { + r.message_event_internal(args.message_id.into()) + .map(|e| (e.event.message_index, e.index)) + }) + { + state.push_notification( + vec![args.recipient], + Notification::GroupMessageTipped(GroupMessageTipped { + chat_id: state.env.canister_id().into(), + thread_root_message_index: args.thread_root_message_index, + message_index, + message_event_index, + group_name: state.data.chat.name.clone(), + tipped_by: user_id, + tipped_by_name: args.username, + tipped_by_display_name: args.display_name, + tip: format_crypto_amount_with_symbol(amount, token.decimals().unwrap_or(8), token.token_symbol()), + group_avatar_id: state.data.chat.avatar.as_ref().map(|a| a.id), + }), + ); + } handle_activity_notification(state); Success } TipMessageResult::MessageNotFound => MessageNotFound, TipMessageResult::CannotTipSelf => CannotTipSelf, - TipMessageResult::MessageSenderMismatch => MessageSenderMismatch, + TipMessageResult::RecipientMismatch => RecipientMismatch, TipMessageResult::UserNotInGroup => UserNotInGroup, TipMessageResult::NotAuthorized => NotAuthorized, TipMessageResult::UserSuspended => UserSuspended, diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index 07d95d3886..48db10f494 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Add `mention_all_members` group permission ([#4405](https://github.com/open-chat-labs/open-chat/pull/4405)) +- Implement tipping messages ([#4420](https://github.com/open-chat-labs/open-chat/pull/4420)) +- Implement notifications for message tips ([#4427](https://github.com/open-chat-labs/open-chat/pull/4427)) ## [[2.0.852](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.852-user)] - 2023-09-18 diff --git a/backend/canisters/user/api/src/updates/c2c_tip_message.rs b/backend/canisters/user/api/src/updates/c2c_tip_message.rs index 162748b563..b09d2ad6a3 100644 --- a/backend/canisters/user/api/src/updates/c2c_tip_message.rs +++ b/backend/canisters/user/api/src/updates/c2c_tip_message.rs @@ -7,6 +7,9 @@ pub struct Args { pub thread_root_message_index: Option, pub message_id: MessageId, pub transfer: CompletedCryptoTransaction, + pub username: String, + pub display_name: Option, + pub user_avatar_id: Option, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/user/impl/src/updates/c2c_tip_message.rs b/backend/canisters/user/impl/src/updates/c2c_tip_message.rs index e96834429d..44545beba9 100644 --- a/backend/canisters/user/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/user/impl/src/updates/c2c_tip_message.rs @@ -1,7 +1,9 @@ use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; -use types::{EventIndex, UserId}; +use chat_events::{Reader, TipMessageResult}; +use ledger_utils::format_crypto_amount_with_symbol; +use types::{DirectMessageTipped, EventIndex, Notification, UserId}; use user_canister::c2c_tip_message::{Response::*, *}; #[update_msgpack] @@ -17,15 +19,39 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { if let Some(chat) = state.data.direct_chats.get_mut(&user_id.into()) { let now = state.env.now(); let my_user_id = state.env.canister_id().into(); - chat.events.tip_message( - user_id, - my_user_id, - EventIndex::default(), - args.thread_root_message_index, - args.message_id, - args.transfer, - now, - ); + let token = args.transfer.token(); + let amount = args.transfer.units(); + + if matches!( + chat.events.tip_message( + user_id, + my_user_id, + EventIndex::default(), + args.thread_root_message_index, + args.message_id, + args.transfer, + now, + ), + TipMessageResult::Success + ) { + if let Some(event) = chat + .events + .main_events_reader(now) + .message_event_internal(args.message_id.into()) + { + let notification = Notification::DirectMessageTipped(DirectMessageTipped { + them: user_id, + thread_root_message_index: args.thread_root_message_index, + message_index: event.event.message_index, + message_event_index: event.index, + username: args.username, + display_name: args.display_name, + tip: format_crypto_amount_with_symbol(amount, token.decimals().unwrap_or(8), token.token_symbol()), + user_avatar_id: args.user_avatar_id, + }); + state.push_notification(my_user_id, notification); + } + } } Success } diff --git a/backend/canisters/user/impl/src/updates/tip_message.rs b/backend/canisters/user/impl/src/updates/tip_message.rs index 50c7ad70b5..30abc83adf 100644 --- a/backend/canisters/user/impl/src/updates/tip_message.rs +++ b/backend/canisters/user/impl/src/updates/tip_message.rs @@ -12,8 +12,8 @@ use user_canister::tip_message::{Response::*, *}; async fn tip_message(args: Args) -> Response { run_regular_jobs(); - let message_sender = match read_state(|state| prepare(&args, state)) { - Ok(user_id) => user_id, + let prepare_result = match read_state(|state| prepare(&args, state)) { + Ok(ok) => ok, Err(response) => return *response, }; @@ -25,21 +25,30 @@ async fn tip_message(args: Args) -> Response { match args.chat { Chat::Direct(chat_id) => mutate_state(|state| { - tip_direct_chat_message(chat_id, args.thread_root_message_index, args.message_id, transfer, state) + tip_direct_chat_message( + prepare_result, + chat_id, + args.thread_root_message_index, + args.message_id, + transfer, + state, + ) }), Chat::Group(chat_id) => { use group_canister::c2c_tip_message::Response; let args = group_canister::c2c_tip_message::Args { - message_sender, + recipient: prepare_result.recipient, thread_root_message_index: args.thread_root_message_index, message_id: args.message_id, transfer, + username: prepare_result.username, + display_name: prepare_result.display_name, }; match group_canister_c2c_client::c2c_tip_message(chat_id.into(), &args).await { Ok(Response::Success) => Success, Ok(Response::MessageNotFound) => MessageNotFound, Ok(Response::CannotTipSelf) => CannotTipSelf, - Ok(Response::MessageSenderMismatch) => TransferNotToMessageSender, + Ok(Response::RecipientMismatch) => TransferNotToMessageSender, Ok(Response::NotAuthorized) => NotAuthorized, Ok(Response::GroupFrozen) => ChatFrozen, Ok(Response::UserNotInGroup) => ChatNotFound, @@ -50,17 +59,19 @@ async fn tip_message(args: Args) -> Response { Chat::Channel(community_id, channel_id) => { use community_canister::c2c_tip_message::Response; let args = community_canister::c2c_tip_message::Args { - message_sender, + recipient: prepare_result.recipient, channel_id, thread_root_message_index: args.thread_root_message_index, message_id: args.message_id, transfer, + username: prepare_result.username, + display_name: prepare_result.display_name, }; match community_canister_c2c_client::c2c_tip_message(community_id.into(), &args).await { Ok(Response::Success) => Success, Ok(Response::MessageNotFound) => MessageNotFound, Ok(Response::CannotTipSelf) => CannotTipSelf, - Ok(Response::MessageSenderMismatch) => TransferNotToMessageSender, + Ok(Response::RecipientMismatch) => TransferNotToMessageSender, Ok(Response::NotAuthorized) => NotAuthorized, Ok(Response::CommunityFrozen) => ChatFrozen, Ok(Response::UserSuspended) => UserSuspended, @@ -71,7 +82,14 @@ async fn tip_message(args: Args) -> Response { } } -fn prepare(args: &Args, state: &RuntimeState) -> Result> { +struct PrepareResult { + my_user_id: UserId, + recipient: UserId, + username: String, + display_name: Option, +} + +fn prepare(args: &Args, state: &RuntimeState) -> Result> { if args.transfer.is_zero() { return Err(Box::new(TransferCannotBeZero)); } @@ -99,11 +117,17 @@ fn prepare(args: &Args, state: &RuntimeState) -> Result> { } else if args.transfer.is_zero() { Err(Box::new(TransferCannotBeZero)) } else { - Ok(recipient) + Ok(PrepareResult { + my_user_id, + recipient, + username: state.data.username.value.clone(), + display_name: state.data.display_name.value.clone(), + }) } } fn tip_direct_chat_message( + prepare_result: PrepareResult, chat_id: ChatId, thread_root_message_index: Option, message_id: MessageId, @@ -111,10 +135,9 @@ fn tip_direct_chat_message( state: &mut RuntimeState, ) -> Response { if let Some(chat) = state.data.direct_chats.get_mut(&chat_id) { - let my_user_id = state.env.canister_id().into(); let now = state.env.now(); match chat.events.tip_message( - my_user_id, + prepare_result.my_user_id, chat.them, EventIndex::default(), thread_root_message_index, @@ -127,6 +150,9 @@ fn tip_direct_chat_message( thread_root_message_index, message_id, transfer, + username: prepare_result.username, + display_name: prepare_result.display_name, + user_avatar_id: state.data.avatar.value.as_ref().map(|a| a.id), }; state.data.fire_and_forget_handler.send( chat_id.into(), @@ -137,7 +163,7 @@ fn tip_direct_chat_message( } TipMessageResult::MessageNotFound => MessageNotFound, TipMessageResult::CannotTipSelf => CannotTipSelf, - TipMessageResult::MessageSenderMismatch => TransferNotToMessageSender, + TipMessageResult::RecipientMismatch => TransferNotToMessageSender, } } else { ChatNotFound diff --git a/backend/libraries/chat_events/src/chat_events.rs b/backend/libraries/chat_events/src/chat_events.rs index 13a70b8e28..7a5c07ad26 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -545,7 +545,7 @@ impl ChatEvents { pub fn tip_message( &mut self, user_id: UserId, - message_sender: UserId, + recipient: UserId, min_visible_event_index: EventIndex, thread_root_message_index: Option, message_id: MessageId, @@ -560,8 +560,8 @@ impl ChatEvents { if message.sender == user_id { return CannotTipSelf; } - if message.sender != message_sender { - return MessageSenderMismatch; + if message.sender != recipient { + return RecipientMismatch; } message.tips.push((user_id, transfer)); @@ -1344,7 +1344,7 @@ pub enum TipMessageResult { Success, MessageNotFound, CannotTipSelf, - MessageSenderMismatch, + RecipientMismatch, } pub enum ReservePrizeResult { diff --git a/backend/libraries/group_chat_core/src/lib.rs b/backend/libraries/group_chat_core/src/lib.rs index 685c0ccf3d..257b910fc6 100644 --- a/backend/libraries/group_chat_core/src/lib.rs +++ b/backend/libraries/group_chat_core/src/lib.rs @@ -870,7 +870,7 @@ impl GroupChatCore { pub fn tip_message( &mut self, user_id: UserId, - message_sender: UserId, + recipient: UserId, thread_root_message_index: Option, message_id: MessageId, transfer: CompletedCryptoTransaction, @@ -891,7 +891,7 @@ impl GroupChatCore { self.events .tip_message( user_id, - message_sender, + recipient, min_visible_event_index, thread_root_message_index, message_id, @@ -1696,7 +1696,7 @@ impl From for AddRemoveReactionResult { pub enum TipMessageResult { Success, MessageNotFound, - MessageSenderMismatch, + RecipientMismatch, CannotTipSelf, NotAuthorized, UserNotInGroup, @@ -1708,7 +1708,7 @@ impl From for TipMessageResult { match value { chat_events::TipMessageResult::Success => TipMessageResult::Success, chat_events::TipMessageResult::MessageNotFound => TipMessageResult::MessageNotFound, - chat_events::TipMessageResult::MessageSenderMismatch => TipMessageResult::MessageSenderMismatch, + chat_events::TipMessageResult::RecipientMismatch => TipMessageResult::RecipientMismatch, chat_events::TipMessageResult::CannotTipSelf => TipMessageResult::CannotTipSelf, } } diff --git a/backend/libraries/ledger_utils/src/lib.rs b/backend/libraries/ledger_utils/src/lib.rs index 3512f0a261..cb195d6439 100644 --- a/backend/libraries/ledger_utils/src/lib.rs +++ b/backend/libraries/ledger_utils/src/lib.rs @@ -81,6 +81,10 @@ pub fn calculate_transaction_hash(sender: CanisterId, args: &TransferArgs) -> Tr transaction.hash() } +pub fn format_crypto_amount_with_symbol(units: u128, decimals: u8, symbol: &str) -> String { + format!("{} {symbol}", format_crypto_amount(units, decimals)) +} + pub fn format_crypto_amount(units: u128, decimals: u8) -> String { let subdividable_by = 10u128.pow(decimals as u32); let whole_units = units / subdividable_by; diff --git a/backend/libraries/types/can.did b/backend/libraries/types/can.did index 83eb602fc5..696d1b2e83 100644 --- a/backend/libraries/types/can.did +++ b/backend/libraries/types/can.did @@ -173,6 +173,17 @@ type DirectReactionAddedNotification = record { user_avatar_id : opt nat; }; +type DirectMessageTippedNotification = record { + them : UserId; + thread_root_message_index : opt MessageIndex; + message_index : MessageIndex; + message_event_index : EventIndex; + username : text; + display_name : opt text; + tip : text; + user_avatar_id : opt nat; +}; + type FieldTooShortResult = record { length_provided : nat32; min_length : nat32; @@ -625,6 +636,35 @@ type GroupReactionAddedNotification = record { group_avatar_id : opt nat; }; +type ChannelMessageTippedNotification = record { + community_id : CommunityId; + channel_id : ChannelId; + thread_root_message_index : opt MessageIndex; + message_index : MessageIndex; + message_event_index : EventIndex; + community_name : text; + channel_name : text; + tipped_by : UserId; + tipped_by_name : text; + tipped_by_display_name : opt text; + tip : text; + community_avatar_id : opt nat; + channel_avatar_id : opt nat; +}; + +type GroupMessageTippedNotification = record { + chat_id : ChatId; + thread_root_message_index : opt MessageIndex; + message_index : MessageIndex; + message_event_index : EventIndex; + group_name : text; + tipped_by : UserId; + tipped_by_name : text; + tipped_by_display_name : opt text; + tip : text; + group_avatar_id : opt nat; +}; + type GroupReplyContext = record { event_index : EventIndex; }; @@ -772,6 +812,9 @@ type Notification = variant { ChannelReactionAdded : ChannelReactionAddedNotification; DirectReactionAdded : DirectReactionAddedNotification; GroupReactionAdded : GroupReactionAddedNotification; + ChannelMessageTipped : ChannelMessageTippedNotification; + DirectMessageTipped : DirectMessageTippedNotification; + GroupMessageTipped : GroupMessageTippedNotification; }; type NotificationEnvelope = record { diff --git a/backend/libraries/types/src/notifications.rs b/backend/libraries/types/src/notifications.rs index 09a1983c0c..46d3e15c02 100644 --- a/backend/libraries/types/src/notifications.rs +++ b/backend/libraries/types/src/notifications.rs @@ -20,6 +20,9 @@ pub enum Notification { DirectReactionAdded(DirectReactionAddedNotification), GroupReactionAdded(GroupReactionAddedNotification), ChannelReactionAdded(ChannelReactionAddedNotification), + DirectMessageTipped(DirectMessageTipped), + GroupMessageTipped(GroupMessageTipped), + ChannelMessageTipped(ChannelMessageTipped), } #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] @@ -130,6 +133,49 @@ pub struct ChannelReactionAddedNotification { pub channel_avatar_id: Option, } +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct DirectMessageTipped { + pub them: UserId, + pub thread_root_message_index: Option, + pub message_index: MessageIndex, + pub message_event_index: EventIndex, + pub username: String, + pub display_name: Option, + pub tip: String, // formatted amount, eg. "0.1 CHAT" + pub user_avatar_id: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct GroupMessageTipped { + pub chat_id: ChatId, + pub thread_root_message_index: Option, + pub message_index: MessageIndex, + pub message_event_index: EventIndex, + pub group_name: String, + pub tipped_by: UserId, + pub tipped_by_name: String, + pub tipped_by_display_name: Option, + pub tip: String, + pub group_avatar_id: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ChannelMessageTipped { + pub community_id: CommunityId, + pub channel_id: ChannelId, + pub thread_root_message_index: Option, + pub message_index: MessageIndex, + pub message_event_index: EventIndex, + pub community_name: String, + pub channel_name: String, + pub tipped_by: UserId, + pub tipped_by_name: String, + pub tipped_by_display_name: Option, + pub tip: String, + pub community_avatar_id: Option, + pub channel_avatar_id: Option, +} + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct CryptoTransferDetails { pub recipient: UserId, @@ -177,5 +223,5 @@ fn notification_length() { let bytes = candid::encode_one(notification).unwrap().len(); - assert!(bytes < 630, "{bytes}"); + assert!(bytes < 850, "{bytes}"); }