From 80154858f964e335b80152f646fb2d9a707319b3 Mon Sep 17 00:00:00 2001 From: Matt Grogan Date: Tue, 13 Aug 2024 15:21:54 +0100 Subject: [PATCH] Next batch of achievements (#6230) --- Cargo.lock | 2 + backend/canisters/community/CHANGELOG.md | 6 +++ backend/canisters/community/api/can.did | 5 +++ .../api/src/updates/accept_p2p_swap.rs | 1 + .../api/src/updates/join_video_call.rs | 2 + .../api/src/updates/register_poll_vote.rs | 1 + .../src/updates/set_member_display_name.rs | 1 + .../src/updates/set_video_call_presence.rs | 1 + backend/canisters/community/impl/src/lib.rs | 19 ++++----- .../impl/src/updates/accept_p2p_swap.rs | 14 ++++++- .../impl/src/updates/add_reaction.rs | 14 ++++++- .../impl/src/updates/c2c_tip_message.rs | 9 ++++- .../impl/src/updates/delete_messages.rs | 6 ++- .../impl/src/updates/edit_message.rs | 6 ++- .../impl/src/updates/register_poll_vote.rs | 9 +++++ .../impl/src/updates/remove_reaction.rs | 2 +- .../impl/src/updates/send_message.rs | 11 ++++- .../src/updates/set_member_display_name.rs | 10 +++++ .../src/updates/set_video_call_presence.rs | 9 +++++ backend/canisters/group/CHANGELOG.md | 4 ++ backend/canisters/group/api/can.did | 4 ++ .../group/api/src/updates/accept_p2p_swap.rs | 1 + .../group/api/src/updates/join_video_call.rs | 2 + .../api/src/updates/register_poll_vote.rs | 1 + .../src/updates/set_video_call_presence.rs | 1 + backend/canisters/group/impl/src/lib.rs | 25 +++++------- .../group/impl/src/updates/accept_p2p_swap.rs | 14 ++++++- .../group/impl/src/updates/add_reaction.rs | 14 ++++++- .../group/impl/src/updates/c2c_tip_message.rs | 9 ++++- .../group/impl/src/updates/delete_messages.rs | 6 ++- .../group/impl/src/updates/edit_message.rs | 6 ++- .../impl/src/updates/register_poll_vote.rs | 9 +++++ .../group/impl/src/updates/remove_reaction.rs | 2 +- .../group/impl/src/updates/send_message.rs | 11 ++++- .../src/updates/set_video_call_presence.rs | 14 ++++++- backend/canisters/user/CHANGELOG.md | 1 + .../user/impl/src/updates/accept_p2p_swap.rs | 8 +++- .../user/impl/src/updates/add_reaction.rs | 2 +- .../c2c_notify_user_canister_events.rs | 10 ++++- .../user/impl/src/updates/claim_daily_chit.rs | 4 ++ .../user/impl/src/updates/join_video_call.rs | 4 +- .../user/impl/src/updates/remove_reaction.rs | 2 +- .../impl/src/updates/set_message_reminder.rs | 4 +- backend/integration_tests/src/client/group.rs | 6 ++- .../integration_tests/src/p2p_swap_tests.rs | 1 + .../libraries/chat_events/src/chat_events.rs | 10 +++-- backend/libraries/group_chat_core/src/lib.rs | 4 +- .../group_community_common/Cargo.toml | 3 ++ .../src/achievements.rs | 40 +++++++++++++++++++ .../group_community_common/src/lib.rs | 2 + backend/libraries/types/can.did | 1 + backend/libraries/types/src/achievement.rs | 19 +++++---- frontend/app/src/i18n/cn.json | 9 ++++- frontend/app/src/i18n/de.json | 9 ++++- frontend/app/src/i18n/en.json | 7 +++- frontend/app/src/i18n/es.json | 9 ++++- frontend/app/src/i18n/fa.json | 19 ++++++++- frontend/app/src/i18n/fr.json | 9 ++++- frontend/app/src/i18n/hi.json | 9 ++++- frontend/app/src/i18n/it.json | 9 ++++- frontend/app/src/i18n/iw.json | 9 ++++- frontend/app/src/i18n/jp.json | 9 ++++- frontend/app/src/i18n/pl.json | 9 ++++- frontend/app/src/i18n/ru.json | 9 ++++- frontend/app/src/i18n/uk.json | 9 ++++- frontend/app/src/i18n/vi.json | 9 ++++- .../src/services/community/candid/idl.js | 5 +++ .../src/services/community/candid/types.d.ts | 10 ++++- .../services/community/community.client.ts | 21 ++++++++-- .../src/services/group/candid/idl.js | 8 +++- .../src/services/group/candid/types.d.ts | 9 ++++- .../src/services/group/group.client.ts | 11 ++++- .../src/services/groupIndex/candid/types.d.ts | 1 + .../services/localUserIndex/candid/types.d.ts | 1 + .../services/notifications/candid/types.d.ts | 1 + .../src/services/online/candid/types.d.ts | 1 + .../src/services/openchatAgent.ts | 23 +++++++++-- .../services/proposalsBot/candid/types.d.ts | 1 + .../src/services/registry/candid/types.d.ts | 1 + .../services/storageBucket/candid/types.d.ts | 1 + .../services/storageIndex/candid/types.d.ts | 1 + .../services/translations/candid/types.d.ts | 1 + .../src/services/user/candid/idl.js | 1 + .../src/services/user/candid/types.d.ts | 1 + .../src/services/user/mappers.ts | 3 ++ .../src/services/userIndex/candid/types.d.ts | 1 + frontend/openchat-client/src/openchat.ts | 17 ++++++++ frontend/openchat-shared/src/domain/chit.ts | 1 + frontend/openchat-shared/src/domain/worker.ts | 7 +++- frontend/openchat-worker/src/worker.ts | 17 ++++++-- 90 files changed, 539 insertions(+), 111 deletions(-) create mode 100644 backend/libraries/group_community_common/src/achievements.rs diff --git a/Cargo.lock b/Cargo.lock index d497ed23ff..92e8bcb6e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2974,11 +2974,13 @@ name = "group_community_common" version = "0.1.0" dependencies = [ "candid", + "fire_and_forget_handler", "icrc-ledger-types", "msgpack", "serde", "serde_repr", "types", + "user_canister", ] [[package]] diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 8eebb9937e..7571b88a1d 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Changed + +- Support next batch of achievements ([#6230](https://github.com/open-chat-labs/open-chat/pull/6230)) + +## [[2.0.1287](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1287-community)] - 2024-08-13 + ### Added - Add `external_url` property to channel ([#6226](https://github.com/open-chat-labs/open-chat/pull/6226)) diff --git a/backend/canisters/community/api/can.did b/backend/canisters/community/api/can.did index 8bc26e1125..4bab442833 100644 --- a/backend/canisters/community/api/can.did +++ b/backend/canisters/community/api/can.did @@ -299,6 +299,7 @@ type AcceptP2PSwapArgs = record { thread_root_message_index : opt MessageIndex; message_id : MessageId; pin : opt text; + new_achievement : bool; }; type AcceptP2PSwapResponse = variant { @@ -647,6 +648,7 @@ type ImportGroupResponse = variant { type JoinVideoCallArgs = record { channel_id : ChannelId; message_id : MessageId; + new_achievement : bool; }; type JoinVideoCallResponse = variant { @@ -697,6 +699,7 @@ type RegisterPollVoteArgs = record { message_index : MessageIndex; poll_option : nat32; operation : VoteOperation; + new_achievement : bool; }; type RegisterPollVoteResponse = variant { @@ -859,6 +862,7 @@ type SendMessageSuccess = record { type SetMemberDisplayNameArgs = record { display_name : opt text; + new_achievement : bool; }; type SetMemberDisplayNameResponse = variant { @@ -875,6 +879,7 @@ type SetVideoCallPresenceArgs = record { channel_id : ChannelId; message_id : MessageId; presence : VideoCallPresence; + new_achievement : bool; }; type SetVideoCallPresenceResponse = variant { diff --git a/backend/canisters/community/api/src/updates/accept_p2p_swap.rs b/backend/canisters/community/api/src/updates/accept_p2p_swap.rs index dd5bf30c69..7d5852f482 100644 --- a/backend/canisters/community/api/src/updates/accept_p2p_swap.rs +++ b/backend/canisters/community/api/src/updates/accept_p2p_swap.rs @@ -8,6 +8,7 @@ pub struct Args { pub thread_root_message_index: Option, pub message_id: MessageId, pub pin: Option, + pub new_achievement: bool, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/community/api/src/updates/join_video_call.rs b/backend/canisters/community/api/src/updates/join_video_call.rs index 4cf1241607..c8208020d1 100644 --- a/backend/canisters/community/api/src/updates/join_video_call.rs +++ b/backend/canisters/community/api/src/updates/join_video_call.rs @@ -6,6 +6,7 @@ use types::{ChannelId, MessageId, VideoCallPresence}; pub struct Args { pub channel_id: ChannelId, pub message_id: MessageId, + pub new_achievement: bool, } pub type Response = crate::set_video_call_presence::Response; @@ -16,6 +17,7 @@ impl From for crate::set_video_call_presence::Args { channel_id: value.channel_id, message_id: value.message_id, presence: VideoCallPresence::Default, + new_achievement: value.new_achievement, } } } diff --git a/backend/canisters/community/api/src/updates/register_poll_vote.rs b/backend/canisters/community/api/src/updates/register_poll_vote.rs index 0eb98284f9..e8a93070e6 100644 --- a/backend/canisters/community/api/src/updates/register_poll_vote.rs +++ b/backend/canisters/community/api/src/updates/register_poll_vote.rs @@ -9,6 +9,7 @@ pub struct Args { pub message_index: MessageIndex, pub poll_option: u32, pub operation: VoteOperation, + pub new_achievement: bool, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/community/api/src/updates/set_member_display_name.rs b/backend/canisters/community/api/src/updates/set_member_display_name.rs index 5f8eed65eb..bc9807b377 100644 --- a/backend/canisters/community/api/src/updates/set_member_display_name.rs +++ b/backend/canisters/community/api/src/updates/set_member_display_name.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub display_name: Option, + pub new_achievement: bool, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/community/api/src/updates/set_video_call_presence.rs b/backend/canisters/community/api/src/updates/set_video_call_presence.rs index 05072a4ab3..553ba1f6ff 100644 --- a/backend/canisters/community/api/src/updates/set_video_call_presence.rs +++ b/backend/canisters/community/api/src/updates/set_video_call_presence.rs @@ -7,6 +7,7 @@ pub struct Args { pub channel_id: ChannelId, pub message_id: MessageId, pub presence: VideoCallPresence, + pub new_achievement: bool, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/community/impl/src/lib.rs b/backend/canisters/community/impl/src/lib.rs index 062bf75b49..adf0799347 100644 --- a/backend/canisters/community/impl/src/lib.rs +++ b/backend/canisters/community/impl/src/lib.rs @@ -12,7 +12,9 @@ use event_store_producer::{EventStoreClient, EventStoreClientBuilder, EventStore use event_store_producer_cdk_runtime::CdkRuntime; use fire_and_forget_handler::FireAndForgetHandler; use group_chat_core::AccessRulesInternal; -use group_community_common::{PaymentReceipts, PaymentRecipient, PendingPayment, PendingPaymentReason, PendingPaymentsQueue}; +use group_community_common::{ + Achievements, PaymentReceipts, PaymentRecipient, PendingPayment, PendingPaymentReason, PendingPaymentsQueue, +}; use instruction_counts_log::{InstructionCountEntry, InstructionCountFunctionId, InstructionCountsLog}; use model::{events::CommunityEvents, invited_users::InvitedUsers, members::CommunityMemberInternal}; use msgpack::serialize_then_unwrap; @@ -24,12 +26,11 @@ use std::cell::RefCell; use std::ops::Deref; use std::time::Duration; use types::{ - AccessGate, Achievement, BuildVersion, CanisterId, ChatMetrics, CommunityCanisterCommunitySummary, CommunityMembership, + AccessGate, BuildVersion, CanisterId, ChatMetrics, CommunityCanisterCommunitySummary, CommunityMembership, CommunityPermissions, CommunityRole, Cryptocurrency, Cycles, Document, Empty, FrozenGroupInfo, Milliseconds, Notification, PaymentGate, Rules, TimestampMillis, Timestamped, UserId, UserType, }; use types::{CommunityId, SNS_FEE_SHARE_PERCENT}; -use user_canister::c2c_notify_achievement; use utils::env::Environment; use utils::regular_jobs::RegularJobs; use utils::time::MINUTE_IN_MS; @@ -275,15 +276,6 @@ impl RuntimeState { }, } } - - fn notify_user_of_achievements(&self, user_id: UserId, achievements: Vec) { - let args = c2c_notify_achievement::Args { achievements }; - self.data.fire_and_forget_handler.send( - user_id.into(), - "c2c_notify_achievement_msgpack".to_string(), - serialize_then_unwrap(args), - ); - } } fn init_instruction_counts_log() -> InstructionCountsLog { @@ -333,6 +325,8 @@ struct Data { #[serde(with = "serde_bytes")] ic_root_key: Vec, event_store_client: EventStoreClient, + #[serde(default)] + achievements: Achievements, } impl Data { @@ -429,6 +423,7 @@ impl Data { event_store_client: EventStoreClientBuilder::new(local_group_index_canister_id, CdkRuntime::default()) .with_flush_delay(Duration::from_millis(5 * MINUTE_IN_MS)) .build(), + achievements: Achievements::default(), } } diff --git a/backend/canisters/community/impl/src/updates/accept_p2p_swap.rs b/backend/canisters/community/impl/src/updates/accept_p2p_swap.rs index 1a1a24d42e..50caa42311 100644 --- a/backend/canisters/community/impl/src/updates/accept_p2p_swap.rs +++ b/backend/canisters/community/impl/src/updates/accept_p2p_swap.rs @@ -5,7 +5,7 @@ use canister_tracing_macros::trace; use community_canister::accept_p2p_swap::{Response::*, *}; use ic_cdk::update; use icrc_ledger_types::icrc1::transfer::TransferError; -use types::{AcceptSwapSuccess, ChannelId, Chat, MessageId, MessageIndex, P2PSwapLocation, UserId}; +use types::{AcceptSwapSuccess, Achievement, ChannelId, Chat, MessageId, MessageIndex, P2PSwapLocation, UserId}; #[update] #[trace] @@ -15,6 +15,7 @@ async fn accept_p2p_swap(args: Args) -> Response { let channel_id = args.channel_id; let thread_root_message_index = args.thread_root_message_index; let message_id = args.message_id; + let new_achievement = args.new_achievement; let ReserveP2PSwapResult { user_id, c2c_args } = match mutate_state(|state| reserve_p2p_swap(args, state)) { Ok(result) => result, @@ -31,6 +32,17 @@ async fn accept_p2p_swap(args: Args) -> Response { message_id, transaction_index, ); + + if new_achievement { + mutate_state(|state| { + state.data.achievements.notify_user( + user_id, + vec![Achievement::AcceptedP2PSwapOffer], + &mut state.data.fire_and_forget_handler, + ); + }); + } + Success(AcceptSwapSuccess { token1_txn_in: transaction_index, }) diff --git a/backend/canisters/community/impl/src/updates/add_reaction.rs b/backend/canisters/community/impl/src/updates/add_reaction.rs index 8e5df39e3e..3eacdace3f 100644 --- a/backend/canisters/community/impl/src/updates/add_reaction.rs +++ b/backend/canisters/community/impl/src/updates/add_reaction.rs @@ -39,7 +39,7 @@ fn add_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { now, &mut state.data.event_store_client, ) { - AddRemoveReactionResult::Success => { + AddRemoveReactionResult::Success(sender) => { if let Some(message) = should_push_notification(&args, user_id, &channel.chat) { push_notification( args, @@ -54,9 +54,19 @@ fn add_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { handle_activity_notification(state); if new_achievement { - state.notify_user_of_achievements(user_id, vec![Achievement::ReactedToMessage]); + state.data.achievements.notify_user( + user_id, + vec![Achievement::ReactedToMessage], + &mut state.data.fire_and_forget_handler, + ); } + state.data.achievements.notify_user( + sender, + vec![Achievement::HadMessageReactedTo], + &mut state.data.fire_and_forget_handler, + ); + Success } AddRemoveReactionResult::NoChange => NoChange, 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 0f8b4e713e..acfed6d2b6 100644 --- a/backend/canisters/community/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/community/impl/src/updates/c2c_tip_message.rs @@ -6,7 +6,7 @@ use chat_events::{Reader, TipMessageArgs}; 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}; +use types::{Achievement, ChannelMessageTipped, EventIndex, Notification}; #[update(msgpack = true)] #[trace] @@ -69,6 +69,13 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { }); state.push_notification(vec![args.recipient], notification); } + + state.data.achievements.notify_user( + args.recipient, + vec![Achievement::HadMessageTipped], + &mut state.data.fire_and_forget_handler, + ); + handle_activity_notification(state); Success } diff --git a/backend/canisters/community/impl/src/updates/delete_messages.rs b/backend/canisters/community/impl/src/updates/delete_messages.rs index bedbd6f9f6..e34d96c499 100644 --- a/backend/canisters/community/impl/src/updates/delete_messages.rs +++ b/backend/canisters/community/impl/src/updates/delete_messages.rs @@ -103,7 +103,11 @@ fn delete_messages_impl(user_id: UserId, args: Args, state: &mut RuntimeState) - handle_activity_notification(state); if args.new_achievement { - state.notify_user_of_achievements(user_id, vec![Achievement::DeletedMessage]); + state.data.achievements.notify_user( + user_id, + vec![Achievement::DeletedMessage], + &mut state.data.fire_and_forget_handler, + ); } Success diff --git a/backend/canisters/community/impl/src/updates/edit_message.rs b/backend/canisters/community/impl/src/updates/edit_message.rs index 32df2034db..d225cd2a59 100644 --- a/backend/canisters/community/impl/src/updates/edit_message.rs +++ b/backend/canisters/community/impl/src/updates/edit_message.rs @@ -45,7 +45,11 @@ fn edit_message_impl(args: Args, state: &mut RuntimeState) -> Response { handle_activity_notification(state); if args.new_achievement { - state.notify_user_of_achievements(sender, vec![Achievement::EditedMessage]); + state.data.achievements.notify_user( + sender, + vec![Achievement::EditedMessage], + &mut state.data.fire_and_forget_handler, + ); } Success diff --git a/backend/canisters/community/impl/src/updates/register_poll_vote.rs b/backend/canisters/community/impl/src/updates/register_poll_vote.rs index 6869556349..6386d17ff1 100644 --- a/backend/canisters/community/impl/src/updates/register_poll_vote.rs +++ b/backend/canisters/community/impl/src/updates/register_poll_vote.rs @@ -4,6 +4,7 @@ use canister_tracing_macros::trace; use chat_events::{RegisterPollVoteArgs, RegisterPollVoteResult}; use community_canister::register_poll_vote::{Response::*, *}; use ic_cdk::update; +use types::Achievement; #[update] #[trace] @@ -56,6 +57,14 @@ fn register_poll_vote_impl(args: Args, state: &mut RuntimeState) -> Response { match result { RegisterPollVoteResult::Success(votes) => { + if args.new_achievement { + state.data.achievements.notify_user( + user_id, + vec![Achievement::VotedOnPoll], + &mut state.data.fire_and_forget_handler, + ); + } + handle_activity_notification(state); Success(votes) } diff --git a/backend/canisters/community/impl/src/updates/remove_reaction.rs b/backend/canisters/community/impl/src/updates/remove_reaction.rs index 88f663ad4b..96be073f8c 100644 --- a/backend/canisters/community/impl/src/updates/remove_reaction.rs +++ b/backend/canisters/community/impl/src/updates/remove_reaction.rs @@ -31,7 +31,7 @@ fn remove_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { .chat .remove_reaction(user_id, args.thread_root_message_index, args.message_id, args.reaction, now) { - AddRemoveReactionResult::Success => { + AddRemoveReactionResult::Success(_) => { handle_activity_notification(state); Success } diff --git a/backend/canisters/community/impl/src/updates/send_message.rs b/backend/canisters/community/impl/src/updates/send_message.rs index 2093e0c71f..bf1ab790b0 100644 --- a/backend/canisters/community/impl/src/updates/send_message.rs +++ b/backend/canisters/community/impl/src/updates/send_message.rs @@ -247,9 +247,18 @@ fn process_send_message_result( ); if new_achievement { - state.notify_user_of_achievements( + state.data.achievements.notify_user( sender, Achievement::from_message(false, &result.message_event.event, thread_root_message_index.is_some()), + &mut state.data.fire_and_forget_handler, + ); + } + + if let MessageContent::Crypto(c) = &result.message_event.event.content { + state.data.achievements.notify_user( + c.recipient, + vec![Achievement::ReceivedCrypto], + &mut state.data.fire_and_forget_handler, ); } diff --git a/backend/canisters/community/impl/src/updates/set_member_display_name.rs b/backend/canisters/community/impl/src/updates/set_member_display_name.rs index 51afd1a658..2d3c0c47d2 100644 --- a/backend/canisters/community/impl/src/updates/set_member_display_name.rs +++ b/backend/canisters/community/impl/src/updates/set_member_display_name.rs @@ -2,6 +2,7 @@ use crate::{activity_notifications::handle_activity_notification, mutate_state, use canister_tracing_macros::trace; use community_canister::set_member_display_name::{Response::*, *}; use ic_cdk::update; +use types::Achievement; use utils::text_validation::{validate_display_name, UsernameValidationError}; #[update] @@ -37,6 +38,15 @@ fn set_member_display_name_impl(args: Args, state: &mut RuntimeState) -> Respons }; state.data.members.set_display_name(user_id, args.display_name, now); + + if args.new_achievement { + state.data.achievements.notify_user( + user_id, + vec![Achievement::SetCommunityDisplayName], + &mut state.data.fire_and_forget_handler, + ); + } + handle_activity_notification(state); Success } diff --git a/backend/canisters/community/impl/src/updates/set_video_call_presence.rs b/backend/canisters/community/impl/src/updates/set_video_call_presence.rs index 6cffc7782f..b4cb671be2 100644 --- a/backend/canisters/community/impl/src/updates/set_video_call_presence.rs +++ b/backend/canisters/community/impl/src/updates/set_video_call_presence.rs @@ -3,6 +3,7 @@ use canister_tracing_macros::trace; use chat_events::SetVideoCallPresenceResult; use community_canister::set_video_call_presence::{Response::*, *}; use ic_cdk::update; +use types::Achievement; #[update] #[trace] @@ -37,6 +38,14 @@ pub(crate) fn set_video_call_presence_impl(args: Args, state: &mut RuntimeState) now, ) { SetVideoCallPresenceResult::Success => { + if args.new_achievement { + state.data.achievements.notify_user( + user_id, + vec![Achievement::JoinedCall], + &mut state.data.fire_and_forget_handler, + ); + } + handle_activity_notification(state); Success } diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index 0bb1f8e73b..65f642e53e 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Changed + +- Support next batch of achievements ([#6230](https://github.com/open-chat-labs/open-chat/pull/6230)) + ## [[2.0.1273](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1273-group)] - 2024-07-31 ### Changed diff --git a/backend/canisters/group/api/can.did b/backend/canisters/group/api/can.did index d6414beee5..eaae876e9d 100644 --- a/backend/canisters/group/api/can.did +++ b/backend/canisters/group/api/can.did @@ -80,6 +80,7 @@ type EndVideoCallResponse = variant { type JoinVideoCallArgs = record { message_id : MessageId; + new_achievement : bool; }; type JoinVideoCallResponse = variant { @@ -94,6 +95,7 @@ type JoinVideoCallResponse = variant { type SetVideoCallPresenceArgs = record { message_id : MessageId; presence : VideoCallPresence; + new_achievement : bool; }; type SetVideoCallPresenceResponse = variant { @@ -144,6 +146,7 @@ type RegisterPollVoteArgs = record { message_index : MessageIndex; poll_option : nat32; operation : VoteOperation; + new_achievement : bool; correlation_id : nat64; }; @@ -162,6 +165,7 @@ type AcceptP2PSwapArgs = record { thread_root_message_index : opt MessageIndex; message_id : MessageId; pin : opt text; + new_achievement : bool; }; type AcceptP2PSwapResponse = variant { diff --git a/backend/canisters/group/api/src/updates/accept_p2p_swap.rs b/backend/canisters/group/api/src/updates/accept_p2p_swap.rs index 2c4bcb7425..bb8f4e90ca 100644 --- a/backend/canisters/group/api/src/updates/accept_p2p_swap.rs +++ b/backend/canisters/group/api/src/updates/accept_p2p_swap.rs @@ -7,6 +7,7 @@ pub struct Args { pub thread_root_message_index: Option, pub message_id: MessageId, pub pin: Option, + pub new_achievement: bool, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/group/api/src/updates/join_video_call.rs b/backend/canisters/group/api/src/updates/join_video_call.rs index bc806d4e3a..f46bce2301 100644 --- a/backend/canisters/group/api/src/updates/join_video_call.rs +++ b/backend/canisters/group/api/src/updates/join_video_call.rs @@ -5,6 +5,7 @@ use types::{MessageId, VideoCallPresence}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub message_id: MessageId, + pub new_achievement: bool, } pub type Response = crate::set_video_call_presence::Response; @@ -14,6 +15,7 @@ impl From for crate::set_video_call_presence::Args { crate::set_video_call_presence::Args { message_id: value.message_id, presence: VideoCallPresence::Default, + new_achievement: value.new_achievement, } } } diff --git a/backend/canisters/group/api/src/updates/register_poll_vote.rs b/backend/canisters/group/api/src/updates/register_poll_vote.rs index 52f2d79e7b..135a67ab02 100644 --- a/backend/canisters/group/api/src/updates/register_poll_vote.rs +++ b/backend/canisters/group/api/src/updates/register_poll_vote.rs @@ -8,6 +8,7 @@ pub struct Args { pub message_index: MessageIndex, pub poll_option: u32, pub operation: VoteOperation, + pub new_achievement: bool, pub correlation_id: u64, } diff --git a/backend/canisters/group/api/src/updates/set_video_call_presence.rs b/backend/canisters/group/api/src/updates/set_video_call_presence.rs index 1a7d3d2470..1ad34a274c 100644 --- a/backend/canisters/group/api/src/updates/set_video_call_presence.rs +++ b/backend/canisters/group/api/src/updates/set_video_call_presence.rs @@ -6,6 +6,7 @@ use types::{MessageId, VideoCallPresence}; pub struct Args { pub message_id: MessageId, pub presence: VideoCallPresence, + pub new_achievement: bool, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/group/impl/src/lib.rs b/backend/canisters/group/impl/src/lib.rs index 60e8eee7df..e8ac686bfe 100644 --- a/backend/canisters/group/impl/src/lib.rs +++ b/backend/canisters/group/impl/src/lib.rs @@ -14,7 +14,9 @@ use fire_and_forget_handler::FireAndForgetHandler; use group_chat_core::{ AddResult as AddMemberResult, GroupChatCore, GroupMemberInternal, GroupRoleInternal, InvitedUsersResult, UserInvitation, }; -use group_community_common::{PaymentReceipts, PaymentRecipient, PendingPayment, PendingPaymentReason, PendingPaymentsQueue}; +use group_community_common::{ + Achievements, PaymentReceipts, PaymentRecipient, PendingPayment, PendingPaymentReason, PendingPaymentsQueue, +}; use instruction_counts_log::{InstructionCountEntry, InstructionCountFunctionId, InstructionCountsLog}; use msgpack::serialize_then_unwrap; use notifications_canister::c2c_push_notification; @@ -26,12 +28,11 @@ use std::collections::{HashMap, HashSet}; use std::ops::Deref; use std::time::Duration; use types::{ - AccessGate, Achievement, BuildVersion, CanisterId, ChatId, ChatMetrics, CommunityId, Cryptocurrency, Cycles, Document, - Empty, EventIndex, FrozenGroupInfo, GroupCanisterGroupChatSummary, GroupMembership, GroupPermissions, GroupSubtype, - MessageIndex, Milliseconds, MultiUserChat, Notification, PaymentGate, Rules, TimestampMillis, Timestamped, UserId, - UserType, MAX_THREADS_IN_SUMMARY, SNS_FEE_SHARE_PERCENT, + AccessGate, BuildVersion, CanisterId, ChatId, ChatMetrics, CommunityId, Cryptocurrency, Cycles, Document, Empty, + EventIndex, FrozenGroupInfo, GroupCanisterGroupChatSummary, GroupMembership, GroupPermissions, GroupSubtype, MessageIndex, + Milliseconds, MultiUserChat, Notification, PaymentGate, Rules, TimestampMillis, Timestamped, UserId, UserType, + MAX_THREADS_IN_SUMMARY, SNS_FEE_SHARE_PERCENT, }; -use user_canister::c2c_notify_achievement; use utils::consts::OPENCHAT_BOT_USER_ID; use utils::env::Environment; use utils::regular_jobs::RegularJobs; @@ -415,15 +416,6 @@ impl RuntimeState { }, } } - - fn notify_user_of_achievements(&self, user_id: UserId, achievements: Vec) { - let args = c2c_notify_achievement::Args { achievements }; - self.data.fire_and_forget_handler.send( - user_id.into(), - "c2c_notify_achievement_msgpack".to_string(), - serialize_then_unwrap(args), - ); - } } #[derive(Serialize, Deserialize)] @@ -458,6 +450,8 @@ struct Data { #[serde(with = "serde_bytes")] pub ic_root_key: Vec, pub event_store_client: EventStoreClient, + #[serde(default)] + achievements: Achievements, } fn init_instruction_counts_log() -> InstructionCountsLog { @@ -548,6 +542,7 @@ impl Data { event_store_client: EventStoreClientBuilder::new(local_group_index_canister_id, CdkRuntime::default()) .with_flush_delay(Duration::from_millis(5 * MINUTE_IN_MS)) .build(), + achievements: Achievements::default(), } } diff --git a/backend/canisters/group/impl/src/updates/accept_p2p_swap.rs b/backend/canisters/group/impl/src/updates/accept_p2p_swap.rs index c1bbaf63bf..e9df99fdf5 100644 --- a/backend/canisters/group/impl/src/updates/accept_p2p_swap.rs +++ b/backend/canisters/group/impl/src/updates/accept_p2p_swap.rs @@ -5,7 +5,7 @@ use canister_tracing_macros::trace; use group_canister::accept_p2p_swap::{Response::*, *}; use ic_cdk::update; use icrc_ledger_types::icrc1::transfer::TransferError; -use types::{AcceptSwapSuccess, Chat, MessageId, MessageIndex, P2PSwapLocation, UserId}; +use types::{AcceptSwapSuccess, Achievement, Chat, MessageId, MessageIndex, P2PSwapLocation, UserId}; #[update] #[trace] @@ -14,6 +14,7 @@ async fn accept_p2p_swap(args: Args) -> Response { let thread_root_message_index = args.thread_root_message_index; let message_id = args.message_id; + let new_achievement = args.new_achievement; let ReserveP2PSwapResult { user_id, c2c_args } = match mutate_state(|state| reserve_p2p_swap(args, state)) { Ok(result) => result, @@ -29,6 +30,17 @@ async fn accept_p2p_swap(args: Args) -> Response { message_id, transaction_index, ); + + if new_achievement { + mutate_state(|state| { + state.data.achievements.notify_user( + user_id, + vec![Achievement::AcceptedP2PSwapOffer], + &mut state.data.fire_and_forget_handler, + ); + }); + } + Success(AcceptSwapSuccess { token1_txn_in: transaction_index, }) diff --git a/backend/canisters/group/impl/src/updates/add_reaction.rs b/backend/canisters/group/impl/src/updates/add_reaction.rs index c009bb8e40..72f051f4dc 100644 --- a/backend/canisters/group/impl/src/updates/add_reaction.rs +++ b/backend/canisters/group/impl/src/updates/add_reaction.rs @@ -32,11 +32,21 @@ fn add_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { now, &mut state.data.event_store_client, ) { - AddRemoveReactionResult::Success => { + AddRemoveReactionResult::Success(sender) => { if args.new_achievement { - state.notify_user_of_achievements(user_id, vec![Achievement::ReactedToMessage]); + state.data.achievements.notify_user( + user_id, + vec![Achievement::ReactedToMessage], + &mut state.data.fire_and_forget_handler, + ); } + state.data.achievements.notify_user( + sender, + vec![Achievement::HadMessageReactedTo], + &mut state.data.fire_and_forget_handler, + ); + handle_activity_notification(state); handle_notification(args, user_id, state); 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 3c24697f2c..7acff8f804 100644 --- a/backend/canisters/group/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/group/impl/src/updates/c2c_tip_message.rs @@ -6,7 +6,7 @@ use chat_events::{Reader, TipMessageArgs}; 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}; +use types::{Achievement, EventIndex, GroupMessageTipped, Notification}; #[update(msgpack = true)] #[trace] @@ -67,6 +67,13 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { }), ); } + + state.data.achievements.notify_user( + args.recipient, + vec![Achievement::HadMessageTipped], + &mut state.data.fire_and_forget_handler, + ); + handle_activity_notification(state); Success } diff --git a/backend/canisters/group/impl/src/updates/delete_messages.rs b/backend/canisters/group/impl/src/updates/delete_messages.rs index 66b8e1f26c..74aed0c3a5 100644 --- a/backend/canisters/group/impl/src/updates/delete_messages.rs +++ b/backend/canisters/group/impl/src/updates/delete_messages.rs @@ -96,7 +96,11 @@ fn delete_messages_impl(user_id: UserId, args: Args, state: &mut RuntimeState) - handle_activity_notification(state); if args.new_achievement { - state.notify_user_of_achievements(user_id, vec![Achievement::DeletedMessage]); + state.data.achievements.notify_user( + user_id, + vec![Achievement::DeletedMessage], + &mut state.data.fire_and_forget_handler, + ); } Success diff --git a/backend/canisters/group/impl/src/updates/edit_message.rs b/backend/canisters/group/impl/src/updates/edit_message.rs index 72b09cf1ca..d14bd42106 100644 --- a/backend/canisters/group/impl/src/updates/edit_message.rs +++ b/backend/canisters/group/impl/src/updates/edit_message.rs @@ -48,7 +48,11 @@ fn edit_message_impl(args: Args, state: &mut RuntimeState) -> Response { handle_activity_notification(state); if args.new_achievement { - state.notify_user_of_achievements(sender, vec![Achievement::EditedMessage]); + state.data.achievements.notify_user( + sender, + vec![Achievement::EditedMessage], + &mut state.data.fire_and_forget_handler, + ); } Success diff --git a/backend/canisters/group/impl/src/updates/register_poll_vote.rs b/backend/canisters/group/impl/src/updates/register_poll_vote.rs index c9961cf097..34f022465f 100644 --- a/backend/canisters/group/impl/src/updates/register_poll_vote.rs +++ b/backend/canisters/group/impl/src/updates/register_poll_vote.rs @@ -4,6 +4,7 @@ use canister_tracing_macros::trace; use chat_events::{RegisterPollVoteArgs, RegisterPollVoteResult}; use group_canister::register_poll_vote::{Response::*, *}; use ic_cdk::update; +use types::Achievement; #[update] #[trace] @@ -41,6 +42,14 @@ fn register_poll_vote_impl(args: Args, state: &mut RuntimeState) -> Response { match result { RegisterPollVoteResult::Success(votes) => { + if args.new_achievement { + state.data.achievements.notify_user( + user_id, + vec![Achievement::VotedOnPoll], + &mut state.data.fire_and_forget_handler, + ); + } + handle_activity_notification(state); Success(votes) } diff --git a/backend/canisters/group/impl/src/updates/remove_reaction.rs b/backend/canisters/group/impl/src/updates/remove_reaction.rs index 915124ff62..eede8ca600 100644 --- a/backend/canisters/group/impl/src/updates/remove_reaction.rs +++ b/backend/canisters/group/impl/src/updates/remove_reaction.rs @@ -26,7 +26,7 @@ fn remove_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { .chat .remove_reaction(user_id, args.thread_root_message_index, args.message_id, args.reaction, now) { - AddRemoveReactionResult::Success => { + AddRemoveReactionResult::Success(_) => { handle_activity_notification(state); Success } diff --git a/backend/canisters/group/impl/src/updates/send_message.rs b/backend/canisters/group/impl/src/updates/send_message.rs index cb8414555a..b397c1f7d5 100644 --- a/backend/canisters/group/impl/src/updates/send_message.rs +++ b/backend/canisters/group/impl/src/updates/send_message.rs @@ -177,9 +177,18 @@ fn process_send_message_result( handle_activity_notification(state); if new_achievement { - state.notify_user_of_achievements( + state.data.achievements.notify_user( sender, Achievement::from_message(false, &result.message_event.event, thread_root_message_index.is_some()), + &mut state.data.fire_and_forget_handler, + ); + } + + if let MessageContent::Crypto(c) = &result.message_event.event.content { + state.data.achievements.notify_user( + c.recipient, + vec![Achievement::ReceivedCrypto], + &mut state.data.fire_and_forget_handler, ); } diff --git a/backend/canisters/group/impl/src/updates/set_video_call_presence.rs b/backend/canisters/group/impl/src/updates/set_video_call_presence.rs index 9dc0463340..cbd90d5689 100644 --- a/backend/canisters/group/impl/src/updates/set_video_call_presence.rs +++ b/backend/canisters/group/impl/src/updates/set_video_call_presence.rs @@ -3,6 +3,7 @@ use canister_tracing_macros::trace; use chat_events::SetVideoCallPresenceResult; use group_canister::set_video_call_presence::{Response::*, *}; use ic_cdk::update; +use types::Achievement; #[update] #[trace] @@ -25,16 +26,25 @@ pub(crate) fn set_video_call_presence_impl(args: Args, state: &mut RuntimeState) } let now = state.env.now(); + let user_id = member.user_id; - if let Some(min_visible_event_index) = state.data.chat.min_visible_event_index(Some(member.user_id)) { + if let Some(min_visible_event_index) = state.data.chat.min_visible_event_index(Some(user_id)) { match state.data.chat.events.set_video_call_presence( - member.user_id, + user_id, args.message_id, args.presence, min_visible_event_index, now, ) { SetVideoCallPresenceResult::Success => { + if args.new_achievement { + state.data.achievements.notify_user( + user_id, + vec![Achievement::JoinedCall], + &mut state.data.fire_and_forget_handler, + ); + } + handle_activity_notification(state); Success } diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index 86408be3c6..5a377a5cfd 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configure message visibility to non-members of public channels/groups ([#6152](https://github.com/open-chat-labs/open-chat/pull/6152)) - Ensure user has never joined a group or community before marking empty ([#6186](https://github.com/open-chat-labs/open-chat/pull/6186)) - Add 365 day streak achievement ([#6189](https://github.com/open-chat-labs/open-chat/pull/6189)) +- Support next batch of achievements ([#6230](https://github.com/open-chat-labs/open-chat/pull/6230)) ## [[2.0.1263](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1263-user)] - 2024-07-26 diff --git a/backend/canisters/user/impl/src/updates/accept_p2p_swap.rs b/backend/canisters/user/impl/src/updates/accept_p2p_swap.rs index d6d48daff5..d24d2d9397 100644 --- a/backend/canisters/user/impl/src/updates/accept_p2p_swap.rs +++ b/backend/canisters/user/impl/src/updates/accept_p2p_swap.rs @@ -9,8 +9,8 @@ use ic_cdk::update; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError}; use types::{ - AcceptP2PSwapResult, AcceptSwapSuccess, CanisterId, Chat, EventIndex, P2PSwapLocation, P2PSwapStatus, ReserveP2PSwapResult, - ReserveP2PSwapSuccess, TimestampMillis, UserId, + AcceptP2PSwapResult, AcceptSwapSuccess, Achievement, CanisterId, Chat, EventIndex, P2PSwapLocation, P2PSwapStatus, + ReserveP2PSwapResult, ReserveP2PSwapSuccess, TimestampMillis, UserId, }; use user_canister::accept_p2p_swap::{Response::*, *}; use user_canister::{P2PSwapStatusChange, UserCanisterEvent}; @@ -93,10 +93,14 @@ async fn accept_p2p_swap(args: Args) -> Response { })), ); crate::jobs::push_user_canister_events::start_job_if_required(state); + state + .data + .award_achievement_and_notify(Achievement::AcceptedP2PSwapOffer, now); } } }); NotifyEscrowCanisterOfDepositJob::run(content.swap_id); + Success(AcceptSwapSuccess { token1_txn_in: index }) } Err(response) => { diff --git a/backend/canisters/user/impl/src/updates/add_reaction.rs b/backend/canisters/user/impl/src/updates/add_reaction.rs index 8874d044ed..c5ec50cec7 100644 --- a/backend/canisters/user/impl/src/updates/add_reaction.rs +++ b/backend/canisters/user/impl/src/updates/add_reaction.rs @@ -39,7 +39,7 @@ fn add_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { }, Some(&mut state.data.event_store_client), ) { - AddRemoveReactionResult::Success => { + AddRemoveReactionResult::Success(_) => { let thread_root_message_id = args.thread_root_message_index.map(|i| chat.main_message_index_to_id(i)); state.push_user_canister_event( 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 360a54b511..5a571af60c 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 @@ -228,25 +228,29 @@ fn toggle_reaction(args: ToggleReactionArgs, caller_user_id: UserId, state: &mut if let Some(chat) = state.data.direct_chats.get_mut(&caller_user_id.into()) { let thread_root_message_index = args.thread_root_message_id.map(|id| chat.main_message_id_to_index(id)); + let now = state.env.now(); + let add_remove_reaction_args = AddRemoveReactionArgs { user_id: caller_user_id, min_visible_event_index: EventIndex::default(), thread_root_message_index, message_id: args.message_id, reaction: args.reaction.clone(), - now: state.env.now(), + now, }; if args.added { if matches!( chat.events.add_reaction::(add_remove_reaction_args, None), - AddRemoveReactionResult::Success + AddRemoveReactionResult::Success(_) ) && !state.data.suspended.value { if let Some((recipient, notification)) = build_notification(args, chat) { state.push_notification(recipient, notification); } } + + state.data.award_achievement_and_notify(Achievement::HadMessageReactedTo, now); } else { chat.events.remove_reaction(add_remove_reaction_args); } @@ -330,6 +334,8 @@ fn tip_message(args: user_canister::TipMessageArgs, caller_user_id: UserId, stat }); state.push_notification(my_user_id, notification); } + + state.data.award_achievement_and_notify(Achievement::HadMessageTipped, now); } } } diff --git a/backend/canisters/user/impl/src/updates/claim_daily_chit.rs b/backend/canisters/user/impl/src/updates/claim_daily_chit.rs index d6a45209f8..87e4ee1062 100644 --- a/backend/canisters/user/impl/src/updates/claim_daily_chit.rs +++ b/backend/canisters/user/impl/src/updates/claim_daily_chit.rs @@ -48,6 +48,10 @@ fn claim_daily_chit_impl(state: &mut RuntimeState) -> Response { state.data.award_achievement(Achievement::Streak30, now); } + if streak >= 100 { + state.data.award_achievement(Achievement::Streak100, now); + } + if streak >= 365 { state.data.award_achievement(Achievement::Streak365, now); } diff --git a/backend/canisters/user/impl/src/updates/join_video_call.rs b/backend/canisters/user/impl/src/updates/join_video_call.rs index 322e9a69d9..5a74e70a41 100644 --- a/backend/canisters/user/impl/src/updates/join_video_call.rs +++ b/backend/canisters/user/impl/src/updates/join_video_call.rs @@ -3,7 +3,7 @@ use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_tracing_macros::trace; use chat_events::SetVideoCallPresenceResult; use ic_cdk::update; -use types::{EventIndex, UserId, VideoCallPresence}; +use types::{Achievement, EventIndex, UserId, VideoCallPresence}; use user_canister::{ join_video_call::{Response::*, *}, JoinVideoCall, UserCanisterEvent, @@ -45,6 +45,8 @@ fn join_video_call_impl(args: Args, state: &mut RuntimeState) -> Response { })), ); + state.data.award_achievement_and_notify(Achievement::JoinedCall, now); + Success } SetVideoCallPresenceResult::MessageNotFound => MessageNotFound, diff --git a/backend/canisters/user/impl/src/updates/remove_reaction.rs b/backend/canisters/user/impl/src/updates/remove_reaction.rs index 4175d4b2c9..8ef0505cc3 100644 --- a/backend/canisters/user/impl/src/updates/remove_reaction.rs +++ b/backend/canisters/user/impl/src/updates/remove_reaction.rs @@ -32,7 +32,7 @@ fn remove_reaction_impl(args: Args, state: &mut RuntimeState) -> Response { reaction: args.reaction.clone(), now, }) { - AddRemoveReactionResult::Success => { + AddRemoveReactionResult::Success(_) => { let thread_root_message_id = args.thread_root_message_index.map(|i| chat.main_message_index_to_id(i)); state.push_user_canister_event( diff --git a/backend/canisters/user/impl/src/updates/set_message_reminder.rs b/backend/canisters/user/impl/src/updates/set_message_reminder.rs index 1f170ab36c..4eb649a9da 100644 --- a/backend/canisters/user/impl/src/updates/set_message_reminder.rs +++ b/backend/canisters/user/impl/src/updates/set_message_reminder.rs @@ -5,7 +5,7 @@ use canister_tracing_macros::trace; use chat_events::{MessageContentInternal, MessageReminderCreatedContentInternal}; use ic_cdk::update; use rand::RngCore; -use types::FieldTooLongResult; +use types::{Achievement, FieldTooLongResult}; use user_canister::set_message_reminder_v2::{Response::*, *}; use user_canister::C2CReplyContext; @@ -71,5 +71,7 @@ fn set_message_reminder_impl(args: Args, state: &mut RuntimeState) -> Response { now, ); + state.data.award_achievement_and_notify(Achievement::SentReminder, now); + Success(reminder_id) } diff --git a/backend/integration_tests/src/client/group.rs b/backend/integration_tests/src/client/group.rs index 46b9c243b3..b1fc961c24 100644 --- a/backend/integration_tests/src/client/group.rs +++ b/backend/integration_tests/src/client/group.rs @@ -131,6 +131,7 @@ pub mod happy_path { message_index, poll_option, operation: VoteOperation::RegisterVote, + new_achievement: false, correlation_id: 0, }, ); @@ -368,7 +369,10 @@ pub mod happy_path { env, sender, group_chat_id.into(), - &group_canister::join_video_call::Args { message_id }, + &group_canister::join_video_call::Args { + message_id, + new_achievement: false, + }, ); match response { diff --git a/backend/integration_tests/src/p2p_swap_tests.rs b/backend/integration_tests/src/p2p_swap_tests.rs index ac51e2393a..66a2dd925c 100644 --- a/backend/integration_tests/src/p2p_swap_tests.rs +++ b/backend/integration_tests/src/p2p_swap_tests.rs @@ -197,6 +197,7 @@ fn p2p_swap_in_group_succeeds() { thread_root_message_index: None, message_id, pin: None, + new_achievement: false, }, ); diff --git a/backend/libraries/chat_events/src/chat_events.rs b/backend/libraries/chat_events/src/chat_events.rs index 3b84acd135..ec8aa7b173 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -613,6 +613,8 @@ impl ChatEvents { return AddRemoveReactionResult::NoChange; } + let sender = message.sender; + if let Some(client) = event_store_client { let payload = ReactionAddedEventPayload { message_type: message.content.message_type(), @@ -641,7 +643,7 @@ impl ChatEvents { self.last_updated_timestamps .mark_updated(args.thread_root_message_index, event_index, args.now); - AddRemoveReactionResult::Success + AddRemoveReactionResult::Success(sender) } else { AddRemoveReactionResult::MessageNotFound } @@ -668,6 +670,8 @@ impl ChatEvents { message.reactions.retain(|(_, u)| !u.is_empty()); } + let sender = message.sender; + self.last_updated_timestamps .mark_updated(args.thread_root_message_index, event_index, args.now); @@ -679,7 +683,7 @@ impl ChatEvents { args.now, ); - AddRemoveReactionResult::Success + AddRemoveReactionResult::Success(sender) } else { AddRemoveReactionResult::MessageNotFound } @@ -1965,7 +1969,7 @@ pub struct AddRemoveReactionArgs { } pub enum AddRemoveReactionResult { - Success, + Success(UserId), NoChange, MessageNotFound, } diff --git a/backend/libraries/group_chat_core/src/lib.rs b/backend/libraries/group_chat_core/src/lib.rs index c48b3a27ba..ae0f7d59c7 100644 --- a/backend/libraries/group_chat_core/src/lib.rs +++ b/backend/libraries/group_chat_core/src/lib.rs @@ -1843,7 +1843,7 @@ pub struct SendMessageSuccess { } pub enum AddRemoveReactionResult { - Success, + Success(UserId), NoChange, InvalidReaction, MessageNotFound, @@ -1855,7 +1855,7 @@ pub enum AddRemoveReactionResult { impl From for AddRemoveReactionResult { fn from(value: chat_events::AddRemoveReactionResult) -> Self { match value { - chat_events::AddRemoveReactionResult::Success => AddRemoveReactionResult::Success, + chat_events::AddRemoveReactionResult::Success(sender) => AddRemoveReactionResult::Success(sender), chat_events::AddRemoveReactionResult::NoChange => AddRemoveReactionResult::NoChange, chat_events::AddRemoveReactionResult::MessageNotFound => AddRemoveReactionResult::MessageNotFound, } diff --git a/backend/libraries/group_community_common/Cargo.toml b/backend/libraries/group_community_common/Cargo.toml index be3c56066f..34e52ae160 100644 --- a/backend/libraries/group_community_common/Cargo.toml +++ b/backend/libraries/group_community_common/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" [dependencies] candid = { workspace = true } +fire_and_forget_handler = { path = "../fire_and_forget_handler" } icrc-ledger-types = { workspace = true } +msgpack = { path = "../msgpack" } serde = { workspace = true } serde_repr = { workspace = true } types = { path = "../types" } +user_canister = { path = "../../canisters/user/api" } [dev-dependencies] msgpack = { path = "../msgpack" } diff --git a/backend/libraries/group_community_common/src/achievements.rs b/backend/libraries/group_community_common/src/achievements.rs new file mode 100644 index 0000000000..2c97d88093 --- /dev/null +++ b/backend/libraries/group_community_common/src/achievements.rs @@ -0,0 +1,40 @@ +use fire_and_forget_handler::FireAndForgetHandler; +use msgpack::serialize_then_unwrap; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use types::{Achievement, UserId}; +use user_canister::c2c_notify_achievement; + +#[derive(Serialize, Deserialize, Default)] +pub struct Achievements { + achievements: HashMap>, +} + +impl Achievements { + pub fn notify_user( + &mut self, + user_id: UserId, + achievements: Vec, + fire_and_forget_handler: &mut FireAndForgetHandler, + ) { + if achievements.is_empty() { + return; + } + + let existing = self.achievements.entry(user_id).or_default(); + + let achievements: Vec<_> = achievements.into_iter().filter(|a| existing.insert(a.clone())).collect(); + + if achievements.is_empty() { + return; + } + + let args = c2c_notify_achievement::Args { achievements }; + + fire_and_forget_handler.send( + user_id.into(), + "c2c_notify_achievement_msgpack".to_string(), + serialize_then_unwrap(args), + ); + } +} diff --git a/backend/libraries/group_community_common/src/lib.rs b/backend/libraries/group_community_common/src/lib.rs index 7b2493919f..d808afbe6e 100644 --- a/backend/libraries/group_community_common/src/lib.rs +++ b/backend/libraries/group_community_common/src/lib.rs @@ -1,5 +1,7 @@ +mod achievements; mod payment_receipts; mod pending_payments_queue; +pub use achievements::*; pub use payment_receipts::*; pub use pending_payments_queue::*; diff --git a/backend/libraries/types/can.did b/backend/libraries/types/can.did index a18b94f408..3df0a02f71 100644 --- a/backend/libraries/types/can.did +++ b/backend/libraries/types/can.did @@ -1949,6 +1949,7 @@ type Achievement = variant { Streak7; Streak14; Streak30; + Streak100; Streak365; SentPoll; VotedOnPoll; diff --git a/backend/libraries/types/src/achievement.rs b/backend/libraries/types/src/achievement.rs index 0ce1048e2a..c9ffe648c2 100644 --- a/backend/libraries/types/src/achievement.rs +++ b/backend/libraries/types/src/achievement.rs @@ -18,8 +18,8 @@ pub enum Achievement { Streak7, Streak14, Streak30, + Streak100, Streak365, - SentPoll, SentText, SentImage, @@ -39,20 +39,19 @@ pub enum Achievement { TippedMessage, DeletedMessage, ForwardedMessage, + ProvedUniquePersonhood, - SentReminder, - VotedOnPoll, ReceivedCrypto, HadMessageReactedTo, HadMessageTipped, + VotedOnPoll, + SentReminder, JoinedCall, - ProvedUniquePersonhood, - SetCommunityDisplayName, - ChangedTheme, - SwappedFromWallet, AcceptedP2PSwapOffer, - EnabledDisappearingMessages, + SetCommunityDisplayName, + PinnedMessage, + SwappedFromWallet, FavouritedChat, FollowedThread, SuggestedTranslation, @@ -66,6 +65,9 @@ pub enum Achievement { SetGroupAccessGate, SetCommunityAccessGate, JoinedGatedGroupOrCommunity, + + ChangedTheme, + EnabledDisappearingMessages, OwnGroupWithOneDiamondMember, OwnGroupWithTenDiamondMembers, OwnGroupWithOneHundredDiamondMembers, @@ -91,6 +93,7 @@ impl Achievement { Achievement::Streak7 => 3000, Achievement::Streak14 => 5000, Achievement::Streak30 => 10000, + Achievement::Streak100 => 20000, Achievement::Streak365 => 50000, Achievement::ProvedUniquePersonhood => 10000, Achievement::SetCommunityDisplayName => 700, diff --git a/frontend/app/src/i18n/cn.json b/frontend/app/src/i18n/cn.json index 82edfa8397..c0f1ea93c2 100644 --- a/frontend/app/src/i18n/cn.json +++ b/frontend/app/src/i18n/cn.json @@ -385,7 +385,9 @@ "disclaimer": "此内容是 OpenChat 的外部内容。有关此内容的任何问题都应报告给社区所有者", "error": "外部内容加载失败", "frozen": "外部内容已被平台管理员冻结", - "initialising": "初始化外部内容" + "initialising": "初始化外部内容", + "label": "外部内容", + "name": "外部内容网址" }, "failedToCopyLinkToClipboard": "无法将链接复制到剪贴板", "failedToCopyToClipboard": "无法复制到粘贴板 {account}", @@ -613,7 +615,7 @@ "favourited_chat": "将你的第一次聊天标记为收藏", "followed_thread": "第一次关注帖子", "forwarded_message": "转发第一条消息", - "had_message_tipped": "你的第一条信息被泄露", + "had_message_tipped": "收到你的第一条消息提示", "joined_call": "加入了你的第一次通话", "joined_community": "加入你的第一个社区", "joined_gated_group_or_community": "加入你的第一个封闭群组/频道", @@ -642,6 +644,7 @@ "sent_meme": "发送了你的第一条 meme 消息", "sent_poll": "发送了您的第一个投票", "sent_prize": "已发送您的一等奖", + "sent_reminder": "已发送第一条提醒", "sent_swap_offer": "发送第一个 P2P 交换要约", "sent_text": "发送了第一条短信", "sent_video": "发送了你的第一个视频", @@ -653,9 +656,11 @@ "set_group_access_gate": "设置第一个群组/通道访问门", "showChitPopup": "通知我我的成就", "started_call": "开始第一次通话", + "streak_100": "已实现 100 天连续", "streak_14": "已连续 14 天", "streak_3": "已连续 3 天", "streak_30": "已连续 30 天", + "streak_365": "已实现 365 天连续", "streak_7": "已连续 7 天", "suggested_translation": "建议你的第一个翻译", "swapped_from_wallet": "从钱包中兑换了第一个代币", diff --git a/frontend/app/src/i18n/de.json b/frontend/app/src/i18n/de.json index 46874af947..96617f8fa2 100644 --- a/frontend/app/src/i18n/de.json +++ b/frontend/app/src/i18n/de.json @@ -385,7 +385,9 @@ "disclaimer": "Dieser Inhalt ist extern zu OpenChat. Alle Probleme mit diesem Inhalt sollten dem Community-Eigentümer gemeldet werden.", "error": "Externer Inhalt konnte nicht geladen werden", "frozen": "Externer Inhalt wurde von einem Plattformmoderator eingefroren", - "initialising": "Initialisieren externer Inhalte" + "initialising": "Initialisieren externer Inhalte", + "label": "Externer Inhalt", + "name": "Externe Inhalts-URL" }, "failedToCopyLinkToClipboard": "Link kann nicht in die Zwischenablage kopiert werden", "failedToCopyToClipboard": "Kopieren in die Zwischenablage nicht möglich {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Den ersten Chat als Favorit markiert", "followed_thread": "Das erste Mal, dass ich einem Thread folge", "forwarded_message": "Erste Nachricht weitergeleitet", - "had_message_tipped": "Ihre erste Nachricht wurde getippt", + "had_message_tipped": "Ihren ersten Nachrichtentipp erhalten", "joined_call": "Bei Ihrem ersten Anruf dabei", "joined_community": "Ihrer ersten Community beigetreten", "joined_gated_group_or_community": "Ihrer ersten geschlossenen Gruppe/Ihrem ersten geschlossenen Kanal beigetreten", @@ -642,6 +644,7 @@ "sent_meme": "Deine erste Meme-Nachricht gesendet", "sent_poll": "Senden Sie Ihre erste Umfrage", "sent_prize": "Versendet Ihren ersten Preis", + "sent_reminder": "Erste Erinnerung gesendet", "sent_swap_offer": "Erstes P2P-Tauschangebot gesendet", "sent_text": "Deine erste Textnachricht gesendet", "sent_video": "Dein erstes Video gesendet", @@ -653,9 +656,11 @@ "set_group_access_gate": "Richten Sie Ihr erstes Gruppen-/Kanalzugriffstor ein", "showChitPopup": "Benachrichtigen Sie mich über meine Erfolge", "started_call": "Ersten Anruf gestartet", + "streak_100": "Eine 100-Tage-Serie erreicht", "streak_14": "Eine 14-tägige Erfolgsserie erreicht", "streak_3": "3 Tage lang erfolgreich gewesen", "streak_30": "30 Tage lang erfolgreich gewesen", + "streak_365": "Eine 365-Tage-Serie erreicht", "streak_7": "Eine 7-Tage-Serie erreicht", "suggested_translation": "Ihre erste Übersetzung wurde vorgeschlagen", "swapped_from_wallet": "Dein erstes Token aus der Wallet getauscht", diff --git a/frontend/app/src/i18n/en.json b/frontend/app/src/i18n/en.json index ddc58425f4..9db5a8e234 100644 --- a/frontend/app/src/i18n/en.json +++ b/frontend/app/src/i18n/en.json @@ -615,7 +615,7 @@ "favourited_chat": "Marked your first chat as a favourite", "followed_thread": "First time following a thread", "forwarded_message": "Forwarded first message", - "had_message_tipped": "Had your first message tipped", + "had_message_tipped": "Received your first message tip", "joined_call": "Joined your first call", "joined_community": "Joined your first community", "joined_gated_group_or_community": "Joined your first gated group/channel", @@ -644,6 +644,7 @@ "sent_meme": "Sent your first meme message", "sent_poll": "Sent your first poll", "sent_prize": "Sent your first prize", + "sent_reminder": "Sent your first reminder", "sent_swap_offer": "Sent first P2P swap offer", "sent_text": "Sent your first text message", "sent_video": "Sent your first video", @@ -655,9 +656,11 @@ "set_group_access_gate": "Set your first group/channel access gate", "showChitPopup": "Notify me of my achievements", "started_call": "Started first call", + "streak_100": "Achieved a 100 day streak", "streak_14": "Achieved a 14 day streak", "streak_3": "Achieved a 3 day streak", "streak_30": "Achieved a 30 day streak", + "streak_365": "Achieved a 365 day streak", "streak_7": "Achieved a 7 day streak", "suggested_translation": "Suggested your fiurst translation", "swapped_from_wallet": "Swapped your first token from the wallet", @@ -1569,4 +1572,4 @@ "yourChats": "Your chats", "youreBlocked": "Sorry - you have been blocked from this group", "yourRoleChanged": "{changedBy} changed your role from {oldRole} to {newRole}" -} +} \ No newline at end of file diff --git a/frontend/app/src/i18n/es.json b/frontend/app/src/i18n/es.json index 6da7a99caa..0a46fd9587 100644 --- a/frontend/app/src/i18n/es.json +++ b/frontend/app/src/i18n/es.json @@ -385,7 +385,9 @@ "disclaimer": "Este contenido es externo a OpenChat. Cualquier problema con este contenido debe ser informado al propietario de la comunidad.", "error": "No se pudo cargar el contenido externo", "frozen": "El moderador de la plataforma ha congelado el contenido externo", - "initialising": "Inicializando contenido externo" + "initialising": "Inicializando contenido externo", + "label": "Contenido externo", + "name": "URL de contenido externo" }, "failedToCopyLinkToClipboard": "No se puede copiar el enlace al portapapeles", "failedToCopyToClipboard": "No se puede copiar al portapapeles", @@ -613,7 +615,7 @@ "favourited_chat": "Marcó su primer chat como favorito", "followed_thread": "Primera vez que sigo un hilo.", "forwarded_message": "Primer mensaje reenviado", - "had_message_tipped": "¿Tuviste propina en tu primer mensaje?", + "had_message_tipped": "Recibí tu primer mensaje de sugerencia", "joined_call": "Se unió a su primera llamada", "joined_community": "Se unió a su primera comunidad", "joined_gated_group_or_community": "Se unió a su primer grupo/canal privado", @@ -642,6 +644,7 @@ "sent_meme": "Envió su primer mensaje meme", "sent_poll": "Envió su primera encuesta", "sent_prize": "Enviaste tu primer premio", + "sent_reminder": "Envió tu primer recordatorio", "sent_swap_offer": "Primera oferta de intercambio P2P enviada", "sent_text": "Envió su primer mensaje de texto", "sent_video": "Envió tu primer vídeo", @@ -653,9 +656,11 @@ "set_group_access_gate": "Configura tu primera puerta de acceso a grupo/canal", "showChitPopup": "Notificarme de mis logros", "started_call": "Comenzó la primera llamada", + "streak_100": "Consiguió una racha de 100 días", "streak_14": "Logró una racha de 14 días", "streak_3": "Logró una racha de 3 días", "streak_30": "Logró una racha de 30 días", + "streak_365": "Logró una racha de 365 días", "streak_7": "Logró una racha de 7 días", "suggested_translation": "Sugirió su primera traducción.", "swapped_from_wallet": "Cambiaste tu primer token de la billetera", diff --git a/frontend/app/src/i18n/fa.json b/frontend/app/src/i18n/fa.json index faa5686059..a3207d1df2 100644 --- a/frontend/app/src/i18n/fa.json +++ b/frontend/app/src/i18n/fa.json @@ -385,7 +385,9 @@ "disclaimer": "این محتوا برای OpenChat خارجی است. هر گونه مشکل در مورد این محتوا باید به مالک انجمن گزارش شود", "error": "محتوای خارجی بارگیری نشد", "frozen": "محتوای خارجی توسط ناظر پلتفرم مسدود شده است", - "initialising": "مقداردهی اولیه محتوای خارجی" + "initialising": "مقداردهی اولیه محتوای خارجی", + "label": "محتوای خارجی", + "name": "آدرس محتوای خارجی" }, "failedToCopyLinkToClipboard": "پیوند در کلیپ بورد کپی نشد", "failedToCopyToClipboard": "امکان کپی در کلیپ بورد وجود ندارد. {account}", @@ -447,6 +449,11 @@ "goToLatestMessage": "به آخرین پیام بروید", "group": { "addGroupPhoto": "عکس {level} را اضافه کنید", + "addMembers": "اضافه کردن اعضای کانال", + "addMembersFailed": "هنگام افزودن اعضا خطایی روی داد", + "addMembersPartialSuccess": "برخی از اعضا نمی توانند اضافه شوند", + "addMembersTab": "اضافه کردن اعضا", + "addOrInviteUsers": "اعضا را اضافه یا دعوت کنید", "advanced": "پیشرفته", "back": "بازگشت", "create": "ایجاد {level}", @@ -464,12 +471,16 @@ "image": "تصویر {level}", "inviteUsers": "کاربران {level} را دعوت کنید", "inviteUsersFailed": "در دعوت از کاربران خطایی روی داد", + "inviteUsersTab": "دعوت از کاربران", "members": "اعضا", "name": "نام {level}", "next": "بعد", "privateGroup": "خصوصی {level}", "privateGroupInfo": "کاربران نمی توانند آزادانه به این {level} بپیوندند. آنها باید یا اضافه شوند یا توسط یک عضو مجاز دعوت شوند.", "publicGroup": "عمومی {level}", + "searchForCommunityMember": "جستجوی یک عضو انجمن", + "searchForUser": "جستجوی کاربر", + "shareTab": "به اشتراک بگذارید", "tooManyInvites": "شما فقط می توانید 100 دعوتنامه برجسته داشته باشید", "update": "به‌روزرسانی {level}", "usersInvited": "پیام دعوت ارسال شد!", @@ -604,7 +615,7 @@ "favourited_chat": "اولین گپ شما را به عنوان مورد علاقه علامت گذاری کرد", "followed_thread": "اولین باری که یک موضوع را دنبال می کنم", "forwarded_message": "اولین پیام فوروارد شد", - "had_message_tipped": "اولین پیام شما نوک شده بود", + "had_message_tipped": "اولین نکته پیام شما را دریافت کردم", "joined_call": "به اولین تماس شما ملحق شد", "joined_community": "به اولین انجمن شما پیوست", "joined_gated_group_or_community": "به اولین گروه/کانال دردار شما پیوست", @@ -633,6 +644,7 @@ "sent_meme": "اولین پیام میم خود را ارسال کرد", "sent_poll": "اولین نظرسنجی خود را ارسال کردید", "sent_prize": "اولین جایزه خود را ارسال کرد", + "sent_reminder": "اولین یادآوری خود را ارسال کرد", "sent_swap_offer": "اولین پیشنهاد مبادله P2P ارسال شد", "sent_text": "اولین پیامک خود را ارسال کرد", "sent_video": "اولین ویدیوی خود را ارسال کرد", @@ -644,9 +656,11 @@ "set_group_access_gate": "اولین دروازه دسترسی گروه/کانال خود را تنظیم کنید", "showChitPopup": "من را از دستاوردهایم آگاه کن", "started_call": "تماس اول شروع شد", + "streak_100": "به یک 100 روزه دست یافت", "streak_14": "به یک دوره 14 روزه دست یافت", "streak_3": "به یک رگشت 3 روزه دست یافت", "streak_30": "به یک دوره 30 روزه دست یافت", + "streak_365": "به یک دوره 365 روزه دست یافت", "streak_7": "به یک دوره 7 روزه دست یافت", "suggested_translation": "اولین ترجمه شما را پیشنهاد کرد", "swapped_from_wallet": "اولین توکن خود را از کیف پول عوض کردید", @@ -824,6 +838,7 @@ "percLeft": "{perc}% باقی مانده است", "performingUpgrade": "انجام ارتقا", "permissions": { + "addMembers": "اضافه کردن اعضا", "allMembers": "همه اعضا می توانند", "changeRoles": "تغییر نقش اعضا", "createPolls": "ایجاد نظرسنجی", diff --git a/frontend/app/src/i18n/fr.json b/frontend/app/src/i18n/fr.json index 78e912a71f..c22a069a1e 100644 --- a/frontend/app/src/i18n/fr.json +++ b/frontend/app/src/i18n/fr.json @@ -385,7 +385,9 @@ "disclaimer": "Ce contenu est externe à OpenChat. Tout problème avec ce contenu doit être signalé au propriétaire de la communauté", "error": "Le chargement du contenu externe a échoué", "frozen": "Le contenu externe a été gelé par un modérateur de la plateforme", - "initialising": "Initialisation du contenu externe" + "initialising": "Initialisation du contenu externe", + "label": "Contenu externe", + "name": "URL du contenu externe" }, "failedToCopyLinkToClipboard": "Impossible de copier le lien dans le presse-papiers", "failedToCopyToClipboard": "Impossible de copier dans le presse-papiers {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Vous avez marqué votre première discussion comme favorite", "followed_thread": "Première fois que je suis un fil", "forwarded_message": "Premier message transféré", - "had_message_tipped": "Votre premier message a été prévenu", + "had_message_tipped": "J'ai reçu votre premier message d'astuce", "joined_call": "A rejoint votre premier appel", "joined_community": "Vous avez rejoint votre première communauté", "joined_gated_group_or_community": "Vous avez rejoint votre premier groupe/canal fermé", @@ -642,6 +644,7 @@ "sent_meme": "J'ai envoyé ton premier message mème", "sent_poll": "Envoyé votre premier sondage", "sent_prize": "Envoyé votre premier prix", + "sent_reminder": "Envoyez votre premier rappel", "sent_swap_offer": "Première offre d'échange P2P envoyée", "sent_text": "Vous avez envoyé votre premier SMS", "sent_video": "Envoyé votre première vidéo", @@ -653,9 +656,11 @@ "set_group_access_gate": "Définissez votre première porte d'accès à un groupe/canal", "showChitPopup": "M'informer de mes réalisations", "started_call": "Premier appel lancé", + "streak_100": "A réalisé une séquence de 100 jours", "streak_14": "A réalisé une séquence de 14 jours", "streak_3": "A réalisé une séquence de 3 jours", "streak_30": "A réalisé une séquence de 30 jours", + "streak_365": "A réalisé une séquence de 365 jours", "streak_7": "A réalisé une séquence de 7 jours", "suggested_translation": "Vous avez suggéré votre première traduction", "swapped_from_wallet": "Vous avez échangé votre premier jeton du portefeuille", diff --git a/frontend/app/src/i18n/hi.json b/frontend/app/src/i18n/hi.json index 38cecbb50d..f9cc9ea59c 100644 --- a/frontend/app/src/i18n/hi.json +++ b/frontend/app/src/i18n/hi.json @@ -385,7 +385,9 @@ "disclaimer": "यह सामग्री OpenChat से बाहर की है। इस सामग्री से जुड़ी किसी भी समस्या की रिपोर्ट समुदाय के स्वामी को करनी चाहिए", "error": "बाह्य सामग्री लोड करने में विफल", "frozen": "बाहरी सामग्री को प्लेटफ़ॉर्म मॉडरेटर द्वारा फ़्रीज़ कर दिया गया है", - "initialising": "बाह्य सामग्री आरंभ करना" + "initialising": "बाह्य सामग्री आरंभ करना", + "label": "बाह्य सामग्री", + "name": "बाह्य सामग्री यूआरएल" }, "failedToCopyLinkToClipboard": "क्लिपबोर्ड पर लिंक कॉपी करने में असमर्थ", "failedToCopyToClipboard": "क्लिपबोर्ड पर कॉपी करने में असमर्थ. {account}", @@ -613,7 +615,7 @@ "favourited_chat": "आपकी पहली चैट को पसंदीदा के रूप में चिह्नित किया गया", "followed_thread": "पहली बार किसी धागे का अनुसरण कर रहा हूँ", "forwarded_message": "पहला संदेश अग्रेषित किया गया", - "had_message_tipped": "क्या आपका पहला संदेश टिप किया गया था?", + "had_message_tipped": "आपका पहला संदेश टिप प्राप्त हुआ", "joined_call": "आपकी पहली कॉल में शामिल हुए", "joined_community": "अपने पहले समुदाय में शामिल हुए", "joined_gated_group_or_community": "अपने पहले गेटेड ग्रुप/चैनल में शामिल हुए", @@ -642,6 +644,7 @@ "sent_meme": "अपना पहला मीम संदेश भेजा", "sent_poll": "अपना पहला पोल भेजा", "sent_prize": "अपना प्रथम पुरस्कार भेजा", + "sent_reminder": "आपका पहला अनुस्मारक भेजा गया", "sent_swap_offer": "पहला P2P स्वैप प्रस्ताव भेजा गया", "sent_text": "अपना पहला टेक्स्ट संदेश भेजा", "sent_video": "अपना पहला वीडियो भेजा", @@ -653,9 +656,11 @@ "set_group_access_gate": "अपना पहला समूह/चैनल एक्सेस गेट सेट करें", "showChitPopup": "मुझे मेरी उपलब्धियों के बारे में सूचित करें", "started_call": "पहली कॉल शुरू की", + "streak_100": "100 दिन का सिलसिला हासिल किया", "streak_14": "14 दिन का सिलसिला हासिल किया", "streak_3": "3 दिन की लकीर हासिल की", "streak_30": "30 दिन का सिलसिला हासिल किया", + "streak_365": "365 दिन की लकीर हासिल की", "streak_7": "7 दिन का सिलसिला हासिल किया", "suggested_translation": "आपका पहला अनुवाद सुझाया गया", "swapped_from_wallet": "वॉलेट से अपना पहला टोकन स्वैप किया", diff --git a/frontend/app/src/i18n/it.json b/frontend/app/src/i18n/it.json index d213c0db8b..c2ae0a54e2 100644 --- a/frontend/app/src/i18n/it.json +++ b/frontend/app/src/i18n/it.json @@ -385,7 +385,9 @@ "disclaimer": "Questo contenuto è esterno a OpenChat. Eventuali problemi con questo contenuto devono essere segnalati al proprietario della Community", "error": "Impossibile caricare il contenuto esterno", "frozen": "Il contenuto esterno è stato congelato da un moderatore della piattaforma", - "initialising": "Inizializzazione del contenuto esterno" + "initialising": "Inizializzazione del contenuto esterno", + "label": "Contenuto esterno", + "name": "URL del contenuto esterno" }, "failedToCopyLinkToClipboard": "Impossibile copiare il collegamento negli appunti", "failedToCopyToClipboard": "Impossibile copiare negli appunti {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Contrassegnata la tua prima chat come preferita", "followed_thread": "La prima volta che seguo un thread", "forwarded_message": "Primo messaggio inoltrato", - "had_message_tipped": "Il tuo primo messaggio aveva ricevuto una soffiata", + "had_message_tipped": "Ricevuto il tuo primo messaggio suggerimento", "joined_call": "Partecipa alla tua prima chiamata", "joined_community": "Ti sei unito alla tua prima community", "joined_gated_group_or_community": "Ti sei unito al tuo primo gruppo/canale delimitato", @@ -642,6 +644,7 @@ "sent_meme": "Hai inviato il tuo primo messaggio meme", "sent_poll": "Inviato il tuo primo sondaggio", "sent_prize": "Inviato il tuo primo premio", + "sent_reminder": "Inviato il tuo primo promemoria", "sent_swap_offer": "Inviata la prima offerta di scambio P2P", "sent_text": "Inviato il tuo primo messaggio di testo", "sent_video": "Hai inviato il tuo primo video", @@ -653,9 +656,11 @@ "set_group_access_gate": "Imposta il tuo primo cancello di accesso al gruppo/canale", "showChitPopup": "Avvisami dei miei risultati", "started_call": "Iniziata la prima chiamata", + "streak_100": "Ha raggiunto una serie di 100 giorni", "streak_14": "Hai ottenuto una serie di 14 giorni consecutivi", "streak_3": "Hai ottenuto una serie di 3 giorni consecutivi", "streak_30": "Raggiunto un periodo di 30 giorni consecutivi", + "streak_365": "Ha raggiunto una serie di 365 giorni", "streak_7": "Raggiunto un record di 7 giorni consecutivi", "suggested_translation": "Suggerita la tua prima traduzione", "swapped_from_wallet": "Hai scambiato il tuo primo token dal portafoglio", diff --git a/frontend/app/src/i18n/iw.json b/frontend/app/src/i18n/iw.json index c43a57ac3d..56d088d110 100644 --- a/frontend/app/src/i18n/iw.json +++ b/frontend/app/src/i18n/iw.json @@ -385,7 +385,9 @@ "disclaimer": "תוכן זה חיצוני ל-OpenChat. יש לדווח לבעל הקהילה על כל בעיה בתוכן זה", "error": "הטעינה של תוכן חיצוני נכשלה", "frozen": "תוכן חיצוני הוקפא על ידי מנחה פלטפורמה", - "initialising": "אתחול תוכן חיצוני" + "initialising": "אתחול תוכן חיצוני", + "label": "תוכן חיצוני", + "name": "כתובת אתר של תוכן חיצוני" }, "failedToCopyLinkToClipboard": "לא ניתן להעתיק את הקישור ללוח", "failedToCopyToClipboard": "לא ניתן להעתיק ללוח. {account}", @@ -613,7 +615,7 @@ "favourited_chat": "סימן את הצ'אט הראשון שלך כמועדף", "followed_thread": "פעם ראשונה עוקב אחר שרשור", "forwarded_message": "הועברה הודעה ראשונה", - "had_message_tipped": "ההודעה הראשונה שלך קיבלה טיפ", + "had_message_tipped": "קיבלתי את טיפ ההודעה הראשונה שלך", "joined_call": "הצטרף לשיחה הראשונה שלך", "joined_community": "הצטרף לקהילה הראשונה שלך", "joined_gated_group_or_community": "הצטרף לקבוצה/ערוץ הסגור הראשון שלך", @@ -642,6 +644,7 @@ "sent_meme": "שלחת את הודעת המם הראשונה שלך", "sent_poll": "שלח את הסקר הראשון שלך", "sent_prize": "שלח את הפרס הראשון שלך", + "sent_reminder": "שלח את התזכורת הראשונה שלך", "sent_swap_offer": "נשלחה הצעת חילופי P2P ראשונה", "sent_text": "שלח את הודעת הטקסט הראשונה שלך", "sent_video": "שלח את הסרטון הראשון שלך", @@ -653,9 +656,11 @@ "set_group_access_gate": "הגדר את שער הגישה לקבוצה/ערוץ הראשון שלך", "showChitPopup": "הודע לי על ההישגים שלי", "started_call": "התחילה שיחה ראשונה", + "streak_100": "השיג רצף של 100 ימים", "streak_14": "השיג רצף של 14 ימים", "streak_3": "השיג רצף של 3 ימים", "streak_30": "השיג רצף של 30 יום", + "streak_365": "השיג רצף של 365 ימים", "streak_7": "השיג רצף של 7 ימים", "suggested_translation": "הציע את התרגום הראשון שלך", "swapped_from_wallet": "החלפת את האסימון הראשון שלך מהארנק", diff --git a/frontend/app/src/i18n/jp.json b/frontend/app/src/i18n/jp.json index 5e30470702..2804466dbb 100644 --- a/frontend/app/src/i18n/jp.json +++ b/frontend/app/src/i18n/jp.json @@ -385,7 +385,9 @@ "disclaimer": "このコンテンツはOpenChatの外部にあります。このコンテンツに関する問題があればコミュニティのオーナーに報告してください。", "error": "外部コンテンツの読み込みに失敗しました", "frozen": "外部コンテンツはプラットフォームモデレーターによって凍結されました", - "initialising": "外部コンテンツの初期化" + "initialising": "外部コンテンツの初期化", + "label": "外部コンテンツ", + "name": "外部コンテンツのURL" }, "failedToCopyLinkToClipboard": "リンクをクリップボードにコピーできません", "failedToCopyToClipboard": "クリップボードにコピーができません. {account}", @@ -613,7 +615,7 @@ "favourited_chat": "最初のチャットをお気に入りとしてマークしました", "followed_thread": "初めてスレッドをフォローする", "forwarded_message": "最初のメッセージを転送しました", - "had_message_tipped": "最初のメッセージにチップを贈った", + "had_message_tipped": "最初のメッセージのヒントを受け取りました", "joined_call": "最初の通話に参加しました", "joined_community": "最初のコミュニティに参加しました", "joined_gated_group_or_community": "最初のゲートグループ/チャンネルに参加しました", @@ -642,6 +644,7 @@ "sent_meme": "最初のミームメッセージを送信しました", "sent_poll": "最初のアンケートを送信しました", "sent_prize": "最初の賞品を発送しました", + "sent_reminder": "最初のリマインダーを送信しました", "sent_swap_offer": "最初のP2Pスワップオファーを送信しました", "sent_text": "最初のテキストメッセージを送信しました", "sent_video": "最初のビデオを送信しました", @@ -653,9 +656,11 @@ "set_group_access_gate": "最初のグループ/チャンネルアクセスゲートを設定する", "showChitPopup": "私の成果を通知する", "started_call": "最初の通話を開始しました", + "streak_100": "100日連続達成", "streak_14": "14日間連続記録を達成", "streak_3": "3日連続達成", "streak_30": "30日間連続達成", + "streak_365": "365日連続達成", "streak_7": "7日間連続達成", "suggested_translation": "最初の翻訳を提案しました", "swapped_from_wallet": "ウォレットから最初のトークンを交換しました", diff --git a/frontend/app/src/i18n/pl.json b/frontend/app/src/i18n/pl.json index 436453f96c..7e45082302 100644 --- a/frontend/app/src/i18n/pl.json +++ b/frontend/app/src/i18n/pl.json @@ -385,7 +385,9 @@ "disclaimer": "Ta treść jest zewnętrzna w stosunku do OpenChat. Wszelkie problemy z tą treścią należy zgłaszać właścicielowi społeczności", "error": "Nie udało się załadować zawartości zewnętrznej", "frozen": "Treść zewnętrzna została zamrożona przez moderatora platformy", - "initialising": "Inicjowanie zawartości zewnętrznej" + "initialising": "Inicjowanie zawartości zewnętrznej", + "label": "Treść zewnętrzna", + "name": "Adres URL treści zewnętrznej" }, "failedToCopyLinkToClipboard": "Nie można skopiować linku do schowka", "failedToCopyToClipboard": "Nie można skopiować do schowka. {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Oznaczono Twój pierwszy czat jako ulubiony", "followed_thread": "Pierwszy raz śledzę wątek", "forwarded_message": "Przekazano pierwszą wiadomość", - "had_message_tipped": "Twoja pierwsza wiadomość została przekazana napiwek", + "had_message_tipped": "Otrzymałem Twoją pierwszą wiadomość z poradą", "joined_call": "Dołączyłem do Twojej pierwszej rozmowy", "joined_community": "Dołączyłeś do swojej pierwszej społeczności", "joined_gated_group_or_community": "Dołączyłeś do swojej pierwszej zamkniętej grupy/kanału", @@ -642,6 +644,7 @@ "sent_meme": "Wysłałeś swoją pierwszą wiadomość w postaci mema", "sent_poll": "Wysłałeś swoją pierwszą ankietę", "sent_prize": "Wysłałeś pierwszą nagrodę", + "sent_reminder": "Wysłano pierwsze przypomnienie", "sent_swap_offer": "Wysłano pierwszą ofertę wymiany P2P", "sent_text": "Wysłałeś pierwszą wiadomość tekstową", "sent_video": "Wysłałeś swój pierwszy film", @@ -653,9 +656,11 @@ "set_group_access_gate": "Ustaw swoją pierwszą bramkę dostępu do grupy/kanału", "showChitPopup": "Powiadamiaj mnie o moich osiągnięciach", "started_call": "Rozpoczął pierwszą rozmowę", + "streak_100": "Osiągnięto 100-dniową passę", "streak_14": "Osiągnięto passę 14 dni", "streak_3": "Osiągnięto 3-dniową passę", "streak_30": "Osiągnięto passę 30 dni", + "streak_365": "Osiągnięto 365-dniową passę", "streak_7": "Osiągnięto passę 7 dni", "suggested_translation": "Zasugerowałem Twoje pierwsze tłumaczenie", "swapped_from_wallet": "Wymieniłeś swój pierwszy token z portfela", diff --git a/frontend/app/src/i18n/ru.json b/frontend/app/src/i18n/ru.json index 19f4752faf..7996864141 100644 --- a/frontend/app/src/i18n/ru.json +++ b/frontend/app/src/i18n/ru.json @@ -385,7 +385,9 @@ "disclaimer": "Этот контент является внешним по отношению к OpenChat. О любых проблемах с этим контентом следует сообщать владельцу сообщества", "error": "Не удалось загрузить внешний контент", "frozen": "Внешний контент был заморожен модератором платформы", - "initialising": "Инициализация внешнего контента" + "initialising": "Инициализация внешнего контента", + "label": "Внешний контент", + "name": "URL внешнего контента" }, "failedToCopyLinkToClipboard": "Не удалось скопировать ссылку в буфер обмена", "failedToCopyToClipboard": "Невозможно скопировать в буфер обмена. {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Отметил ваш первый чат как избранный", "followed_thread": "Впервые слежу за темой", "forwarded_message": "Переслано первое сообщение", - "had_message_tipped": "Если бы ваше первое сообщение было подсказано", + "had_message_tipped": "Получил ваш первый совет по сообщению", "joined_call": "Присоединился к вашему первому звонку", "joined_community": "Присоединился к вашему первому сообществу", "joined_gated_group_or_community": "Присоединился к вашей первой закрытой группе/каналу", @@ -642,6 +644,7 @@ "sent_meme": "Отправил первое мем-сообщение", "sent_poll": "Отправил свой первый опрос", "sent_prize": "Отправил свой первый приз", + "sent_reminder": "Отправил ваше первое напоминание", "sent_swap_offer": "Отправлено первое предложение обмена P2P", "sent_text": "Отправил первое текстовое сообщение", "sent_video": "Отправил первое видео", @@ -653,9 +656,11 @@ "set_group_access_gate": "Установите свой первый шлюз доступа к группе/каналу", "showChitPopup": "Сообщите мне о моих достижениях", "started_call": "Начал первый звонок", + "streak_100": "Достигнута 100-дневная серия", "streak_14": "Достигнута 14-дневная серия", "streak_3": "Достигнута 3-дневная серия", "streak_30": "Достигнута 30-дневная серия", + "streak_365": "Достигнута серия в 365 дней", "streak_7": "Достигнута 7-дневная серия", "suggested_translation": "Предложил ваш первый перевод", "swapped_from_wallet": "Выменил свой первый токен из кошелька", diff --git a/frontend/app/src/i18n/uk.json b/frontend/app/src/i18n/uk.json index 90df6d78de..ebd9a25247 100644 --- a/frontend/app/src/i18n/uk.json +++ b/frontend/app/src/i18n/uk.json @@ -385,7 +385,9 @@ "disclaimer": "Цей вміст є зовнішнім для OpenChat. Про будь-які проблеми з цим вмістом слід повідомляти власника спільноти", "error": "Не вдалося завантажити зовнішній вміст", "frozen": "Зовнішній вміст було заморожено модератором платформи", - "initialising": "Ініціалізація зовнішнього вмісту" + "initialising": "Ініціалізація зовнішнього вмісту", + "label": "Зовнішній вміст", + "name": "URL-адреса зовнішнього вмісту" }, "failedToCopyLinkToClipboard": "Не вдалося скопіювати посилання в буфер обміну", "failedToCopyToClipboard": "Неможливо скопіювати в буфер обміну. {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Позначив свій перший чат як улюблений", "followed_thread": "Перший раз слідкую за темою", "forwarded_message": "Переслано перше повідомлення", - "had_message_tipped": "Отримав підказку про ваше перше повідомлення", + "had_message_tipped": "Отримав ваше перше повідомлення", "joined_call": "Приєднався до вашого першого виклику", "joined_community": "Приєднався до вашої першої спільноти", "joined_gated_group_or_community": "Приєднався до вашої першої закритої групи/каналу", @@ -642,6 +644,7 @@ "sent_meme": "Надіслав своє перше мем-повідомлення", "sent_poll": "Надіслав своє перше опитування", "sent_prize": "Надіслав свій перший приз", + "sent_reminder": "Надіслано перше нагадування", "sent_swap_offer": "Надіслано першу пропозицію обміну P2P", "sent_text": "Надіслав своє перше текстове повідомлення", "sent_video": "Надіслав своє перше відео", @@ -653,9 +656,11 @@ "set_group_access_gate": "Встановіть перший шлюз доступу до групи/каналу", "showChitPopup": "Повідомляйте мене про мої досягнення", "started_call": "Розпочав перший дзвінок", + "streak_100": "Досягнуто 100-денної поспіль", "streak_14": "Досягнуто 14-денної поспіль", "streak_3": "Досягнуто 3-денної поспіль", "streak_30": "Досягнуто 30-денної поспіль", + "streak_365": "365-денна поспіль", "streak_7": "Досягнуто 7-денної серії", "suggested_translation": "Запропонував свій перший переклад", "swapped_from_wallet": "Обміняв свій перший жетон із гаманця", diff --git a/frontend/app/src/i18n/vi.json b/frontend/app/src/i18n/vi.json index 4b2a240b94..84ce96851e 100644 --- a/frontend/app/src/i18n/vi.json +++ b/frontend/app/src/i18n/vi.json @@ -385,7 +385,9 @@ "disclaimer": "Nội dung này nằm ngoài OpenChat. Bất kỳ vấn đề nào với nội dung này phải được báo cáo cho chủ sở hữu Cộng đồng", "error": "Nội dung bên ngoài không tải được", "frozen": "Nội dung bên ngoài đã bị đóng băng bởi người kiểm duyệt nền tảng", - "initialising": "Khởi tạo nội dung bên ngoài" + "initialising": "Khởi tạo nội dung bên ngoài", + "label": "Nội dung bên ngoài", + "name": "URL nội dung bên ngoài" }, "failedToCopyLinkToClipboard": "Không thể sao chép liên kết vào khay nhớ tạm", "failedToCopyToClipboard": "Không thể sao chép {account}", @@ -613,7 +615,7 @@ "favourited_chat": "Đã đánh dấu cuộc trò chuyện đầu tiên của bạn là cuộc trò chuyện yêu thích", "followed_thread": "Lần đầu tiên theo dõi một chủ đề", "forwarded_message": "Đã chuyển tiếp tin nhắn đầu tiên", - "had_message_tipped": "Tin nhắn đầu tiên của bạn đã được tip", + "had_message_tipped": "Đã nhận được tin nhắn đầu tiên của bạn", "joined_call": "Đã tham gia cuộc gọi đầu tiên của bạn", "joined_community": "Đã tham gia cộng đồng đầu tiên của bạn", "joined_gated_group_or_community": "Đã tham gia nhóm/kênh kiểm soát đầu tiên của bạn", @@ -642,6 +644,7 @@ "sent_meme": "Đã gửi tin nhắn meme đầu tiên của bạn", "sent_poll": "Đã gửi cuộc thăm dò đầu tiên của bạn", "sent_prize": "Đã gửi giải thưởng đầu tiên của bạn", + "sent_reminder": "Đã gửi lời nhắc nhở đầu tiên của bạn", "sent_swap_offer": "Đã gửi ưu đãi hoán đổi P2P đầu tiên", "sent_text": "Đã gửi tin nhắn văn bản đầu tiên của bạn", "sent_video": "Đã gửi video đầu tiên của bạn", @@ -653,9 +656,11 @@ "set_group_access_gate": "Đặt cổng truy cập nhóm/kênh đầu tiên của bạn", "showChitPopup": "Thông báo cho tôi về thành tích của tôi", "started_call": "Đã bắt đầu cuộc gọi đầu tiên", + "streak_100": "Đã đạt được chuỗi 100 ngày", "streak_14": "Đạt được chuỗi 14 ngày", "streak_3": "Đạt được chuỗi 3 ngày", "streak_30": "Đạt được chuỗi 30 ngày", + "streak_365": "Đã đạt được chuỗi 365 ngày", "streak_7": "Đạt được chuỗi 7 ngày", "suggested_translation": "Đã đề xuất bản dịch đầu tiên của bạn", "swapped_from_wallet": "Hoán đổi token đầu tiên của bạn từ ví", diff --git a/frontend/openchat-agent/src/services/community/candid/idl.js b/frontend/openchat-agent/src/services/community/candid/idl.js index 22f1a5aec6..c9a9764b53 100644 --- a/frontend/openchat-agent/src/services/community/candid/idl.js +++ b/frontend/openchat-agent/src/services/community/candid/idl.js @@ -6,6 +6,7 @@ export const idlFactory = ({ IDL }) => { const AcceptP2PSwapArgs = IDL.Record({ 'pin' : IDL.Opt(IDL.Text), 'channel_id' : ChannelId, + 'new_achievement' : IDL.Bool, 'message_id' : MessageId, 'thread_root_message_index' : IDL.Opt(MessageIndex), }); @@ -1325,6 +1326,7 @@ export const idlFactory = ({ IDL }) => { }); const JoinVideoCallArgs = IDL.Record({ 'channel_id' : ChannelId, + 'new_achievement' : IDL.Bool, 'message_id' : MessageId, }); const JoinVideoCallResponse = IDL.Variant({ @@ -1393,6 +1395,7 @@ export const idlFactory = ({ IDL }) => { }); const RegisterPollVoteArgs = IDL.Record({ 'channel_id' : ChannelId, + 'new_achievement' : IDL.Bool, 'poll_option' : IDL.Nat32, 'operation' : VoteOperation, 'thread_root_message_index' : IDL.Opt(MessageIndex), @@ -1684,6 +1687,7 @@ export const idlFactory = ({ IDL }) => { 'RulesNotAccepted' : IDL.Null, }); const SetMemberDisplayNameArgs = IDL.Record({ + 'new_achievement' : IDL.Bool, 'display_name' : IDL.Opt(IDL.Text), }); const SetMemberDisplayNameResponse = IDL.Variant({ @@ -1702,6 +1706,7 @@ export const idlFactory = ({ IDL }) => { }); const SetVideoCallPresenceArgs = IDL.Record({ 'channel_id' : ChannelId, + 'new_achievement' : IDL.Bool, 'presence' : VideoCallPresence, 'message_id' : MessageId, }); diff --git a/frontend/openchat-agent/src/services/community/candid/types.d.ts b/frontend/openchat-agent/src/services/community/candid/types.d.ts index f367a9114f..3567c961fa 100644 --- a/frontend/openchat-agent/src/services/community/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/community/candid/types.d.ts @@ -5,6 +5,7 @@ import type { IDL } from '@dfinity/candid'; export interface AcceptP2PSwapArgs { 'pin' : [] | [string], 'channel_id' : ChannelId, + 'new_achievement' : boolean, 'message_id' : MessageId, 'thread_root_message_index' : [] | [MessageIndex], } @@ -65,6 +66,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | @@ -1380,6 +1382,7 @@ export type InviteCodeResponse = { 'NotAuthorized' : null } | { 'UserNotInCommunity' : null }; export interface JoinVideoCallArgs { 'channel_id' : ChannelId, + 'new_achievement' : boolean, 'message_id' : MessageId, } export type JoinVideoCallResponse = { 'AlreadyEnded' : null } | @@ -1832,6 +1835,7 @@ export interface PushEventResult { export type Reaction = string; export interface RegisterPollVoteArgs { 'channel_id' : ChannelId, + 'new_achievement' : boolean, 'poll_option' : number, 'operation' : VoteOperation, 'thread_root_message_index' : [] | [MessageIndex], @@ -2081,7 +2085,10 @@ export interface SendMessageSuccess { 'expires_at' : [] | [TimestampMillis], 'message_index' : MessageIndex, } -export interface SetMemberDisplayNameArgs { 'display_name' : [] | [string] } +export interface SetMemberDisplayNameArgs { + 'new_achievement' : boolean, + 'display_name' : [] | [string], +} export type SetMemberDisplayNameResponse = { 'DisplayNameInvalid' : null } | { 'Success' : null } | { 'DisplayNameTooLong' : number } | @@ -2091,6 +2098,7 @@ export type SetMemberDisplayNameResponse = { 'DisplayNameInvalid' : null } | { 'DisplayNameTooShort' : number }; export interface SetVideoCallPresenceArgs { 'channel_id' : ChannelId, + 'new_achievement' : boolean, 'presence' : VideoCallPresence, 'message_id' : MessageId, } diff --git a/frontend/openchat-agent/src/services/community/community.client.ts b/frontend/openchat-agent/src/services/community/community.client.ts index 7cd8e52569..10be0905ee 100644 --- a/frontend/openchat-agent/src/services/community/community.client.ts +++ b/frontend/openchat-agent/src/services/community/community.client.ts @@ -1028,7 +1028,8 @@ export class CommunityClient extends CandidService { messageIdx: number, answerIdx: number, voteType: "register" | "delete", - threadRootMessageIndex?: number, + threadRootMessageIndex: number | undefined, + newAchievement: boolean, ): Promise { return this.handleResponse( this.service.register_poll_vote({ @@ -1037,6 +1038,7 @@ export class CommunityClient extends CandidService { poll_option: answerIdx, operation: voteType === "register" ? { RegisterVote: null } : { DeleteVote: null }, message_index: messageIdx, + new_achievement: newAchievement, }), registerPollVoteResponse, ); @@ -1307,10 +1309,14 @@ export class CommunityClient extends CandidService { ); } - setMemberDisplayName(displayName: string | undefined): Promise { + setMemberDisplayName( + displayName: string | undefined, + newAchievement: boolean, + ): Promise { return this.handleResponse( this.service.set_member_display_name({ display_name: apiOptional(identity, displayName), + new_achievement: newAchievement, }), setMemberDisplayNameResponse, ); @@ -1362,6 +1368,7 @@ export class CommunityClient extends CandidService { threadRootMessageIndex: number | undefined, messageId: bigint, pin: string | undefined, + newAchievement: boolean, ): Promise { return this.handleResponse( this.service.accept_p2p_swap({ @@ -1369,6 +1376,7 @@ export class CommunityClient extends CandidService { thread_root_message_index: apiOptional(identity, threadRootMessageIndex), message_id: messageId, pin: apiOptional(identity, pin), + new_achievement: newAchievement, }), acceptP2PSwapResponse, ); @@ -1389,11 +1397,16 @@ export class CommunityClient extends CandidService { ); } - joinVideoCall(channelId: string, messageId: bigint): Promise { + joinVideoCall( + channelId: string, + messageId: bigint, + newAchievement: boolean, + ): Promise { return this.handleResponse( this.service.join_video_call({ message_id: messageId, channel_id: BigInt(channelId), + new_achievement: newAchievement, }), joinVideoCallResponse, ); @@ -1403,12 +1416,14 @@ export class CommunityClient extends CandidService { channelId: string, messageId: bigint, presence: VideoCallPresence, + newAchievement: boolean, ): Promise { return this.handleResponse( this.service.set_video_call_presence({ channel_id: BigInt(channelId), message_id: messageId, presence: apiVideoCallPresence(presence), + new_achievement: newAchievement, }), setVideoCallPresence, ); diff --git a/frontend/openchat-agent/src/services/group/candid/idl.js b/frontend/openchat-agent/src/services/group/candid/idl.js index d41bf33582..36db5018a2 100644 --- a/frontend/openchat-agent/src/services/group/candid/idl.js +++ b/frontend/openchat-agent/src/services/group/candid/idl.js @@ -4,6 +4,7 @@ export const idlFactory = ({ IDL }) => { const MessageIndex = IDL.Nat32; const AcceptP2PSwapArgs = IDL.Record({ 'pin' : IDL.Opt(IDL.Text), + 'new_achievement' : IDL.Bool, 'message_id' : MessageId, 'thread_root_message_index' : IDL.Opt(MessageIndex), }); @@ -969,7 +970,10 @@ export const idlFactory = ({ IDL }) => { 'NotAuthorized' : IDL.Null, 'Success' : IDL.Record({ 'code' : IDL.Opt(IDL.Nat64) }), }); - const JoinVideoCallArgs = IDL.Record({ 'message_id' : MessageId }); + const JoinVideoCallArgs = IDL.Record({ + 'new_achievement' : IDL.Bool, + 'message_id' : MessageId, + }); const JoinVideoCallResponse = IDL.Variant({ 'GroupFrozen' : IDL.Null, 'AlreadyEnded' : IDL.Null, @@ -1074,6 +1078,7 @@ export const idlFactory = ({ IDL }) => { 'DeleteVote' : IDL.Null, }); const RegisterPollVoteArgs = IDL.Record({ + 'new_achievement' : IDL.Bool, 'poll_option' : IDL.Nat32, 'operation' : VoteOperation, 'correlation_id' : IDL.Nat64, @@ -1290,6 +1295,7 @@ export const idlFactory = ({ IDL }) => { 'Owner' : IDL.Null, }); const SetVideoCallPresenceArgs = IDL.Record({ + 'new_achievement' : IDL.Bool, 'presence' : VideoCallPresence, 'message_id' : MessageId, }); diff --git a/frontend/openchat-agent/src/services/group/candid/types.d.ts b/frontend/openchat-agent/src/services/group/candid/types.d.ts index 6022bcb877..545e34bf50 100644 --- a/frontend/openchat-agent/src/services/group/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/group/candid/types.d.ts @@ -4,6 +4,7 @@ import type { IDL } from '@dfinity/candid'; export interface AcceptP2PSwapArgs { 'pin' : [] | [string], + 'new_achievement' : boolean, 'message_id' : MessageId, 'thread_root_message_index' : [] | [MessageIndex], } @@ -62,6 +63,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | @@ -1217,7 +1219,10 @@ export type InvalidPollReason = { 'DuplicateOptions' : null } | export type InviteCodeArgs = {}; export type InviteCodeResponse = { 'NotAuthorized' : null } | { 'Success' : { 'code' : [] | [bigint] } }; -export interface JoinVideoCallArgs { 'message_id' : MessageId } +export interface JoinVideoCallArgs { + 'new_achievement' : boolean, + 'message_id' : MessageId, +} export type JoinVideoCallResponse = { 'GroupFrozen' : null } | { 'AlreadyEnded' : null } | { 'UserNotInGroup' : null } | @@ -1663,6 +1668,7 @@ export interface PushEventResult { } export type Reaction = string; export interface RegisterPollVoteArgs { + 'new_achievement' : boolean, 'poll_option' : number, 'operation' : VoteOperation, 'correlation_id' : bigint, @@ -1846,6 +1852,7 @@ export interface SendMessageV2Args { 'thread_root_message_index' : [] | [MessageIndex], } export interface SetVideoCallPresenceArgs { + 'new_achievement' : boolean, 'presence' : VideoCallPresence, 'message_id' : MessageId, } diff --git a/frontend/openchat-agent/src/services/group/group.client.ts b/frontend/openchat-agent/src/services/group/group.client.ts index 88cd3fb127..ada95fdb5d 100644 --- a/frontend/openchat-agent/src/services/group/group.client.ts +++ b/frontend/openchat-agent/src/services/group/group.client.ts @@ -822,7 +822,8 @@ export class GroupClient extends CandidService { messageIdx: number, answerIdx: number, voteType: "register" | "delete", - threadRootMessageIndex?: number, + threadRootMessageIndex: number | undefined, + newAchievement: boolean, ): Promise { return this.handleResponse( this.groupService.register_poll_vote({ @@ -830,6 +831,7 @@ export class GroupClient extends CandidService { poll_option: answerIdx, operation: voteType === "register" ? { RegisterVote: null } : { DeleteVote: null }, message_index: messageIdx, + new_achievement: newAchievement, correlation_id: generateUint64(), }), registerPollVoteResponse, @@ -994,12 +996,14 @@ export class GroupClient extends CandidService { threadRootMessageIndex: number | undefined, messageId: bigint, pin: string | undefined, + newAchievement: boolean, ): Promise { return this.handleResponse( this.groupService.accept_p2p_swap({ thread_root_message_index: apiOptional(identity, threadRootMessageIndex), message_id: messageId, pin: apiOptional(identity, pin), + new_achievement: newAchievement, }), acceptP2PSwapResponse, ); @@ -1018,10 +1022,11 @@ export class GroupClient extends CandidService { ); } - joinVideoCall(messageId: bigint): Promise { + joinVideoCall(messageId: bigint, newAchievement: boolean): Promise { return this.handleResponse( this.groupService.join_video_call({ message_id: messageId, + new_achievement: newAchievement, }), joinVideoCallResponse, ); @@ -1030,11 +1035,13 @@ export class GroupClient extends CandidService { setVideoCallPresence( messageId: bigint, presence: VideoCallPresence, + newAchievement: boolean, ): Promise { return this.handleResponse( this.groupService.set_video_call_presence({ message_id: messageId, presence: apiVideoCallPresence(presence), + new_achievement: newAchievement, }), setVideoCallPresence, ); diff --git a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts index f78b429c9c..54acba4cf5 100644 --- a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts index 40dc64a9a8..5dabc1657c 100644 --- a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts @@ -51,6 +51,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts index cadda32352..d783060997 100644 --- a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/online/candid/types.d.ts b/frontend/openchat-agent/src/services/online/candid/types.d.ts index c1e23b852d..34a0fd9333 100644 --- a/frontend/openchat-agent/src/services/online/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/online/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/openchatAgent.ts b/frontend/openchat-agent/src/services/openchatAgent.ts index 8f615844ac..5ce74e845a 100644 --- a/frontend/openchat-agent/src/services/openchatAgent.ts +++ b/frontend/openchat-agent/src/services/openchatAgent.ts @@ -2570,7 +2570,8 @@ export class OpenChatAgent extends EventTarget { messageIdx: number, answerIdx: number, voteType: "register" | "delete", - threadRootMessageIndex?: number, + threadRootMessageIndex: number | undefined, + newAchievement: boolean, ): Promise { if (offline()) return Promise.resolve("offline"); @@ -2581,6 +2582,7 @@ export class OpenChatAgent extends EventTarget { answerIdx, voteType, threadRootMessageIndex, + newAchievement, ); case "channel": return this.communityClient(chatId.communityId).registerPollVote( @@ -2589,6 +2591,7 @@ export class OpenChatAgent extends EventTarget { answerIdx, voteType, threadRootMessageIndex, + newAchievement, ); } } @@ -3132,10 +3135,11 @@ export class OpenChatAgent extends EventTarget { setMemberDisplayName( communityId: string, display_name: string | undefined, + newAchievement: boolean, ): Promise { if (offline()) return Promise.resolve("offline"); - return this.communityClient(communityId).setMemberDisplayName(display_name); + return this.communityClient(communityId).setMemberDisplayName(display_name, newAchievement); } deleteUserGroups( @@ -3364,6 +3368,7 @@ export class OpenChatAgent extends EventTarget { threadRootMessageIndex: number | undefined, messageId: bigint, pin: string | undefined, + newAchievement: boolean, ): Promise { if (chatId.kind === "channel") { return this.communityClient(chatId.communityId).acceptP2PSwap( @@ -3371,12 +3376,14 @@ export class OpenChatAgent extends EventTarget { threadRootMessageIndex, messageId, pin, + newAchievement, ); } else if (chatId.kind === "group_chat") { return this.getGroupClient(chatId.groupId).acceptP2PSwap( threadRootMessageIndex, messageId, pin, + newAchievement, ); } else { return this.userClient.acceptP2PSwap( @@ -3429,14 +3436,19 @@ export class OpenChatAgent extends EventTarget { } } - joinVideoCall(chatId: ChatIdentifier, messageId: bigint): Promise { + joinVideoCall( + chatId: ChatIdentifier, + messageId: bigint, + newAchievement: boolean, + ): Promise { if (chatId.kind === "channel") { return this.communityClient(chatId.communityId).joinVideoCall( chatId.channelId, messageId, + newAchievement, ); } else if (chatId.kind === "group_chat") { - return this.getGroupClient(chatId.groupId).joinVideoCall(messageId); + return this.getGroupClient(chatId.groupId).joinVideoCall(messageId, newAchievement); } else { return this.userClient.joinVideoCall(chatId.userId, messageId); } @@ -3446,6 +3458,7 @@ export class OpenChatAgent extends EventTarget { chatId: MultiUserChatIdentifier, messageId: bigint, presence: VideoCallPresence, + newAchievement: boolean, ): Promise { switch (chatId.kind) { case "channel": @@ -3453,11 +3466,13 @@ export class OpenChatAgent extends EventTarget { chatId.channelId, messageId, presence, + newAchievement, ); case "group_chat": return this.getGroupClient(chatId.groupId).setVideoCallPresence( messageId, presence, + newAchievement, ); } } diff --git a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts index 1c2c7babbf..d74508619e 100644 --- a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/registry/candid/types.d.ts b/frontend/openchat-agent/src/services/registry/candid/types.d.ts index 617c73b95b..8efa2cf042 100644 --- a/frontend/openchat-agent/src/services/registry/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/registry/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts index 6f5cc8bdaf..f8c156e447 100644 --- a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts index 97d99764d2..390e18666d 100644 --- a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/translations/candid/types.d.ts b/frontend/openchat-agent/src/services/translations/candid/types.d.ts index a8052cc74a..4cae3ba41c 100644 --- a/frontend/openchat-agent/src/services/translations/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/translations/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/user/candid/idl.js b/frontend/openchat-agent/src/services/user/candid/idl.js index 1c0e8d154a..aa1c6fa9e7 100644 --- a/frontend/openchat-agent/src/services/user/candid/idl.js +++ b/frontend/openchat-agent/src/services/user/candid/idl.js @@ -174,6 +174,7 @@ export const idlFactory = ({ IDL }) => { 'StartedCall' : IDL.Null, 'ChosenAsGroupOwner' : IDL.Null, 'TippedMessage' : IDL.Null, + 'Streak100' : IDL.Null, 'Streak365' : IDL.Null, 'SentGiphy' : IDL.Null, 'SetCommunityAccessGate' : IDL.Null, diff --git a/frontend/openchat-agent/src/services/user/candid/types.d.ts b/frontend/openchat-agent/src/services/user/candid/types.d.ts index 390daa5176..43f0f16463 100644 --- a/frontend/openchat-agent/src/services/user/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/user/candid/types.d.ts @@ -62,6 +62,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-agent/src/services/user/mappers.ts b/frontend/openchat-agent/src/services/user/mappers.ts index 09e1390657..849571741b 100644 --- a/frontend/openchat-agent/src/services/user/mappers.ts +++ b/frontend/openchat-agent/src/services/user/mappers.ts @@ -393,6 +393,9 @@ export function achievementType(candid: ApiAchievement): Achievement { if ("Streak7" in candid) { return "streak_7"; } + if ("Streak100" in candid) { + return "streak_100"; + } if ("Streak365" in candid) { return "streak_365"; } diff --git a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts index 69c7d2a9c8..3f93baa3be 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak100' : null } | { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | diff --git a/frontend/openchat-client/src/openchat.ts b/frontend/openchat-client/src/openchat.ts index d718c7aece..7ef91c1626 100644 --- a/frontend/openchat-client/src/openchat.ts +++ b/frontend/openchat-client/src/openchat.ts @@ -1539,10 +1539,15 @@ export class OpenChat extends OpenChatAgentWorker { id: CommunityIdentifier, displayName: string | undefined, ): Promise { + const newAchievement = !this._liveState.globalState.achievements.has( + "set_community_display_name", + ); + return this.sendRequest({ kind: "setMemberDisplayName", communityId: id.communityId, displayName, + newAchievement, }).then((resp) => { if (resp === "success") { communityStateStore.updateProp(id, "members", (ms) => { @@ -1972,6 +1977,8 @@ export class OpenChat extends OpenChatAgentWorker { userId, }); + const newAchievement = !this._liveState.globalState.achievements.has("voted_on_poll"); + return this.sendRequest({ kind: "registerPollVote", chatId, @@ -1979,6 +1986,7 @@ export class OpenChat extends OpenChatAgentWorker { answerIdx, voteType: type, threadRootMessageIndex, + newAchievement, }) .then((resp) => resp === "success") .catch(() => false); @@ -5759,12 +5767,15 @@ export class OpenChat extends OpenChatAgentWorker { reservedBy: this._liveState.user.userId, }); + const newAchievement = !this._liveState.globalState.achievements.has("accepted_swap_offer"); + return this.sendRequest({ kind: "acceptP2PSwap", chatId, threadRootMessageIndex, messageId, pin, + newAchievement, }) .then((resp) => { localMessageUpdates.setP2PSwapStatus( @@ -5816,10 +5827,13 @@ export class OpenChat extends OpenChatAgentWorker { } joinVideoCall(chatId: ChatIdentifier, messageId: bigint): Promise { + const newAchievement = !this._liveState.globalState.achievements.has("joined_call"); + return this.sendRequest({ kind: "joinVideoCall", chatId, messageId, + newAchievement, }); } @@ -5828,11 +5842,14 @@ export class OpenChat extends OpenChatAgentWorker { messageId: bigint, presence: VideoCallPresence, ): Promise { + const newAchievement = !this._liveState.globalState.achievements.has("joined_call"); + return this.sendRequest({ kind: "setVideoCallPresence", chatId, messageId, presence, + newAchievement, }) .then((resp) => resp === "success") .catch(() => false); diff --git a/frontend/openchat-shared/src/domain/chit.ts b/frontend/openchat-shared/src/domain/chit.ts index 777d9cf930..0725f5f976 100644 --- a/frontend/openchat-shared/src/domain/chit.ts +++ b/frontend/openchat-shared/src/domain/chit.ts @@ -57,6 +57,7 @@ export const achievements = [ "streak_7", "streak_14", "streak_30", + "streak_100", "streak_365", "sent_reminder", "proved_unique_personhood", diff --git a/frontend/openchat-shared/src/domain/worker.ts b/frontend/openchat-shared/src/domain/worker.ts index fc5755c706..d90e03d074 100644 --- a/frontend/openchat-shared/src/domain/worker.ts +++ b/frontend/openchat-shared/src/domain/worker.ts @@ -418,6 +418,7 @@ type SetVideoCallPresence = { chatId: MultiUserChatIdentifier; messageId: bigint; presence: VideoCallPresence; + newAchievement: boolean; }; type GetLocalUserIndexForUser = { @@ -436,6 +437,7 @@ type JoinVideoCall = { kind: "joinVideoCall"; chatId: ChatIdentifier; messageId: bigint; + newAchievement: boolean; }; type ProposeTranslation = { @@ -891,7 +893,8 @@ type RegisterPollVote = { messageIdx: number; answerIdx: number; voteType: "register" | "delete"; - threadRootMessageIndex?: number; + threadRootMessageIndex: number | undefined; + newAchievement: boolean; kind: "registerPollVote"; }; @@ -1670,6 +1673,7 @@ type UpdateRegistry = { type SetMemberDisplayName = { communityId: string; displayName: string | undefined; + newAchievement: boolean; kind: "setMemberDisplayName"; }; @@ -1714,6 +1718,7 @@ type AcceptP2PSwap = { threadRootMessageIndex: number | undefined; messageId: bigint; pin: string | undefined; + newAchievement: boolean; kind: "acceptP2PSwap"; }; diff --git a/frontend/openchat-worker/src/worker.ts b/frontend/openchat-worker/src/worker.ts index 9507347959..1e852ee982 100644 --- a/frontend/openchat-worker/src/worker.ts +++ b/frontend/openchat-worker/src/worker.ts @@ -538,6 +538,7 @@ self.addEventListener("message", (msg: MessageEvent) => payload.answerIdx, payload.voteType, payload.threadRootMessageIndex, + payload.newAchievement, ), ); break; @@ -1450,7 +1451,11 @@ self.addEventListener("message", (msg: MessageEvent) => executeThenReply( payload, correlationId, - agent.setMemberDisplayName(payload.communityId, payload.displayName), + agent.setMemberDisplayName( + payload.communityId, + payload.displayName, + payload.newAchievement, + ), ); break; @@ -1628,6 +1633,7 @@ self.addEventListener("message", (msg: MessageEvent) => payload.threadRootMessageIndex, payload.messageId, payload.pin, + payload.newAchievement, ), ); break; @@ -1648,7 +1654,7 @@ self.addEventListener("message", (msg: MessageEvent) => executeThenReply( payload, correlationId, - agent.joinVideoCall(payload.chatId, payload.messageId), + agent.joinVideoCall(payload.chatId, payload.messageId, payload.newAchievement), ); break; @@ -1668,7 +1674,12 @@ self.addEventListener("message", (msg: MessageEvent) => executeThenReply( payload, correlationId, - agent.setVideoCallPresence(payload.chatId, payload.messageId, payload.presence), + agent.setVideoCallPresence( + payload.chatId, + payload.messageId, + payload.presence, + payload.newAchievement, + ), ); break;