Skip to content

Commit

Permalink
fix: search local contacts (#10894)
Browse files Browse the repository at this point in the history
* fix: add search local social account

* fix: search loading

* fix: add useMemo dep

* chore: code review

* chore: code review

* refactor: code style (#10895)

---------

Co-authored-by: guanbinrui <52657989+guanbinrui@users.noreply.github.com>
  • Loading branch information
beyond009 and guanbinrui committed Sep 28, 2023
1 parent 738e262 commit 8fd1857
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 57 deletions.
5 changes: 5 additions & 0 deletions packages/mask/background/services/identity/profile/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ProfileRecord,
queryPersonasDB,
queryProfilesDB,
queryProfileDB,
} from '../../../database/persona/db.js'
import { hasLocalKeyOf } from '../../../database/persona/helper.js'
import { toProfileInformation } from '../../__utils__/convert.js'
Expand All @@ -17,6 +18,10 @@ export async function queryProfilesInformation(identifiers: ProfileIdentifier[])
return toProfileInformation(profiles).mustNotAwaitThisWithInATransaction
}

export async function queryProfileInformation(identifier: ProfileIdentifier): Promise<ProfileRecord | null> {
return await queryProfileDB(identifier)
}

/** @deprecated */
export async function hasLocalKey(identifier: ProfileIdentifier) {
return hasLocalKeyOf(identifier)
Expand Down
48 changes: 33 additions & 15 deletions packages/mask/src/extension/popups/hooks/useFriends.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { isProfileIdentifier } from '@masknet/shared'
import { EMPTY_LIST, type BindingProof, type ECKeyIdentifier, type ProfileIdentifier } from '@masknet/shared-base'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { first } from 'lodash-es'
import { useCallback } from 'react'
import { useCurrentPersona } from '../../../components/DataSource/useCurrentPersona.js'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { isProfileIdentifier } from '@masknet/shared'
import { EMPTY_LIST, type ECKeyIdentifier, type ProfileIdentifier } from '@masknet/shared-base'
import Services from '#services'
import { useCurrentPersona } from '../../../components/DataSource/useCurrentPersona.js'
import { type RelationRecord } from '../../../../background/database/persona/type.js'

export type FriendsInformation = Friend & {
profiles: BindingProof[]
id: string
}

export type Friend = {
export interface Friend {
persona: ECKeyIdentifier
profile?: ProfileIdentifier
avatar?: string
Expand Down Expand Up @@ -61,12 +56,11 @@ export function useFriendsPaged() {
if (friends.length === 10) break
const x = records[i]
if (isProfileIdentifier(x.profile)) {
const res = first(await Services.Identity.queryProfilesInformation([x.profile]))
if (res?.linkedPersona !== undefined && res?.linkedPersona !== currentPersona?.identifier)
const profile = await Services.Identity.queryProfileInformation(x.profile)
if (profile?.linkedPersona && profile.linkedPersona !== currentPersona?.identifier)
friends.push({
persona: res.linkedPersona,
persona: profile.linkedPersona,
profile: x.profile,
avatar: res.avatar,
})
} else {
if (x.profile !== currentPersona?.identifier) friends.push({ persona: x.profile })
Expand All @@ -93,5 +87,29 @@ export function useFriendsPaged() {
refetch,
status,
fetchRelationStatus,
records,
}
}

export function useFriendFromList(searchedRecords: RelationRecord[]) {
const currentPersona = useCurrentPersona()
return useQuery(['search-local', searchedRecords], async () => {
return (
await Promise.all(
searchedRecords.map<Promise<Friend | undefined>>(async (x) => {
if (!isProfileIdentifier(x.profile)) return
const profile = await Services.Identity.queryProfileInformation(x.profile)
if (
profile?.linkedPersona !== undefined &&
profile?.linkedPersona.publicKeyAsHex !== currentPersona?.identifier.publicKeyAsHex
)
return {
persona: profile.linkedPersona,
profile: x.profile,
}
return
}),
)
).filter((x): x is Friend => typeof x !== 'undefined' && Object.hasOwn(x, 'persona'))
})
}
84 changes: 57 additions & 27 deletions packages/mask/src/extension/popups/hooks/useFriendsFromSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
import { ECKeyIdentifier, EMPTY_LIST, type NextIDPersonaBindings } from '@masknet/shared-base'
import { uniqBy } from 'lodash-es'
import { useMemo } from 'react'
import type { Friend } from './useFriends.js'
import { uniqBy } from 'lodash-es'
import { ECKeyIdentifier, EMPTY_LIST, type NextIDPersonaBindings } from '@masknet/shared-base'
import { useCurrentLinkedPersona } from '@masknet/shared'
import type { Friend } from './useFriends.js'
import { profilesFilter } from './useFriendProfiles.js'
import { PlatformSort } from '../pages/Friends/common.js'
import { PlatformSort, type FriendNetwork, type Profile } from '../pages/Friends/common.js'

export type NextIDPersonaBindingsWithIdentifier = NextIDPersonaBindings & { linkedPersona: ECKeyIdentifier } & {
export type NextIDPersonaBindingsWithIdentifier = Omit<NextIDPersonaBindings, 'proofs'> & { proofs: Profile[] } & {
linkedPersona: ECKeyIdentifier
} & {
isLocal?: boolean
}

export function useFriendsFromSearch(
localSearchedResult: Friend[],
searchResult?: NextIDPersonaBindings[],
localList?: Friend[],
searchValue?: string,
): NextIDPersonaBindingsWithIdentifier[] {
const currentIdentifier = useCurrentLinkedPersona()
return useMemo(() => {
if (!searchResult?.length) return EMPTY_LIST
if (!searchResult?.length && !localSearchedResult?.length) return EMPTY_LIST
const localProfiles: NextIDPersonaBindingsWithIdentifier[] =
localSearchedResult
?.filter((x) => x.persona.publicKeyAsHex !== currentIdentifier?.identifier.publicKeyAsHex && x.profile)
.map((item) => {
const profile = item.profile!
return {
proofs: [
{
platform: profile.network as FriendNetwork,
identity: profile.userId,
is_valid: true,
last_checked_at: '',
name: profile.userId,
created_at: '',
},
],
linkedPersona: item.persona,
activated_at: '',
persona: item.persona.publicKeyAsHex,
isLocal: true,
}
}) ?? EMPTY_LIST
const profiles: NextIDPersonaBindingsWithIdentifier[] = searchResult
.filter((x) => x.persona !== currentIdentifier?.identifier.publicKeyAsHex)
.map((item) => {
const filtered = item.proofs.filter(profilesFilter)
const identifier = ECKeyIdentifier.fromHexPublicKeyK256(item.persona).expect(
`${item.persona} should be a valid hex public key in k256`,
)
filtered.sort((a, b) => PlatformSort[a.platform] - PlatformSort[b.platform])
const searchItem = filtered.findIndex((x) => x.identity === searchValue || x.name === searchValue)
if (searchItem !== -1) filtered.unshift(filtered.splice(searchItem, 1)[0])
return {
proofs: uniqBy(filtered, ({ identity }) => identity),
linkedPersona: identifier,
activated_at: item.activated_at,
persona: item.persona,
isLocal: localList
? localList.some((x) => x.persona.publicKeyAsHex === identifier.publicKeyAsHex)
: false,
}
})
return uniqBy(profiles, ({ linkedPersona }) => linkedPersona.publicKeyAsHex)
}, [searchResult, localList, currentIdentifier])
? searchResult
.filter((x) => x.persona !== currentIdentifier?.identifier.publicKeyAsHex)
.map((item) => {
const filtered = item.proofs.filter(profilesFilter)
const identifier = ECKeyIdentifier.fromHexPublicKeyK256(item.persona).expect(
`${item.persona} should be a valid hex public key in k256`,
)
filtered.sort((a, b) => PlatformSort[a.platform] - PlatformSort[b.platform])
const searchItem = filtered.findIndex((x) => x.identity === searchValue || x.name === searchValue)
if (searchItem !== -1) filtered.unshift(filtered.splice(searchItem, 1)[0])
return {
proofs: uniqBy(filtered, ({ identity }) => identity),
linkedPersona: identifier,
activated_at: item.activated_at,
persona: item.persona,
isLocal: localList
? localList.some((x) => x.persona.publicKeyAsHex === identifier.publicKeyAsHex)
: false,
}
})
: EMPTY_LIST
return uniqBy(
localProfiles ? localProfiles.concat(profiles) : profiles,
({ linkedPersona }) => linkedPersona.publicKeyAsHex,
)
}, [searchResult, localList, currentIdentifier, localSearchedResult])
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import { ActionButton, makeStyles, usePopupCustomSnackbar } from '@masknet/theme
import { Box, Typography, Link, useTheme, ButtonBase as Button, Avatar } from '@mui/material'
import {
formatPersonaFingerprint,
type BindingProof,
PopupRoutes,
ProfileIdentifier,
ECKeyIdentifier,
NextIDPlatform,
} from '@masknet/shared-base'
import { CopyButton, PersonaContext } from '@masknet/shared'
import { NextIDPlatform } from '@masknet/shared-base'
import { attachNextIDToProfile } from '../../../../../utils/utils.js'
import { ConnectedAccounts } from './ConnectedAccounts/index.js'
import { useI18N } from '../../../../../utils/i18n-next-ui.js'
import Services from '#services'
import { type Friend, useFriendProfiles } from '../../../hooks/index.js'
import { type Profile } from '../common.js'

const useStyles = makeStyles()((theme) => ({
card: {
Expand Down Expand Up @@ -58,7 +58,7 @@ const useStyles = makeStyles()((theme) => ({

interface ContactCardProps {
avatar?: string
proofProfiles?: BindingProof[]
proofProfiles?: Profile[]
nextId?: string
publicKey?: string
isLocal?: boolean
Expand Down
37 changes: 28 additions & 9 deletions packages/mask/src/extension/popups/pages/Friends/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
import { memo, useState, useMemo } from 'react'
import { FriendsHomeUI } from './UI.js'
import { useFriendsPaged, useTitle, useSearchValue, useFriendsFromSearch } from '../../../hooks/index.js'
import { EMPTY_LIST } from '@masknet/shared-base'
import { useI18N } from '../../../../../utils/i18n-next-ui.js'
import { EMPTY_LIST, NextIDPlatform } from '@masknet/shared-base'
import { resolveNextIDPlatform } from '@masknet/shared'
import { useInfiniteQuery } from '@tanstack/react-query'
import { NextIDProof } from '@masknet/web3-providers'
import { NextIDProof, Fuse } from '@masknet/web3-providers'
import { FriendsHomeUI } from './UI.js'
import {
useFriendsPaged,
useTitle,
useSearchValue,
useFriendsFromSearch,
useFriendFromList,
} from '../../../hooks/index.js'
import { useI18N } from '../../../../../utils/i18n-next-ui.js'

const FriendsHome = memo(function FriendsHome() {
const { t } = useI18N()
useTitle(t('popups_encrypted_friends'))

const { data, fetchNextPage, isLoading, refetch, status, fetchRelationStatus } = useFriendsPaged()
const { data, fetchNextPage, isLoading, refetch, status, fetchRelationStatus, records } = useFriendsPaged()
const friends = useMemo(() => data?.pages.flatMap((x) => x.friends) ?? EMPTY_LIST, [data])
const [searchValue, setSearchValue] = useState('')
const type = resolveNextIDPlatform(searchValue)
const { loading: resolveLoading, value: keyword = '' } = useSearchValue(searchValue, type)
const fuse = useMemo(() => {
return Fuse.create(records, {
keys: ['profile.userId'],
isCaseSensitive: false,
ignoreLocation: true,
threshold: 0,
})
}, [records])
const searchedRecords = useMemo(() => {
if (!keyword || type !== NextIDPlatform.Twitter) return EMPTY_LIST
return fuse.search(keyword).map((item) => item.item)
}, [fuse, keyword, type])
const { isLoading: isSearchRecordLoading, data: localSearchedList = EMPTY_LIST } =
useFriendFromList(searchedRecords)
const {
isLoading: searchLoading,
isInitialLoading,
Expand All @@ -36,15 +56,14 @@ const FriendsHome = memo(function FriendsHome() {
},
)
const searchResult = useMemo(() => searchResultArray?.pages.flat() ?? EMPTY_LIST, [searchResultArray])
const searchedList = useFriendsFromSearch(searchResult, friends, keyword)

const searchedList = useFriendsFromSearch(localSearchedList, searchResult, friends, keyword)
return (
<FriendsHomeUI
friends={data?.pages ?? EMPTY_LIST}
loading={
isLoading ||
resolveLoading ||
(!!keyword && !!type ? searchLoading : isInitialLoading) ||
(!!keyword && !!type ? searchLoading || isSearchRecordLoading : isInitialLoading) ||
status === 'loading' ||
fetchRelationStatus === 'loading'
}
Expand Down
2 changes: 2 additions & 0 deletions packages/mask/src/extension/popups/pages/Friends/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type Profile = Omit<BindingProof, 'platform'> & {
platform: NextIDPlatform | EnhanceableSite.Twitter | EnhanceableSite.Facebook | EnhanceableSite.Instagram
}

export type FriendNetwork = EnhanceableSite.Twitter | EnhanceableSite.Facebook | EnhanceableSite.Instagram

export type SupportedPlatforms =
| NextIDPlatform.Ethereum
| NextIDPlatform.GitHub
Expand Down
6 changes: 3 additions & 3 deletions packages/plugins/Calendar/src/SiteAdaptor/CalendarContent.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/* cspell: disable */
import React, { useState, useMemo } from 'react'
import { Tab } from '@mui/material'
import { TabContext, TabPanel } from '@mui/lab'
import { safeUnreachable } from '@masknet/kit'
import { PluginID } from '@masknet/shared-base'
import { useIsMinimalMode } from '@masknet/plugin-infra/content-script'
import { makeStyles, MaskTabList, useTabs } from '@masknet/theme'
import { Tab } from '@mui/material'
import { TabContext, TabPanel } from '@mui/lab'
import { useLocationChange } from '@masknet/shared-base-ui'
import { DatePickerTab } from './components/DatePickerTab.js'
import { useEventList, useNFTList, useNewsList } from '../hooks/useEventList.js'
import { NewsList } from './components/NewsList.js'
import { EventList } from './components/EventList.js'
import { NFTList } from './components/NFTList.js'
import { Footer } from './components/Footer.js'
import { useI18N } from '../locales/i18n_generated.js'
import { useLocationChange } from '@masknet/shared-base-ui'

const useStyles = makeStyles()((theme) => ({
calendar: {
Expand Down

0 comments on commit 8fd1857

Please sign in to comment.