feat: npub permalink

This commit is contained in:
Shusui MOYATANI
2023-07-09 14:43:36 +09:00
parent dca108084a
commit 0189016a8b
10 changed files with 190 additions and 118 deletions

View File

@@ -10,6 +10,7 @@ import i18nextInstance from '@/i18n/i18n';
import { I18NextProvider } from '@/i18n/useTranslation'; import { I18NextProvider } from '@/i18n/useTranslation';
const Home = lazy(() => import('@/pages/Home')); const Home = lazy(() => import('@/pages/Home'));
const Permalink = lazy(() => import('@/pages/Permalink'));
const Hello = lazy(() => import('@/pages/Hello')); const Hello = lazy(() => import('@/pages/Hello'));
const NotFound = lazy(() => import('@/pages/NotFound')); const NotFound = lazy(() => import('@/pages/NotFound'));
@@ -52,6 +53,7 @@ const App: Component = () => {
<Routes> <Routes>
<Route path="/hello" element={<Hello />} /> <Route path="/hello" element={<Hello />} />
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/:id" element={<Permalink />} />
<Route path="/*" element={<NotFound />} /> <Route path="/*" element={<NotFound />} />
</Routes> </Routes>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -13,7 +13,7 @@ const Columns = () => {
const { config } = useConfig(); const { config } = useConfig();
return ( return (
<div class="scrollbar flex h-full snap-x snap-mandatory flex-row overflow-y-hidden overflow-x-scroll"> <div class="flex h-full snap-x snap-mandatory flex-row overflow-scroll scroll-smooth">
<For each={config().columns}> <For each={config().columns}>
{(column, index) => { {(column, index) => {
const columnIndex = () => index() + 1; const columnIndex = () => index() + 1;

View File

@@ -1,4 +1,4 @@
import { Show, Switch, Match } from 'solid-js'; import { Show, Switch, Match, Component } from 'solid-js';
import About from '@/components/modal/About'; import About from '@/components/modal/About';
import AddColumn from '@/components/modal/AddColumn'; import AddColumn from '@/components/modal/AddColumn';
@@ -8,7 +8,7 @@ import useModalState from '@/hooks/useModalState';
import usePubkey from '@/nostr/usePubkey'; import usePubkey from '@/nostr/usePubkey';
import ensureNonNull from '@/utils/ensureNonNull'; import ensureNonNull from '@/utils/ensureNonNull';
const GlobalModal = () => { const GlobalModal: Component = () => {
const pubkey = usePubkey(); const pubkey = usePubkey();
const { modalState, showProfile, closeModal } = useModalState(); const { modalState, showProfile, closeModal } = useModalState();

View File

@@ -243,8 +243,11 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
return ( return (
<BasicModal onClose={() => props.onClose?.()}> <BasicModal onClose={() => props.onClose?.()}>
<Show when={profileQuery.isFetched} fallback={<>loading</>}> <Show
<Show when={profile()?.banner} fallback={<div class="h-12 shrink-0" />} keyed> when={profileQuery.isFetched && profile()?.banner}
fallback={<div class="h-12 shrink-0" />}
keyed
>
{(bannerUrl) => ( {(bannerUrl) => (
<div class="h-40 w-full shrink-0 sm:h-52"> <div class="h-40 w-full shrink-0 sm:h-52">
<img src={bannerUrl} alt="header" class="h-full w-full object-cover" /> <img src={bannerUrl} alt="header" class="h-full w-full object-cover" />
@@ -254,7 +257,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
<div class="mt-[-54px] flex items-end gap-4 px-4 pt-4"> <div class="mt-[-54px] flex items-end gap-4 px-4 pt-4">
<div class="flex-1 shrink-0"> <div class="flex-1 shrink-0">
<div class="h-28 w-28 rounded-lg shadow-md"> <div class="h-28 w-28 rounded-lg shadow-md">
<Show when={profile()?.picture} keyed> <Show when={profileQuery.isFetched && profile()?.picture} keyed>
{(pictureUrl) => ( {(pictureUrl) => (
<img <img
src={pictureUrl} src={pictureUrl}
@@ -265,6 +268,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</Show> </Show>
</div> </div>
</div> </div>
<Show when={myPubkey() != null}>
<div class="flex shrink-0 flex-col items-center gap-1"> <div class="flex shrink-0 flex-col items-center gap-1">
<div class="flex flex-row justify-start gap-1"> <div class="flex flex-row justify-start gap-1">
<Switch> <Switch>
@@ -284,7 +288,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</Match> </Match>
<Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}> <Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}>
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base"> <span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
{i18n()('profile.loading')} {i18n()('general.loading')}
</span> </span>
</Match> </Match>
<Match when={following()}> <Match when={following()}>
@@ -323,16 +327,18 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</div> </div>
<Switch> <Switch>
<Match when={userFollowingQuery.isLoading}> <Match when={userFollowingQuery.isLoading}>
<div class="shrink-0 text-xs">{i18n()('profile.loading')}</div> <div class="shrink-0 text-xs">{i18n()('general.loading')}</div>
</Match> </Match>
<Match when={followed()}> <Match when={followed()}>
<div class="shrink-0 text-xs">{i18n()('profile.followsYou')}</div> <div class="shrink-0 text-xs">{i18n()('profile.followsYou')}</div>
</Match> </Match>
</Switch> </Switch>
</div> </div>
</Show>
</div> </div>
<div class="flex items-start px-4 pt-2"> <div class="flex items-start px-4 pt-2">
<div class="h-16 shrink overflow-hidden"> <div class="h-16 shrink overflow-hidden">
<Show when={profileQuery.isLoading}>{i18n()('general.loading')}</Show>
<Show when={(profile()?.display_name?.length ?? 0) > 0}> <Show when={(profile()?.display_name?.length ?? 0) > 0}>
<div class="truncate text-xl font-bold">{profile()?.display_name}</div> <div class="truncate text-xl font-bold">{profile()?.display_name}</div>
</Show> </Show>
@@ -422,7 +428,6 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</Show> </Show>
</ul> </ul>
</Show> </Show>
</Show>
<Switch> <Switch>
<Match when={modal() === 'Following'}> <Match when={modal() === 'Following'}>
<UserList data={userFollowingPubkeys()} pubkeyExtractor={(e) => e} onClose={closeModal} /> <UserList data={userFollowingPubkeys()} pubkeyExtractor={(e) => e} onClose={closeModal} />

View File

@@ -78,8 +78,8 @@ const InitialConfig = (): Config => ({
customEmojis: {}, customEmojis: {},
dateFormat: 'relative', dateFormat: 'relative',
keepOpenPostForm: false, keepOpenPostForm: false,
useEmojiReaction: false, useEmojiReaction: true,
showEmojiReaction: false, showEmojiReaction: true,
showMedia: true, showMedia: true,
hideCount: false, hideCount: false,
mutedPubkeys: [], mutedPubkeys: [],
@@ -92,7 +92,7 @@ const deserializer = (json: string): Config =>
({ ({
...InitialConfig(), ...InitialConfig(),
...JSON.parse(json), ...JSON.parse(json),
} as Config); }) as Config;
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer); const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
const [config, setConfig] = createStoreWithStorage('RabbitConfig', InitialConfig(), storage); const [config, setConfig] = createStoreWithStorage('RabbitConfig', InitialConfig(), storage);

View File

@@ -1,6 +1,7 @@
import { createSignal } from 'solid-js'; import { createSignal } from 'solid-js';
type ModalState = type ModalState =
| { type: 'Login' }
| { type: 'Profile'; pubkey: string } | { type: 'Profile'; pubkey: string }
| { type: 'ProfileEdit' } | { type: 'ProfileEdit' }
| { type: 'UserTimeline'; pubkey: string } | { type: 'UserTimeline'; pubkey: string }
@@ -11,6 +12,9 @@ type ModalState =
const [modalState, setModalState] = createSignal<ModalState>({ type: 'Closed' }); const [modalState, setModalState] = createSignal<ModalState>({ type: 'Closed' });
const useModalState = () => { const useModalState = () => {
const showLogin = () => {
setModalState({ type: 'Login' });
};
const showProfile = (pubkey: string) => { const showProfile = (pubkey: string) => {
setModalState({ type: 'Profile', pubkey }); setModalState({ type: 'Profile', pubkey });
}; };
@@ -29,6 +33,7 @@ const useModalState = () => {
return { return {
modalState, modalState,
setModalState, setModalState,
showLogin,
showProfile, showProfile,
showProfileEdit, showProfileEdit,
showAddColumn, showAddColumn,

View File

@@ -1,6 +1,10 @@
import ja from '@/locales/ja'; import ja from '@/locales/ja';
export default { export default {
general: {
loading: 'Loading',
updating: 'Updating',
},
posting: { posting: {
placeholder: "What's happening?", placeholder: "What's happening?",
contentWarning: 'Content warning', contentWarning: 'Content warning',
@@ -34,8 +38,6 @@ export default {
following: 'Following', following: 'Following',
followers: 'Followers', followers: 'Followers',
loadFollowers: 'Load', loadFollowers: 'Load',
loading: 'Loading',
updating: 'Updating',
editProfile: 'Edit', editProfile: 'Edit',
follow: 'Follow', follow: 'Follow',
unfollow: 'Unfollow', unfollow: 'Unfollow',

View File

@@ -1,4 +1,8 @@
export default { export default {
general: {
loading: '読み込み中',
updating: '更新中',
},
posting: { posting: {
placeholder: 'いまどうしてる?', placeholder: 'いまどうしてる?',
contentWarning: 'コンテンツ警告を設定', contentWarning: 'コンテンツ警告を設定',
@@ -32,8 +36,6 @@ export default {
following: 'フォロー', following: 'フォロー',
followers: 'フォロワー', followers: 'フォロワー',
loadFollowers: '読み込む', loadFollowers: '読み込む',
loading: '読み込み中',
updating: '更新中',
editProfile: '編集', editProfile: '編集',
follow: 'フォロー', follow: 'フォロー',
unfollow: 'フォロー解除', unfollow: 'フォロー解除',

View File

@@ -6,6 +6,7 @@ import Columns from '@/components/column/Columns';
import GlobalModal from '@/components/modal/GlobalModal'; import GlobalModal from '@/components/modal/GlobalModal';
import SideBar from '@/components/SideBar'; import SideBar from '@/components/SideBar';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import usePersistStatus from '@/hooks/usePersistStatus'; import usePersistStatus from '@/hooks/usePersistStatus';
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys'; import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
import usePool from '@/nostr/usePool'; import usePool from '@/nostr/usePool';

55
src/pages/Permalink.tsx Normal file
View File

@@ -0,0 +1,55 @@
import { createEffect, onMount } from 'solid-js';
import { useNavigate, useParams } from '@solidjs/router';
import { nip19 } from 'nostr-tools';
import GlobalModal from '@/components/modal/GlobalModal';
import SideBar from '@/components/SideBar';
import useModalState from '@/hooks/useModalState';
import usePersistStatus from '@/hooks/usePersistStatus';
const Permalink = () => {
const navigate = useNavigate();
const params = useParams();
const { modalState, showProfile } = useModalState();
const { persistStatus } = usePersistStatus();
const navigateTop = () => {
if (persistStatus().loggedIn) {
navigate('/');
} else {
navigate('/hello');
}
};
onMount(() => {
if (params.id != null) {
try {
const decoded = nip19.decode(params.id);
if (decoded.type === 'npub') {
showProfile(decoded.data);
}
} catch (err) {
window.alert('Invalid ID');
navigateTop();
}
} else {
navigateTop();
}
});
createEffect(() => {
if (modalState().type === 'Closed') {
navigateTop();
}
});
return (
<div class="absolute inset-0 flex w-screen touch-manipulation flex-row overflow-hidden">
<SideBar />
<GlobalModal />
</div>
);
};
export default Permalink;