From 19445a42ab5a9221463e5e7bbe1b0454be2c63c4 Mon Sep 17 00:00:00 2001 From: digi-monkey Date: Mon, 25 Dec 2023 14:13:13 +0800 Subject: [PATCH 01/14] feat: fetch note preview for event page --- src/core/api/preview.ts | 36 +++++++ src/pages/event/[eventId].page.tsx | 149 ++++++++++++++++++----------- 2 files changed, 130 insertions(+), 55 deletions(-) create mode 100644 src/core/api/preview.ts diff --git a/src/core/api/preview.ts b/src/core/api/preview.ts new file mode 100644 index 00000000..1e05be63 --- /dev/null +++ b/src/core/api/preview.ts @@ -0,0 +1,36 @@ +import { EventId } from 'core/nostr/type'; + +export interface NotePreview { + title: string; + content: string; + image?: string; + authorProfile?: { + name: string; + picture: string; + }; + created_at: number; +} + +export async function getNotePreview(eventId: EventId) { + const timeout = 2000; + const url = 'https://nostr-preview.fly.dev' + `/e/${eventId}`; + try { + const res = await fetchWithTimeout(url, timeout, { cache: 'force-cache' }); + const jsonRes = await res.json(); + if (jsonRes.status === 'ok') { + return jsonRes.data as NotePreview; + } + } catch (err: any) { + console.log(err.message); + } + + return null; +} + +async function fetchWithTimeout(url: string, timeout: number, options: any) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const response = await fetch(url, { ...options, signal: controller.signal }); + clearTimeout(timeoutId); + return response; +} diff --git a/src/pages/event/[eventId].page.tsx b/src/pages/event/[eventId].page.tsx index 233db83c..d59e20ff 100644 --- a/src/pages/event/[eventId].page.tsx +++ b/src/pages/event/[eventId].page.tsx @@ -1,94 +1,133 @@ -import { connect } from 'react-redux'; import { useRouter } from 'next/router'; import { useCallWorker } from 'hooks/useWorker'; import { EventWithSeen } from 'pages/type'; import { useTranslation } from 'next-i18next'; -import { loginMapStateToProps } from 'pages/helper'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { BaseLayout, Left, Right } from 'components/BaseLayout'; import { useState, useEffect } from 'react'; -import { createCallRelay } from 'core/worker/util'; import { dexieDb } from 'core/db'; +import { NotePreview, getNotePreview } from 'core/api/preview'; +import Head from 'next/head'; import PostItems from 'components/PostItems'; import Comments from 'components/Comments'; import PageTitle from 'components/PageTitle'; import Icon from 'components/Icon'; -export const EventPage = () => { +export default function EventPage({ + notePreview, +}: { + notePreview: NotePreview | null; +}) { const { t } = useTranslation(); const router = useRouter(); + const { worker } = useCallWorker(); const { eventId } = router.query as { eventId: string }; - - const { worker, newConn, wsConnectStatus } = useCallWorker(); const [rootEvent, setRootEvent] = useState(); - useEffect(() => { - if (!eventId) return; + const subRootEvent = async () => { if (!worker) return; - const callRelay = createCallRelay(newConn); - worker.subMsgByEventIds([eventId], undefined, callRelay); - }, [eventId, worker, newConn]); + const handle = worker.subMsgByEventIds([eventId]).getIterator(); + for await (const data of handle) { + if (data.event.id === eventId) { + setRootEvent(data.event); + break; + } + } + }; useEffect(() => { if (!eventId) return; - if (!worker) return; dexieDb.event.get(eventId).then(event => { if (!event) { - worker.subMsgByEventIds([eventId]).iterating({ - cb: event => { - setRootEvent(event); - }, - }); + subRootEvent(); return; } setRootEvent(event); }); - }, [eventId, worker]); + }, [eventId]); return ( - - -
- router.back()} - width={24} - height={24} - type="icon-arrow-left" - /> - } - /> + <> + + {notePreview?.title || 'nostr short note'} + + + + + + - {rootEvent && ( - <> - + + + + + + + + +
+ router.back()} + width={24} + height={24} + type="icon-arrow-left" + /> + } + /> - - - )} -
-
- -
- ); -}; + {rootEvent && ( + <> + -export default connect(loginMapStateToProps)(EventPage); + + + )} +
+
+ +
+ + ); +} -export const getStaticProps = async ({ locale }: { locale: string }) => ({ - props: { - ...(await serverSideTranslations(locale, ['common'])), - }, -}); +export const getStaticProps = async ({ + params, + locale, +}: { + params: { eventId: string }; + locale: string; +}) => { + const { eventId } = params; + const preview = await getNotePreview(eventId); + return { + props: { + notePreview: preview, + ...(await serverSideTranslations(locale, ['common'])), + }, + }; +}; export const getStaticPaths = () => ({ paths: [], fallback: true }); From 44d98f86f3f7146145d926102c5888ac5d5bdabf Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Mon, 25 Dec 2023 21:41:47 +0800 Subject: [PATCH 02/14] refactor: display new comming msg when database has no data (#414) * refacotr: extract useRestoreScrollPos hook * refactor: display new comming msg when database has no data --- .../MsgFeed/hook/useRestoreScrollPos.ts | 23 ++++ src/components/MsgFeed/index.tsx | 117 +++++++++++------- 2 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 src/components/MsgFeed/hook/useRestoreScrollPos.ts diff --git a/src/components/MsgFeed/hook/useRestoreScrollPos.ts b/src/components/MsgFeed/hook/useRestoreScrollPos.ts new file mode 100644 index 00000000..a8644d34 --- /dev/null +++ b/src/components/MsgFeed/hook/useRestoreScrollPos.ts @@ -0,0 +1,23 @@ +import { scrollPositionCache } from 'core/cache/query'; +import { useEffect } from 'react'; + +export function useRestoreScrollPos( + scrollHeight: number, + queryCacheId: string, + canRestore: boolean, +) { + // remember and restore the last visit position in the feed + // todo: better way to do this? + useEffect(() => { + if (scrollHeight > 0) { + scrollPositionCache.set(queryCacheId, scrollHeight); + } + }, [scrollHeight]); + + useEffect(() => { + const pos = scrollPositionCache.get(queryCacheId); + if (pos && canRestore) { + window.scrollTo({ top: pos, behavior: 'instant' as ScrollBehavior }); + } + }, [queryCacheId, canRestore]); +} diff --git a/src/components/MsgFeed/index.tsx b/src/components/MsgFeed/index.tsx index 34b12d64..20d2a4bf 100644 --- a/src/components/MsgFeed/index.tsx +++ b/src/components/MsgFeed/index.tsx @@ -1,5 +1,5 @@ import { Button, message } from 'antd'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CallWorker } from 'core/worker/caller'; import { Filter, WellKnownEventKind } from 'core/nostr/type'; import { _handleEvent } from 'components/Comments/util'; @@ -12,12 +12,9 @@ import { validateFilter } from './util'; import { mergeAndSortUniqueDbEvents } from 'utils/common'; import { noticePubEventResult } from 'components/PubEventNotice'; import { Loader } from 'components/Loader'; -import { - createQueryCacheId, - queryCache, - scrollPositionCache, -} from 'core/cache/query'; +import { createQueryCacheId, queryCache } from 'core/cache/query'; import { useIntersectionObserver, useInterval } from 'usehooks-ts'; +import { useRestoreScrollPos } from './hook/useRestoreScrollPos'; import PullToRefresh from 'react-simple-pull-to-refresh'; import classNames from 'classnames'; @@ -48,9 +45,14 @@ export const MsgFeed: React.FC = ({ const [msgList, setMsgList] = useState([]); const [newComingMsg, setNewComingMsg] = useState([]); const [isLoadingMsg, setIsLoadingMsg] = useState(false); + const [isSubNewComingMsg, setIsSubNewComingMsg] = useState(false); const [isPullRefreshing, setIsPullRefreshing] = useState(false); const [isLoadMore, setIsLoadMore] = useState(false); + const [isDBNoData, setIsDBNoData] = useState(false); + const SUB_NEW_MSG_INTERVAL = 2000; // milsecs + + // check if newMsgNotify UI is in view, if not, display floating one const newMsgNotifyRef = useRef(null); const entry = useIntersectionObserver(newMsgNotifyRef, {}); const isNotifyVisible = !!entry?.isIntersecting; @@ -69,12 +71,20 @@ export const MsgFeed: React.FC = ({ }), [msgFilter, isValidEvent, relayUrls], ); + const queryLoading = useMemo( + () => isLoadingMsg && !isPullRefreshing, + [isLoadingMsg, isPullRefreshing], + ); + const fetchWhenDBNoDataLoading = useMemo( + () => isDBNoData && isSubNewComingMsg, + [isDBNoData, isSubNewComingMsg], + ); const scrollHeight = useScrollValue(); - const subNewMsg = async () => { + const subNewComingMsg = useCallback(async () => { if (!worker) return; if (!msgFilter || !validateFilter(msgFilter)) return; - if (isLoadingMsg || isPullRefreshing) return; + if (isLoadingMsg || isPullRefreshing || isSubNewComingMsg) return; const request = async (latest: number | undefined) => { let since = msgFilter.since; @@ -145,8 +155,6 @@ export const MsgFeed: React.FC = ({ }); events = mergeAndSortUniqueDbEvents(events as any, events as any); console.log('sub diff: ', events, events.length, filter); - setNewComingMsg(prev => mergeAndSortUniqueDbEvents(events as any, prev)); - dataStream.unsubscribe(); console.debug('finished sub msg!'); @@ -159,15 +167,37 @@ export const MsgFeed: React.FC = ({ }, }); } + + if (events.length > 0) { + if (isDBNoData && msgList.length === 0) { + setMsgList(events as DbEvent[]); + setIsDBNoData(false); + } else { + setNewComingMsg(prev => + mergeAndSortUniqueDbEvents(events as any, prev), + ); + } + } }; const latest = memoMsgList[0]?.created_at || 0; - request(latest); - }; - - const query = async () => { - if (!msgFilter || !validateFilter(msgFilter)) return [] as DbEvent[]; + setIsSubNewComingMsg(true); + await request(latest); + setIsSubNewComingMsg(false); + }, [ + worker, + msgFilter, + isValidEvent, + isLoadingMsg, + isPullRefreshing, + isSubNewComingMsg, + ]); + + const loadMsgFromDb = useCallback(async () => { + if (!msgFilter || !validateFilter(msgFilter)) return; setIsLoadingMsg(true); + setNewComingMsg([]); + setMsgList([]); // get from cache first const cache = queryCache.get(queryCacheId); @@ -192,14 +222,31 @@ export const MsgFeed: React.FC = ({ }); events = mergeAndSortUniqueDbEvents(events, events); console.log('query: ', events.length, relayUrls, msgFilter); - setMsgList(events); - // save cache - if (events.length > 0) { - queryCache.set(queryCacheId, events); + + if (events.length === 0) { + if (msgList.length === 0) { + setIsDBNoData(true); + } + setIsLoadingMsg(false); + return; } + // save cache + setMsgList(events); + queryCache.set(queryCacheId, events); + setIsDBNoData(false); + setIsLoadingMsg(false); - }; + }, [msgFilter, isValidEvent, relayUrls, queryCache]); + + useLastReplyEvent({ msgList: memoMsgList, worker }); + useInterval(subNewComingMsg, SUB_NEW_MSG_INTERVAL); + useRestoreScrollPos(scrollHeight, queryCacheId, memoMsgList.length > 0); + + useEffect(() => { + if (!worker?.relayGroupId || relayUrls.length === 0) return; + loadMsgFromDb(); + }, [msgFilter, worker?.relayGroupId, relayUrls]); const loadMore = async () => { if (!worker) return; @@ -237,32 +284,6 @@ export const MsgFeed: React.FC = ({ setIsLoadMore(false); }; - useLastReplyEvent({ msgList: memoMsgList, worker }); - useInterval(subNewMsg, 8000); - - // remember and restore the last visit position in the feed - // todo: better way to do this? - useEffect(() => { - if (scrollHeight > 0) { - scrollPositionCache.set(queryCacheId, scrollHeight); - } - }, [scrollHeight]); - useEffect(() => { - const pos = scrollPositionCache.get(queryCacheId); - if (pos && memoMsgList.length > 0) { - window.scrollTo({ top: pos, behavior: 'instant' as ScrollBehavior }); - } - }, [queryCacheId, memoMsgList.length > 0]); - - useEffect(() => { - if (!worker?.relayGroupId || !worker?.relays || worker?.relays.length === 0) - return; - setNewComingMsg([]); - setMsgList([]); - - query(); - }, [msgFilter, worker?.relayGroupId]); - const onClickNewMsg = () => { setMsgList(prev => { const newData = mergeAndSortUniqueDbEvents(newComingMsg, prev); @@ -280,7 +301,7 @@ export const MsgFeed: React.FC = ({ setIsPullRefreshing(true); worker?.subFilter({ filter: msgFilter }); - await query(); + await loadMsgFromDb(); setIsPullRefreshing(false); }; @@ -299,7 +320,7 @@ export const MsgFeed: React.FC = ({ return ( <> - + <> {newComingMsg.length > 0 && ( From e0de57af05b6605ed1f91767cfb0bc93101d0de5 Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Mon, 25 Dec 2023 21:42:21 +0800 Subject: [PATCH 03/14] chore: add copy share link on PostItem menu (#415) --- src/components/PostItems/PostUser/menu.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/PostItems/PostUser/menu.tsx b/src/components/PostItems/PostUser/menu.tsx index a13d2f97..9f6dceb5 100644 --- a/src/components/PostItems/PostUser/menu.tsx +++ b/src/components/PostItems/PostUser/menu.tsx @@ -6,6 +6,22 @@ import Icon from 'components/Icon'; export function PostUserMenu({ event, publicKey, extraMenu }) { const items: MenuProps['items'] = [ + { + label: 'copy share link', + key: 'copy-share-link', + onClick: async () => { + try { + const { copyToClipboard } = await import('utils/common'); + const shareLink = `${window.location.origin}/event/${event.id}`; + await copyToClipboard(shareLink); + message.success( + 'Link copy! paste to web2 platform to share nostr contents!', + ); + } catch (error: any) { + message.error(`share link copy failed! ${error.message}`); + } + }, + }, { label: 'copy note id', key: '0', From 7ef649d820e6af2b12c549c85a341f8fa6ce3590 Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Mon, 25 Dec 2023 21:42:55 +0800 Subject: [PATCH 04/14] fix: event page fetching deps with newConn/worker (#416) --- src/pages/event/[eventId].page.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/event/[eventId].page.tsx b/src/pages/event/[eventId].page.tsx index d59e20ff..e686bd11 100644 --- a/src/pages/event/[eventId].page.tsx +++ b/src/pages/event/[eventId].page.tsx @@ -7,6 +7,7 @@ import { BaseLayout, Left, Right } from 'components/BaseLayout'; import { useState, useEffect } from 'react'; import { dexieDb } from 'core/db'; import { NotePreview, getNotePreview } from 'core/api/preview'; +import { createCallRelay } from 'core/worker/util'; import Head from 'next/head'; import PostItems from 'components/PostItems'; @@ -21,24 +22,31 @@ export default function EventPage({ }) { const { t } = useTranslation(); const router = useRouter(); - const { worker } = useCallWorker(); + const { worker, newConn } = useCallWorker(); const { eventId } = router.query as { eventId: string }; const [rootEvent, setRootEvent] = useState(); const subRootEvent = async () => { if (!worker) return; - const handle = worker.subMsgByEventIds([eventId]).getIterator(); + const callRelay = createCallRelay(newConn); + const handle = worker + .subMsgByEventIds([eventId], undefined, callRelay) + .getIterator(); for await (const data of handle) { if (data.event.id === eventId) { + worker.subMetadata([data.event.pubkey]); + setRootEvent(data.event); break; } } + handle.unsubscribe(); }; useEffect(() => { if (!eventId) return; + if (rootEvent && rootEvent.id === eventId) return; dexieDb.event.get(eventId).then(event => { if (!event) { @@ -47,7 +55,7 @@ export default function EventPage({ } setRootEvent(event); }); - }, [eventId]); + }, [eventId, worker, newConn]); return ( <> From d7bfb7c7ab9ef88cd9b0cd704132e76122314526 Mon Sep 17 00:00:00 2001 From: Yuexun Jiang Date: Tue, 26 Dec 2023 18:05:01 +0800 Subject: [PATCH 05/14] refactor: refactor BaseLayout to improve UX and fix bugs (#417) * refactor: refactor Container components and update styles * refactor: refactoring BaseLayout and Container components * feat: refactor BaseLayout component * feat: adjust BaseLayout grid cols * refactor: update dropdownMenu component * feat: refactor and improve dropdown menu and drawer components * refactor: update BaseLayout for improved functionality and styling * refactor: add note publish dialog * fix: fix type issues * refactor: adjust styling and fix bugs * feat: update user/nav menu when not loggined * refactor: fill main centent when right aside is empty * fix: fix user avatar size * fix: fix review issue * fix: fix drawer height * fix: force fix post content padding issue * fix: try fix --- package.json | 4 + src/components/BaseLayout/add-note.tsx | 42 ++ src/components/BaseLayout/drawer.tsx | 81 ++++ src/components/BaseLayout/hooks.ts | 2 +- src/components/BaseLayout/index.module.scss | 358 ------------------ src/components/BaseLayout/index.tsx | 96 ++--- src/components/BaseLayout/mobile.tsx | 146 ------- src/components/BaseLayout/nav-link.tsx | 51 +++ src/components/BaseLayout/navbar.tsx | 149 ++++++++ src/components/BaseLayout/pc.tsx | 131 ------- src/components/BaseLayout/profile.tsx | 58 +++ src/components/BaseLayout/tabbar.tsx | 78 ++++ src/components/BaseLayout/utils.tsx | 95 ++--- src/components/Container/index.module.scss | 5 - src/components/Container/index.tsx | 15 - src/components/PostItems/PostItem/ui.tsx | 3 +- src/components/PostItems/index.module.scss | 14 +- .../PubNoteTextarea/index.module.scss | 4 - src/components/RelaySelector/index.tsx | 2 +- src/components/RelaySelector/util.ts | 2 +- .../shared/Cascader/index.stories.tsx | 28 +- src/components/shared/Cascader/index.tsx | 2 +- src/components/shared/Cascader/option.tsx | 2 +- .../shared/Cascader/{types.ts => type.ts} | 0 .../shared/ui/Badge/index.stories.tsx | 20 + src/components/shared/ui/Badge/index.tsx | 27 ++ .../shared/ui/Drawer/index.stories.tsx | 48 +++ src/components/shared/ui/Drawer/index.tsx | 113 ++++++ .../shared/ui/DropdownMenu/index.stories.tsx | 67 ++++ .../shared/ui/DropdownMenu/index.tsx | 80 ++++ src/components/shared/ui/DropdownMenu/type.ts | 23 ++ src/pages/home/index.module.scss | 4 +- src/pages/setting/index.page.tsx | 19 +- src/styles/tailwind.css | 18 + tailwind.config.js | 101 ++++- yarn.lock | 78 ++++ 36 files changed, 1146 insertions(+), 820 deletions(-) create mode 100644 src/components/BaseLayout/add-note.tsx create mode 100644 src/components/BaseLayout/drawer.tsx delete mode 100644 src/components/BaseLayout/index.module.scss delete mode 100644 src/components/BaseLayout/mobile.tsx create mode 100644 src/components/BaseLayout/nav-link.tsx create mode 100644 src/components/BaseLayout/navbar.tsx delete mode 100644 src/components/BaseLayout/pc.tsx create mode 100644 src/components/BaseLayout/profile.tsx create mode 100644 src/components/BaseLayout/tabbar.tsx delete mode 100644 src/components/Container/index.module.scss delete mode 100644 src/components/Container/index.tsx rename src/components/shared/Cascader/{types.ts => type.ts} (100%) create mode 100644 src/components/shared/ui/Badge/index.stories.tsx create mode 100644 src/components/shared/ui/Badge/index.tsx create mode 100644 src/components/shared/ui/Drawer/index.stories.tsx create mode 100644 src/components/shared/ui/Drawer/index.tsx create mode 100644 src/components/shared/ui/DropdownMenu/index.stories.tsx create mode 100644 src/components/shared/ui/DropdownMenu/index.tsx create mode 100644 src/components/shared/ui/DropdownMenu/type.ts diff --git a/package.json b/package.json index 5ce6c86b..25598e0c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "@next/bundle-analyzer": "14.0.3", "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.0", + "@radix-ui/react-avatar": "1.0.4", + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-hover-card": "1.0.7", "@radix-ui/react-popover": "1.0.7", "@reduxjs/toolkit": "1.7.1", @@ -129,6 +132,7 @@ "typescript": "4.7.4", "usehooks-ts": "2.9.1", "uuid": "9.0.0", + "vaul": "0.8.0", "wasm": "file:./wasm/pkg", "web-vitals": "2.1.2" }, diff --git a/src/components/BaseLayout/add-note.tsx b/src/components/BaseLayout/add-note.tsx new file mode 100644 index 00000000..d83b725c --- /dev/null +++ b/src/components/BaseLayout/add-note.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import * as Dialog from '@radix-ui/react-dialog'; +import { useTranslation } from 'react-i18next'; +import { cn } from 'utils/classnames'; +import PubNoteTextarea from 'components/PubNoteTextarea'; + +type AddNoteDialogProps = { + children: React.ReactNode; +}; + +const AddNoteDialog = (props: AddNoteDialogProps) => { + const { t } = useTranslation(); + const [opened, setOpened] = useState(false); + + return ( + + {props.children} + + + +
+ + {t('baseLayout.modal.title')} + + + {t('baseLayout.modal.desc')} + +
+ setOpened(false)} /> +
+
+
+ ); +}; + +export default AddNoteDialog; diff --git a/src/components/BaseLayout/drawer.tsx b/src/components/BaseLayout/drawer.tsx new file mode 100644 index 00000000..47b75b00 --- /dev/null +++ b/src/components/BaseLayout/drawer.tsx @@ -0,0 +1,81 @@ +import Icon from 'components/Icon'; +import { + Drawer, + DrawerTrigger, + DrawerContent, + DrawerClose, +} from 'components/shared/ui/Drawer'; +import { EventSetMetadataContent } from 'core/nostr/type'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FiMenu, FiX } from 'react-icons/fi'; +import { useSelector } from 'react-redux'; +import { RootState } from 'store/configureStore'; +import { useWindowSize } from 'usehooks-ts'; +import { NavLink } from './nav-link'; +import { Profile } from './profile'; +import { MenuItem, UserMenus } from './utils'; + +export type UserDrawerProps = { + user?: EventSetMetadataContent; +}; + +export function UserDrawer(props: UserDrawerProps) { + const { user } = props; + const { t } = useTranslation(); + const [opened, setOpened] = useState(false); + const { height } = useWindowSize(); + const isLoggedIn = useSelector( + (state: RootState) => state.loginReducer.isLoggedIn, + ); + + const userMenus = UserMenus.reduce((result, item) => { + if (!isLoggedIn && item.needLogin) { + return result; + } + result.push(item); + return result; + }, [] as MenuItem[]); + + return ( + + +
+ +
+
+ +
+
+
+ + + +
+ +
+
    + {userMenus.map(item => ( + { + setOpened(false); + return true; + }} + > + + {t(item.title)} + + ))} +
+
+
+
+ ); +} diff --git a/src/components/BaseLayout/hooks.ts b/src/components/BaseLayout/hooks.ts index ebad4625..15ae5dfb 100644 --- a/src/components/BaseLayout/hooks.ts +++ b/src/components/BaseLayout/hooks.ts @@ -17,7 +17,7 @@ export function useUserInfo() { if (!worker) return; if (!isValidPublicKey(myPublicKey)) return; worker.subMetadata([myPublicKey]); - }, [worker]); + }, [worker, myPublicKey]); useEffect(() => { if (!isValidPublicKey(myPublicKey)) return; diff --git a/src/components/BaseLayout/index.module.scss b/src/components/BaseLayout/index.module.scss deleted file mode 100644 index b6613401..00000000 --- a/src/components/BaseLayout/index.module.scss +++ /dev/null @@ -1,358 +0,0 @@ -.pcPadNav { - padding-right: var(--basePadding5); - position: sticky; - top: 0; - z-index: 2; - height: 0; - - @include for-pad { - padding-right: initial; - } - - .logo { - padding: var(--basePadding4) var(--basePadding5); - - a { - display: inline-block; - } - - img { - width: 40px; - height: 40px; - object-fit: contain; - vertical-align: middle; - } - } - - ul { - padding: 0; - margin: 0; - margin-top: var(--basePadding7); - list-style: none; - - li { - display: flex; - - &:hover { - background-color: var(--hover-color); - } - padding: { - left: var(--basePadding5); - right: var(--basePadding5); - } - } - - li, - a { - display: flex; - flex: 1; - text-decoration: none; - align-items: center; - height: 56px; - border-radius: 50px; - cursor: pointer; - - span { - @include font-subheader1; - - color: var(--neutral-09); - } - - @include for-pad { - justify-content: center; - } - - &.active { - svg { - fill: var(--neutral-09); - } - - span { - @include font-subheader1-bold; - - color: var(--neutral-09); - } - } - - :global { - .ant-dropdown-trigger { - @include for-desktop { - display: flex; - width: 100%; - } - - h1 { - margin: 0 0 0 var(--basePadding4); - - @include font-header3; - - @include for-pad { - display: none; - } - } - } - } - - svg { - width: 24px; - height: 24px; - fill: var(--neutral-06); - } - - > span { - margin-left: var(--basePadding3); - color: var(--neutral-06); - - @include for-pad { - display: none; - } - } - } - } - - button { - height: 44px; - margin-top: var(--basePadding6); - - @include for-pad { - width: 40px !important; - height: 40px; - display: flex; - justify-content: center; - align-items: center; - margin-left: var(--basePadding5); - - svg { - width: 24px; - height: 24px; - fill: var(--neutral-white); - } - } - } -} - -.pcPadNavUserMenu { - :global { - .ant-dropdown-menu { - border-radius: var(--baseBorderRadius8); - - &:empty { - display: none; - } - } - - .ant-dropdown-menu-item { - height: 40px; - line-height: 40px; - - svg { - width: 24px; - height: 24px; - } - } - } - - li { - @include font-label; - } -} - -.pcPadMain { - display: grid; - grid-template-columns: repeat(9, 1fr); - grid-template-areas: 'left left left left left left left left left'; - - @include for-pad { - grid-template-columns: repeat(7, 1fr); - grid-template-areas: none; - } - - &.rightExists { - grid-template-areas: 'left left left left left left right right right'; - - @include for-pad { - grid-template-areas: 'left left left left left left left left left'; - grid-template-areas: none; - } - } - - .left { - grid-area: left; - - @include for-pad { - grid-area: initial; - grid-column: span 6; - } - } - - .right { - height: 100%; - border-left: 1px solid var(--neutral-02); - padding-left: var(--basePadding5); - grid-area: right; - - @include for-pad { - display: none; - } - } -} - -.modal { - .modalCoseIcons { - width: 24px; - height: 24px; - } - - :global { - .ant-modal-content { - @include for-mobile { - padding: var(--basePadding2); - } - } - } -} - -.mobile { - grid-column: span 4; - - header { - position: sticky; - top: 0; - left: 0; - z-index: 4; - display: flex; - justify-content: space-between; - gap: var(--basePadding2); - align-items: center; - padding: var(--basePadding4); - height: var(--mobileHeaderHeight); - background-color: rgb(255 255 255 / 50%); - backdrop-filter: blur(5px); - - :global { - .ant-avatar { - width: 32px; - height: 32px; - } - } - - img { - width: 32px; - height: 32px; - } - - svg { - width: 24px; - height: 24px; - } - } - - main { - padding: { - bottom: calc(var(--basePadding5) * 5); - } - } - - footer { - position: fixed; - bottom: 0; - left: 0; - z-index: 2; - box-sizing: border-box; - width: 100%; - height: var(--mobileFooterHeight); - background-color: rgb(255 255 255 / 50%); - backdrop-filter: blur(5px); - padding: { - left: var(--basePadding4); - right: var(--basePadding4); - } - - ul { - display: flex; - list-style: none; - padding: 0; - justify-content: space-between; - align-items: flex-end; - - li { - &.active { - svg { - fill: var(--neutral-09); - } - } - - &.add { - span { - display: inline-block; - background-color: var(--primary-06); - border-radius: var(--baseBorderRadius8); - padding: 7px 11px; - line-height: 0; - - svg { - width: 14px; - height: 14px; - fill: var(--neutral-white); - } - } - } - - svg { - width: 20px; - height: 20px; - fill: var(--neutral-06); - } - } - } - } -} - -.mobileDrawer { - padding: var(--basePadding4); - - .close { - width: 24px; - height: 24px; - float: right; - } - - .userTitle { - h1 { - @include font-header3; - } - } - - ul { - padding: 0; - margin: 0; - list-style: none; - - li { - padding: { - top: var(--basePadding4); - bottom: var(--basePadding4); - } - - svg { - width: 24px; - height: 24px; - vertical-align: middle; - } - - &:last-child { - position: absolute; - bottom: 0; - width: 100%; - border-top: 1px solid var(--neutral-02); - } - } - } - - :global { - .ant-drawer-body { - padding: 0; - position: relative; - } - } -} diff --git a/src/components/BaseLayout/index.tsx b/src/components/BaseLayout/index.tsx index 9ea8efc0..2456119c 100644 --- a/src/components/BaseLayout/index.tsx +++ b/src/components/BaseLayout/index.tsx @@ -1,18 +1,10 @@ -import { Modal } from 'antd'; import { useUserInfo } from './hooks'; import { RelaySelector } from 'components/RelaySelector'; -import { useMatchMobile } from 'hooks/useMediaQuery'; -import { useTranslation } from 'next-i18next'; -import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; - -import React, { useState } from 'react'; -import Icon from 'components/Icon'; -import Mobile from './mobile'; -import styles from './index.module.scss'; -import PcPadNav from './pc'; -import Container from 'components/Container'; -import classNames from 'classnames'; -import PubNoteTextarea from '../PubNoteTextarea'; +import React, { useCallback, useState } from 'react'; +import Navbar from './navbar'; +import { UserDrawer } from './drawer'; +import { Tabbar } from './tabbar'; +import { cn } from 'utils/classnames'; export interface BaseLayoutProps { children: React.ReactNode; @@ -39,19 +31,11 @@ export const Right: React.FC = ({ children }) => (
{children}
); -export const BaseLayout: React.FC = ({ - children, - silent, - metaPage, -}) => { - const { t } = useTranslation(); +export const BaseLayout: React.FC = ({ children }) => { const { myProfile } = useUserInfo(); - const isMobile = useMatchMobile(); const leftNodes: React.ReactNode[] = []; const rightNodes: React.ReactNode[] = []; - const [openWrite, setOpenWrite] = useState(false); - React.Children.forEach(children, (child: React.ReactNode) => { if (!React.isValidElement(child)) return; if (child.type === Left) leftNodes.push(child); @@ -59,42 +43,40 @@ export const BaseLayout: React.FC = ({ }); return ( - - {isMobile ? ( - - ) : ( - <> - -
-
-
+
+
+ +
+
+
+
+
- {leftNodes} -
- {rightNodes.length > 0 && ( -
-
{rightNodes}
-
- )} -
- - )} - setOpenWrite(false)} - closeIcon={} - > -

{t('baseLayout.modal.desc')}

- setOpenWrite(false)} /> -
- + + {leftNodes} +
+ +
+
+
+ {rightNodes.length > 0 && ( +
+
{rightNodes}
+
+ )} + + ); }; diff --git a/src/components/BaseLayout/mobile.tsx b/src/components/BaseLayout/mobile.tsx deleted file mode 100644 index 28109e85..00000000 --- a/src/components/BaseLayout/mobile.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Paths } from 'constants/path'; -import { useRouter } from 'next/router'; -import { RootState } from 'store/configureStore'; -import { useDispatch, useSelector } from 'react-redux'; -import { useTranslation } from 'react-i18next'; -import { Avatar, Badge, Drawer } from 'antd'; -import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; -import { EventSetMetadataContent } from 'core/nostr/type'; -import { MenuId, NavMenus, UserMenus, navClick } from './utils'; -import { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { RelaySelector } from 'components/RelaySelector'; -import { shortifyNPub } from 'core/nostr/content'; -import { Nip19, Nip19DataType } from 'core/nip/19'; -import { useNotification } from 'hooks/useNotification'; - -import Icon from 'components/Icon'; -import styles from './index.module.scss'; -import classNames from 'classnames'; - -interface Props { - body: React.ReactNode[]; - user?: EventSetMetadataContent; - setOpenWrite: Dispatch>; -} - -const Mobile: React.FC = ({ body, user, setOpenWrite }) => { - const router = useRouter(); - const isLoggedIn = useSelector( - (state: RootState) => state.loginReducer.isLoggedIn, - ); - const myPublicKey = useReadonlyMyPublicKey(); - const isNewUnread = useNotification(); - const dispatch = useDispatch(); - const doLogout = () => { - dispatch({ - type: 'LOGOUT', - }); - router.push(Paths.login); - }; - - const { t } = useTranslation(); - const [nav, setNav] = useState([]); - const [open, setOpen] = useState(false); - - useEffect(() => { - const result = NavMenus.filter(item => - [ - MenuId.home, - MenuId.communities, - MenuId.relays, - MenuId.notifications, - ].includes(item.id), - ); - result.splice(2, 0, { - id: MenuId.add, - icon: , - title: 'nav.menu.blogDashboard', - link: Paths.write, - }); - setNav(result); - }, []); - - return ( -
-
- {isLoggedIn ? ( - setOpen(true)} /> - ) : ( - } - onClick={() => router.push({ pathname: Paths.login })} - /> - )} -
- -
-
-
{body}
-
-
    - {nav.map((item, key) => ( -
  • { - item.id === MenuId.add - ? setOpenWrite(true) - : navClick(item, myPublicKey, router, isLoggedIn, t); - }} - className={classNames({ - [styles.active]: item.link === router.pathname, - [styles.add]: item.id === MenuId.add, - })} - > - {item.id === MenuId.add ? ( - {item.icon} - ) : item.id === MenuId.notifications && isNewUnread ? ( - {item.icon} - ) : ( - item.icon - )} -
  • - ))} -
-
- setOpen(false)} - open={open} - className={styles.mobileDrawer} - > - setOpen(false)} - /> -
- -

- {user?.name || - user?.display_name || - shortifyNPub(Nip19.encode(myPublicKey, Nip19DataType.Npubkey))} -

-
-
    - {UserMenus.map((item, key) => ( -
  • { - item?.id === MenuId.signOut - ? doLogout() - : navClick(item, myPublicKey, router, isLoggedIn, t); - }} - > - {item?.icon} {item && t(item.title)} -
  • - ))} -
-
-
- ); -}; - -export default Mobile; diff --git a/src/components/BaseLayout/nav-link.tsx b/src/components/BaseLayout/nav-link.tsx new file mode 100644 index 00000000..319fb4a2 --- /dev/null +++ b/src/components/BaseLayout/nav-link.tsx @@ -0,0 +1,51 @@ +import { Paths } from 'constants/path'; +import { useMyPublicKey } from 'hooks/useMyPublicKey'; +import { useRouter } from 'next/router'; +import { createElement, PropsWithChildren, useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { getNavLink, MenuId, MenuItem } from './utils'; + +type NavLinkProps = PropsWithChildren<{ + item: MenuItem; + className?: string; + as?: string | React.ComponentType; + onClick?: (item: MenuItem) => void; +}>; + +export function NavLink(props: NavLinkProps) { + const { item, children } = props; + const myPublicKey = useMyPublicKey(); + const router = useRouter(); + const dispatch = useDispatch(); + + const link = useMemo( + () => getNavLink(item, myPublicKey), + [item, myPublicKey], + ); + + const onClick = useCallback(() => { + const success = props.onClick?.(item); + if (!success) { + return; + } + if (item.id === MenuId.signOut) { + dispatch({ + type: 'LOGOUT', + }); + router.push(Paths.login); + return; + } + router.push(link); + }, [dispatch, item, link, router, props]); + + const component = props.as ?? 'div'; + + return createElement( + component, + { + className: props.className, + onClick, + }, + children, + ); +} diff --git a/src/components/BaseLayout/navbar.tsx b/src/components/BaseLayout/navbar.tsx new file mode 100644 index 00000000..63eb230f --- /dev/null +++ b/src/components/BaseLayout/navbar.tsx @@ -0,0 +1,149 @@ +import { Paths } from 'constants/path'; +import { useRouter } from 'next/router'; +import { RootState } from 'store/configureStore'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; +import { EventSetMetadataContent } from 'core/nostr/type'; +import { MenuId, NavMenus, UserMenus, getNavLink, MenuItem } from './utils'; +import Link from 'next/link'; +import Icon from 'components/Icon'; +import dynamic from 'next/dynamic'; +import { useNotification } from 'hooks/useNotification'; +import { cn } from 'utils/classnames'; +import { Profile } from './profile'; +import { + DropdownMenuContent, + DropdownMenuItem, + DropdownMenu, + DropdownMenuTrigger, +} from 'components/shared/ui/DropdownMenu'; +import { Badge, BadgeDot } from 'components/shared/ui/Badge'; +import AddNoteDialog from './add-note'; +import { NavLink } from './nav-link'; + +const Navbar = ({ user }: { user?: EventSetMetadataContent }) => { + const { t } = useTranslation(); + const router = useRouter(); + const myPublicKey = useReadonlyMyPublicKey(); + const isLoggedIn = useSelector( + (state: RootState) => state.loginReducer.isLoggedIn, + ); + const isNewUnread = useNotification(); + + const userMenus = UserMenus.reduce((result, item) => { + if (!item || item.id === MenuId.bookmarks) return result; + result.push(item); + return result; + }, [] as MenuItem[]); + + const navMenus = NavMenus.reduce((result, item) => { + if (!isLoggedIn && item.needLogin) { + return result; + } + result.push(item); + return result; + }, [] as MenuItem[]); + + return ( + + ); +}; + +export default dynamic(() => Promise.resolve(Navbar), { + ssr: false, +}); diff --git a/src/components/BaseLayout/pc.tsx b/src/components/BaseLayout/pc.tsx deleted file mode 100644 index 9da4ef70..00000000 --- a/src/components/BaseLayout/pc.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Paths } from 'constants/path'; -import { useRouter } from 'next/router'; -import { RootState } from 'store/configureStore'; -import { useDispatch, useSelector } from 'react-redux'; -import { useMatchPad } from 'hooks/useMediaQuery'; -import { MenuItemType } from 'antd/es/menu/hooks/useItems'; -import { useTranslation } from 'react-i18next'; -import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; -import { EventSetMetadataContent } from 'core/nostr/type'; -import { Avatar, Badge, Button, Dropdown } from 'antd'; -import { Dispatch, SetStateAction } from 'react'; -import { MenuId, NavMenus, UserMenus, navClick, getNavLink } from './utils'; - -import Link from 'next/link'; -import Icon from 'components/Icon'; -import styles from './index.module.scss'; -import dynamic from 'next/dynamic'; -import { useNotification } from 'hooks/useNotification'; -import { shortifyPublicKey } from 'core/nostr/content'; - -const PcPadNav = ({ - user, - setOpenWrite, -}: { - user?: EventSetMetadataContent; - setOpenWrite: Dispatch>; -}) => { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const isPad = useMatchPad(); - const router = useRouter(); - const myPublicKey = useReadonlyMyPublicKey(); - const isLoggedIn = useSelector( - (state: RootState) => state.loginReducer.isLoggedIn, - ); - const isNewUnread = useNotification(); - const doLogout = () => { - dispatch({ - type: 'LOGOUT', - }); - router.push(Paths.login); - }; - - const userMenus = UserMenus.reduce((result, item) => { - if (!item || item.id === MenuId.bookmarks) return result; - - result.push({ - icon: item?.icon, - key: item?.id, - label: t(item?.title), - onClick: () => - item.id === MenuId.signOut - ? doLogout() - : navClick(item, myPublicKey, router, isLoggedIn, t), - }); - return result; - }, [] as MenuItemType[]); - - return ( - - ); -}; - -export default dynamic(() => Promise.resolve(PcPadNav), { - ssr: false, -}); diff --git a/src/components/BaseLayout/profile.tsx b/src/components/BaseLayout/profile.tsx new file mode 100644 index 00000000..ab371ce8 --- /dev/null +++ b/src/components/BaseLayout/profile.tsx @@ -0,0 +1,58 @@ +import * as Avatar from '@radix-ui/react-avatar'; +import { shortifyPublicKey } from 'core/nostr/content'; +import Icon from 'components/Icon'; +import { EventSetMetadataContent } from 'core/nostr/type'; +import { useTranslation } from 'react-i18next'; +import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; +import { cn } from 'utils/classnames'; +import { useCallback } from 'react'; +import { useRouter } from 'next/router'; +import { Paths } from 'constants/path'; + +type ProfileProps = { + user?: EventSetMetadataContent; + showName?: boolean; + className?: string; +}; + +export function Profile(props: ProfileProps) { + const { t } = useTranslation(); + const { user, showName, className } = props; + const myPublicKey = useReadonlyMyPublicKey(); + const isLoggedIn = !!myPublicKey; + const router = useRouter(); + + const onClick = useCallback(() => { + if (!isLoggedIn) { + router.push(Paths.login); + return; + } + router.push(Paths.user + myPublicKey); + }, [isLoggedIn, router, myPublicKey]); + + return ( +
+ + {user && } + + + + +

+ {isLoggedIn + ? user?.name || shortifyPublicKey(myPublicKey) + : t('nav.menu.signIn')} +

+
+ ); +} diff --git a/src/components/BaseLayout/tabbar.tsx b/src/components/BaseLayout/tabbar.tsx new file mode 100644 index 00000000..7403b28a --- /dev/null +++ b/src/components/BaseLayout/tabbar.tsx @@ -0,0 +1,78 @@ +import Icon from 'components/Icon'; +import { Paths } from 'constants/path'; +import { useRouter } from 'next/router'; +import { useCallback, useMemo } from 'react'; +import { cn } from 'utils/classnames'; +import AddNoteDialog from './add-note'; +import { NavLink } from './nav-link'; +import { NavMenus, MenuId, MenuItem } from './utils'; + +export function Tabbar() { + const router = useRouter(); + + const navs = useMemo(() => { + const result = NavMenus.filter(item => + [ + MenuId.home, + MenuId.communities, + MenuId.relays, + MenuId.notifications, + ].includes(item.id), + ); + result.splice(2, 0, { + id: MenuId.add, + icon: 'icon-plus', + title: 'nav.menu.blogDashboard', + link: Paths.write, + }); + return result; + }, []); + + const renderItem = useCallback( + (item: MenuItem) => { + return ( + item.id !== MenuId.add} + > +
+ +
+
+ ); + }, + [router], + ); + + return ( +
+
    + {navs.map(item => ( + <> + {item.id === MenuId.add ? ( + {renderItem(item)} + ) : ( + renderItem(item) + )} + + ))} +
+
+ ); +} diff --git a/src/components/BaseLayout/utils.tsx b/src/components/BaseLayout/utils.tsx index f1458cb0..f1f08685 100644 --- a/src/components/BaseLayout/utils.tsx +++ b/src/components/BaseLayout/utils.tsx @@ -1,6 +1,4 @@ import { Paths } from 'constants/path'; -import Icon from 'components/Icon'; -import { message } from 'antd'; export enum MenuId { home = 'home', @@ -19,135 +17,126 @@ export enum MenuId { about = 'about', } -export const NavMenus = [ +export type MenuItem = { + id: MenuId; + icon: string; + title: string; + link: string; + needLogin?: boolean; +}; + +export const NavMenus: MenuItem[] = [ { id: MenuId.home, - icon: , + icon: 'icon-home', title: 'nav.menu.home', link: Paths.home, }, { id: MenuId.communities, - icon: , + icon: 'icon-explore', title: 'nav.menu.communities', link: Paths.communities, }, { id: MenuId.relays, - icon: , + icon: 'icon-Relay', title: 'nav.menu.relays', link: Paths.relay, }, { id: MenuId.bookmarks, - icon: , + icon: 'icon-bookmark', title: 'nav.menu.bookmarks', link: Paths.bookmarks, + needLogin: true, }, { id: MenuId.search, - icon: , + icon: 'icon-search', title: 'nav.menu.search', link: Paths.search, }, { id: MenuId.notifications, - icon: , + icon: 'icon-notification', title: 'nav.menu.notifications', link: Paths.notification, + needLogin: true, }, { id: MenuId.preference, - icon: , + icon: 'icon-Gear', title: 'nav.menu.preference', link: Paths.preference, }, { id: MenuId.profile, - icon: , + icon: 'icon-user', title: 'nav.menu.profile', link: Paths.user, + needLogin: true, }, { id: MenuId.about, - icon: , + icon: 'icon-emoji', title: 'nav.menu.about', link: Paths.about, }, ]; -export const UserMenus = [ +export const UserMenus: MenuItem[] = [ { id: MenuId.profile, - icon: , + icon: 'icon-user', title: 'nav.menu.profile', link: Paths.user, + needLogin: true, + }, + { + id: MenuId.bookmarks, + icon: 'icon-bookmark', + title: 'nav.menu.bookmarks', + link: Paths.bookmarks, + needLogin: true, }, - NavMenus[3], { id: MenuId.search, - icon: , + icon: 'icon-search', title: 'nav.menu.search', link: Paths.search, }, - , { id: MenuId.drafts, - icon: , + icon: 'icon-Draft', title: 'nav.menu.drafts', link: Paths.draft, + needLogin: true, }, { id: MenuId.settings, - icon: , + icon: 'icon-Gear', title: 'nav.menu.setting', link: Paths.setting, }, { id: MenuId.about, - icon: , + icon: 'icon-emoji', title: 'nav.menu.about', link: Paths.about, }, { id: MenuId.signOut, - icon: , + icon: 'icon-Move-out', title: 'nav.menu.signOut', link: Paths.login, + needLogin: true, }, ]; -/** - * Get nav link - * @param item - * @param myPublicKey - * @returns {string} - */ -export const getNavLink = (item, myPublicKey) => { - let link = item.link; - if (item.id === MenuId.profile) link = item.link + myPublicKey; - return link; -}; - -/** - * Navigation action - * @param item - * @param myPublicKey - * @param router - * @param isLoggedIn - * @param t - * @returns {undefined} - */ -export const navClick = (item, myPublicKey, router, isLoggedIn, t) => { - const link: string | undefined = getNavLink(item, myPublicKey); - - if ( - !isLoggedIn && - [MenuId.notifications, MenuId.bookmarks].includes(item.id) - ) { - message.warning(t('comment.login')); - return; +export const getNavLink = (item: MenuItem, myPublicKey: string) => { + if (item.id === MenuId.profile) { + return `${item.link}/${myPublicKey}`; } - - router.push(link); + return item.link; }; diff --git a/src/components/Container/index.module.scss b/src/components/Container/index.module.scss deleted file mode 100644 index 2e7669d8..00000000 --- a/src/components/Container/index.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -.container { - @include font-body; - - color: var(--neutral-09); -} diff --git a/src/components/Container/index.tsx b/src/components/Container/index.tsx deleted file mode 100644 index b9e7f67c..00000000 --- a/src/components/Container/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import classNames from 'classnames'; -import styles from './index.module.scss'; - -interface ContainerProps { - children: React.ReactNode; -} - -const Container: React.FC = ({ children }) => { - return ( -
- {children} -
- ); -}; -export default Container; diff --git a/src/components/PostItems/PostItem/ui.tsx b/src/components/PostItems/PostItem/ui.tsx index c0650729..22d28356 100644 --- a/src/components/PostItems/PostItem/ui.tsx +++ b/src/components/PostItems/PostItem/ui.tsx @@ -8,6 +8,7 @@ import { EventSetMetadataContent } from 'core/nostr/type'; import styles from '../index.module.scss'; import dynamic from 'next/dynamic'; +import { cn } from 'utils/classnames'; const PostUser = dynamic( async () => { @@ -62,7 +63,7 @@ export const PostUI: React.FC = ({ event={event} extraMenu={extraMenu} /> -
+
{content} = 767px) { + padding-left: calc(var(--padding) * 14); } } } -.post .embed{ - padding: 0; +.post .embed { + padding: 0; border-bottom: none; } @@ -42,7 +42,7 @@ } } -.communityPostTitle{ +.communityPostTitle { display: flex; justify-content: flex-start; align-items: center; @@ -55,5 +55,5 @@ svg { width: 18px; height: 18px; - } + } } diff --git a/src/components/PubNoteTextarea/index.module.scss b/src/components/PubNoteTextarea/index.module.scss index 2885a01c..5e76726e 100644 --- a/src/components/PubNoteTextarea/index.module.scss +++ b/src/components/PubNoteTextarea/index.module.scss @@ -6,10 +6,6 @@ padding: 10px; border-radius: var(--baseBorderRadius8); - @include for-mobile{ - margin: 0; - } - &.focus{ border: 1px solid var(--primary-06); } diff --git a/src/components/RelaySelector/index.tsx b/src/components/RelaySelector/index.tsx index 4d9c3316..7f58e744 100644 --- a/src/components/RelaySelector/index.tsx +++ b/src/components/RelaySelector/index.tsx @@ -1,5 +1,5 @@ import { Cascader } from 'components/shared/Cascader'; -import { ICascaderOption } from 'components/shared/Cascader/types'; +import { ICascaderOption } from 'components/shared/Cascader/type'; import { Paths } from 'constants/path'; import { NIP_65_RELAY_LIST } from 'constants/relay'; import { Nip65 } from 'core/nip/65'; diff --git a/src/components/RelaySelector/util.ts b/src/components/RelaySelector/util.ts index 62785750..6444e2bb 100644 --- a/src/components/RelaySelector/util.ts +++ b/src/components/RelaySelector/util.ts @@ -1,7 +1,7 @@ import { RelayGroupMap } from 'core/relay/group/type'; import { RelayFooterMenus, RelayMode } from './type'; import { WsConnectStatus } from 'core/worker/type'; -import { ICascaderOption } from 'components/shared/Cascader/types'; +import { ICascaderOption } from 'components/shared/Cascader/type'; export function getSelectGroupId(groups: RelayGroupMap) { return Array.from(groups.keys()).filter(key => groups.get(key) != null); diff --git a/src/components/shared/Cascader/index.stories.tsx b/src/components/shared/Cascader/index.stories.tsx index 53523226..31bea324 100644 --- a/src/components/shared/Cascader/index.stories.tsx +++ b/src/components/shared/Cascader/index.stories.tsx @@ -11,19 +11,20 @@ type Story = StoryObj; const options = [ [ { - value: 'Relay Group', - children: [ - { - value: 'NIP Relay List', - }, - { - value: 'Default Group', - }, - ], + label: 'NIP Relay List', + value: 'NIPRelayList', group: 'Global', }, { - value: 'Single Relay', + label: 'Default Group', + value: 'DefaultGroup', + group: 'Global', + }, + ], + [ + { + label: 'Single Relay', + value: 'SingleRelay', children: [ { value: 'relay.nostr.band', @@ -32,12 +33,11 @@ const options = [ value: 'relay.snort.social', }, ], - group: 'Global', }, { - value: 'Script', + label: 'Relay Script', + value: 'script', disabled: true, - group: 'Rules', }, ], ]; @@ -45,7 +45,7 @@ const options = [ export const Primary: Story = { render: () => (
- +
), }; diff --git a/src/components/shared/Cascader/index.tsx b/src/components/shared/Cascader/index.tsx index 49c3862e..b25eb961 100644 --- a/src/components/shared/Cascader/index.tsx +++ b/src/components/shared/Cascader/index.tsx @@ -7,7 +7,7 @@ import { useMemo, useState, } from 'react'; -import { ICascaderOption } from './types'; +import { ICascaderOption } from './type'; import { CascaderOption } from './option'; import { cn } from 'utils/classnames'; diff --git a/src/components/shared/Cascader/option.tsx b/src/components/shared/Cascader/option.tsx index 82cb390a..8f9eeef7 100644 --- a/src/components/shared/Cascader/option.tsx +++ b/src/components/shared/Cascader/option.tsx @@ -1,4 +1,4 @@ -import { ICascaderOption } from './types'; +import { ICascaderOption } from './type'; import { FiCheck } from 'react-icons/fi'; import { FiChevronRight } from 'react-icons/fi'; import * as HoverCard from '@radix-ui/react-hover-card'; diff --git a/src/components/shared/Cascader/types.ts b/src/components/shared/Cascader/type.ts similarity index 100% rename from src/components/shared/Cascader/types.ts rename to src/components/shared/Cascader/type.ts diff --git a/src/components/shared/ui/Badge/index.stories.tsx b/src/components/shared/ui/Badge/index.stories.tsx new file mode 100644 index 00000000..1f751166 --- /dev/null +++ b/src/components/shared/ui/Badge/index.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BadgeDot, Badge } from '.'; + +const meta: Meta = { + component: Badge, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + render: () => ( +
+ + Badge + + +
+ ), +}; diff --git a/src/components/shared/ui/Badge/index.tsx b/src/components/shared/ui/Badge/index.tsx new file mode 100644 index 00000000..26d96799 --- /dev/null +++ b/src/components/shared/ui/Badge/index.tsx @@ -0,0 +1,27 @@ +import { forwardRef } from 'react'; +import { cn } from 'utils/classnames'; + +const Badge = forwardRef< + HTMLDivElement, + React.PropsWithChildren<{ className?: string }> +>(({ className, children }, ref) => ( +
+ {children} +
+)); +Badge.displayName = 'BadgeRoot'; + +const BadgeDot = forwardRef( + ({ className }, ref) => ( + + ), +); +BadgeDot.displayName = 'BadgeDot'; + +export { Badge, BadgeDot }; diff --git a/src/components/shared/ui/Drawer/index.stories.tsx b/src/components/shared/ui/Drawer/index.stories.tsx new file mode 100644 index 00000000..7fc2b3b0 --- /dev/null +++ b/src/components/shared/ui/Drawer/index.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '.'; + +const meta: Meta = { + component: Drawer, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + render: () => ( +
+ + Open Drawer + +
+ + Move Goal + + Set your daily activity goal. + + +
+
+ Decrease +
+
+ + + Cancel + +
+
+
+
+ ), +}; diff --git a/src/components/shared/ui/Drawer/index.tsx b/src/components/shared/ui/Drawer/index.tsx new file mode 100644 index 00000000..6b14a3ad --- /dev/null +++ b/src/components/shared/ui/Drawer/index.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { cn } from 'utils/classnames'; +import { Drawer as DrawerPrimitive } from 'vaul'; + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +); +Drawer.displayName = 'Drawer'; + +const DrawerTrigger = DrawerPrimitive.Trigger; +const DrawerPortal = DrawerPrimitive.Portal; +const DrawerClose = DrawerPrimitive.Close; + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)); +DrawerContent.displayName = 'DrawerContent'; + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DrawerHeader.displayName = 'DrawerHeader'; + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DrawerFooter.displayName = 'DrawerFooter'; + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/src/components/shared/ui/DropdownMenu/index.stories.tsx b/src/components/shared/ui/DropdownMenu/index.stories.tsx new file mode 100644 index 00000000..da26d3f9 --- /dev/null +++ b/src/components/shared/ui/DropdownMenu/index.stories.tsx @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ +import type { Meta, StoryObj } from '@storybook/react'; +import Icon from 'components/Icon'; +import { + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenu, + DropdownMenuTrigger, +} from '.'; + +const meta: Meta = { + component: DropdownMenu, +}; + +export default meta; +type Story = StoryObj; + +const items = [ + { + icon: 'icon-user', + label: '1st menu item', + value: '1', + }, + { + icon: 'icon-user', + label: '2nd menu item', + value: '2', + }, +]; + +export const Primary: Story = { + render: () => ( +
+ + DropdownMenu + + Group + {items.map(item => ( + {item.label} + ))} + + +
+ ), +}; + +export const WithIcon: Story = { + render: () => ( +
+ + DropdownMenu + + {items.map(item => ( + + + {item.label} + + ))} + + +
+ ), +}; diff --git a/src/components/shared/ui/DropdownMenu/index.tsx b/src/components/shared/ui/DropdownMenu/index.tsx new file mode 100644 index 00000000..1849deb6 --- /dev/null +++ b/src/components/shared/ui/DropdownMenu/index.tsx @@ -0,0 +1,80 @@ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { forwardRef } from 'react'; +import { cn } from 'utils/classnames'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +DropdownMenuTrigger.displayName = 'DropdownMenuTrigger'; + +const DropdownMenuContent = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, children, ...props }, ref) => ( + + + {children} + + +)); +DropdownMenuContent.displayName = 'DropdownMenuContent'; + +const DropdownMenuLabel = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +DropdownMenuLabel.displayName = 'DropdownMenuLabel'; + +const DropdownMenuItem = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +DropdownMenuItem.displayName = 'DropdownMenuItem'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuItem, +}; diff --git a/src/components/shared/ui/DropdownMenu/type.ts b/src/components/shared/ui/DropdownMenu/type.ts new file mode 100644 index 00000000..957db392 --- /dev/null +++ b/src/components/shared/ui/DropdownMenu/type.ts @@ -0,0 +1,23 @@ +export type DropdownMenuItemType = { + type: 'item'; + icon?: string; + label: string; + value: string; + disabled?: boolean; + [key: string]: any; +}; + +export type DropdownMenuGroupType = { + type: 'group'; + label: string; + children: DropdownMenuItemType[]; +}; + +export type DropdownMenuDividerType = { + type: 'divider'; +}; + +export type DropdownMenuItem = + | DropdownMenuItemType + | DropdownMenuGroupType + | DropdownMenuDividerType; diff --git a/src/pages/home/index.module.scss b/src/pages/home/index.module.scss index 166f4e0a..f7ab1872 100644 --- a/src/pages/home/index.module.scss +++ b/src/pages/home/index.module.scss @@ -203,7 +203,7 @@ align-self: stretch; position: sticky; top: 0; - z-index: 100; + z-index: 10; background: var(--neutral-01); :global { @@ -236,8 +236,8 @@ .mobileFilter { position: sticky; top: 64px; - z-index: 100; background: var(--neutral-01); + z-index: 10; :global { .ant-segmented { diff --git a/src/pages/setting/index.page.tsx b/src/pages/setting/index.page.tsx index 9f5db739..3a76f979 100644 --- a/src/pages/setting/index.page.tsx +++ b/src/pages/setting/index.page.tsx @@ -52,6 +52,9 @@ export const EditProfilePage = ({ commitId }) => { const signEvent = useSelector( (state: RootState) => state.loginReducer.signEvent, ); + const isLoggedIn = useSelector( + (state: RootState) => state.loginReducer.isLoggedIn, + ); const [profile, setProfile] = useState< EventSetMetadataContent & { created_at: number } >(); @@ -251,12 +254,16 @@ export const EditProfilePage = ({ commitId }) => { - + {isLoggedIn ? ( + + ) : ( + + )} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index b5c61c95..a9b7c80e 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -1,3 +1,21 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer components { + .body { + @apply font-noto text-sm leading-6 font-normal; + } + + .subheader1 { + @apply font-poppins text-base leading-6 font-normal; + } + + .subheader1-bold { + @apply font-poppins text-base leading-6 font-semibold; + } + + .label { + @apply font-noto text-sm leading-[22px] font-medium; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index faeee872..58107586 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -16,15 +16,15 @@ module.exports = { brand: '#598022', primary: { DEFAULT: '#183F00', - 900: '#183F00', - 800: '#2E5203', - 700: '#44661B', + 100: '#fbfff0', + 200: '#ddeeba', + 300: '#c0e085', + 400: '#98c354', + 500: '#739f36', 600: '#598022', - 500: '#739F36', - 400: '#98C354', - 300: '#C0E085', - 200: '#DDEEBA', - 100: '#FBFFF0', + 700: '#44661b', + 800: '#2e5203', + 900: '#183f00', }, gray: { 900: '#1E1E1E', @@ -37,14 +37,50 @@ module.exports = { 200: '#EDEDED', 100: '#F9F9F9', }, - surface: { - '01': '#F9F9F9', - '01-accent': '#F2F2F2', - '02': '#FFFFFF', - '02-accent': '#EDEDED', - '03': '#F7FAF5', - '04': '#DDEEBA', - inverse: '#1E1E1E', + neutral: { + 100: '#f9f9f9', + 200: '#ededed', + 300: '#d2d2d2', + 400: '#bcbcbc', + 500: '#8e8e8e', + 600: '#787878', + 700: '#626262', + 800: '#4b4b4b', + 900: '#1e1e1e', + white: '#fff', + }, + red: { + 100: '#fff6f5', + 200: '#ffd7d6', + 300: '#db8784', + 400: '#c25b5b', + 500: '#9e3f3f', + 600: '#802226', + 700: '#661417', + 800: '#530707', + 900: '#400002', + }, + blue: { + 100: '#f4f8ff', + 200: '#baceee', + 300: '#86a8e0', + 400: '#547fc3', + 500: '#365e9f', + 600: '#224680', + 700: '#1b3866', + 800: '#0a2653', + 900: '#001840', + }, + yellow: { + 100: '#fffced', + 200: '#fcf3c4', + 300: '#ffe58f', + 400: '#ebbc3e', + 500: '#bd8c1a', + 600: '#8f6511', + 700: '#7a5008', + 800: '#593505', + 900: '#4e2c00', }, text: { primary: '#1E1E1E', @@ -56,12 +92,45 @@ module.exports = { inverseLink: '#C0E085', disabled: '#BCBCBC', }, + surface: { + '01': '#F9F9F9', + '01-accent': '#F2F2F2', + '02': '#FFFFFF', + '02-accent': '#EDEDED', + '03': '#F7FAF5', + '04': '#DDEEBA', + inverse: '#1E1E1E', + }, + border: { + primary: '598022', + '01': '#EDEDED', + '02': '#D2D2D2', + '03': '#787878', + }, + overflay: { + '01': 'rgba(30, 30, 30, 0.08)', + '02': 'rgba(30, 30, 30, 0.35)', + }, conditional: { hover01: `rgba(30, 30, 30, 0.08)`, selected01: '#FBFFF0', selected02: '#C0E085', }, }, + animation: { + 'overlay-show': 'overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)', + 'content-show': 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)', + }, + keyframes: { + overlayShow: { + from: { opacity: 0 }, + to: { opacity: 1 }, + }, + contentShow: { + from: { opacity: 0, transform: 'translate(-50%, -48%) scale(0.96)' }, + to: { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' }, + }, + }, }, }, plugins: [], diff --git a/yarn.lock b/yarn.lock index 38d55ad2..42b2aa48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3413,6 +3413,17 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-avatar@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" + integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-collection@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" @@ -3438,6 +3449,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@1.0.5", "@radix-ui/react-dialog@^1.0.4": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-direction@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" @@ -3469,6 +3501,20 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-escape-keydown" "1.0.3" +"@radix-ui/react-dropdown-menu@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63" + integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-menu" "2.0.6" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-focus-guards@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" @@ -3520,6 +3566,31 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-menu@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" + integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-popover@1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c" @@ -18674,6 +18745,13 @@ vary@~1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vaul@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.8.0.tgz#f2155dcb5561d33df4f0794603e91e35672a8d58" + integrity sha512-9nUU2jIObJvJZxeQU1oVr/syKo5XqbRoOMoTEt0hHlWify4QZFlqTh6QSN/yxoKzNrMeEQzxbc3XC/vkPLOIqw== + dependencies: + "@radix-ui/react-dialog" "^1.0.4" + vfile-message@^3.0.0: version "3.1.4" resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" From 183613184f30bb2846f46f6f246094afa9b5ac13 Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:11:35 +0800 Subject: [PATCH 06/14] Build: update ci.yaml with develop branch (#419) --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fbd40741..6c4c8bda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,9 +3,11 @@ on: pull_request: branches: - master + - develop push: branches: - master + - develop jobs: lint: From a56bad180263c49c0d7d0306e6be382f56e6dacc Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:12:03 +0800 Subject: [PATCH 07/14] fix: meta tag not overwrite the _document (#420) --- src/pages/_app.page.tsx | 23 +++++++++++++++++++++++ src/pages/_document.page.tsx | 25 ------------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/pages/_app.page.tsx b/src/pages/_app.page.tsx index 948436b2..2515a9c2 100644 --- a/src/pages/_app.page.tsx +++ b/src/pages/_app.page.tsx @@ -34,6 +34,29 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => { name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> + + + {/* twitter */} + + + + + + + + {/* ogp */} + + + + + + diff --git a/src/pages/_document.page.tsx b/src/pages/_document.page.tsx index 54067fa1..e04b7d50 100644 --- a/src/pages/_document.page.tsx +++ b/src/pages/_document.page.tsx @@ -13,13 +13,7 @@ class FlyCatDocument extends Document { - - - {/* apple pwa */} @@ -33,25 +27,6 @@ class FlyCatDocument extends Document { - {/* twitter */} - - - - - - - - {/* ogp */} - - - - - - - {/* fonts */} From 91184d75ec96fa736433b97a2032488de012079c Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Wed, 27 Dec 2023 00:17:51 +0800 Subject: [PATCH 08/14] refactor: simplify msg filter options to one layer (#424) * refacotor: simplify msg filter options to one layer * fix: use different key and add global-all filter * fix: hide follow mode filter when not sign-in * fix: query noscript and global isValidEvent * refactor: seperate query and sub noscript * fix: rm newconn for query deps --- src/{pages/home => core/msg-filter}/filter.ts | 113 +++++++------- src/{pages/home => core/msg-filter}/util.ts | 17 --- src/pages/home/constants.ts | 3 +- src/pages/home/custom-filter.tsx | 122 --------------- src/pages/home/hooks/useQueryNoscript.ts | 84 +++++++++++ .../{hooks.ts => hooks/useSubContactList.ts} | 0 src/pages/home/index.tsx | 139 +++++++++--------- src/pages/perf/index.page.tsx | 10 +- 8 files changed, 216 insertions(+), 272 deletions(-) rename src/{pages/home => core/msg-filter}/filter.ts (58%) rename src/{pages/home => core/msg-filter}/util.ts (59%) delete mode 100644 src/pages/home/custom-filter.tsx create mode 100644 src/pages/home/hooks/useQueryNoscript.ts rename src/pages/home/{hooks.ts => hooks/useSubContactList.ts} (100%) 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
From b2bc85f66af935a4e8cf97359822771d99d33823 Mon Sep 17 00:00:00 2001 From: Yuexun Jiang Date: Wed, 27 Dec 2023 11:25:30 +0800 Subject: [PATCH 09/14] fix: fix navbar dropdown menu and main centent padding bottom (#426) * fix: fix navbar dropdown menu and main centent padding bottom * style: improve styling in RelaySelector component - Improved readability of the `displayRender` function in `index.tsx` - Updated CSS classes for background and font in the `displayRender` function - Addressed changes for better rendering in `RelaySelector` component --- src/components/BaseLayout/index.tsx | 2 +- src/components/BaseLayout/nav-link.tsx | 2 +- src/components/RelaySelector/index.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/BaseLayout/index.tsx b/src/components/BaseLayout/index.tsx index 2456119c..d127037f 100644 --- a/src/components/BaseLayout/index.tsx +++ b/src/components/BaseLayout/index.tsx @@ -58,7 +58,7 @@ export const BaseLayout: React.FC = ({ children }) => { }, )} > -
+
diff --git a/src/components/BaseLayout/nav-link.tsx b/src/components/BaseLayout/nav-link.tsx index 319fb4a2..dac5fc1a 100644 --- a/src/components/BaseLayout/nav-link.tsx +++ b/src/components/BaseLayout/nav-link.tsx @@ -25,7 +25,7 @@ export function NavLink(props: NavLinkProps) { const onClick = useCallback(() => { const success = props.onClick?.(item); - if (!success) { + if (props.onClick && !success) { return; } if (item.id === MenuId.signOut) { diff --git a/src/components/RelaySelector/index.tsx b/src/components/RelaySelector/index.tsx index 7f58e744..07ea96e4 100644 --- a/src/components/RelaySelector/index.tsx +++ b/src/components/RelaySelector/index.tsx @@ -179,11 +179,11 @@ export function RelaySelector({
- + {mode ? toLabel(toRelayMode(mode)) : toLabel(RelayMode.Group)}
- + {toConnectStatus( groupId ?? 'default', wsConnectStatus, From c2955e0206fec19f5ee96cfbb820f938b4897e36 Mon Sep 17 00:00:00 2001 From: Yuexun Jiang Date: Wed, 27 Dec 2023 13:36:11 +0800 Subject: [PATCH 10/14] refactor: update post rendering logic and update translations. (#428) --- public/locales/en/common.json | 1 + public/locales/zh/common.json | 1 + .../PostItems/PostContent/content.tsx | 1 + .../PostItems/PostContent/index.tsx | 6 +-- src/components/PostItems/PostItem/index.tsx | 13 +++++- .../PostItems/PostUser/index.module.scss | 43 ------------------- src/components/PostItems/PostUser/index.tsx | 40 +++++++++++------ src/components/PostItems/PostUser/menu.tsx | 13 +++--- src/components/PostItems/index.tsx | 3 +- src/hooks/useTimeSince.ts | 41 ++++++++---------- src/styles/tailwind.css | 12 ++++++ 11 files changed, 82 insertions(+), 92 deletions(-) delete mode 100644 src/components/PostItems/PostUser/index.module.scss diff --git a/public/locales/en/common.json b/public/locales/en/common.json index bfdf4882..96d79e28 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -4,6 +4,7 @@ "noNumberData": "?" }, "timeSince": { + "now": "just now", "seconds": "seconds ago", "minutes": "minutes ago", "hours": "hours ago", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 9ea2483c..523d7dc4 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -4,6 +4,7 @@ "noNumberData": "?" }, "timeSince": { + "now": "刚刚", "seconds": "秒钟前", "minutes": "分钟前", "hours": "小时前", diff --git a/src/components/PostItems/PostContent/content.tsx b/src/components/PostItems/PostContent/content.tsx index 0a91e789..ace8e1be 100644 --- a/src/components/PostItems/PostContent/content.tsx +++ b/src/components/PostItems/PostContent/content.tsx @@ -96,6 +96,7 @@ export const renderContent = (elements: ParsedFragment[], isNsfw = false) => { src={element.content} width={15} height={15} + alt={element.content} className="custom-emoji" />, ); diff --git a/src/components/PostItems/PostContent/index.tsx b/src/components/PostItems/PostContent/index.tsx index 1bc5b894..9ebe984a 100644 --- a/src/components/PostItems/PostContent/index.tsx +++ b/src/components/PostItems/PostContent/index.tsx @@ -1,6 +1,5 @@ import { EventTags } from 'core/nostr/type'; import { Event } from 'core/nostr/Event'; -import { useTranslation } from 'next-i18next'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Button } from 'antd'; import { CallWorker } from 'core/worker/caller'; @@ -34,7 +33,6 @@ export const PostContent: React.FC = ({ showLastReplyToEvent = true, isExpanded = false, }) => { - const { t } = useTranslation(); const router = useRouter(); const lastReplyToEventId = useMemo(() => { @@ -45,7 +43,7 @@ export const PostContent: React.FC = ({ }) .pop(); return lastReply?.id; - }, [msgEvent.id]); + }, [msgEvent]); const [expanded, setExpanded] = useState(isExpanded); const [content, setContent] = useState([]); @@ -63,7 +61,7 @@ export const PostContent: React.FC = ({ useEffect(() => { const elements = doTextTransformer(msgEvent.id, msgEvent.content, []); setContent(renderContent(elements, isNsfwEvent(msgEvent))); - }, []); + }, [msgEvent]); const contentStyle = { cursor: 'pointer' }; const onContentClick = e => { diff --git a/src/components/PostItems/PostItem/index.tsx b/src/components/PostItems/PostItem/index.tsx index 017d8ca0..986ac0c2 100644 --- a/src/components/PostItems/PostItem/index.tsx +++ b/src/components/PostItems/PostItem/index.tsx @@ -46,7 +46,7 @@ const PostArticle = dynamic( const PostContent = dynamic( async () => { - const { PostContent } = await import('../PostContent/index'); + const { PostContent } = await import('../PostContent'); return PostContent; }, { @@ -159,7 +159,16 @@ export const PostItem: React.FC = ({ extraHeader={extraHeader} /> ); - }, [event.id, profile]); + }, [ + event, + profile, + worker, + showLastReplyToEvent, + showFromCommunity, + extraMenu, + extraHeader, + isExpanded, + ]); return render; }; diff --git a/src/components/PostItems/PostUser/index.module.scss b/src/components/PostItems/PostUser/index.module.scss deleted file mode 100644 index 7763a92b..00000000 --- a/src/components/PostItems/PostUser/index.module.scss +++ /dev/null @@ -1,43 +0,0 @@ -.postUser { - display: flex; - justify-content: space-between; - - .user { - display: flex; - - :global { - .ant-avatar { - width: 44px; - height: 44px; - } - } - - .info { - margin-left: var(--basePadding3); - - > a { - @include font-subheader2; - - color: var(--neutral-09); - text-decoration: none; - } - - p { - margin: 0; - - @include font-footnote; - - color: var(--neutral-06); - } - } - } - - .slot { - .more { - width: 18px; - height: 18px; - color: var(--neutral-06); - cursor: pointer; - } - } -} diff --git a/src/components/PostItems/PostUser/index.tsx b/src/components/PostItems/PostUser/index.tsx index 6f742933..c6875141 100644 --- a/src/components/PostItems/PostUser/index.tsx +++ b/src/components/PostItems/PostUser/index.tsx @@ -1,4 +1,4 @@ -import { Avatar, message } from 'antd'; +import { message } from 'antd'; import { Paths } from 'constants/path'; import { useTimeSince } from 'hooks/useTimeSince'; import { EventWithSeen } from 'pages/type'; @@ -7,8 +7,7 @@ import { useEffect, useState } from 'react'; import { isNip05DomainName } from 'core/nip/05'; import { PostUserMenu } from './menu'; import { EventSetMetadataContent } from 'core/nostr/type'; - -import styles from './index.module.scss'; +import * as Avatar from '@radix-ui/react-avatar'; import Link from 'next/link'; interface PostUserProps { @@ -37,17 +36,34 @@ const PostUser: React.FC = ({ } }, [profile]); + const name = + profile?.display_name || + profile?.name || + `${publicKey.slice(0, 8)}...${publicKey.slice(-8)}`; + return ( -
-
- - +
+
+ + + + + {name.slice(0, 2)} + + -
- {profile?.name || '...'} -

- -

+
+ + {name} + +
diff --git a/src/components/PostItems/PostUser/menu.tsx b/src/components/PostItems/PostUser/menu.tsx index 9f6dceb5..a0301f99 100644 --- a/src/components/PostItems/PostUser/menu.tsx +++ b/src/components/PostItems/PostUser/menu.tsx @@ -1,7 +1,5 @@ import type { MenuProps } from 'antd'; import { Dropdown, Modal, message } from 'antd'; - -import styles from './index.module.scss'; import Icon from 'components/Icon'; export function PostUserMenu({ event, publicKey, extraMenu }) { @@ -90,10 +88,11 @@ export function PostUserMenu({ event, publicKey, extraMenu }) { } return ( -
- - - -
+ + + ); } diff --git a/src/components/PostItems/index.tsx b/src/components/PostItems/index.tsx index 9a85251c..ee66b9e4 100644 --- a/src/components/PostItems/index.tsx +++ b/src/components/PostItems/index.tsx @@ -6,10 +6,9 @@ import { dexieDb } from 'core/db'; import { DbEvent } from 'core/db/schema'; import { deserializeMetadata } from 'core/nostr/content'; import { EventWithSeen } from 'pages/type'; - import LazyLoad from 'react-lazyload'; import dynamic from 'next/dynamic'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; const PostItem = dynamic( async () => { diff --git a/src/hooks/useTimeSince.ts b/src/hooks/useTimeSince.ts index c416939f..1869df2b 100644 --- a/src/hooks/useTimeSince.ts +++ b/src/hooks/useTimeSince.ts @@ -1,34 +1,31 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'next-i18next'; export function useTimeSince(timestamp: number): string { const { t } = useTranslation(); - const [timeSince, setTimeSince] = useState(''); - useEffect(() => { + const timeSince = useMemo(() => { const currentTime = Date.now() / 1000; - const timeDifference = currentTime - timestamp; + const timeDifference = Math.max(currentTime - timestamp, 0); + + if (timeDifference === 0) { + return t('timeSince.now') as string; + } if (timeDifference < 60) { - setTimeSince(`${Math.floor(timeDifference)} ${t('timeSince.seconds')}`); - } else if (timeDifference < 3600) { - setTimeSince( - `${Math.floor(timeDifference / 60)} ${t('timeSince.minutes')}`, - ); - } else if (timeDifference < 86400) { - setTimeSince( - `${Math.floor(timeDifference / 3600)} ${t('timeSince.hours')}`, - ); - } else if (timeDifference < 2592000) { - setTimeSince( - `${Math.floor(timeDifference / 86400)} ${t('timeSince.days')}`, - ); - } else { - setTimeSince( - `${Math.floor(timeDifference / 2592000)} ${t('timeSince.month')}`, - ); + return `${Math.floor(timeDifference)} ${t('timeSince.seconds')}`; + } + if (timeDifference < 3600) { + return `${Math.floor(timeDifference / 60)} ${t('timeSince.minutes')}`; + } + if (timeDifference < 86400) { + return `${Math.floor(timeDifference / 3600)} ${t('timeSince.hours')}`; + } + if (timeDifference < 2592000) { + return `${Math.floor(timeDifference / 86400)} ${t('timeSince.days')}`; } - }, [timestamp]); + return `${Math.floor(timeDifference / 2592000)} ${t('timeSince.month')}`; + }, [timestamp, t]); return timeSince; } diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index a9b7c80e..a9d3286e 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -15,7 +15,19 @@ @apply font-poppins text-base leading-6 font-semibold; } + .subheader2 { + @apply font-noto text-base leading-6 font-medium; + } + .label { @apply font-noto text-sm leading-[22px] font-medium; } + + .label-bold { + @apply font-noto text-sm leading-[22px] font-semibold; + } + + .footnote { + @apply font-noto text-xs leading-[18px] font-medium; + } } From e0f3cd60a4a3b80ed0645122cbada1466db73dbd Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:02:56 +0800 Subject: [PATCH 11/14] fix: use redux publickey for selector hooks (#429) * fix: use redux publickey for selector hooks * fix: record pubkey for sign-in redux --- .../RelaySelector/hooks/useSelectedRelay.ts | 15 ++++++++++++--- src/components/RelaySelector/index.tsx | 2 +- src/store/loginReducer.ts | 6 ++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/RelaySelector/hooks/useSelectedRelay.ts b/src/components/RelaySelector/hooks/useSelectedRelay.ts index 88fa594a..c662cde7 100644 --- a/src/components/RelaySelector/hooks/useSelectedRelay.ts +++ b/src/components/RelaySelector/hooks/useSelectedRelay.ts @@ -1,6 +1,10 @@ import { useEffect, useMemo } from 'react'; import { useLocalStorage } from 'usehooks-ts'; import { RelayMode } from '../type'; +import { useSelector } from 'react-redux'; +import { RootState } from 'store/configureStore'; +import { useReadonlyMyPublicKey } from 'hooks/useMyPublicKey'; +import { isValidPublicKey } from 'utils/validator'; const SELECTED_RELAY_MODE_KEY = 'relay-selector:selected-mode:{{pubKey}}'; const SELECTED_RELAY_GROUP_ID_KEY = @@ -23,14 +27,19 @@ const getLegacyLocalValue = (key: string) => { } }; -export function useSelectedRelay(myPublicKey: string) { +export function useSelectedRelay() { + const localPubkey = useReadonlyMyPublicKey(); + const pubkey = useSelector( + (state: RootState) => state.loginReducer.publicKey, + ); + const myPublicKey = isValidPublicKey(localPubkey) ? localPubkey : pubkey; const selectedModeKey = SELECTED_RELAY_MODE_KEY.replace( '{{pubKey}}', - myPublicKey, + myPublicKey ?? 'unknown', ); const selectedGroupIdKey = SELECTED_RELAY_GROUP_ID_KEY.replace( '{{pubKey}}', - myPublicKey, + myPublicKey ?? 'unknown', ); const [selectedMode = getLegacyLocalValue(selectedModeKey), setSelectedMode] = diff --git a/src/components/RelaySelector/index.tsx b/src/components/RelaySelector/index.tsx index 07ea96e4..8e49f44a 100644 --- a/src/components/RelaySelector/index.tsx +++ b/src/components/RelaySelector/index.tsx @@ -43,7 +43,7 @@ export function RelaySelector({ const myPublicKey = useReadonlyMyPublicKey(); const defaultGroup = useDefaultGroup(); const [relayGroupMap, setRelayGroupMap] = useState(new Map()); - const [selectedRelay, setSelectedRelay] = useSelectedRelay(myPublicKey); + const [selectedRelay, setSelectedRelay] = useSelectedRelay(); useEffect(() => { if (newConnCallback) { diff --git a/src/store/loginReducer.ts b/src/store/loginReducer.ts index f773b711..cc8d36a4 100644 --- a/src/store/loginReducer.ts +++ b/src/store/loginReducer.ts @@ -178,6 +178,7 @@ export async function getLoginInfo(request: LoginRequest): Promise { return { mode, isLoggedIn: isLoggedIn, + publicKey: pk, getPublicKey: async () => { return await window.nostr!.getPublicKey(); }, @@ -198,6 +199,7 @@ export async function getLoginInfo(request: LoginRequest): Promise { return { mode, isLoggedIn: isLoggedIn, + publicKey: pk, getPublicKey: async () => { return await requestPublicKeyFromDotBit(request.didAlias!); }, @@ -219,6 +221,7 @@ export async function getLoginInfo(request: LoginRequest): Promise { return { mode, isLoggedIn: true, + publicKey: pk, getPublicKey: async () => { return await requestPublicKeyFromNip05DomainName( request.nip05DomainName!, @@ -244,6 +247,7 @@ export async function getLoginInfo(request: LoginRequest): Promise { return { mode, isLoggedIn: isLoggedIn, + publicKey: pk, getPublicKey: getPublicKey, signEvent: createMetamaskSignEvent(request.evmUsername), evmUsername: request.evmUsername, @@ -264,6 +268,7 @@ export async function getLoginInfo(request: LoginRequest): Promise { return { mode, + publicKey: pk, isLoggedIn: isLoggedIn, getPublicKey: getPublicKey, signEvent: createWalletConnectSignEvent(request.evmUsername), @@ -279,6 +284,7 @@ export async function getLoginInfo(request: LoginRequest): Promise { return { mode, + publicKey: pk, isLoggedIn: isLoggedIn, getPublicKey: async () => { return await joyIdNostr.getPublicKey(); From d4826769069706c3d2ce468cfc2c85092d140a6f Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Wed, 27 Dec 2023 23:20:18 +0800 Subject: [PATCH 12/14] chore: rm the ignorying floating notify (#430) --- src/components/MsgFeed/index.tsx | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/components/MsgFeed/index.tsx b/src/components/MsgFeed/index.tsx index 20d2a4bf..e5d637ab 100644 --- a/src/components/MsgFeed/index.tsx +++ b/src/components/MsgFeed/index.tsx @@ -13,7 +13,7 @@ import { mergeAndSortUniqueDbEvents } from 'utils/common'; import { noticePubEventResult } from 'components/PubEventNotice'; import { Loader } from 'components/Loader'; import { createQueryCacheId, queryCache } from 'core/cache/query'; -import { useIntersectionObserver, useInterval } from 'usehooks-ts'; +import { useInterval } from 'usehooks-ts'; import { useRestoreScrollPos } from './hook/useRestoreScrollPos'; import PullToRefresh from 'react-simple-pull-to-refresh'; @@ -52,11 +52,6 @@ export const MsgFeed: React.FC = ({ const SUB_NEW_MSG_INTERVAL = 2000; // milsecs - // check if newMsgNotify UI is in view, if not, display floating one - const newMsgNotifyRef = useRef(null); - const entry = useIntersectionObserver(newMsgNotifyRef, {}); - const isNotifyVisible = !!entry?.isIntersecting; - const relayUrls = useMemo( () => worker?.relays.map(r => r.url) || [], [worker?.relays], @@ -291,9 +286,6 @@ export const MsgFeed: React.FC = ({ return newData; }); setNewComingMsg([]); - if (!isNotifyVisible) { - window.scrollTo({ top: 0 }); - } }; const onPullToRefresh = async () => { @@ -324,15 +316,7 @@ export const MsgFeed: React.FC = ({ <> {newComingMsg.length > 0 && ( -
- -
- )} - - {newComingMsg.length > 0 && !isNotifyVisible && ( -
+
From 31526a97689546bc81f3c7d5b152fa6b07a44e24 Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:55:48 +0800 Subject: [PATCH 13/14] chore: fix and rm hashTag filter options (#432) * chore: fix pc filter fixed top * chore: remove #hashtag filter * chore: filter the test noscript --- src/core/msg-filter/filter.ts | 42 ------------------------ src/pages/home/hooks/useQueryNoscript.ts | 14 +++++--- src/pages/home/index.module.scss | 2 +- 3 files changed, 11 insertions(+), 47 deletions(-) diff --git a/src/core/msg-filter/filter.ts b/src/core/msg-filter/filter.ts index ebe40c14..cd15d763 100644 --- a/src/core/msg-filter/filter.ts +++ b/src/core/msg-filter/filter.ts @@ -16,9 +16,6 @@ export enum MsgFilterKey { globalAll = 'Global-All', media = 'Media', zh = 'Chinese', - foodstr = 'Foodstr', - bitcoin = 'Bitcoin', - meme = 'Meme', } export enum MsgFilterMode { @@ -108,45 +105,6 @@ export const defaultMsgFilters: MsgFilter[] = [ mode: MsgFilterMode.global, description: 'global posts which language is Chinese', }, - { - key: MsgFilterKey.foodstr, - label: '#Foodstr', - filter: { - kinds: [WellKnownEventKind.text_note], - '#t': ['foodstr'], - } as Filter, - isValidEvent: (event: Event) => { - return event.kind === WellKnownEventKind.text_note; - }, - mode: MsgFilterMode.global, - description: 'global posts including #Foodstr tag', - }, - { - key: MsgFilterKey.meme, - label: '#Meme', - filter: { - kinds: [WellKnownEventKind.text_note], - '#t': ['meme'], - } as Filter, - isValidEvent: (event: Event) => { - return event.kind === WellKnownEventKind.text_note; - }, - mode: MsgFilterMode.global, - description: 'global posts including #meme tag', - }, - { - key: MsgFilterKey.bitcoin, - label: '#Bitcoin', - filter: { - kinds: [WellKnownEventKind.text_note], - '#t': ['bitcoin'], - } as Filter, - isValidEvent: (event: Event) => { - return event.kind === WellKnownEventKind.text_note; - }, - mode: MsgFilterMode.global, - description: 'global posts including #bitcoin tag', - }, ]; export const defaultMsgFiltersMap = defaultMsgFilters.reduce( diff --git a/src/pages/home/hooks/useQueryNoscript.ts b/src/pages/home/hooks/useQueryNoscript.ts index aa5fa15d..c19f5d2b 100644 --- a/src/pages/home/hooks/useQueryNoscript.ts +++ b/src/pages/home/hooks/useQueryNoscript.ts @@ -34,10 +34,16 @@ export function useQueryNoScript({ const filter: Filter = Nip188.createQueryNoscriptFilter([]); const relayUrls = worker.relays.map(r => r.url); - const scriptEvents = await dbQuery.matchFilterRelay( - filter, - relayUrls, - Nip188.isValidCustomMsgFilterNoscript(), + const scriptEvents = ( + await dbQuery.matchFilterRelay( + filter, + relayUrls, + Nip188.isValidCustomMsgFilterNoscript(), + ) + ).filter( + e => + e.id !== + '9cfe0338768a5896df7c2939cb362dbb431eb1a7f02a57c2c37bda041b27adc5', ); const noscripts = scriptEvents.map((e, idx) => { const authorPubkey = Nip19.encode(e.pubkey, Nip19DataType.Npubkey).slice( diff --git a/src/pages/home/index.module.scss b/src/pages/home/index.module.scss index f7ab1872..0b3edaa5 100644 --- a/src/pages/home/index.module.scss +++ b/src/pages/home/index.module.scss @@ -202,7 +202,7 @@ gap: 10px; align-self: stretch; position: sticky; - top: 0; + top: 64px; z-index: 10; background: var(--neutral-01); From a7531914c994b705c1031fcbb46c1abcc7aa930d Mon Sep 17 00:00:00 2001 From: digi-monkey <105776364+digi-monkey@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:06:05 +0800 Subject: [PATCH 14/14] chore: bump v0.2.8 (#433) --- package.json | 2 +- src/pages/home/updates.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 25598e0c..0ef69c15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flycat", - "version": "0.2.7", + "version": "0.2.8", "private": true, "scripts": { "wasm:profile": "cd wasm && rustc --crate-type cdylib --target wasm32-unknown-unknown -C lto -C opt-level=z -o profile.wasm examples/profile.rs", diff --git a/src/pages/home/updates.ts b/src/pages/home/updates.ts index 59358744..f5585bdb 100644 --- a/src/pages/home/updates.ts +++ b/src/pages/home/updates.ts @@ -1,24 +1,30 @@ export const updates = [ { content: - '[12.19] v0.2.7 release: refactor timeline with cache/improve performance/bug fixes', + '[12.19] v0.2.8: simplify filter options/refactor UI with TailwindCSS/bug fixes', isNew: true, + url: 'https://github.com/digi-monkey/flycat-web/releases/tag/v0.2.8', + }, + { + content: + '[12.19] v0.2.7: refactor timeline with cache/improve performance/bug fixes', + isNew: false, url: 'https://github.com/digi-monkey/flycat-web/releases/tag/v0.2.7', }, { content: - '[10.25] v0.2.6 release: add #meme filter/wasm script experiment and nip05 hosting', + '[10.25] v0.2.6: add #meme filter/wasm script experiment and nip05 hosting', isNew: false, url: 'https://github.com/digi-monkey/flycat-web/releases/tag/v0.2.6', }, { content: - '[9.27] v0.2.5 release: add hashtags and nip05-name/dotbit-alias in user profile/article URL', + '[9.27] v0.2.5: add hashtags and nip05-name/dotbit-alias in user profile/article URL', isNew: false, url: 'https://github.com/digi-monkey/flycat-web/releases/tag/v0.2.5', }, { - content: '[9.13] v0.2.4 release with timeline refactoring', + content: '[9.13] v0.2.4: timeline refactoring', isNew: false, url: 'https://github.com/digi-monkey/flycat-web/releases/tag/v0.2.4', },