diff --git a/src/pages/home/filter.ts b/src/core/msg-filter/filter.ts similarity index 58% rename from src/pages/home/filter.ts rename to src/core/msg-filter/filter.ts index 6f5c47e5..ebe40c14 100644 --- a/src/pages/home/filter.ts +++ b/src/core/msg-filter/filter.ts @@ -10,32 +10,37 @@ const mixKinds = [ WellKnownEventKind.reposts, ]; -export enum HomeMsgFilterType { - all = 'All', - article = 'Article', +export enum MsgFilterKey { + follow = 'Follow', + followArticle = 'Follow-Article', + globalAll = 'Global-All', media = 'Media', - flycat = 'Flycat', zh = 'Chinese', foodstr = 'Foodstr', - nostr = 'Nostr', - dev = 'Dev', bitcoin = 'Bitcoin', - photography = 'Photography', - art = 'Art', meme = 'Meme', } -export interface HomeMsgFilter { - type: HomeMsgFilterType; +export enum MsgFilterMode { + global = 'Global', + follow = 'Follow', + custom = 'Custom', +} + +export interface MsgFilter { + key: MsgFilterKey | string; label: string; filter: Filter; isValidEvent?: (event: Event) => boolean; + mode: MsgFilterMode; + description?: string; + wasm?: ArrayBuffer | undefined; } -export const homeMsgFilters: HomeMsgFilter[] = [ +export const defaultMsgFilters: MsgFilter[] = [ { - type: HomeMsgFilterType.all, - label: 'All', + key: MsgFilterKey.follow, + label: 'Follow', filter: { limit: 50, kinds: mixKinds, @@ -43,10 +48,12 @@ export const homeMsgFilters: HomeMsgFilter[] = [ isValidEvent: (event: Event) => { return mixKinds.includes(event.kind); }, + mode: MsgFilterMode.follow, + description: "all your followings's mixed posts", }, { - type: HomeMsgFilterType.article, - label: 'Article', + key: MsgFilterKey.followArticle, + label: 'Follow-Article', filter: { limit: 50, kinds: [WellKnownEventKind.long_form], @@ -54,9 +61,24 @@ export const homeMsgFilters: HomeMsgFilter[] = [ isValidEvent: (event: Event) => { return event.kind === WellKnownEventKind.long_form; }, + mode: MsgFilterMode.follow, + description: "all your followings's long-form posts", }, { - type: HomeMsgFilterType.media, + key: MsgFilterKey.globalAll, + label: 'Global', + filter: { + limit: 50, + kinds: mixKinds, + }, + isValidEvent: (event: Event) => { + return mixKinds.includes(event.kind); + }, + mode: MsgFilterMode.global, + description: "all the realtime global's mixed posts", + }, + { + key: MsgFilterKey.media, label: 'Media', filter: { limit: 50, @@ -68,9 +90,11 @@ export const homeMsgFilters: HomeMsgFilter[] = [ stringHasImageUrl(event.content) ); }, + mode: MsgFilterMode.global, + description: 'global posts including at least one picture', }, { - type: HomeMsgFilterType.zh, + key: MsgFilterKey.zh, label: 'Chinese', filter: { kinds: [WellKnownEventKind.text_note], @@ -81,9 +105,11 @@ export const homeMsgFilters: HomeMsgFilter[] = [ isChineseLang(event.content) ); }, + mode: MsgFilterMode.global, + description: 'global posts which language is Chinese', }, { - type: HomeMsgFilterType.foodstr, + key: MsgFilterKey.foodstr, label: '#Foodstr', filter: { kinds: [WellKnownEventKind.text_note], @@ -92,9 +118,11 @@ export const homeMsgFilters: HomeMsgFilter[] = [ isValidEvent: (event: Event) => { return event.kind === WellKnownEventKind.text_note; }, + mode: MsgFilterMode.global, + description: 'global posts including #Foodstr tag', }, { - type: HomeMsgFilterType.meme, + key: MsgFilterKey.meme, label: '#Meme', filter: { kinds: [WellKnownEventKind.text_note], @@ -103,9 +131,11 @@ export const homeMsgFilters: HomeMsgFilter[] = [ isValidEvent: (event: Event) => { return event.kind === WellKnownEventKind.text_note; }, + mode: MsgFilterMode.global, + description: 'global posts including #meme tag', }, { - type: HomeMsgFilterType.bitcoin, + key: MsgFilterKey.bitcoin, label: '#Bitcoin', filter: { kinds: [WellKnownEventKind.text_note], @@ -114,48 +144,15 @@ export const homeMsgFilters: HomeMsgFilter[] = [ isValidEvent: (event: Event) => { return event.kind === WellKnownEventKind.text_note; }, - }, - { - type: HomeMsgFilterType.photography, - label: '#Photography', - filter: { - kinds: [WellKnownEventKind.text_note], - '#t': ['photography'], - } as Filter, - isValidEvent: (event: Event) => { - return event.kind === WellKnownEventKind.text_note; - }, - }, - { - type: HomeMsgFilterType.art, - label: '#Art', - filter: { - kinds: [WellKnownEventKind.text_note], - '#t': ['art'], - } as Filter, - isValidEvent: (event: Event) => { - return event.kind === WellKnownEventKind.text_note; - }, - }, - { - type: HomeMsgFilterType.flycat, - label: 'Flycat', - filter: { - kinds: [WellKnownEventKind.text_note], - } as Filter, - isValidEvent: (event: Event) => { - return ( - event.kind === WellKnownEventKind.text_note && - event.content.includes('flycat') - ); - }, + mode: MsgFilterMode.global, + description: 'global posts including #bitcoin tag', }, ]; -export const homeMsgFiltersMap = homeMsgFilters.reduce( +export const defaultMsgFiltersMap = defaultMsgFilters.reduce( (map, filter) => ({ ...map, - [filter.type]: filter, + [filter.key]: filter, }), - {} as Record, + {} as Record, ); diff --git a/src/pages/home/util.ts b/src/core/msg-filter/util.ts similarity index 59% rename from src/pages/home/util.ts rename to src/core/msg-filter/util.ts index 31e63afa..bdb05b8e 100644 --- a/src/pages/home/util.ts +++ b/src/core/msg-filter/util.ts @@ -1,22 +1,5 @@ import { franc } from 'franc-min'; -const selectedTabKeyStorageKey = 'home-selected-tab-key'; -const selectedFilterStorageKey = 'home-selected-filter'; - -export function updateLastSelectedTabKeyAndFilter( - tabKey: string, - filter: string, -) { - localStorage.setItem(selectedTabKeyStorageKey, tabKey); - localStorage.setItem(selectedFilterStorageKey, filter); -} - -export function getLastSelectedTabKeyAndFilter() { - const selectedTabKey = localStorage.getItem(selectedTabKeyStorageKey); - const selectedFilter = localStorage.getItem(selectedFilterStorageKey); - return { selectedFilter, selectedTabKey }; -} - export function isChineseLang(text: string) { // Count the number of Kanji, Hiragana, and Katakana characters in the text const kanjiCount = (text.match(/[\u4e00-\u9faf]/g) || []).length; diff --git a/src/pages/home/constants.ts b/src/pages/home/constants.ts index 3b0f86b6..be1bf784 100644 --- a/src/pages/home/constants.ts +++ b/src/pages/home/constants.ts @@ -1,2 +1 @@ -export const SELECTED_TAB_KEY_STORAGE_KEY = 'home-selected-tab-key-v1'; -export const SELECTED_FILTER_STORAGE_KEY = 'home-selected-filter-v1'; +export const SELECTED_FILTER_STORAGE_KEY = 'home-selected-filter-v2'; diff --git a/src/pages/home/custom-filter.tsx b/src/pages/home/custom-filter.tsx deleted file mode 100644 index 59d6bde9..00000000 --- a/src/pages/home/custom-filter.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Segmented, Tooltip } from 'antd'; -import { MsgSubProp } from 'components/MsgFeed'; -import { dbQuery } from 'core/db'; -import { Nip188 } from 'core/nip/188'; -import { Nip19, Nip19DataType } from 'core/nip/19'; -import { Filter } from 'core/nostr/type'; -import { CallWorker } from 'core/worker/caller'; -import { initSync, is_valid_event } from 'pages/noscript/filter-binding'; -import { useEffect, useState } from 'react'; -import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; -import { isValidPublicKey } from 'utils/validator'; - -import Link from 'next/link'; - -export interface CustomFilterProp { - worker: CallWorker | undefined; - onMsgPropChange: (prop: MsgSubProp) => any; -} - -export interface MsgFilterNoscript { - label: string; - filter: Filter; - wasm?: ArrayBuffer; - description?: string; -} - -export const CustomFilter: React.FC = ({ - worker, - onMsgPropChange, -}) => { - const myPublicKey = useReadonlyMyPublicKey(); - const [selectFilter, setSelectFilter] = useState(); - const [filterOptions, setFilterOptions] = useState([]); - - const whitelist = [ - '45c41f21e1cf715fa6d9ca20b8e002a574db7bb49e96ee89834c66dac5446b7a', - ]; - if (isValidPublicKey(myPublicKey)) { - whitelist.push(myPublicKey); - } - - const queryNoscript = async () => { - if (!worker) return []; - - const filter: Filter = Nip188.createQueryNoscriptFilter(whitelist); - worker.subFilter({ filter }); - const relayUrls = worker.relays.map(r => r.url); - const scriptEvents = await dbQuery.matchFilterRelay( - filter, - relayUrls, - Nip188.isValidCustomMsgFilterNoscript(), - ); - const noscripts = scriptEvents.map((e, idx) => { - const authorPubkey = Nip19.encode(e.pubkey, Nip19DataType.Npubkey).slice( - 0, - 12, - ); - const id = e.tags.find(t => t[0] === 'd') - ? (e.tags.find(t => t[0] === 'd') as any)[1] - : 'unknown-id'; - const description = e.tags.find(t => t[0] === 'description') - ? (e.tags.find(t => t[0] === 'description') as any)[1] - : 'no description'; - const script: MsgFilterNoscript = { - label: `${id}@${authorPubkey}`, - description, - filter: Nip188.parseNoscriptMsgFilterTag(e), - wasm: Nip188.parseNoscript(e), - }; - return script; - }); - console.log('noscripts: ', noscripts); - setFilterOptions(noscripts); - return noscripts; - }; - - useEffect(() => { - queryNoscript(); - }, [worker]); - - useEffect(() => { - if (!selectFilter) return; - - const f = filterOptions.find(opt => opt.label === selectFilter); - if (!f) return console.log('opt not found'); - - const msgSubProp: MsgSubProp = { - msgFilter: f.filter, - }; - - if (f.wasm) { - initSync(f.wasm); - msgSubProp.isValidEvent = is_valid_event; - } - onMsgPropChange(msgSubProp); - }, [selectFilter]); - - return ( -
-
- This is a experiment feature which enable custom timeline experience via - loading a custom nostr script(a short wasm filtering program) composed - by users instead of flycat or any other platforms/clients. For security - concern, it only fetch scripts from whitelist now. You can try{' '} - make your own nostr scripts -
- - setSelectFilter(val as any)} - options={filterOptions.map(option => ({ - label: ( - - {option.label} - - ), - value: option.label, - }))} - /> -
- ); -}; diff --git a/src/pages/home/hooks/useQueryNoscript.ts b/src/pages/home/hooks/useQueryNoscript.ts new file mode 100644 index 00000000..aa5fa15d --- /dev/null +++ b/src/pages/home/hooks/useQueryNoscript.ts @@ -0,0 +1,84 @@ +import { dbQuery } from 'core/db'; +import { Nip188 } from 'core/nip/188'; +import { Nip19, Nip19DataType } from 'core/nip/19'; +import { Filter } from 'core/nostr/type'; +import { CallWorker } from 'core/worker/caller'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + MsgFilter, + MsgFilterKey, + MsgFilterMode, +} from '../../../core/msg-filter/filter'; +import { createCallRelay } from 'core/worker/util'; + +export function useQueryNoScript({ + worker, + newConn, +}: { + worker: CallWorker | undefined; + newConn: string[]; +}) { + const [filterOptions, setFilterOptions] = useState([]); + + useEffect(() => { + if (!worker) return; + + const filter: Filter = Nip188.createQueryNoscriptFilter([]); + const callRelay = createCallRelay(newConn); + worker.subFilter({ filter, callRelay }); + }, [worker, newConn]); + + const queryNoscript = useCallback(async () => { + if (!worker) return; + + const filter: Filter = Nip188.createQueryNoscriptFilter([]); + + const relayUrls = worker.relays.map(r => r.url); + const scriptEvents = await dbQuery.matchFilterRelay( + filter, + relayUrls, + Nip188.isValidCustomMsgFilterNoscript(), + ); + const noscripts = scriptEvents.map((e, idx) => { + const authorPubkey = Nip19.encode(e.pubkey, Nip19DataType.Npubkey).slice( + 0, + 12, + ); + const id = e.tags.find(t => t[0] === 'd') + ? (e.tags.find(t => t[0] === 'd') as any)[1] + : 'unknown-id'; + const description = e.tags.find(t => t[0] === 'description') + ? (e.tags.find(t => t[0] === 'description') as any)[1] + : 'no description'; + const noscript: MsgFilter = { + key: `${id}@${authorPubkey}`, + label: `${id}@${authorPubkey}`, + description, + filter: Nip188.parseNoscriptMsgFilterTag(e), + wasm: Nip188.parseNoscript(e), + mode: MsgFilterMode.global, + }; + return noscript; + }); + console.log('noscripts: ', noscripts); + setFilterOptions(noscripts); + }, [worker]); + + useEffect(() => { + queryNoscript(); + }, [worker]); + + const noscriptFiltersMaps = useMemo( + () => + filterOptions.reduce( + (map, filter) => ({ + ...map, + [filter.key]: filter, + }), + {} as Record, + ), + [filterOptions], + ); + + return noscriptFiltersMaps; +} diff --git a/src/pages/home/hooks.ts b/src/pages/home/hooks/useSubContactList.ts similarity index 100% rename from src/pages/home/hooks.ts rename to src/pages/home/hooks/useSubContactList.ts diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 3afabb61..70682960 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -22,17 +22,20 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { LoginMode, SignEvent } from 'store/loginReducer'; import { isValidPublicKey } from 'utils/validator'; -import { CustomFilter } from './custom-filter'; -import { homeMsgFilters, homeMsgFiltersMap, HomeMsgFilterType } from './filter'; +import { useQueryNoScript } from './hooks/useQueryNoscript'; +import { + MsgFilterMode, + defaultMsgFiltersMap, + MsgFilterKey, + MsgFilter, +} from '../../core/msg-filter/filter'; import { trendingTags } from './hashtags'; -import { useSubContactList } from './hooks'; +import { useSubContactList } from './hooks/useSubContactList'; import styles from './index.module.scss'; import { updates } from './updates'; import { useLocalStorage } from 'usehooks-ts'; -import { - SELECTED_FILTER_STORAGE_KEY, - SELECTED_TAB_KEY_STORAGE_KEY, -} from './constants'; +import { SELECTED_FILTER_STORAGE_KEY } from './constants'; +import { initSync, is_valid_event } from 'pages/noscript/filter-binding'; export interface HomePageProps { isLoggedIn: boolean; @@ -40,12 +43,6 @@ export interface HomePageProps { signEvent?: SignEvent; } -enum TabKey { - Follow = 'follow', - Global = 'global', - Custom = 'custom', -} - const HomePage = ({ isLoggedIn }: HomePageProps) => { const { t } = useTranslation(); @@ -54,15 +51,12 @@ const HomePage = ({ isLoggedIn }: HomePageProps) => { const { worker, newConn } = useCallWorker(); const isMobile = useMatchMobile(); - const defaultTabActivateKey = isLoggedIn ? TabKey.Follow : TabKey.Global; - const defaultSelectedFilter = HomeMsgFilterType.all; + const defaultSelectedFilter = isLoggedIn + ? MsgFilterKey.follow + : MsgFilterKey.globalAll; - const [lastSelectedTabKey, setLastSelectedTabKey] = useLocalStorage( - SELECTED_TAB_KEY_STORAGE_KEY, - defaultTabActivateKey, - ); const [lastSelectedFilter, setLastSelectedFilter] = - useLocalStorage( + useLocalStorage( SELECTED_FILTER_STORAGE_KEY, defaultSelectedFilter, ); @@ -73,6 +67,27 @@ const HomePage = ({ isLoggedIn }: HomePageProps) => { useState(false); useSubContactList(myPublicKey, newConn, worker); + const noscriptFiltersMap = useQueryNoScript({ worker, newConn }); + + const filtersMap = useMemo(() => { + const filter: Record = { + ...defaultMsgFiltersMap, + ...noscriptFiltersMap, + }; + if (!isLoggedIn || !isValidPublicKey(myPublicKey)) { + return Object.values(filter) + .filter(v => v.mode !== MsgFilterMode.follow) + .reduce( + (map, filter) => ({ + ...map, + [filter.key]: filter, + }), + {} as Record, + ); + } + return filter; + }, [defaultMsgFiltersMap, noscriptFiltersMap, isLoggedIn, myPublicKey]); + useLiveQuery(() => { if (!isLoggedIn) { setAlreadyQueryMyContact(true); @@ -117,37 +132,39 @@ const HomePage = ({ isLoggedIn }: HomePageProps) => { ); const onMsgFilterChanged = useCallback(() => { - if ( - !lastSelectedTabKey || - !lastSelectedFilter || - lastSelectedTabKey === TabKey.Custom - ) { + if (!lastSelectedFilter) { return; } - const selectedMsgFilter = homeMsgFiltersMap[lastSelectedFilter] ?? {}; + const selectedMsgFilter = filtersMap[lastSelectedFilter]; if (!selectedMsgFilter) { return; } const msgFilter = cloneDeep(selectedMsgFilter.filter); - const isValidEvent = selectedMsgFilter.isValidEvent; + let isValidEvent = selectedMsgFilter.isValidEvent; + if (selectedMsgFilter.wasm) { + initSync(selectedMsgFilter.wasm); + isValidEvent = is_valid_event; + } let placeholder: ReactNode | null = null; - if (lastSelectedTabKey === TabKey.Follow) { - if (!isLoggedIn || myPublicKey == null || myPublicKey.length === 0) { - return; - } + if (!msgFilter.authors) { + if (selectedMsgFilter.mode === MsgFilterMode.follow) { + if (!isLoggedIn || !isValidPublicKey(myPublicKey)) { + return; + } - const followings: PublicKey[] = myContactEvent - ? parsePubKeyFromTags(myContactEvent.tags) - : []; - if (!followings.includes(myPublicKey)) { - followings.push(myPublicKey); - } - placeholder = emptyFollowPlaceholder; - if (followings.length > 0) { - msgFilter.authors = followings; + const followings: PublicKey[] = myContactEvent + ? parsePubKeyFromTags(myContactEvent.tags) + : []; + if (!followings.includes(myPublicKey)) { + followings.push(myPublicKey); + } + placeholder = emptyFollowPlaceholder; + if (followings.length > 0) { + msgFilter.authors = followings; + } } } @@ -163,7 +180,7 @@ const HomePage = ({ isLoggedIn }: HomePageProps) => { myContactEvent, myPublicKey, lastSelectedFilter, - lastSelectedTabKey, + noscriptFiltersMap, ]); useEffect(() => { @@ -176,37 +193,23 @@ const HomePage = ({ isLoggedIn }: HomePageProps) => { {!isMobile && } -
- setLastSelectedTabKey(key as TabKey)} - /> -
- {lastSelectedTabKey === TabKey.Custom ? ( - - ) : ( - - setLastSelectedFilter(val as HomeMsgFilterType) - } - options={homeMsgFilters.map(val => { - return { - value: val.type, - label: val.label, - }; - })} - /> - )} + setLastSelectedFilter(val as MsgFilterKey)} + options={Object.values(filtersMap).map(val => { + return { + value: val.key, + label: val.label, + }; + })} + />
+
+ {filtersMap[lastSelectedFilter]?.description} +
diff --git a/src/pages/perf/index.page.tsx b/src/pages/perf/index.page.tsx index 26fc56a4..a51c5f2a 100644 --- a/src/pages/perf/index.page.tsx +++ b/src/pages/perf/index.page.tsx @@ -5,7 +5,7 @@ import { dbQuery } from 'core/db'; import { DbEvent } from 'core/db/schema'; import { useCallWorker } from 'hooks/useWorker'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { HomeMsgFilter, homeMsgFilters } from 'pages/home/filter'; +import { MsgFilter, defaultMsgFilters } from 'core/msg-filter/filter'; import { useEffect, useState } from 'react'; const Perf = () => { @@ -155,7 +155,7 @@ const Perf = () => { const [queryTime, setQueryTime] = useState(0); const { worker } = useCallWorker(); - const query = async (msgFilter: HomeMsgFilter) => { + const query = async (msgFilter: MsgFilter) => { setQueryTime(0); setMsg([]); const start = performance.now(); @@ -173,7 +173,7 @@ const Perf = () => { }; useEffect(() => { - const msgFilter = homeMsgFilters[selectNumber]; + const msgFilter = defaultMsgFilters[selectNumber]; query(msgFilter); }, [selectNumber]); @@ -181,13 +181,13 @@ const Perf = () => {
- {homeMsgFilters.map((item, index) => ( + {defaultMsgFilters.map((item, index) => ( ))}
- selected {homeMsgFilters[selectNumber].label}, time:{' '} + selected {defaultMsgFilters[selectNumber].label}, time:{' '} {queryTime.toLocaleString()} ms