mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 05:54:19 +01:00
feat: user list
This commit is contained in:
64
src/components/event/ProfileListItem.tsx
Normal file
64
src/components/event/ProfileListItem.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Component, Show, Switch, Match } from 'solid-js';
|
||||
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
|
||||
export type ProfileListItemProps = {
|
||||
pubkey: string;
|
||||
onShowProfile?: () => void;
|
||||
};
|
||||
|
||||
const ProfileListItem: Component<ProfileListItemProps> = (props) => {
|
||||
const { profile, query } = useProfile(() => ({
|
||||
pubkey: props.pubkey,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="profile-icon h-6 w-6 shrink-0 overflow-hidden"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
props.onShowProfile?.();
|
||||
}}
|
||||
>
|
||||
<Show when={profile()?.picture} keyed>
|
||||
{(url) => <img src={url} alt="icon" class="h-full w-full rounded object-cover" />}
|
||||
</Show>
|
||||
</button>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<div class="flex justify-between gap-1 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="profile flex min-w-0 truncate hover:text-blue-500"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
props?.onShowProfile?.();
|
||||
}}
|
||||
>
|
||||
<span class="profile flex min-w-0 truncate hover:text-blue-500">
|
||||
<Show when={(profile()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="profile-name truncate pr-1 font-bold hover:underline">
|
||||
{profile()?.display_name}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="profile-username truncate text-zinc-600">
|
||||
<Show
|
||||
when={profile()?.name}
|
||||
fallback={`@${npubEncodeFallback(props.pubkey)}`}
|
||||
keyed
|
||||
>
|
||||
{(name) => `@${name}`}
|
||||
</Show>
|
||||
{/* TODO <Match when={profile()?.nip05 != null}>@{author()?.nip05}</Match> */}
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileListItem;
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Show, For, createSignal, createMemo, type JSX, type Component } from 'solid-js';
|
||||
import {
|
||||
Show,
|
||||
For,
|
||||
createSignal,
|
||||
createMemo,
|
||||
type JSX,
|
||||
type Component,
|
||||
Switch,
|
||||
Match,
|
||||
} from 'solid-js';
|
||||
|
||||
import { createMutation } from '@tanstack/solid-query';
|
||||
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
||||
@@ -17,6 +26,7 @@ import ContentWarningDisplay from '@/components/event/textNote/ContentWarningDis
|
||||
import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
|
||||
import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay';
|
||||
import EventDebugModal from '@/components/modal/EventDebugModal';
|
||||
import UserList from '@/components/modal/UserList';
|
||||
import NotePostForm from '@/components/NotePostForm';
|
||||
import Post from '@/components/Post';
|
||||
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
||||
@@ -95,8 +105,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
const [reacted, setReacted] = createSignal(false);
|
||||
const [reposted, setReposted] = createSignal(false);
|
||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||
const [showEventDebug, setShowEventDebug] = createSignal(false);
|
||||
const [modal, setModal] = createSignal<'EventDebugModal' | 'Reactions' | 'Reposts' | null>(null);
|
||||
|
||||
const closeReplyForm = () => setShowReplyForm(false);
|
||||
const closeModal = () => setModal(null);
|
||||
|
||||
const event = createMemo(() => textNote(props.event));
|
||||
|
||||
@@ -190,7 +202,19 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
{
|
||||
content: () => 'JSONを確認',
|
||||
onSelect: () => {
|
||||
setShowEventDebug(true);
|
||||
setModal('EventDebugModal');
|
||||
},
|
||||
},
|
||||
{
|
||||
content: () => 'リポスト一覧',
|
||||
onSelect: () => {
|
||||
setModal('Reposts');
|
||||
},
|
||||
},
|
||||
{
|
||||
content: () => 'リアクション一覧',
|
||||
onSelect: () => {
|
||||
setModal('Reactions');
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -437,9 +461,33 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
timelineContext?.setTimeline({ type: 'Replies', event: props.event });
|
||||
}}
|
||||
/>
|
||||
<Show when={showEventDebug()}>
|
||||
<EventDebugModal event={props.event} onClose={() => setShowEventDebug(false)} />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={modal() === 'EventDebugModal'}>
|
||||
<EventDebugModal event={props.event} onClose={closeModal} />
|
||||
</Match>
|
||||
<Match when={modal() === 'Reactions'}>
|
||||
<UserList
|
||||
data={reactions()}
|
||||
pubkeyExtractor={(ev) => ev.pubkey}
|
||||
renderInfo={({ content }) => (
|
||||
<div class="w-6">
|
||||
<Show
|
||||
when={content === '+'}
|
||||
fallback={<span class="truncate text-base">{content}</span>}
|
||||
>
|
||||
<span class="inline-block h-3 w-3 pt-[1px] text-rose-400">
|
||||
<HeartSolid />
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={modal() === 'Reposts'}>
|
||||
<UserList data={reposts()} pubkeyExtractor={(ev) => ev.pubkey} onClose={closeModal} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import uniq from 'lodash/uniq';
|
||||
|
||||
import ContextMenu, { MenuItem } from '@/components/ContextMenu';
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import UserList from '@/components/modal/UserList';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
import useConfig from '@/core/useConfig';
|
||||
@@ -51,6 +52,8 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
const [updatingContacts, setUpdatingContacts] = createSignal(false);
|
||||
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
||||
const [showFollowers, setShowFollowers] = createSignal(false);
|
||||
const [modal, setModal] = createSignal<'Following' | null>(false);
|
||||
const closeModal = () => setModal(null);
|
||||
|
||||
const { profile, query: profileQuery } = useProfile(() => ({
|
||||
pubkey: props.pubkey,
|
||||
@@ -341,7 +344,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex border-t px-4 py-2">
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<button class="flex flex-1 flex-col items-start" onClick={() => setModal('Following')}>
|
||||
<div class="text-sm">フォロー</div>
|
||||
<div class="text-xl">
|
||||
<Show
|
||||
@@ -351,7 +354,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
{userFollowingPubkeys().length}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<Show when={!config().hideCount}>
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<div class="text-sm">フォロワー</div>
|
||||
@@ -389,6 +392,11 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</ul>
|
||||
</Show>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={modal() === 'Following'}>
|
||||
<UserList data={userFollowingPubkeys()} pubkeyExtractor={(e) => e} onClose={closeModal} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<ul class="border-t p-1">
|
||||
<Timeline events={events()} />
|
||||
</ul>
|
||||
|
||||
41
src/components/modal/UserList.tsx
Normal file
41
src/components/modal/UserList.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { For, JSX, Show } from 'solid-js';
|
||||
|
||||
import ProfileListItem from '@/components/event/ProfileListItem';
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
|
||||
export type UserListProps<T> = {
|
||||
data: T[];
|
||||
pubkeyExtractor: (e: T) => string;
|
||||
renderInfo?: (e: T) => JSX.Element;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const UserList = <T,>(props: UserListProps<T>): JSX.Element => {
|
||||
const { showProfile } = useModalState();
|
||||
|
||||
return (
|
||||
<BasicModal onClose={props.onClose}>
|
||||
<div class="px-4 py-2">
|
||||
<div>{props.data.length} 件</div>
|
||||
<div>
|
||||
<For each={props.data}>
|
||||
{(e) => {
|
||||
const pubkey = () => props.pubkeyExtractor(e);
|
||||
return (
|
||||
<div class="flex border-t py-1">
|
||||
<Show when={props.renderInfo} keyed>
|
||||
{(render) => render(e)}
|
||||
</Show>
|
||||
<ProfileListItem pubkey={pubkey()} onShowProfile={() => showProfile(pubkey())} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserList;
|
||||
Reference in New Issue
Block a user