split MessagesPage into smaller files
This commit is contained in:
parent
2e54104618
commit
8bc323489e
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export interface NoteTimeProps {
|
||||
from: number;
|
||||
@ -15,8 +15,8 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
const { from, fallback } = props;
|
||||
|
||||
const absoluteTime = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'long',
|
||||
dateStyle: "medium",
|
||||
timeStyle: "long",
|
||||
}).format(from);
|
||||
|
||||
const isoDate = new Date(from).toISOString();
|
||||
@ -35,14 +35,14 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
} else {
|
||||
if (fromDate.getFullYear() === currentTime.getFullYear()) {
|
||||
return fromDate.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
} else {
|
||||
return fromDate.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
124
packages/app/src/Pages/Messages/MessagesPage.tsx
Normal file
124
packages/app/src/Pages/Messages/MessagesPage.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import "./MessagesPage.css";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import UnreadCount from "@/Element/UnreadCount";
|
||||
import ProfileImage from "@/Element/User/ProfileImage";
|
||||
import { parseId } from "@/SnortUtils";
|
||||
import NoteToSelf from "@/Element/User/NoteToSelf";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import usePageWidth from "@/Hooks/usePageWidth";
|
||||
import NoteTime from "@/Element/Event/NoteTime";
|
||||
import DmWindow from "@/Element/Chat/DmWindow";
|
||||
import { Chat, ChatType, useChatSystem } from "@/chat";
|
||||
import { ChatParticipantProfile } from "@/Element/Chat/ChatParticipant";
|
||||
import classNames from "classnames";
|
||||
import ProfileDmActions from "@/Pages/Messages/ProfileDmActions";
|
||||
import NewChatWindow from "@/Pages/Messages/NewChatWindow";
|
||||
|
||||
const TwoCol = 768;
|
||||
const ThreeCol = 1500;
|
||||
|
||||
export default function MessagesPage() {
|
||||
const login = useLogin();
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [chat, setChat] = useState<string>();
|
||||
const pageWidth = usePageWidth();
|
||||
|
||||
useEffect(() => {
|
||||
const parsedId = parseId(id ?? "");
|
||||
setChat(id ? parsedId : undefined);
|
||||
}, [id]);
|
||||
const chats = useChatSystem();
|
||||
|
||||
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]);
|
||||
|
||||
function openChat(e: React.MouseEvent<HTMLDivElement>, type: ChatType, id: string) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigate(`/messages/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
function noteToSelf(chat: Chat) {
|
||||
return (
|
||||
<div className="flex p" key={chat.id} onClick={e => openChat(e, chat.type, chat.id)}>
|
||||
<NoteToSelf className="grow" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function conversationIdent(cx: Chat) {
|
||||
if (cx.participants.length === 1) {
|
||||
return <ChatParticipantProfile participant={cx.participants[0]} />;
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center grow pfp-overlap">
|
||||
{cx.participants.map(v => (
|
||||
<ProfileImage pubkey={v.id} link="" showUsername={false} profile={v.profile} />
|
||||
))}
|
||||
{cx.title ?? <FormattedMessage defaultMessage="Group Chat" id="eXT2QQ" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function conversation(cx: Chat) {
|
||||
if (!login.publicKey) return null;
|
||||
const participants = cx.participants.map(a => a.id);
|
||||
if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(cx);
|
||||
|
||||
const isActive = cx.id === chat;
|
||||
return (
|
||||
<div
|
||||
className={classNames("flex items-center p", { active: isActive })}
|
||||
key={cx.id}
|
||||
onClick={e => openChat(e, cx.type, cx.id)}>
|
||||
{conversationIdent(cx)}
|
||||
<div className="nowrap">
|
||||
<small>
|
||||
<NoteTime
|
||||
from={cx.lastMessage * 1000}
|
||||
fallback={formatMessage({ defaultMessage: "Just now", id: "bxv59V" })}
|
||||
/>
|
||||
</small>
|
||||
{cx.unread > 0 && <UnreadCount unread={cx.unread} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dm-page">
|
||||
{(pageWidth >= TwoCol || !chat) && (
|
||||
<div className="chat-list">
|
||||
<div className="flex items-center p justify-between">
|
||||
<button disabled={unreadCount <= 0} type="button">
|
||||
<FormattedMessage defaultMessage="Mark all read" id="ShdEie" />
|
||||
</button>
|
||||
<NewChatWindow />
|
||||
</div>
|
||||
{chats
|
||||
.sort((a, b) => {
|
||||
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
|
||||
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
|
||||
if (aSelf || bSelf) {
|
||||
return aSelf ? -1 : 1;
|
||||
}
|
||||
return b.lastMessage > a.lastMessage ? 1 : -1;
|
||||
})
|
||||
.map(conversation)}
|
||||
</div>
|
||||
)}
|
||||
{chat ? <DmWindow id={chat} /> : pageWidth >= TwoCol && <div></div>}
|
||||
{pageWidth >= ThreeCol && chat && (
|
||||
<div>
|
||||
<ProfileDmActions id={chat} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
145
packages/app/src/Pages/Messages/NewChatWindow.tsx
Normal file
145
packages/app/src/Pages/Messages/NewChatWindow.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useUserSearch } from "@snort/system-react";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { appendDedupe, debounce } from "@/SnortUtils";
|
||||
import { ChatType, createChatLink } from "@/chat";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import Modal from "@/Element/Modal";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import ProfileImage from "@/Element/User/ProfileImage";
|
||||
import ProfilePreview from "@/Element/User/ProfilePreview";
|
||||
import { Nip28ChatSystem } from "@/chat/nip28";
|
||||
import { LoginSession, LoginStore } from "@/Login";
|
||||
import { decodeTLV, EventKind } from "@snort/system";
|
||||
import Nip28ChatProfile from "@/Pages/Messages/Nip28ChatProfile";
|
||||
|
||||
export default function NewChatWindow() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [newChat, setNewChat] = useState<Array<string>>([]);
|
||||
const [results, setResults] = useState<Array<string>>([]);
|
||||
const [term, setSearchTerm] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const search = useUserSearch();
|
||||
const login = useLogin();
|
||||
const { system, publisher } = useEventPublisher();
|
||||
|
||||
useEffect(() => {
|
||||
setNewChat([]);
|
||||
setSearchTerm("");
|
||||
setResults(login.follows.item);
|
||||
}, [show]);
|
||||
|
||||
useEffect(() => {
|
||||
return debounce(500, () => {
|
||||
if (term) {
|
||||
search(term).then(setResults);
|
||||
} else {
|
||||
setResults(login.follows.item);
|
||||
}
|
||||
});
|
||||
}, [term]);
|
||||
|
||||
function togglePubkey(a: string) {
|
||||
setNewChat(c => (c.includes(a) ? c.filter(v => v !== a) : appendDedupe(c, [a])));
|
||||
}
|
||||
|
||||
function startChat() {
|
||||
setShow(false);
|
||||
if (newChat.length === 1) {
|
||||
navigate(createChatLink(ChatType.DirectMessage, newChat[0]));
|
||||
} else {
|
||||
navigate(createChatLink(ChatType.PrivateGroupChat, ...newChat));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="flex justify-center new-chat" onClick={() => setShow(true)}>
|
||||
<Icon name="plus" size={16} />
|
||||
</button>
|
||||
{show && (
|
||||
<Modal id="new-chat" onClose={() => setShow(false)} className="new-chat-modal">
|
||||
<div className="flex flex-col g16">
|
||||
<div className="flex justify-between">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="New Chat" id="UT7Nkj" />
|
||||
</h2>
|
||||
<button onClick={startChat}>
|
||||
<FormattedMessage defaultMessage="Start chat" id="v8lolG" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col g8">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Search users" id="JjGgXI" />
|
||||
</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="npub/nprofile/nostr address"
|
||||
value={term}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{newChat.map(a => (
|
||||
<ProfileImage
|
||||
key={`selected-${a}`}
|
||||
pubkey={a}
|
||||
showUsername={false}
|
||||
link=""
|
||||
onClick={() => togglePubkey(a)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="People you follow" id="R81upa" />
|
||||
</p>
|
||||
<div className="user-list flex flex-col g2">
|
||||
{results.map(a => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
pubkey={a}
|
||||
key={`option-${a}`}
|
||||
options={{ about: false, linkToProfile: false }}
|
||||
actions={<></>}
|
||||
onClick={() => togglePubkey(a)}
|
||||
className={newChat.includes(a) ? "active" : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{results.length === 1 && (
|
||||
<Nip28ChatProfile
|
||||
id={results[0]}
|
||||
onClick={async id => {
|
||||
setShow(false);
|
||||
const chats = appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]);
|
||||
LoginStore.updateSession({
|
||||
...login,
|
||||
extraChats: chats,
|
||||
} as LoginSession);
|
||||
const evList = await publisher?.generic(eb => {
|
||||
eb.kind(EventKind.PublicChatsList);
|
||||
chats.forEach(c => {
|
||||
if (c.startsWith("chat281")) {
|
||||
eb.tag(["e", decodeTLV(c)[0].value as string]);
|
||||
}
|
||||
});
|
||||
return eb;
|
||||
});
|
||||
if (evList) {
|
||||
await system.BroadcastEvent(evList);
|
||||
}
|
||||
navigate(createChatLink(ChatType.PublicGroupChat, id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
20
packages/app/src/Pages/Messages/Nip28ChatProfile.tsx
Normal file
20
packages/app/src/Pages/Messages/Nip28ChatProfile.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import { NostrLink, UserMetadata } from "@snort/system";
|
||||
import ProfilePreview from "@/Element/User/ProfilePreview";
|
||||
import React from "react";
|
||||
|
||||
export default function Nip28ChatProfile({ id, onClick }: { id: string; onClick: (id: string) => void }) {
|
||||
const channel = useEventFeed(new NostrLink(CONFIG.eventLinkPrefix, id, 40));
|
||||
if (channel?.data) {
|
||||
const meta = JSON.parse(channel.data.content) as UserMetadata;
|
||||
return (
|
||||
<ProfilePreview
|
||||
pubkey=""
|
||||
profile={meta}
|
||||
options={{ about: false, linkToProfile: false }}
|
||||
actions={<></>}
|
||||
onClick={() => onClick(id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
52
packages/app/src/Pages/Messages/ProfileDmActions.tsx
Normal file
52
packages/app/src/Pages/Messages/ProfileDmActions.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { decodeTLV, TLVEntryType } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import Avatar from "@/Element/User/Avatar";
|
||||
import { getDisplayName } from "@/SnortUtils";
|
||||
import Text from "@/Element/Text";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import React from "react";
|
||||
|
||||
export default function ProfileDmActions({ id }: { id: string }) {
|
||||
const authors = decodeTLV(id)
|
||||
.filter(a => a.type === TLVEntryType.Author)
|
||||
.map(a => a.value as string);
|
||||
const pubkey = authors[0];
|
||||
const profile = useUserProfile(pubkey);
|
||||
const { block, unblock, isBlocked } = useModeration();
|
||||
|
||||
function truncAbout(s?: string) {
|
||||
if ((s?.length ?? 0) > 200) {
|
||||
return `${s?.slice(0, 200)}...`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const blocked = isBlocked(pubkey);
|
||||
return (
|
||||
<>
|
||||
<Avatar pubkey={pubkey} user={profile} size={210} />
|
||||
<h2>{getDisplayName(profile, pubkey)}</h2>
|
||||
<p>
|
||||
<Text
|
||||
id={pubkey}
|
||||
content={truncAbout(profile?.about) ?? ""}
|
||||
tags={[]}
|
||||
creator={pubkey}
|
||||
disableMedia={true}
|
||||
depth={0}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div className="settings-row" onClick={() => (blocked ? unblock(pubkey) : block(pubkey))}>
|
||||
<Icon name="block" />
|
||||
{blocked ? (
|
||||
<FormattedMessage defaultMessage="Unblock" id="nDejmx" />
|
||||
) : (
|
||||
<FormattedMessage defaultMessage="Block" id="Up5U7K" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,321 +0,0 @@
|
||||
import "./MessagesPage.css";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { EventKind, NostrLink, TLVEntryType, UserMetadata, decodeTLV } from "@snort/system";
|
||||
import { useEventFeed, useUserProfile, useUserSearch } from "@snort/system-react";
|
||||
|
||||
import UnreadCount from "@/Element/UnreadCount";
|
||||
import ProfileImage from "@/Element/User/ProfileImage";
|
||||
import { appendDedupe, debounce, parseId, getDisplayName } from "@/SnortUtils";
|
||||
import NoteToSelf from "@/Element/User/NoteToSelf";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import usePageWidth from "@/Hooks/usePageWidth";
|
||||
import NoteTime from "@/Element/Event/NoteTime";
|
||||
import DmWindow from "@/Element/Chat/DmWindow";
|
||||
import Avatar from "@/Element/User/Avatar";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import Text from "@/Element/Text";
|
||||
import { Chat, ChatType, createChatLink, useChatSystem } from "@/chat";
|
||||
import Modal from "@/Element/Modal";
|
||||
import ProfilePreview from "@/Element/User/ProfilePreview";
|
||||
import { LoginSession, LoginStore } from "@/Login";
|
||||
import { Nip28ChatSystem } from "@/chat/nip28";
|
||||
import { ChatParticipantProfile } from "@/Element/Chat/ChatParticipant";
|
||||
import classNames from "classnames";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
|
||||
const TwoCol = 768;
|
||||
const ThreeCol = 1500;
|
||||
|
||||
export default function MessagesPage() {
|
||||
const login = useLogin();
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [chat, setChat] = useState<string>();
|
||||
const pageWidth = usePageWidth();
|
||||
|
||||
useEffect(() => {
|
||||
const parsedId = parseId(id ?? "");
|
||||
setChat(id ? parsedId : undefined);
|
||||
}, [id]);
|
||||
const chats = useChatSystem();
|
||||
|
||||
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]);
|
||||
|
||||
function openChat(e: React.MouseEvent<HTMLDivElement>, type: ChatType, id: string) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigate(`/messages/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
function noteToSelf(chat: Chat) {
|
||||
return (
|
||||
<div className="flex p" key={chat.id} onClick={e => openChat(e, chat.type, chat.id)}>
|
||||
<NoteToSelf className="grow" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function conversationIdent(cx: Chat) {
|
||||
if (cx.participants.length === 1) {
|
||||
return <ChatParticipantProfile participant={cx.participants[0]} />;
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center grow pfp-overlap">
|
||||
{cx.participants.map(v => (
|
||||
<ProfileImage pubkey={v.id} link="" showUsername={false} profile={v.profile} />
|
||||
))}
|
||||
{cx.title ?? <FormattedMessage defaultMessage="Group Chat" id="eXT2QQ" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function conversation(cx: Chat) {
|
||||
if (!login.publicKey) return null;
|
||||
const participants = cx.participants.map(a => a.id);
|
||||
if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(cx);
|
||||
|
||||
const isActive = cx.id === chat;
|
||||
return (
|
||||
<div
|
||||
className={classNames("flex items-center p", { active: isActive })}
|
||||
key={cx.id}
|
||||
onClick={e => openChat(e, cx.type, cx.id)}>
|
||||
{conversationIdent(cx)}
|
||||
<div className="nowrap">
|
||||
<small>
|
||||
<NoteTime
|
||||
from={cx.lastMessage * 1000}
|
||||
fallback={formatMessage({ defaultMessage: "Just now", id: "bxv59V" })}
|
||||
/>
|
||||
</small>
|
||||
{cx.unread > 0 && <UnreadCount unread={cx.unread} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dm-page">
|
||||
{(pageWidth >= TwoCol || !chat) && (
|
||||
<div className="chat-list">
|
||||
<div className="flex items-center p justify-between">
|
||||
<button disabled={unreadCount <= 0} type="button">
|
||||
<FormattedMessage defaultMessage="Mark all read" id="ShdEie" />
|
||||
</button>
|
||||
<NewChatWindow />
|
||||
</div>
|
||||
{chats
|
||||
.sort((a, b) => {
|
||||
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
|
||||
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
|
||||
if (aSelf || bSelf) {
|
||||
return aSelf ? -1 : 1;
|
||||
}
|
||||
return b.lastMessage > a.lastMessage ? 1 : -1;
|
||||
})
|
||||
.map(conversation)}
|
||||
</div>
|
||||
)}
|
||||
{chat ? <DmWindow id={chat} /> : pageWidth >= TwoCol && <div></div>}
|
||||
{pageWidth >= ThreeCol && chat && (
|
||||
<div>
|
||||
<ProfileDmActions id={chat} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileDmActions({ id }: { id: string }) {
|
||||
const authors = decodeTLV(id)
|
||||
.filter(a => a.type === TLVEntryType.Author)
|
||||
.map(a => a.value as string);
|
||||
const pubkey = authors[0];
|
||||
const profile = useUserProfile(pubkey);
|
||||
const { block, unblock, isBlocked } = useModeration();
|
||||
|
||||
function truncAbout(s?: string) {
|
||||
if ((s?.length ?? 0) > 200) {
|
||||
return `${s?.slice(0, 200)}...`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const blocked = isBlocked(pubkey);
|
||||
return (
|
||||
<>
|
||||
<Avatar pubkey={pubkey} user={profile} size={210} />
|
||||
<h2>{getDisplayName(profile, pubkey)}</h2>
|
||||
<p>
|
||||
<Text
|
||||
id={pubkey}
|
||||
content={truncAbout(profile?.about) ?? ""}
|
||||
tags={[]}
|
||||
creator={pubkey}
|
||||
disableMedia={true}
|
||||
depth={0}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div className="settings-row" onClick={() => (blocked ? unblock(pubkey) : block(pubkey))}>
|
||||
<Icon name="block" />
|
||||
{blocked ? (
|
||||
<FormattedMessage defaultMessage="Unblock" id="nDejmx" />
|
||||
) : (
|
||||
<FormattedMessage defaultMessage="Block" id="Up5U7K" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NewChatWindow() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [newChat, setNewChat] = useState<Array<string>>([]);
|
||||
const [results, setResults] = useState<Array<string>>([]);
|
||||
const [term, setSearchTerm] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const search = useUserSearch();
|
||||
const login = useLogin();
|
||||
const { system, publisher } = useEventPublisher();
|
||||
|
||||
useEffect(() => {
|
||||
setNewChat([]);
|
||||
setSearchTerm("");
|
||||
setResults(login.follows.item);
|
||||
}, [show]);
|
||||
|
||||
useEffect(() => {
|
||||
return debounce(500, () => {
|
||||
if (term) {
|
||||
search(term).then(setResults);
|
||||
} else {
|
||||
setResults(login.follows.item);
|
||||
}
|
||||
});
|
||||
}, [term]);
|
||||
|
||||
function togglePubkey(a: string) {
|
||||
setNewChat(c => (c.includes(a) ? c.filter(v => v !== a) : appendDedupe(c, [a])));
|
||||
}
|
||||
|
||||
function startChat() {
|
||||
setShow(false);
|
||||
if (newChat.length === 1) {
|
||||
navigate(createChatLink(ChatType.DirectMessage, newChat[0]));
|
||||
} else {
|
||||
navigate(createChatLink(ChatType.PrivateGroupChat, ...newChat));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="flex justify-center new-chat" onClick={() => setShow(true)}>
|
||||
<Icon name="plus" size={16} />
|
||||
</button>
|
||||
{show && (
|
||||
<Modal id="new-chat" onClose={() => setShow(false)} className="new-chat-modal">
|
||||
<div className="flex flex-col g16">
|
||||
<div className="flex justify-between">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="New Chat" id="UT7Nkj" />
|
||||
</h2>
|
||||
<button onClick={startChat}>
|
||||
<FormattedMessage defaultMessage="Start chat" id="v8lolG" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col g8">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Search users" id="JjGgXI" />
|
||||
</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="npub/nprofile/nostr address"
|
||||
value={term}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{newChat.map(a => (
|
||||
<ProfileImage
|
||||
key={`selected-${a}`}
|
||||
pubkey={a}
|
||||
showUsername={false}
|
||||
link=""
|
||||
onClick={() => togglePubkey(a)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="People you follow" id="R81upa" />
|
||||
</p>
|
||||
<div className="user-list flex flex-col g2">
|
||||
{results.map(a => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
pubkey={a}
|
||||
key={`option-${a}`}
|
||||
options={{ about: false, linkToProfile: false }}
|
||||
actions={<></>}
|
||||
onClick={() => togglePubkey(a)}
|
||||
className={newChat.includes(a) ? "active" : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{results.length === 1 && (
|
||||
<Nip28ChatProfile
|
||||
id={results[0]}
|
||||
onClick={async id => {
|
||||
setShow(false);
|
||||
const chats = appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]);
|
||||
LoginStore.updateSession({
|
||||
...login,
|
||||
extraChats: chats,
|
||||
} as LoginSession);
|
||||
const evList = await publisher?.generic(eb => {
|
||||
eb.kind(EventKind.PublicChatsList);
|
||||
chats.forEach(c => {
|
||||
if (c.startsWith("chat281")) {
|
||||
eb.tag(["e", decodeTLV(c)[0].value as string]);
|
||||
}
|
||||
});
|
||||
return eb;
|
||||
});
|
||||
if (evList) {
|
||||
await system.BroadcastEvent(evList);
|
||||
}
|
||||
navigate(createChatLink(ChatType.PublicGroupChat, id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Nip28ChatProfile({ id, onClick }: { id: string; onClick: (id: string) => void }) {
|
||||
const channel = useEventFeed(new NostrLink(CONFIG.eventLinkPrefix, id, 40));
|
||||
if (channel?.data) {
|
||||
const meta = JSON.parse(channel.data.content) as UserMetadata;
|
||||
return (
|
||||
<ProfilePreview
|
||||
pubkey=""
|
||||
profile={meta}
|
||||
options={{ about: false, linkToProfile: false }}
|
||||
actions={<></>}
|
||||
onClick={() => onClick(id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ import NotificationsPage from "@/Pages/Notifications";
|
||||
import SettingsPage, { SettingsRoutes } from "@/Pages/SettingsPage";
|
||||
import ErrorPage from "@/Pages/ErrorPage";
|
||||
import NostrAddressPage from "@/Pages/NostrAddressPage";
|
||||
import MessagesPage from "@/Pages/MessagesPage";
|
||||
import MessagesPage from "@/Pages/Messages/MessagesPage";
|
||||
import DonatePage from "@/Pages/DonatePage";
|
||||
import SearchPage from "@/Pages/SearchPage";
|
||||
import HelpPage from "@/Pages/HelpPage";
|
||||
|
Loading…
Reference in New Issue
Block a user