mirror of
https://github.com/lumehq/lume.git
synced 2025-09-18 16:01:39 +02:00
add members to channel and update note
This commit is contained in:
@@ -10,7 +10,7 @@ export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ${
|
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 ${
|
||||||
open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800'
|
open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
39
src/app/channel/components/members.tsx
Normal file
39
src/app/channel/components/members.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import MiniMember from '@lume/app/channel/components/miniMember';
|
||||||
|
import { channelMembersAtom } from '@lume/stores/channel';
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
|
||||||
|
export default function ChannelMembers() {
|
||||||
|
const membersAsSet = useAtomValue(channelMembersAtom);
|
||||||
|
const membersAsArray = [...membersAsSet];
|
||||||
|
const miniMembersList = membersAsArray.slice(0, 4);
|
||||||
|
const totalMembers =
|
||||||
|
membersAsArray.length > 0
|
||||||
|
? '+' +
|
||||||
|
Intl.NumberFormat('en-US', {
|
||||||
|
notation: 'compact',
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
}).format(membersAsArray.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="group flex -space-x-2 overflow-hidden hover:-space-x-1">
|
||||||
|
{miniMembersList.map((member, index) => (
|
||||||
|
<MiniMember key={index} pubkey={member} />
|
||||||
|
))}
|
||||||
|
{totalMembers > 0 ? (
|
||||||
|
<div className="inline-block inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 ring-2 ring-zinc-950 transition-all duration-150 ease-in-out group-hover:bg-zinc-800">
|
||||||
|
<span className="text-xs font-medium text-zinc-400 group-hover:text-zinc-200">{totalMembers}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<button className="inline-flex h-8 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm text-white shadow-button">
|
||||||
|
Invite
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,11 +1,13 @@
|
|||||||
import ChannelMessageItem from '@lume/app/channel/components/messages/item';
|
import ChannelMessageItem from '@lume/app/channel/components/messages/item';
|
||||||
import { sortedChannelMessagesAtom } from '@lume/stores/channel';
|
import { sortedChannelMessagesAtom } from '@lume/stores/channel';
|
||||||
|
import { hoursAgo } from '@lume/utils/getDate';
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
export default function ChannelMessageList() {
|
export default function ChannelMessageList() {
|
||||||
|
const now = useRef(new Date());
|
||||||
const virtuosoRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
const data = useAtomValue(sortedChannelMessagesAtom);
|
const data = useAtomValue(sortedChannelMessagesAtom);
|
||||||
|
|
||||||
@@ -29,6 +31,31 @@ export default function ChannelMessageList() {
|
|||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={data}
|
data={data}
|
||||||
itemContent={itemContent}
|
itemContent={itemContent}
|
||||||
|
components={{
|
||||||
|
Header: () => (
|
||||||
|
<div className="relative py-4">
|
||||||
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div className="w-full border-t border-zinc-800" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
|
||||||
|
{hoursAgo(24, now.current).toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
EmptyPlaceholder: () => (
|
||||||
|
<div className="flex flex-col gap-1 text-center">
|
||||||
|
<h3 className="text-sm font-semibold leading-none text-zinc-200">Nothing to see here yet</h3>
|
||||||
|
<p className="text-sm leading-none text-zinc-400">Be the first to share a message in this channel.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
computeItemKey={computeItemKey}
|
computeItemKey={computeItemKey}
|
||||||
initialTopMostItemIndex={data.length - 1}
|
initialTopMostItemIndex={data.length - 1}
|
||||||
alignToBottom={true}
|
alignToBottom={true}
|
||||||
|
@@ -21,7 +21,7 @@ export default function ChannelMetadata({ id, pubkey }: { id: string; pubkey: st
|
|||||||
<img
|
<img
|
||||||
src={metadata?.picture || DEFAULT_AVATAR}
|
src={metadata?.picture || DEFAULT_AVATAR}
|
||||||
alt={id}
|
alt={id}
|
||||||
className="h-8 w-8 rounded bg-zinc-900 object-contain"
|
className="h-8 w-8 rounded bg-zinc-900 object-contain ring-2 ring-zinc-950"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
20
src/app/channel/components/miniMember.tsx
Normal file
20
src/app/channel/components/miniMember.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { DEFAULT_AVATAR } from '@lume/stores/constants';
|
||||||
|
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||||
|
|
||||||
|
export default function MiniMember({ pubkey }: { pubkey: string }) {
|
||||||
|
const { user, isError, isLoading } = useProfile(pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isError || isLoading ? (
|
||||||
|
<div className="h-8 w-8 animate-pulse rounded-md bg-zinc-800"></div>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="inline-block h-8 w-8 rounded-md bg-white ring-2 ring-zinc-950 transition-all duration-150 ease-in-out"
|
||||||
|
src={user?.picture || DEFAULT_AVATAR}
|
||||||
|
alt={user?.pubkey || 'user avatar'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import ChannelBlackList from '@lume/app/channel/components/blacklist';
|
import ChannelBlackList from '@lume/app/channel/components/blacklist';
|
||||||
|
import ChannelMembers from '@lume/app/channel/components/members';
|
||||||
import ChannelMessageForm from '@lume/app/channel/components/messages/form';
|
import ChannelMessageForm from '@lume/app/channel/components/messages/form';
|
||||||
import ChannelMetadata from '@lume/app/channel/components/metadata';
|
import ChannelMetadata from '@lume/app/channel/components/metadata';
|
||||||
import ChannelUpdateModal from '@lume/app/channel/components/updateModal';
|
import ChannelUpdateModal from '@lume/app/channel/components/updateModal';
|
||||||
@@ -54,7 +55,7 @@ export function Page() {
|
|||||||
{
|
{
|
||||||
'#e': [key],
|
'#e': [key],
|
||||||
kinds: [42],
|
kinds: [42],
|
||||||
since: dateToUnix(hoursAgo(72, now.current)),
|
since: dateToUnix(hoursAgo(24, now.current)),
|
||||||
limit: 20,
|
limit: 20,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -62,7 +63,7 @@ export function Page() {
|
|||||||
(event) => {
|
(event) => {
|
||||||
const message: any = event;
|
const message: any = event;
|
||||||
if (hided.includes(event.id)) {
|
if (hided.includes(event.id)) {
|
||||||
message.push({ hide: true });
|
message['hide'] = true;
|
||||||
}
|
}
|
||||||
if (!muted.includes(event.pubkey)) {
|
if (!muted.includes(event.pubkey)) {
|
||||||
setChannelMessages((prev) => [...prev, message]);
|
setChannelMessages((prev) => [...prev, message]);
|
||||||
@@ -89,6 +90,7 @@ export function Page() {
|
|||||||
<ChannelMetadata id={channelID} pubkey={channelPubkey} />
|
<ChannelMetadata id={channelID} pubkey={channelPubkey} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<ChannelMembers />
|
||||||
<ChannelBlackList blacklist={mutedList} />
|
<ChannelBlackList blacklist={mutedList} />
|
||||||
{!isLoading && !isError && account ? (
|
{!isLoading && !isError && account ? (
|
||||||
account.pubkey === channelPubkey && <ChannelUpdateModal id={account.id} />
|
account.pubkey === channelPubkey && <ChannelUpdateModal id={account.id} />
|
||||||
|
@@ -7,35 +7,29 @@ import { memo } from 'react';
|
|||||||
import useSWRSubscription from 'swr/subscription';
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
|
||||||
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||||
const { data, error } = useSWRSubscription(
|
const { data, error } = useSWRSubscription(id ? id : null, (key, { next }) => {
|
||||||
id
|
const pool = new RelayPool(READONLY_RELAYS);
|
||||||
? [
|
const unsubscribe = pool.subscribe(
|
||||||
{
|
[
|
||||||
ids: [id],
|
|
||||||
kinds: [1],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
(key, { next }) => {
|
|
||||||
const pool = new RelayPool(READONLY_RELAYS);
|
|
||||||
const unsubscribe = pool.subscribe(
|
|
||||||
key,
|
|
||||||
READONLY_RELAYS,
|
|
||||||
(event: any) => {
|
|
||||||
next(null, event);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
{
|
||||||
unsubscribeOnEose: true,
|
ids: [key],
|
||||||
}
|
},
|
||||||
);
|
],
|
||||||
|
READONLY_RELAYS,
|
||||||
|
(event: any) => {
|
||||||
|
next(null, event);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative pb-5">
|
<div className="relative pb-5">
|
||||||
@@ -48,8 +42,6 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<div className="h-4 w-16 rounded bg-zinc-700" />
|
<div className="h-4 w-16 rounded bg-zinc-700" />
|
||||||
<span className="text-zinc-500">·</span>
|
|
||||||
<div className="h-4 w-12 rounded bg-zinc-700" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -7,8 +7,6 @@ export const Placeholder = () => {
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<div className="h-4 w-16 rounded bg-zinc-700" />
|
<div className="h-4 w-16 rounded bg-zinc-700" />
|
||||||
<span className="text-zinc-500">·</span>
|
|
||||||
<div className="h-4 w-12 rounded bg-zinc-700" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -13,7 +13,7 @@ export const NoteQuoteRepost = memo(function NoteQuoteRepost({ event }: { event:
|
|||||||
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
||||||
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
|
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
|
||||||
</div>
|
</div>
|
||||||
<RootNote id={rootID} />
|
<RootNote id={rootID} fallback={event.content} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,33 @@ import { memo } from 'react';
|
|||||||
import useSWRSubscription from 'swr/subscription';
|
import useSWRSubscription from 'swr/subscription';
|
||||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export const RootNote = memo(function RootNote({ id }: { id: string }) {
|
export const RootNote = memo(function RootNote({ id, fallback }: { id: string; fallback?: any }) {
|
||||||
|
const parseFallback = fallback.length > 0 ? JSON.parse(fallback) : null;
|
||||||
|
|
||||||
|
const { data, error } = useSWRSubscription(parseFallback ? null : id, (key, { next }) => {
|
||||||
|
const pool = new RelayPool(READONLY_RELAYS);
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ids: [key],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
READONLY_RELAYS,
|
||||||
|
(event: any) => {
|
||||||
|
next(null, event);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const openThread = (e) => {
|
const openThread = (e) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
@@ -17,41 +43,45 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error } = useSWRSubscription(
|
if (parseFallback) {
|
||||||
id
|
return (
|
||||||
? [
|
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
|
||||||
{
|
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} />
|
||||||
ids: [id],
|
<div className="mt-1 pl-[52px]">
|
||||||
kinds: [1],
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
},
|
{contentParser(parseFallback.content, parseFallback.tags)}
|
||||||
]
|
</div>
|
||||||
: null,
|
</div>
|
||||||
(key, { next }) => {
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
|
||||||
const pool = new RelayPool(READONLY_RELAYS);
|
</div>
|
||||||
const unsubscribe = pool.subscribe(
|
);
|
||||||
key,
|
}
|
||||||
READONLY_RELAYS,
|
|
||||||
(event: any) => {
|
|
||||||
next(null, event);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
unsubscribeOnEose: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <div>failed to load</div>}
|
{error && <div>failed to load</div>}
|
||||||
{!data ? (
|
{!data ? (
|
||||||
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
|
<div className="relative z-10 flex h-min animate-pulse select-text flex-col">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
||||||
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="h-4 w-16 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="h-16 w-full rounded bg-zinc-700" />
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
|
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
|
||||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
|
@@ -11,5 +11,12 @@ export const sortedChannelMessagesAtom = atom((get) => {
|
|||||||
return messages.sort((x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at);
|
return messages.sort((x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// channel user list
|
||||||
|
export const channelMembersAtom = atom((get) => {
|
||||||
|
const messages = get(channelMessagesAtom);
|
||||||
|
const uniqueMembers = new Set(messages.map((m: { pubkey: string }) => m.pubkey));
|
||||||
|
return uniqueMembers;
|
||||||
|
});
|
||||||
|
|
||||||
// channel message content
|
// channel message content
|
||||||
export const channelContentAtom = atomWithReset('');
|
export const channelContentAtom = atomWithReset('');
|
||||||
|
Reference in New Issue
Block a user