Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/components/TimelineTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as Tabs from '@radix-ui/react-tabs';
import Icon from 'components/Icon';
import { Timeline } from 'components/Timeline';
import { MsgFilter, MsgFilterKey } from 'core/msg-filter/filter';
import { MsgFilter } from 'core/msg-filter/filter';
import { CallWorker } from 'core/worker/caller';
import { useRouter } from 'next/router';
import { useState } from 'react';

export interface TimelineTabsProp {
Expand All @@ -10,15 +12,19 @@ export interface TimelineTabsProp {
defaultActiveKey?: string;
onActiveKeyChanged?: (val: string) => any;
showDescription?: boolean;
editOptUrl?: string;
}

export function TimelineTabs({
filterOptions,
worker,
defaultActiveKey,
onActiveKeyChanged,
showDescription = true,
showDescription = false,
editOptUrl,
}: TimelineTabsProp) {
const router = useRouter();

const [activeTabKey, setActiveTabKey] = useState<string>(
defaultActiveKey || filterOptions[0].key,
);
Expand Down Expand Up @@ -49,6 +55,13 @@ export function TimelineTabs({
</Tabs.Trigger>
))}
</Tabs.List>
{editOptUrl && (
<Icon
onClick={() => router.push(editOptUrl)}
type="icon-Gear"
className="w-[24px] h-[24px] cursor-pointer m-auto"
/>
)}
</div>
<div className="px-2">
{filterOptions.map(val => (
Expand Down
46 changes: 16 additions & 30 deletions src/core/msg-filter/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,14 @@ export const mixKinds = [
WellKnownEventKind.reposts,
];

export enum MsgFilterKey {
follow = 'Follow',
followArticle = 'Follow-Article',
globalHighLight = 'HighLights',
globalAll = 'Global-All',
media = 'Media',
}

export enum MsgFilterMode {
global = 'Global',
follow = 'Follow',
custom = 'Custom',
}

export interface MsgFilter {
key: MsgFilterKey | string;
key: string;
label: string;
filter?: Filter;
isValidEvent?: (event: Event) => boolean;
Expand All @@ -36,7 +28,7 @@ export interface MsgFilter {

export const defaultMsgFilters: MsgFilter[] = [
{
key: MsgFilterKey.follow,
key: 'Follow',
label: 'Follow',
filter: {
limit: 50,
Expand All @@ -49,30 +41,31 @@ export const defaultMsgFilters: MsgFilter[] = [
description: "all your followings's mixed posts",
},
{
key: MsgFilterKey.followArticle,
label: 'Follow-Article',
key: 'Global-All',
label: 'Global',
filter: {
limit: 50,
kinds: [WellKnownEventKind.long_form],
kinds: mixKinds,
},
isValidEvent: (event: Event) => {
return event.kind === WellKnownEventKind.long_form;
return mixKinds.includes(event.kind);
},
mode: MsgFilterMode.follow,
description: "all your followings's long-form posts",
mode: MsgFilterMode.global,
description: "all the realtime global's mixed posts",
},
/*
{
key: MsgFilterKey.globalAll,
label: 'Global',
key: MsgFilterKey.followArticle,
label: 'Follow-Article',
filter: {
limit: 50,
kinds: mixKinds,
kinds: [WellKnownEventKind.long_form],
},
isValidEvent: (event: Event) => {
return mixKinds.includes(event.kind);
return event.kind === WellKnownEventKind.long_form;
},
mode: MsgFilterMode.global,
description: "all the realtime global's mixed posts",
mode: MsgFilterMode.follow,
description: "all your followings's long-form posts",
},
{
key: MsgFilterKey.globalHighLight,
Expand Down Expand Up @@ -103,12 +96,5 @@ export const defaultMsgFilters: MsgFilter[] = [
mode: MsgFilterMode.global,
description: 'global posts including at least one picture',
},
*/
];

export const defaultMsgFiltersMap = defaultMsgFilters.reduce(
(map, filter) => ({
...map,
[filter.key]: filter,
}),
{} as Record<MsgFilterKey, MsgFilter>,
);
26 changes: 26 additions & 0 deletions src/core/nip/188.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ export class Nip188 {
return rawEvent;
}

static parseNoscriptNaddr(event: Event) {
const d = findTagFirstValue<string>(event.tags, 'd');
return `${event.kind}:${event.pubkey}:${d}`;
}

static parseNoscriptTitle(event: Event) {
return findTagFirstValue<string>(event.tags, 'd');
}

static parseNoscriptDescription(event: Event) {
return findTagFirstValue<string>(event.tags, 'description');
}

static parseNoscriptPicture(event: Event) {
return findTagFirstValue<string>(event.tags, 'picture');
}

static parseNoscript(event: Event) {
const content = event.content;
const code = this.base64ToArrayBuffer(content);
Expand All @@ -93,6 +110,7 @@ export class Nip188 {
const since = findTagValues(tags, 'since');
const until = findTagValues(tags, 'until');
const limit = findTagValues(tags, 'limit');
// note: below is correct, use #e/#d.., not e/d/a/t, because this tag is supposed to be assign value to filter
const e = findTagValues(tags, '#e');
const d = findTagValues(tags, '#d');
const a = findTagValues(tags, '#a');
Expand Down Expand Up @@ -178,6 +196,14 @@ export class Nip188 {
}
}

function findTagFirstValue<T>(tags: Tags, firstLabel: string) {
const tag = tags.find(t => t[0] === firstLabel);
if (tag && tag.length > 1) {
return tag[1] as T;
}
return undefined;
}

function findTag(tags: Tags, firstLabel: string) {
return tags.find(t => t[0] === firstLabel);
}
Expand Down
77 changes: 77 additions & 0 deletions src/pages/filter-market/hook/useFilterNoscripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { CallWorker } from 'core/worker/caller';
import { useQuery } from '@tanstack/react-query';
import { Filter } from 'core/nostr/type';
import { useMemo } from 'react';
import { useQueryMsg } from 'components/TimelineRender/hook/useQueryMsg';
import { Nip188 } from 'core/nip/188';
import { Event } from 'core/nostr/Event';
import { cloneDeep } from 'lodash';

export interface NoscriptItem {
event: Event;
filter: Filter;
title?: string;
description?: string;
picture?: string;
}

export function useFilterNoscript({
worker,
}: {
worker: CallWorker | undefined;
}) {
const filter: Filter = Nip188.createQueryNoscriptFilter([]);
const relayUrls = useMemo(
() => worker?.relays.map(r => r.url) || [],
[worker?.relays],
);
const { queryMsg } = useQueryMsg();
const queryKey = ['filter-market', filter, relayUrls];
const queryFn = async () => {
const data = await queryMsg({ filter, worker });

// prevent db data will not update forever
if (data.length > 0) {
const lastTimestamp = data[0].created_at;
const newFilter = cloneDeep(filter);
newFilter.since = lastTimestamp;
worker?.subFilter({ filter: newFilter });
}

return data;
};
const { data } = useQuery({
queryKey,
queryFn,
retry: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});

const filterNoscripts = useMemo(() => {
if (!data) {
return [];
}

return data.map(e => {
const filter = Nip188.parseNoscriptMsgFilterTag(e);
const title = 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 item: NoscriptItem = {
filter,
title,
description,
event: e,
};
return item;
});
}, [data]);

return filterNoscripts;
}
82 changes: 82 additions & 0 deletions src/pages/filter-market/hook/useFilterOptionSetting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Nip188 } from 'core/nip/188';
import { Event } from 'core/nostr/Event';
import { EventId } from 'core/nostr/type';
import { useMemo } from 'react';
import { useLocalStorage } from 'usehooks-ts';
import { NoscriptItem } from './useFilterNoscripts';
import { useSelector } from 'react-redux';
import { RootState } from 'store/configureStore';
import { MsgFilter, MsgFilterMode } from 'core/msg-filter/filter';

export interface FilterOption extends NoscriptItem {
naddr: string;
disabled: boolean;
}

export interface FilterOptionSetting {
options: FilterOption[];
appDataEventId?: EventId;
}

export function useFilterOptionSetting() {
const myPublicKey = useSelector(
(state: RootState) => state.loginReducer.publicKey,
);
const key = useMemo(() => `filterOptions:${myPublicKey}`, [myPublicKey]);
const defaultSetting: FilterOptionSetting = { options: [] };
const [filterOptionSetting, setFilterOptionSetting] =
useLocalStorage<FilterOptionSetting>(key, defaultSetting);

const addOpt = (event: Event) => {
const option: FilterOption = {
event,
disabled: false,
title: Nip188.parseNoscriptTitle(event),
description: Nip188.parseNoscriptDescription(event),
picture: Nip188.parseNoscriptPicture(event),
naddr: Nip188.parseNoscriptNaddr(event),
filter: Nip188.parseNoscriptMsgFilterTag(event),
};

setFilterOptionSetting(prev => {
const s = prev;
s.options.push(option);
return s;
});
};

const deleteOpt = (event: Event) => {
const naddr = Nip188.parseNoscriptNaddr(event);
setFilterOptionSetting(prev => {
const s = prev;
s.options = s.options.filter(opt => opt.naddr !== naddr);
return s;
});
};

const getOpts = () => {
return filterOptionSetting.options;
};

const isAdded = (event: Event) => {
const naddr = Nip188.parseNoscriptNaddr(event);
return (
filterOptionSetting.options.find(opt => opt.naddr === naddr) != undefined
);
};

const toMsgFilter = (item: FilterOption) => {
const res: MsgFilter = {
key: item.naddr,
label: `${item.title}@${item.event.pubkey.slice(0, 3)}`,
description: item.description,
filter: item.filter,
mode: MsgFilterMode.custom,
wasm: Nip188.parseNoscript(item.event),
selfEvent: item.event,
};
return res;
};

return { addOpt, deleteOpt, getOpts, isAdded, toMsgFilter };
}
Loading