update channel

This commit is contained in:
Ren Amamiya
2023-04-28 14:36:16 +07:00
parent a71502d19e
commit 87e8ee8954
44 changed files with 761 additions and 675 deletions

View File

@@ -1,10 +1,10 @@
import { UserMuted } from '@lume/shared/user/muted';
import MutedItem from '@lume/app/channel/components/mutedItem';
import { Popover, Transition } from '@headlessui/react';
import { MicMute } from 'iconoir-react';
import { Fragment } from 'react';
export const ChannelBlackList = ({ blacklist }: { blacklist: any }) => {
export default function ChannelBlackList({ blacklist }: { blacklist: any }) {
return (
<Popover className="relative">
{({ open }) => (
@@ -39,7 +39,7 @@ export const ChannelBlackList = ({ blacklist }: { blacklist: any }) => {
</div>
<div className="px-3 pb-3 pt-1">
{blacklist.map((item: any) => (
<UserMuted key={item.id} data={item} />
<MutedItem key={item.id} data={item} />
))}
</div>
</div>
@@ -49,4 +49,4 @@ export const ChannelBlackList = ({ blacklist }: { blacklist: any }) => {
)}
</Popover>
);
};
}

View File

@@ -1,20 +1,19 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { AvatarUploader } from '@lume/shared/avatarUploader';
import { RelayContext } from '@lume/shared/relaysProvider';
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { createChannel } from '@lume/utils/storage';
import { Dialog, Transition } from '@headlessui/react';
import { Cancel, Plus } from 'iconoir-react';
import { RelayPool } from 'nostr-relaypool';
import { getEventHash, signEvent } from 'nostr-tools';
import { Fragment, useContext, useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { navigate } from 'vite-plugin-ssr/client/router';
export const CreateChannelModal = () => {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
export default function ChannelCreateModal() {
const { account, isError, isLoading } = useActiveAccount();
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(DEFAULT_AVATAR);
@@ -39,28 +38,33 @@ export const CreateChannelModal = () => {
const onSubmit = (data: any) => {
setLoading(true);
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 40,
pubkey: activeAccount.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, activeAccount.privkey);
if (!isError && !isLoading && account) {
const pool = new RelayPool(WRITEONLY_RELAYS);
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 40,
pubkey: account.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
// publish channel
pool.publish(event, WRITEONLY_RELAYS);
// insert to database
createChannel(event.id, event.pubkey, event.content, event.created_at);
// reset form
reset();
setTimeout(() => {
// close modal
setIsOpen(false);
// redirect to channel page
navigate(`/channel?id=${event.id}`);
}, 2000);
// publish channel
pool.publish(event, WRITEONLY_RELAYS);
// insert to database
createChannel(event.id, event.pubkey, event.content, event.created_at);
// reset form
reset();
setTimeout(() => {
// close modal
setIsOpen(false);
// redirect to channel page
navigate(`/channel?id=${event.id}`);
}, 2000);
} else {
console.log('error');
}
};
useEffect(() => {
@@ -237,4 +241,4 @@ export const CreateChannelModal = () => {
</Transition>
</>
);
};
}

View File

@@ -4,7 +4,7 @@ import { usePageContext } from '@lume/utils/hooks/usePageContext';
import { twMerge } from 'tailwind-merge';
export const ChannelListItem = ({ data }: { data: any }) => {
export default function ChannelsListItem({ data }: { data: any }) {
const channel: any = useChannelMetadata(data.event_id, data.pubkey);
const pageContext = usePageContext();
@@ -13,7 +13,7 @@ export const ChannelListItem = ({ data }: { data: any }) => {
return (
<a
href={`/channel?id=${data.event_id}&pubkey=${data.pubkey}`}
href={`/app/channel?id=${data.event_id}&pubkey=${data.pubkey}`}
className={twMerge(
'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900',
pageID === data.event_id ? 'dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800' : ''
@@ -31,4 +31,4 @@ export const ChannelListItem = ({ data }: { data: any }) => {
</div>
</a>
);
};
}

View File

@@ -0,0 +1,34 @@
import ChannelCreateModal from '@lume/app/channel/components/createModal';
import ChannelsListItem from '@lume/app/channel/components/item';
import { getChannels } from '@lume/utils/storage';
import useSWR from 'swr';
const fetcher = () => getChannels(10, 0);
export default function ChannelsList() {
const { data, error }: any = useSWR('channels', fetcher);
return (
<div className="flex flex-col gap-px">
<>
{error && <div>failed to fetch</div>}
{!data ? (
<>
<div className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div>
<div className="h-3 w-full animate-pulse bg-zinc-800"></div>
</div>
<div className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800"></div>
<div className="h-3 w-full animate-pulse bg-zinc-800"></div>
</div>
</>
) : (
data.map((item: { event_id: string }) => <ChannelsListItem key={item.event_id} data={item} />)
)}
</>
<ChannelCreateModal />
</div>
);
}

View File

@@ -1,12 +1,11 @@
import { ChannelMessageItem } from '@lume/shared/channels/messages/item';
import { Placeholder } from '@lume/shared/note/placeholder';
import ChannelMessageItem from '@lume/app/channel/components/messages/item';
import { sortedChannelMessagesAtom } from '@lume/stores/channel';
import { useAtomValue } from 'jotai';
import { useCallback, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso';
export default function ChannelMessages() {
export default function ChannelMessageList() {
const virtuosoRef = useRef(null);
const data = useAtomValue(sortedChannelMessagesAtom);
@@ -29,7 +28,6 @@ export default function ChannelMessages() {
<Virtuoso
ref={virtuosoRef}
data={data}
components={COMPONENTS}
itemContent={itemContent}
computeItemKey={computeItemKey}
initialTopMostItemIndex={data.length - 1}
@@ -42,7 +40,3 @@ export default function ChannelMessages() {
</div>
);
}
const COMPONENTS = {
EmptyPlaceholder: () => <Placeholder />,
};

View File

@@ -0,0 +1,123 @@
import UserReply from '@lume/app/channel/components/messages/userReply';
import { ImagePicker } from '@lume/shared/form/imagePicker';
import { channelContentAtom, channelReplyAtom } from '@lume/stores/channel';
import { FULL_RELAYS, WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { Cancel } from 'iconoir-react';
import { useAtom, useAtomValue } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { RelayPool } from 'nostr-relaypool';
import { getEventHash, signEvent } from 'nostr-tools';
export default function ChannelMessageForm({ channelID }: { channelID: string | string[] }) {
const { account, isLoading, isError } = useActiveAccount();
const [value, setValue] = useAtom(channelContentAtom);
const resetValue = useResetAtom(channelContentAtom);
const channelReply = useAtomValue(channelReplyAtom);
const resetChannelReply = useResetAtom(channelReplyAtom);
const submitEvent = () => {
let tags: any[][];
if (channelReply.id !== null) {
tags = [
['e', channelID, '', 'root'],
['e', channelReply.id, '', 'reply'],
['p', channelReply.pubkey, ''],
];
} else {
tags = [['e', channelID, '', 'root']];
}
if (!isError && !isLoading && account) {
const pool = new RelayPool(WRITEONLY_RELAYS);
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 42,
pubkey: account.pubkey,
tags: tags,
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
// publish note
pool.publish(event, FULL_RELAYS);
// reset state
resetValue();
// reset channel reply
resetChannelReply();
} else {
console.log('error');
}
};
const handleEnterPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submitEvent();
}
};
const stopReply = () => {
resetChannelReply();
};
return (
<div
className={`relative ${
channelReply.id ? 'h-36' : 'h-24'
} w-full overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20`}
>
{channelReply.id && (
<div className="absolute left-0 top-0 z-10 h-14 w-full p-[2px]">
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
<div className="flex w-full flex-col">
<UserReply pubkey={channelReply.pubkey} />
<div className="-mt-3.5 pl-[32px]">
<div className="text-xs text-zinc-200">{channelReply.content}</div>
</div>
</div>
<button
onClick={() => stopReply()}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
>
<Cancel width={12} height={12} className="text-zinc-100" />
</button>
</div>
</div>
)}
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleEnterPress}
spellCheck={false}
placeholder="Message"
className={`relative ${
channelReply.id ? 'h-36 pt-16' : 'h-24 pt-3'
} w-full resize-none rounded-lg border border-black/5 px-3.5 pb-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500`}
/>
<div className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700">
<ImagePicker type="channel" />
<div className="flex items-center gap-2 pl-2"></div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false}
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import Tooltip from '@lume/shared/tooltip';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { EyeClose } from 'iconoir-react';
import { RelayPool } from 'nostr-relaypool';
import { getEventHash, signEvent } from 'nostr-tools';
export default function MessageHideButton({ id }: { id: string }) {
const { account, isError, isLoading } = useActiveAccount();
const hideMessage = () => {
if (!isError && !isLoading && account) {
const pool = new RelayPool(WRITEONLY_RELAYS);
const event: any = {
content: '',
created_at: dateToUnix(),
kind: 43,
pubkey: account.pubkey,
tags: [['e', id]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
} else {
console.log('error');
}
};
return (
<Tooltip message="Hide this message">
<button
onClick={() => hideMessage()}
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
>
<EyeClose width={16} height={16} className="text-zinc-400" />
</button>
</Tooltip>
);
}

View File

@@ -0,0 +1,31 @@
import MessageHideButton from '@lume/app/channel/components/messages/hideButton';
import MessageMuteButton from '@lume/app/channel/components/messages/muteButton';
import MessageReplyButton from '@lume/app/channel/components/messages/replyButton';
import ChannelMessageUser from '@lume/app/channel/components/messages/user';
import { messageParser } from '@lume/utils/parser';
export default function ChannelMessageItem({ data }: { data: any }) {
const content = messageParser(data.content);
return (
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
<div className="flex flex-col">
<ChannelMessageUser pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[17px] pl-[48px]">
<div className="flex flex-col gap-2">
<div className="whitespace-pre-line break-words break-words text-sm leading-tight">
{data.hide ? <span>[hided message]</span> : content}
</div>
</div>
</div>
</div>
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
<div className="inline-flex h-7 items-center justify-center gap-1 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
<MessageReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
<MessageHideButton id={data.id} />
<MessageMuteButton pubkey={data.pubkey} />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import Tooltip from '@lume/shared/tooltip';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { MicMute } from 'iconoir-react';
import { RelayPool } from 'nostr-relaypool';
import { getEventHash, signEvent } from 'nostr-tools';
export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
const { account, isError, isLoading } = useActiveAccount();
const muteUser = () => {
if (!isError && !isLoading && account) {
const pool = new RelayPool(WRITEONLY_RELAYS);
const event: any = {
content: '',
created_at: dateToUnix(),
kind: 44,
pubkey: account.pubkey,
tags: [['p', pubkey]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
} else {
console.log('error');
}
};
return (
<Tooltip message="Mute this user">
<button
onClick={() => muteUser()}
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
>
<MicMute width={16} height={16} className="text-zinc-400" />
</button>
</Tooltip>
);
}

View File

@@ -4,7 +4,7 @@ import { channelReplyAtom } from '@lume/stores/channel';
import { Reply } from 'iconoir-react';
import { useSetAtom } from 'jotai';
export const ReplyButton = ({ id, pubkey, content }: { id: string; pubkey: string; content: string }) => {
export default function MessageReplyButton({ id, pubkey, content }: { id: string; pubkey: string; content: string }) {
const setChannelReplyAtom = useSetAtom(channelReplyAtom);
const createReply = () => {
@@ -21,4 +21,4 @@ export const ReplyButton = ({ id, pubkey, content }: { id: string; pubkey: strin
</button>
</Tooltip>
);
};
}

View File

@@ -0,0 +1,48 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export default function ChannelMessageUser({ pubkey, time }: { pubkey: string; time: number }) {
const { user, isError, isLoading } = useProfile(pubkey);
return (
<div className="group flex items-start gap-3">
{isError || isLoading ? (
<>
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex items-baseline gap-2 text-sm">
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
</div>
</div>
</>
) : (
<>
<div className="relative h-9 w-9 shrink rounded-md">
<img
src={user?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-9 w-9 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex items-baseline gap-2 text-sm">
<span className="font-semibold leading-none text-zinc-200 group-hover:underline">
{user?.display_name || user?.name || shortenKey(pubkey)}
</span>
<span className="leading-none text-zinc-500">·</span>
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
</div>
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
export default function UserReply({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey);
return (
<div className="group flex items-start gap-1">
{isError || isLoading ? (
<>
<div className="relative h-7 w-7 shrink animate-pulse overflow-hidden rounded bg-zinc-800"></div>
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-xs font-medium leading-none text-zinc-500"></span>
</>
) : (
<>
<div className="relative h-7 w-7 shrink overflow-hidden rounded">
<img
src={user?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-7 w-7 rounded object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<span className="text-xs font-medium leading-none text-zinc-500">
Replying to {user?.name || shortenKey(pubkey)}
</span>
</>
)}
</div>
);
}

View File

@@ -4,7 +4,7 @@ import { useChannelProfile } from '@lume/utils/hooks/useChannelProfile';
import { Copy } from 'iconoir-react';
import { nip19 } from 'nostr-tools';
export const ChannelProfile = ({ id, pubkey }: { id: string; pubkey: string }) => {
export default function ChannelMetadata({ id, pubkey }: { id: string; pubkey: string }) {
const metadata = useChannelProfile(id, pubkey);
const noteID = id ? nip19.noteEncode(id) : null;
@@ -37,4 +37,4 @@ export const ChannelProfile = ({ id, pubkey }: { id: string; pubkey: string }) =
</div>
</div>
);
};
}

View File

@@ -0,0 +1,78 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import { useState } from 'react';
export default function MutedItem({ data }: { data: any }) {
const { user, isError, isLoading } = useProfile(data.content);
const [status, setStatus] = useState(data.status);
const unmute = async () => {
const { updateItemInBlacklist } = await import('@lume/utils/storage');
const res = await updateItemInBlacklist(data.content, 0);
if (res) {
setStatus(0);
}
};
const mute = async () => {
const { updateItemInBlacklist } = await import('@lume/utils/storage');
const res = await updateItemInBlacklist(data.content, 1);
if (res) {
setStatus(1);
}
};
return (
<div className="flex items-center justify-between">
{isError || isLoading ? (
<>
<div className="flex items-center gap-1.5">
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
<div className="h-3 w-16 animate-pulse bg-zinc-800"></div>
<div className="h-2 w-10 animate-pulse bg-zinc-800"></div>
</div>
</div>
</>
) : (
<>
<div className="flex items-center gap-1.5">
<div className="relative h-9 w-9 shrink rounded-md">
<img
src={user?.picture || DEFAULT_AVATAR}
alt={data.content}
className="h-9 w-9 rounded-md object-cover"
loading="lazy"
/>
</div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
<span className="truncate text-sm font-medium leading-none text-zinc-200">
{user?.display_name || user?.name}
</span>
<span className="text-xs leading-none text-zinc-400">{shortenKey(data.content)}</span>
</div>
</div>
<div>
{status === 1 ? (
<button
onClick={() => unmute()}
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Unmute
</button>
) : (
<button
onClick={() => mute()}
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Mute
</button>
)}
</div>
</>
)}
</div>
);
}

View File

@@ -1,19 +1,18 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { AvatarUploader } from '@lume/shared/avatarUploader';
import { RelayContext } from '@lume/shared/relaysProvider';
import { DEFAULT_AVATAR, WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { getChannel, updateChannelMetadata } from '@lume/utils/storage';
import { Dialog, Transition } from '@headlessui/react';
import { Cancel, EditPencil } from 'iconoir-react';
import { RelayPool } from 'nostr-relaypool';
import { getEventHash, signEvent } from 'nostr-tools';
import { Fragment, useContext, useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
export const UpdateChannelModal = ({ id }: { id: string }) => {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
export default function ChannelUpdateModal({ id }: { id: string }) {
const { account, isError, isLoading } = useActiveAccount();
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(DEFAULT_AVATAR);
@@ -36,35 +35,44 @@ export const UpdateChannelModal = ({ id }: { id: string }) => {
} = useForm({
defaultValues: async () => {
const channel = await getChannel(id);
const metadata = JSON.parse(channel.metadata);
// update image state
setImage(metadata.picture);
// set default values
return metadata;
if (channel) {
const metadata = JSON.parse(channel.metadata);
// update image state
setImage(metadata.picture);
// set default values
return metadata;
} else {
return null;
}
},
});
const onSubmit = (data: any) => {
setLoading(true);
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 41,
pubkey: activeAccount.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, activeAccount.privkey);
if (!isError && !isLoading && account) {
const pool = new RelayPool(WRITEONLY_RELAYS);
const event: any = {
content: JSON.stringify(data),
created_at: dateToUnix(),
kind: 41,
pubkey: account.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
// publish channel
pool.publish(event, WRITEONLY_RELAYS);
// update channel metadata in database
updateChannelMetadata(event.id, event.content);
// reset form
reset();
// close modal
setIsOpen(false);
// publish channel
pool.publish(event, WRITEONLY_RELAYS);
// update channel metadata in database
updateChannelMetadata(event.id, event.content);
// reset form
reset();
// close modal
setIsOpen(false);
} else {
console.log('error');
}
};
useEffect(() => {
@@ -236,4 +244,4 @@ export const UpdateChannelModal = ({ id }: { id: string }) => {
</Transition>
</>
);
};
}

View File

@@ -1,34 +1,34 @@
import { ChannelBlackList } from '@lume/shared/channels/channelBlackList';
import { ChannelProfile } from '@lume/shared/channels/channelProfile';
import { UpdateChannelModal } from '@lume/shared/channels/updateChannelModal';
import { FormChannel } from '@lume/shared/form/channel';
import ChannelBlackList from '@lume/app/channel/components/blacklist';
import ChannelMessageForm from '@lume/app/channel/components/messages/form';
import ChannelMetadata from '@lume/app/channel/components/metadata';
import ChannelUpdateModal from '@lume/app/channel/components/updateModal';
import { channelMessagesAtom, channelReplyAtom } from '@lume/stores/channel';
import { FULL_RELAYS } from '@lume/stores/constants';
import { dateToUnix, hoursAgo } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { usePageContext } from '@lume/utils/hooks/usePageContext';
import { arrayObjToPureArr } from '@lume/utils/transform';
import { useSetAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { RelayPool } from 'nostr-relaypool';
import { Suspense, lazy, useRef } from 'react';
import { Suspense, lazy, useEffect, useRef } from 'react';
import useSWRSubscription from 'swr/subscription';
const ChannelMessages = lazy(() => import('@lume/shared/channels/messages'));
let mutedList: any = [];
let activeAccount: any = {};
let activeMutedList: any = [];
let activeHidedList: any = [];
if (typeof window !== 'undefined') {
const { getBlacklist, getActiveBlacklist, getActiveAccount } = await import('@lume/utils/storage');
activeAccount = await getActiveAccount();
const activeAccount = await getActiveAccount();
activeHidedList = await getActiveBlacklist(activeAccount.id, 43);
activeMutedList = await getActiveBlacklist(activeAccount.id, 44);
mutedList = await getBlacklist(activeAccount.id, 44);
}
const ChannelMessageList = lazy(() => import('@lume/app/channel/components/messageList'));
export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
@@ -36,6 +36,8 @@ export function Page() {
const channelID = searchParams.id;
const channelPubkey = searchParams.pubkey;
const { account, isLoading, isError } = useActiveAccount();
const setChannelMessages = useSetAtom(channelMessagesAtom);
const resetChannelMessages = useResetAtom(channelMessagesAtom);
const resetChannelReply = useResetAtom(channelReplyAtom);
@@ -44,29 +46,26 @@ export function Page() {
const hided = arrayObjToPureArr(activeHidedList);
const muted = arrayObjToPureArr(activeMutedList);
useSWRSubscription(channelID, () => {
// reset channel reply
resetChannelReply();
// reset channel messages
resetChannelMessages();
// subscribe for new messages
useSWRSubscription(channelID ? channelID : null, (key: string, {}: any) => {
// subscribe to channel
const pool = new RelayPool(FULL_RELAYS);
const unsubscribe = pool.subscribe(
[
{
'#e': [channelID],
'#e': [key],
kinds: [42],
since: dateToUnix(hoursAgo(48, now.current)),
since: dateToUnix(hoursAgo(72, now.current)),
limit: 20,
},
],
FULL_RELAYS,
(event: { kind: number; tags: string[][]; pubkey: string; id: string }) => {
if (muted.includes(event.pubkey)) {
console.log('muted');
} else if (hided.includes(event.id)) {
console.log('hided');
} else {
setChannelMessages((prev) => [...prev, event]);
(event) => {
const message: any = event;
if (hided.includes(event.id)) {
message.push({ hide: true });
}
if (!muted.includes(event.pubkey)) {
setChannelMessages((prev) => [...prev, message]);
}
}
);
@@ -76,23 +75,34 @@ export function Page() {
};
});
useEffect(() => {
// reset channel reply
resetChannelReply();
// reset channel messages
resetChannelMessages();
});
return (
<div className="flex h-full flex-col justify-between gap-2">
<div className="flex h-11 w-full shrink-0 items-center justify-between">
<div>
<ChannelProfile id={channelID} pubkey={channelPubkey} />
<ChannelMetadata id={channelID} pubkey={channelPubkey} />
</div>
<div className="flex items-center gap-2">
<ChannelBlackList blacklist={mutedList} />
{activeAccount.pubkey === channelPubkey && <UpdateChannelModal id={activeAccount} />}
{!isLoading && !isError && account ? (
account.pubkey === channelPubkey && <ChannelUpdateModal id={account.id} />
) : (
<></>
)}
</div>
</div>
<div className="relative flex w-full flex-1 flex-col justify-between rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
<Suspense fallback={<p>Loading...</p>}>
<ChannelMessages />
<ChannelMessageList />
</Suspense>
<div className="shrink-0 p-3">
<FormChannel eventId={channelID} />
<div className="inline-flex shrink-0 p-3">
<ChannelMessageForm channelID={channelID} />
</div>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import { ImagePreview } from '@lume/app/newsfeed/components/note/preview/image';
import { VideoPreview } from '@lume/app/newsfeed/components/note/preview/video';
import { YoutubePreview } from '@lume/app/newsfeed/components/note/preview/youtube';
import { NoteQuote } from '@lume/app/newsfeed/components/note/quote';
import { NoteMentionUser } from '@lume/app/newsfeed/components/user/mention';
import ImagePreview from '@lume/shared/preview/image';
import VideoPreview from '@lume/shared/preview/video';
import YoutubePreview from '@lume/shared/preview/youtube';
import destr from 'destr';
import reactStringReplace from 'react-string-replace';

View File

@@ -8,32 +8,47 @@ import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const NoteDefaultUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
const profile = useProfile(pubkey);
const { user, isError, isLoading } = useProfile(pubkey);
return (
<div className="group flex h-11 items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-2">
<h5 className="text-sm font-semibold leading-none group-hover:underline">
{profile?.display_name || profile?.name || shortenKey(pubkey)}
</h5>
<span className="text-sm leading-none text-zinc-700"></span>
{isError || isLoading ? (
<>
<div className="relative h-11 w-11 shrink animate-pulse overflow-hidden rounded-md bg-white bg-zinc-800"></div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-2">
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
</div>
<div className="h-2.5 w-14 animate-pulse rounded bg-zinc-800"></div>
</div>
</div>
<span className="text-sm leading-none text-zinc-500">
{profile?.nip05 || shortenKey(pubkey)} {dayjs().to(dayjs.unix(time))}
</span>
</div>
</div>
</>
) : (
<>
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={user?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-2">
<h5 className="text-sm font-semibold leading-none group-hover:underline">
{user?.display_name || user?.name || shortenKey(pubkey)}
</h5>
</div>
<span className="text-sm leading-none text-zinc-500">
{user?.nip05 || shortenKey(pubkey)} {dayjs().to(dayjs.unix(time))}
</span>
</div>
</div>
</>
)}
</div>
);
};

View File

@@ -2,8 +2,15 @@ import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
export const NoteMentionUser = ({ pubkey }: { pubkey: string }) => {
const profile = useProfile(pubkey);
const { user, isError, isLoading } = useProfile(pubkey);
return (
<span className="cursor-pointer text-fuchsia-500">@{profile?.name || profile?.username || shortenKey(pubkey)}</span>
<>
{isError || isLoading ? (
<span className="inline-flex h-4 w-10 animate-pulse rounded bg-zinc-800"></span>
) : (
<span className="cursor-pointer text-fuchsia-500">@{user?.username || user?.name || shortenKey(pubkey)}</span>
)}
</>
);
};

View File

@@ -8,29 +8,44 @@ import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const NoteRepostUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
const profile = useProfile(pubkey);
const { user, isError, isLoading } = useProfile(pubkey);
return (
<div className="group flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex items-baseline gap-2 text-sm">
<h5 className="font-semibold leading-tight group-hover:underline">
{profile?.display_name || profile?.name || shortenKey(pubkey)}{' '}
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
reposted
</span>
</h5>
<span className="leading-tight text-zinc-500">·</span>
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
</div>
{isError || isLoading ? (
<>
<div className="relative h-11 w-11 shrink animate-pulse overflow-hidden rounded-md bg-white bg-zinc-800"></div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-2">
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
</div>
</div>
</div>
</>
) : (
<>
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={user?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex items-baseline gap-2 text-sm">
<h5 className="font-semibold leading-tight group-hover:underline">
{user?.display_name || user?.name || shortenKey(pubkey)}{' '}
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
reposted
</span>
</h5>
<span className="leading-tight text-zinc-500">·</span>
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
</div>
</>
)}
</div>
);
};

View File

@@ -1,6 +1,6 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
export const ActiveAccount = ({ user }: { user: any }) => {
export default function ActiveAccount({ user }: { user: any }) {
const userData = JSON.parse(user.metadata);
return (
@@ -8,4 +8,4 @@ export const ActiveAccount = ({ user }: { user: any }) => {
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
</button>
);
};
}

View File

@@ -0,0 +1,11 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
export default function InactiveAccount({ user }: { user: any }) {
const userData = JSON.parse(user.metadata);
return (
<div className="relative h-11 w-11 shrink rounded-lg">
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
</div>
);
}

View File

@@ -1,20 +0,0 @@
import { ChannelListItem } from '@lume/shared/channels/channelListItem';
import { CreateChannelModal } from '@lume/shared/channels/createChannelModal';
let channels: any = [];
if (typeof window !== 'undefined') {
const { getChannels } = await import('@lume/utils/storage');
channels = await getChannels(100, 0);
}
export default function ChannelList() {
return (
<div className="flex flex-col gap-px">
{channels.map((item: { event_id: string }) => (
<ChannelListItem key={item.event_id} data={item} />
))}
<CreateChannelModal />
</div>
);
}

View File

@@ -1,40 +0,0 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { RelayContext } from '@lume/shared/relaysProvider';
import Tooltip from '@lume/shared/tooltip';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { EyeClose } from 'iconoir-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { useCallback, useContext } from 'react';
export const HideMessageButton = ({ id }: { id: string }) => {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const hideMessage = useCallback(() => {
const event: any = {
content: '',
created_at: dateToUnix(),
kind: 43,
pubkey: activeAccount.pubkey,
tags: [['e', id]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, activeAccount.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
}, [activeAccount.pubkey, activeAccount.privkey, id, pool]);
return (
<Tooltip message="Hide this message">
<button
onClick={() => hideMessage()}
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
>
<EyeClose width={16} height={16} className="text-zinc-400" />
</button>
</Tooltip>
);
};

View File

@@ -1,33 +0,0 @@
import { HideMessageButton } from '@lume/shared/channels/messages/hideMessageButton';
import { MuteButton } from '@lume/shared/channels/messages/muteButton';
import { ReplyButton } from '@lume/shared/channels/messages/replyButton';
import { MessageUser } from '@lume/shared/chats/messageUser';
import { messageParser } from '@lume/utils/parser';
import { memo } from 'react';
export const ChannelMessageItem = memo(function ChannelMessageItem({ data }: { data: any }) {
const content = messageParser(data.content);
return (
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-2 hover:bg-black/20">
<div className="flex flex-col">
<MessageUser pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[17px] pl-[48px]">
<div className="flex flex-col gap-2">
<div className="prose prose-zinc max-w-none break-words text-sm leading-tight dark:prose-invert prose-p:m-0 prose-p:text-sm prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
{content}
</div>
</div>
</div>
</div>
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
<div className="inline-flex h-7 items-center justify-center gap-1 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
<ReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
<HideMessageButton id={data.id} />
<MuteButton pubkey={data.pubkey} />
</div>
</div>
</div>
);
});

View File

@@ -1,40 +0,0 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { RelayContext } from '@lume/shared/relaysProvider';
import Tooltip from '@lume/shared/tooltip';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { MicMute } from 'iconoir-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext } from 'react';
export const MuteButton = ({ pubkey }: { pubkey: string }) => {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const muteUser = () => {
const event: any = {
content: '',
created_at: dateToUnix(),
kind: 44,
pubkey: activeAccount.pubkey,
tags: [['p', pubkey]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, activeAccount.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
};
return (
<Tooltip message="Mute this user">
<button
onClick={() => muteUser()}
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-800"
>
<MicMute width={16} height={16} className="text-zinc-400" />
</button>
</Tooltip>
);
};

View File

@@ -1,38 +1,40 @@
import { AccountContext } from '@lume/shared/accountProvider';
import ActiveAccount from '@lume/shared/accounts/active';
import InactiveAccount from '@lume/shared/accounts/inactive';
import LumeIcon from '@lume/shared/icons/lume';
import { ActiveAccount } from '@lume/shared/multiAccounts/activeAccount';
import { InactiveAccount } from '@lume/shared/multiAccounts/inactiveAccount';
import { APP_VERSION } from '@lume/stores/constants';
import { getAccounts } from '@lume/utils/storage';
import { Plus } from 'iconoir-react';
import { useContext } from 'react';
import useSWR from 'swr';
let accounts: any = [];
if (typeof window !== 'undefined') {
const { getAccounts } = await import('@lume/utils/storage');
accounts = await getAccounts();
}
const fetcher = () => getAccounts();
export default function MultiAccounts() {
const activeAccount: any = useContext(AccountContext);
const { data, error }: any = useSWR('allAccounts', fetcher);
return (
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
<div className="flex flex-col gap-3">
<a
href="/explore"
href="/app/newsfeed/following"
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg bg-zinc-900 hover:bg-zinc-800"
>
<LumeIcon className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
</a>
{accounts.map((account: { pubkey: string }) => {
if (account.pubkey === activeAccount.pubkey) {
return <ActiveAccount key={account.pubkey} user={account} />;
} else {
return <InactiveAccount key={account.pubkey} user={account} />;
}
})}
<>
{error && <div>failed to load</div>}
{!data ? (
<div className="group relative flex h-11 w-11 shrink animate-pulse cursor-pointer items-center justify-center rounded-lg bg-zinc-900"></div>
) : (
data.map((account: { is_active: number; pubkey: string }) => {
if (account.is_active === 1) {
return <ActiveAccount key={account.pubkey} user={account} />;
} else {
return <InactiveAccount key={account.pubkey} user={account} />;
}
})
)}
</>
<a
href="/onboarding"
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-zinc-600 hover:border-zinc-400"

View File

@@ -1,17 +0,0 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { memo } from 'react';
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
const userData = JSON.parse(user.metadata);
const setCurrentUser = () => {
console.log('clicked');
};
return (
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
</button>
);
});

View File

@@ -1,9 +1,8 @@
import ChannelsList from '@lume/app/channel/components/list';
import ActiveLink from '@lume/shared/activeLink';
import ChannelList from '@lume/shared/channels/channelList';
import { Disclosure } from '@headlessui/react';
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
import { Suspense } from 'react';
export default function Navigation() {
return (
@@ -58,9 +57,7 @@ export default function Navigation() {
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Channels</h3>
</Disclosure.Button>
<Disclosure.Panel>
<Suspense fallback={<p>Loading...</p>}>
<ChannelList />
</Suspense>
<ChannelsList />
</Disclosure.Panel>
</div>
)}

View File

@@ -1,4 +1,4 @@
export const ImagePreview = ({ url, size }: { url: string; size: string }) => {
export default function ImagePreview({ url, size }: { url: string; size: string }) {
return (
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
<img
@@ -11,4 +11,4 @@ export const ImagePreview = ({ url, size }: { url: string; size: string }) => {
/>
</div>
);
};
}

View File

@@ -1,6 +1,6 @@
import { MediaOutlet, MediaPlayer } from '@vidstack/react';
export const VideoPreview = ({ url }: { url: string }) => {
export default function VideoPreview({ url }: { url: string }) {
return (
<div onClick={(e) => e.stopPropagation()} className="relative mt-2 flex flex-col overflow-hidden rounded-lg">
<MediaPlayer src={url} poster="" controls>
@@ -8,4 +8,4 @@ export const VideoPreview = ({ url }: { url: string }) => {
</MediaPlayer>
</div>
);
};
}

View File

@@ -5,7 +5,7 @@ function getVideoId(url: string) {
return regex.exec(url)[3];
}
export const YoutubePreview = ({ url }: { url: string }) => {
export default function YoutubePreview({ url }: { url: string }) {
const id = getVideoId(url);
return (
@@ -13,4 +13,4 @@ export const YoutubePreview = ({ url }: { url: string }) => {
<YouTube videoId={id} className="aspect-video xl:w-2/3" opts={{ width: '100%', height: '100%' }} />
</div>
);
};
}

View File

@@ -1,17 +0,0 @@
import { READONLY_RELAYS } from '@lume/stores/constants';
import { RelayPool } from 'nostr-relaypool';
import { createContext, useMemo } from 'react';
export const RelayContext = createContext({});
export default function RelayProvider({ children }: { children: React.ReactNode }) {
const pool = useMemo(() => {
if (typeof window !== 'undefined') {
return new RelayPool(READONLY_RELAYS, { useEventCache: false, logSubscriptions: false });
} else {
return null;
}
}, []);
return <RelayContext.Provider value={pool}>{children}</RelayContext.Provider>;
}

View File

@@ -1,29 +0,0 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import { memo } from 'react';
export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
const profile = useProfile(pubkey);
return (
<div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-full object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-200">
{profile?.display_name || profile?.name}
</span>
<span className="text-sm leading-tight text-zinc-400">{shortenKey(pubkey)}</span>
</div>
</div>
);
});

View File

@@ -1,27 +0,0 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
export const UserFollow = ({ pubkey }: { pubkey: string }) => {
const profile = useProfile(pubkey);
return (
<div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-full object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-200">
{profile?.display_name || profile?.name}
</span>
<span className="text-sm leading-tight text-zinc-400">{shortenKey(pubkey)}</span>
</div>
</div>
);
};

View File

@@ -1,44 +0,0 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { MoreHoriz } from 'iconoir-react';
dayjs.extend(relativeTime);
export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) => {
const profile = useProfile(pubkey);
return (
<div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md border border-white/10 object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="w-full flex-1">
<div className="flex w-full justify-between">
<div className="flex flex-col gap-1 text-sm">
<span className="font-bold leading-tight text-zinc-100">
{profile?.display_name || profile?.name || shortenKey(pubkey)}
</span>
<span className="leading-tight text-zinc-400">
{profile?.username || shortenKey(pubkey)} · {dayjs().to(dayjs.unix(time))}
</span>
</div>
<div>
<button className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800">
<MoreHoriz width={12} height={12} className="text-zinc-500" />
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,24 +0,0 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
export const UserMini = ({ pubkey }: { pubkey: string }) => {
const profile = useProfile(pubkey);
return (
<div className="group flex items-start gap-1">
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-7 w-7 rounded object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<span className="text-xs font-medium leading-none text-zinc-500">
Replying to {profile?.name || shortenKey(pubkey)}
</span>
</div>
);
};

View File

@@ -1,64 +0,0 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import { useState } from 'react';
export const UserMuted = ({ data }: { data: any }) => {
const profile = useProfile(data.content);
const [status, setStatus] = useState(data.status);
const unmute = async () => {
const { updateItemInBlacklist } = await import('@lume/utils/storage');
const res = await updateItemInBlacklist(data.content, 0);
if (res) {
setStatus(0);
}
};
const mute = async () => {
const { updateItemInBlacklist } = await import('@lume/utils/storage');
const res = await updateItemInBlacklist(data.content, 1);
if (res) {
setStatus(1);
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<div className="relative h-9 w-9 shrink rounded-md">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={data.content}
className="h-9 w-9 rounded-md object-cover"
loading="lazy"
/>
</div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
<span className="truncate text-sm font-medium leading-none text-zinc-200">
{profile?.display_name || profile?.name}
</span>
<span className="text-xs leading-none text-zinc-400">{shortenKey(data.content)}</span>
</div>
</div>
<div>
{status === 1 ? (
<button
onClick={() => unmute()}
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Unmute
</button>
) : (
<button
onClick={() => mute()}
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-xs font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Mute
</button>
)}
</div>
</div>
);
};

View File

@@ -1,15 +1,15 @@
import { RelayContext } from '@lume/shared/relaysProvider';
import { READONLY_RELAYS } from '@lume/stores/constants';
import { updateChannelMetadata } from '@lume/utils/storage';
import { getChannel } from '@lume/utils/storage';
import { useCallback, useContext, useEffect, useState } from 'react';
import { RelayPool } from 'nostr-relaypool';
import { useCallback, useEffect, useState } from 'react';
export const useChannelMetadata = (id: string, channelPubkey: string) => {
const pool: any = useContext(RelayContext);
const [metadata, setMetadata] = useState(null);
const fetchFromRelay = useCallback(() => {
const pool = new RelayPool(READONLY_RELAYS);
const unsubscribe = pool.subscribe(
[
{
@@ -53,7 +53,7 @@ export const useChannelMetadata = (id: string, channelPubkey: string) => {
return () => {
unsubscribe();
};
}, [channelPubkey, id, pool]);
}, [channelPubkey, id]);
const getChannelFromDB = useCallback(async () => {
return await getChannel(id);

View File

@@ -1,12 +1,9 @@
import { RelayContext } from '@lume/shared/relaysProvider';
import { READONLY_RELAYS } from '@lume/stores/constants';
import { FULL_RELAYS } from '@lume/stores/constants';
import { useContext } from 'react';
import { RelayPool } from 'nostr-relaypool';
import useSWRSubscription from 'swr/subscription';
export const useChannelProfile = (id: string, channelPubkey: string) => {
const pool: any = useContext(RelayContext);
const { data } = useSWRSubscription(
id
? [
@@ -21,9 +18,10 @@ export const useChannelProfile = (id: string, channelPubkey: string) => {
]
: null,
(key, { next }) => {
const pool = new RelayPool(FULL_RELAYS);
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
FULL_RELAYS,
(event: { kind: number; pubkey: string; content: string }) => {
switch (event.kind) {
case 40:
@@ -41,7 +39,6 @@ export const useChannelProfile = (id: string, channelPubkey: string) => {
undefined,
{
unsubscribeOnEose: true,
logAllEvents: false,
}
);

View File

@@ -3,12 +3,11 @@ import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((r: any) => r.json());
export const useProfile = (pubkey: string) => {
const { data, error } = useSWR(`https://rbr.bio/${pubkey}/metadata.json`, fetcher);
if (error) {
return error;
}
if (data) {
return JSON.parse(data.content);
}
return null;
const { data, error, isLoading } = useSWR(`https://us.rbr.bio/${pubkey}/metadata.json`, fetcher);
return {
user: data ? JSON.parse(data.content ? data.content : null) : null,
isLoading,
isError: error,
};
};

View File

@@ -1,60 +1,9 @@
import { ImagePreview } from '@lume/shared/note/preview/image';
import { VideoPreview } from '@lume/shared/note/preview/video';
import { YoutubePreview } from '@lume/shared/note/preview/youtube';
import { NoteQuote } from '@lume/shared/note/quote';
import { UserMention } from '@lume/shared/user/mention';
import ImagePreview from '@lume/shared/preview/image';
import VideoPreview from '@lume/shared/preview/video';
import YoutubePreview from '@lume/shared/preview/youtube';
import destr from 'destr';
import reactStringReplace from 'react-string-replace';
export const contentParser = (noteContent: any, noteTags: any) => {
let parsedContent = noteContent.trim();
// get data tags
const tags = destr(noteTags);
// handle urls
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
if (match.match(/\.(jpg|jpeg|gif|png|webp)$/i)) {
// image url
return <ImagePreview key={match + i} url={match} size="large" />;
} else if (match.match(/(http:|https:)?(\/\/)?(www\.)?(youtube.com|youtu.be)\/(watch|embed)?(\?v=|\/)?(\S+)?/)) {
// youtube
return <YoutubePreview key={match + i} url={match} />;
} else if (match.match(/\.(mp4|webm)$/i)) {
// video
return <VideoPreview key={match + i} url={match} />;
} else {
return (
<a key={match + i} href={match} className="cursor-pointer text-fuchsia-500" target="_blank" rel="noreferrer">
{match}
</a>
);
}
});
// handle #-hashtags
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
<span key={match + i} className="cursor-pointer text-fuchsia-500">
#{match}
</span>
));
// handle mentions
if (tags && tags.length > 0) {
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
if (tags[match][0] === 'p') {
// @-mentions
return <UserMention key={tags[match][1] + i} pubkey={tags[match][1]} />;
} else if (tags[match][0] === 'e') {
// note-quotes
return <NoteQuote key={tags[match][1] + i} id={tags[match][1]} />;
} else {
return;
}
});
}
return parsedContent;
};
export const messageParser = (noteContent: any) => {
let parsedContent = noteContent.trim();