From d0d3a2ea58d8d9e080b312491a82714ade9ba24a Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Wed, 11 Sep 2024 23:55:10 +0800 Subject: [PATCH] feat: update user profile --- .../messages/ChannelCreateModel.svelte | 11 +- .../messages/ChannelMessages.svelte | 25 +- .../src/lib/components/messages/Chat.svelte | 4 +- .../components/messages/ProfileDetail.svelte | 265 ++++++++++++++++-- .../src/lib/stores/message.ts | 84 +++--- 5 files changed, 315 insertions(+), 74 deletions(-) diff --git a/src/ic_panda_frontend/src/lib/components/messages/ChannelCreateModel.svelte b/src/ic_panda_frontend/src/lib/components/messages/ChannelCreateModel.svelte index 644f56d..8dd86be 100644 --- a/src/ic_panda_frontend/src/lib/components/messages/ChannelCreateModel.svelte +++ b/src/ic_panda_frontend/src/lib/components/messages/ChannelCreateModel.svelte @@ -24,6 +24,7 @@ // Props /** Exposes parent props to this component. */ export let parent: SvelteComponent + export let channelName: string = '' export let add_managers: [Principal, Uint8Array | null][] = [] const modalStore = getModalStore() @@ -35,14 +36,14 @@ let stateInfo: Readable let myInfo: Readable - let validating = false - let submitting = false - let availablePandaBalance = 0n - - let nameInput = '' + let nameInput = channelName let descriptionInput = '' let amount = 0n + let validating = nameInput.trim() !== '' + let submitting = false + let availablePandaBalance = 0n + function checkInput() { const name = nameInput.trim() if (!name) { diff --git a/src/ic_panda_frontend/src/lib/components/messages/ChannelMessages.svelte b/src/ic_panda_frontend/src/lib/components/messages/ChannelMessages.svelte index e75585e..06daa0d 100644 --- a/src/ic_panda_frontend/src/lib/components/messages/ChannelMessages.svelte +++ b/src/ic_panda_frontend/src/lib/components/messages/ChannelMessages.svelte @@ -182,12 +182,14 @@ start, start + 20 ) - let last = 0 + if (messages.length > 0) { - last = messages.at(-1)!.id addMessageInfos(messages) } + bottomLoading = false + await tick() + const last = $messageFeed.at(-1)?.id || latestMessageId if (last >= latestMessageId && !$latestMessage) { latestMessageId = last @@ -210,6 +212,9 @@ messageStart = channelInfo.message_start latestMessageId = channelInfo.latest_message_id lastRead = channelInfo.my_setting.last_read + if (!lastRead) { + lastRead = latestMessageId + } channelAPI = await myState.api.channelAPI(canister) dek = await myState.decryptChannelDEK(channelInfo) await loadPrevMessages(messageStart, lastRead + 1) @@ -217,6 +222,11 @@ scrollIntoView(lastRead) await loadNextMessages(lastRead + 1) + // no scroll + if (elemChat.scrollTop == 0) { + lastRead = $messageFeed.at(-1)!.id + debouncedUpdateMyLastRead() + } } else { goto('/_/messages') } @@ -242,6 +252,7 @@ inMoveUpViewport: (els) => { const [_canister, _channel, mid] = els.at(-1)!.id.split(':') const messageId = parseInt(mid || '') + if (messageId > lastRead) { lastRead = messageId myState.freshMyChannelSetting(canister, id, { last_read: messageId }) @@ -266,6 +277,12 @@ if (info) { latestMessageId = info.id addMessageInfos([info]) + tick().then(() => { + if (elemChat.scrollTop == 0) { + lastRead = $messageFeed.at(-1)!.id + debouncedUpdateMyLastRead() + } + }) } } @@ -280,7 +297,7 @@ >
@@ -358,7 +375,7 @@ {/each}
diff --git a/src/ic_panda_frontend/src/lib/components/messages/Chat.svelte b/src/ic_panda_frontend/src/lib/components/messages/Chat.svelte index 7fa5b07..f105380 100644 --- a/src/ic_panda_frontend/src/lib/components/messages/Chat.svelte +++ b/src/ic_panda_frontend/src/lib/components/messages/Chat.svelte @@ -23,7 +23,7 @@ async function onChatBack() { channelsListActive = true - await sleep(400) + await sleep(300) goto('/_/messages') } @@ -40,7 +40,7 @@ class="relative h-full w-full sm:grid sm:grid-cols-[220px_1fr] md:grid-cols-[280px_1fr] lg:grid-cols-[300px_1fr]" >
diff --git a/src/ic_panda_frontend/src/lib/components/messages/ProfileDetail.svelte b/src/ic_panda_frontend/src/lib/components/messages/ProfileDetail.svelte index 1a48c04..fd7eedf 100644 --- a/src/ic_panda_frontend/src/lib/components/messages/ProfileDetail.svelte +++ b/src/ic_panda_frontend/src/lib/components/messages/ProfileDetail.svelte @@ -2,18 +2,29 @@ import { goto } from '$app/navigation' import { type UserInfo } from '$lib/canisters/message' import { type ProfileInfo } from '$lib/canisters/messageprofile' + import IconCircleSpin from '$lib/components/icons/IconCircleSpin.svelte' import IconEditLine from '$lib/components/icons/IconEditLine.svelte' import Loading from '$lib/components/ui/Loading.svelte' + import { signIn } from '$lib/services/auth' import { toastRun } from '$lib/stores/toast' import { md } from '$lib/utils/markdown' import { myMessageStateAsync, + toDisplayUserInfo, + type DisplayUserInfo, type MyMessageState } from '$src/lib/stores/message' + import { unwrapOption } from '$src/lib/types/result' import { Principal } from '@dfinity/principal' import { Avatar, getModalStore, getToastStore } from '@skeletonlabs/skeleton' import { onMount } from 'svelte' - import { type Readable } from 'svelte/store' + import { + readable, + writable, + type Readable, + type Writable + } from 'svelte/store' + import ChannelCreateModel from './ChannelCreateModel.svelte' import ProfileEditModel from './ProfileEditModel.svelte' import UserRegisterModel from './UserRegisterModel.svelte' @@ -21,10 +32,23 @@ const toastStore = getToastStore() const modalStore = getModalStore() + const myFollowing: Writable = writable([]) let myID: Principal let myState: MyMessageState let userInfo: Readable + let myInfo: (UserInfo & ProfileInfo) | null = null + + async function loadMyFollowing() { + const res: DisplayUserInfo[] = [] + if (myInfo) { + const users = await myState.batchLoadUsersInfo( + myInfo.following.at(0) || [] + ) + res.push(...users.map(toDisplayUserInfo)) + } + myFollowing.set(res) + } async function loadProfile(user: Principal | string) { myState = await myMessageStateAsync() @@ -34,7 +58,8 @@ (typeof user === 'string' && user === myState.id) || (user instanceof Principal && user.compareTo(myID) == 'eq') ) { - userInfo = await myState.loadMyProfile() + myInfo = await myState.loadMyProfile() + userInfo = readable(myInfo) } else { userInfo = await myState.loadProfile(user) } @@ -49,7 +74,7 @@ const bio = profile.bio.trim() if (bio !== $userInfo.bio) { - await myState.updateProfile($userInfo.profile_canister, { + myInfo = await myState.updateProfile({ bio: [bio], remove_channels: [], upsert_channels: [], @@ -57,8 +82,6 @@ unfollow: [] }) } - - userInfo = await myState.loadMyProfile() } function onMeHandler() { @@ -84,46 +107,151 @@ } } + let followingSubmitting = '' + function onFollowHandler(user: Principal, fowllowing: boolean = true) { + toastRun(async () => { + if (myState?.principal.isAnonymous()) { + const res = await signIn({}) + myState = await myMessageStateAsync() + myInfo = await myState.loadMyProfile().catch(() => null) + if (!myInfo && res.success == 'ok') { + modalStore.trigger({ + type: 'component', + component: { + ref: UserRegisterModel, + props: {} + } + }) + } + } else if (!myInfo) { + modalStore.trigger({ + type: 'component', + component: { + ref: UserRegisterModel, + props: {} + } + }) + } else if (!followingSubmitting) { + followingSubmitting = user.toText() + if (fowllowing) { + myInfo = await myState.updateProfile({ + bio: [], + remove_channels: [], + upsert_channels: [], + follow: [user], + unfollow: [] + }) + } else { + myInfo = await myState.updateProfile({ + bio: [], + remove_channels: [], + upsert_channels: [], + follow: [], + unfollow: [user] + }) + } + await loadMyFollowing() + } + }, toastStore).finally(() => { + followingSubmitting = '' + }) + } + + async function onCreateChannelHandler(id: Principal) { + if (myState?.principal.isAnonymous()) { + const res = await signIn({}) + myState = await myMessageStateAsync() + myInfo = await myState.loadMyProfile().catch(() => null) + if (!myInfo && res.success == 'ok') { + modalStore.trigger({ + type: 'component', + component: { + ref: UserRegisterModel, + props: {} + } + }) + } + } else if (!myInfo) { + modalStore.trigger({ + type: 'component', + component: { + ref: UserRegisterModel, + props: {} + } + }) + } else { + // try to fetch the latest ecdh public key + const user = await myState.tryFetchProfile(id) + if (!user) { + return + } + + modalStore.trigger({ + type: 'component', + component: { + ref: ChannelCreateModel, + props: { + channelName: `${user.name}, ${myInfo!.name}`, + add_managers: [ + [user.id, unwrapOption(user.ecdh_pub as [] | [Uint8Array])] + ] + } + } + }) + } + } + onMount(() => { const { abort, finally: onfinally } = toastRun( () => loadProfile(userId), toastStore ) - onfinally((info) => { + onfinally(async (info) => { if (!info) { setTimeout(() => goto('/_/messages'), 2000) - } else if ( - info.id.compareTo(myID) == 'eq' && - info.username.length == 0 && - info.created_at < BigInt(Date.now() - 180 * 1000) - ) { - modalStore.trigger({ - type: 'component', - component: { - ref: UserRegisterModel, - props: {} + } else { + if (info.id.compareTo(myID) == 'eq') { + if ( + info.username.length == 0 && + info.created_at < BigInt(Date.now() - 180 * 1000) + ) { + modalStore.trigger({ + type: 'component', + component: { + ref: UserRegisterModel, + props: {} + } + }) } - }) + await loadMyFollowing() + } else { + myInfo = await myState.loadMyProfile().catch(() => null) + } } }) return abort }) + + $: isMe = $userInfo?.id.compareTo(myID) == 'eq' + $: isFowllowing = + myInfo?.following.at(0)?.some((id) => id.compareTo($userInfo.id) == 'eq') || + false {#if $userInfo}

{$userInfo.name} - {#if $userInfo.id.compareTo(myID) == 'eq'} + {#if isMe} + +

+ {/if} + {#if isMe && $myFollowing.length > 0} +
+
+ Following +
+
+ {#each $myFollowing as member (member._id)} +
+
+ + {member.name} + {#if member.username} + @{member.username} + {/if} +
+
+ + +
+
+ {/each} +
+
+ {/if} {:else}
{/if} + + diff --git a/src/ic_panda_frontend/src/lib/stores/message.ts b/src/ic_panda_frontend/src/lib/stores/message.ts index 610f66a..ba473ef 100644 --- a/src/ic_panda_frontend/src/lib/stores/message.ts +++ b/src/ic_panda_frontend/src/lib/stores/message.ts @@ -153,30 +153,43 @@ export class MyMessageState { private _coseAPI: CoseAPI | null = null private _mks: MasterKey[] = [] private _ek: ECDHKey | null = null + private _myInfo: UserInfo | null = null + private _myProfile: ProfileInfo | null = null private _myChannels = new Map() // keep the latest channel setting private _myChannelsStream = writable([]) private _channelDEKs = new Map() static async with(identity: Identity): Promise { const api = await messageCanisterAPIAsync() - let coseAPI: CoseAPI | null = null + const self = new MyMessageState(identity.getPrincipal(), api) + const now = Date.now() + self._myInfo = api.myInfo + if (!self._myInfo) { + self._myInfo = await self.getCacheUserInfo(now, self.principal) + } + if (self._myInfo) { + await self.setCacheUserInfo(now, self._myInfo) + + const profileAPI = await api.profileAPI(self._myInfo.profile_canister) + await profileAPI.refreshMyProfile() + self._myProfile = profileAPI.myProfile + if (!self._myProfile) { + self._myProfile = await KVS.get('My', `${self.id}:Profile`) + } else { + await KVS.set('My', self._myProfile, `${self.id}:Profile`) + } - if (api.myInfo?.cose_canister.length == 1) { - coseAPI = await api.coseAPI(api.myInfo.cose_canister[0]) + if (self._myInfo.cose_canister.length == 1) { + self._coseAPI = await api.coseAPI(self._myInfo.cose_canister[0]) + } } - - return new MyMessageState(identity.getPrincipal(), api, coseAPI) + return self } - constructor( - principal: Principal, - api: MessageCanisterAPI, - coseAPI: CoseAPI | null - ) { + constructor(principal: Principal, api: MessageCanisterAPI) { this.principal = principal this.id = principal.toText() this.api = api - this._coseAPI = coseAPI this.info = derived(api.myInfoStore, ($info, set) => { if ($info) { this.setCacheUserInfo(Date.now(), $info) @@ -278,11 +291,10 @@ export class MyMessageState { if (!state) { throw new Error('Message state not ready') } - const myInfo = await this.api.myInfo - if (!myInfo) { + if (!this._myInfo) { throw new Error('User info not ready') } - const cose_canister = myInfo.cose_canister[0] + const cose_canister = this._myInfo.cose_canister[0] if (!cose_canister) { throw new Error('username not ready') } @@ -860,26 +872,15 @@ export class MyMessageState { ) } - async loadMyProfile(): Promise> { - const now = Date.now() - let info = await this.getCacheUserInfo(now, this.principal) - if (!info) { - info = await this.api.get_user(this.principal) - await this.setCacheUserInfo(now, info) + async loadMyProfile(): Promise { + if (!this._myInfo) { + throw new Error('My user info not ready') + } + if (!this._myProfile) { + throw new Error('My profile info not ready') } - const profile = await KVS.get('My', `${this.id}:Profile`) - - const api = await this.api.profileAPI(info.profile_canister) - return readable( - { ...info, ...profile } as UserInfo & ProfileInfo, - (set) => { - api.get_profile(info.id).then(async (profile) => { - await KVS.set('My', profile, `${this.id}:Profile`) - set({ ...info, ...profile }) - }) - } - ) + return { ...this._myInfo, ...this._myProfile } } async loadProfile( @@ -965,12 +966,16 @@ export class MyMessageState { } async updateProfile( - profile_canister: Principal, input: UpdateProfileInput - ): Promise { - const api = await this.api.profileAPI(profile_canister) - const profile = await api.update_profile(input) - await KVS.set('Profiles', profile) + ): Promise { + if (!this._myInfo) { + throw new Error('My user info not ready') + } + + const api = await this.api.profileAPI(this._myInfo.profile_canister) + this._myProfile = await api.update_profile(input) + await KVS.set('My', this._myProfile, `${this.id}:Profile`) + return { ...this._myInfo, ...this._myProfile } } async loadChannelInfo( @@ -1103,7 +1108,7 @@ export class MyMessageState { timer = !stopped && setTimeout(task, 3000) } else { - timer = !stopped && setTimeout(task, 9000) + timer = !stopped && setTimeout(task, 7000) } }) } @@ -1137,8 +1142,6 @@ export class MyMessageState { start = end - 20 } - console.log('loadMessages', start, end) - let messages: MessageCacheInfo[] = [] const iter = await KVS.iterate( 'Messages', @@ -1155,7 +1158,6 @@ export class MyMessageState { } if (i < end) { - console.log('loadMessages fetch', i, end) let items = (await api.list_messages( channelId, i,