2023-06-30 14:05:57 +00:00
|
|
|
/** @jsx h */
|
2023-09-06 16:29:27 +00:00
|
|
|
import { Fragment, h } from "https://esm.sh/preact@10.17.1";
|
2023-06-30 14:05:57 +00:00
|
|
|
import { tw } from "https://esm.sh/twind@0.16.16";
|
|
|
|
import { Avatar } from "./components/avatar.tsx";
|
2023-07-07 10:58:58 +00:00
|
|
|
import { CenterClass, IconButtonClass, LinearGradientsClass } from "./components/tw.ts";
|
2023-09-23 21:54:13 +00:00
|
|
|
import { ConversationSummary, sortUserInfo } from "./conversation-list.ts";
|
2023-09-10 17:56:37 +00:00
|
|
|
import { emitFunc } from "../event-bus.ts";
|
2023-07-07 10:58:58 +00:00
|
|
|
import { PinIcon, UnpinIcon } from "./icons/mod.tsx";
|
2023-09-13 18:27:08 +00:00
|
|
|
import { SearchUpdate } from "./search_model.ts";
|
2023-08-28 17:58:05 +00:00
|
|
|
import { PublicKey } from "../lib/nostr-ts/key.ts";
|
2023-06-30 14:05:57 +00:00
|
|
|
import { PinContact, UnpinContact } from "../nostr.ts";
|
2023-07-07 10:58:58 +00:00
|
|
|
import { AddIcon } from "./icons2/add-icon.tsx";
|
2023-09-10 17:56:37 +00:00
|
|
|
import { PrimaryTextColor } from "./style/colors.ts";
|
2023-09-13 18:27:08 +00:00
|
|
|
|
2023-09-23 21:33:30 +00:00
|
|
|
export interface ConversationListRetriever {
|
2023-09-23 21:54:13 +00:00
|
|
|
getContacts: () => Iterable<ConversationSummary>;
|
|
|
|
getStrangers: () => Iterable<ConversationSummary>;
|
2023-09-13 18:27:08 +00:00
|
|
|
}
|
2023-06-30 14:05:57 +00:00
|
|
|
|
2023-09-23 21:33:30 +00:00
|
|
|
export type ConversationGroup = "Contacts" | "Strangers";
|
|
|
|
|
|
|
|
export type ContactUpdate = SelectConversationGroup | SearchUpdate | PinContact | UnpinContact;
|
|
|
|
|
|
|
|
export type SelectConversationGroup = {
|
|
|
|
type: "SelectConversationGroup";
|
|
|
|
group: ConversationGroup;
|
|
|
|
};
|
|
|
|
|
2023-06-30 14:05:57 +00:00
|
|
|
type Props = {
|
2023-09-10 17:56:37 +00:00
|
|
|
emit: emitFunc<ContactUpdate | SearchUpdate>;
|
2023-09-23 21:33:30 +00:00
|
|
|
convoListRetriever: ConversationListRetriever;
|
2023-06-30 14:05:57 +00:00
|
|
|
currentSelected: PublicKey | undefined;
|
2023-09-23 21:33:30 +00:00
|
|
|
selectedContactGroup: ConversationGroup;
|
2023-06-30 14:05:57 +00:00
|
|
|
hasNewMessages: Set<string>;
|
|
|
|
};
|
2023-09-23 21:33:30 +00:00
|
|
|
export function ConversationList(props: Props) {
|
2023-06-30 14:05:57 +00:00
|
|
|
const t = Date.now();
|
|
|
|
|
2023-09-23 21:33:30 +00:00
|
|
|
let contacts = Array.from(props.convoListRetriever.getContacts());
|
|
|
|
let strangers = Array.from(props.convoListRetriever.getStrangers());
|
2023-06-30 14:05:57 +00:00
|
|
|
const listToRender = props.selectedContactGroup == "Contacts" ? contacts : strangers;
|
|
|
|
|
2023-09-13 14:52:01 +00:00
|
|
|
const contactsToRender = [];
|
|
|
|
for (const contact of listToRender) {
|
|
|
|
contactsToRender.push({
|
|
|
|
userInfo: contact,
|
|
|
|
isMarked: props.hasNewMessages.has(contact.pubkey.hex),
|
2023-06-30 14:05:57 +00:00
|
|
|
});
|
2023-09-13 14:52:01 +00:00
|
|
|
}
|
|
|
|
|
2023-06-30 14:05:57 +00:00
|
|
|
return (
|
|
|
|
<div class={tw`h-full flex flex-col mobile:w-full desktop:w-64 bg-[#2F3136]`}>
|
|
|
|
<div
|
2023-07-07 10:58:58 +00:00
|
|
|
class={tw`flex items-center justify-between px-4 h-20 border-b border-[#36393F]`}
|
2023-06-30 14:05:57 +00:00
|
|
|
>
|
|
|
|
<button
|
2023-09-10 17:56:37 +00:00
|
|
|
onClick={async () => {
|
|
|
|
props.emit({
|
2023-06-30 14:05:57 +00:00
|
|
|
type: "StartSearch",
|
|
|
|
});
|
|
|
|
}}
|
2023-07-07 10:58:58 +00:00
|
|
|
class={tw`w-full h-[2.5rem] text-[${PrimaryTextColor}] ${IconButtonClass} ${LinearGradientsClass} hover:bg-gradient-to-l`}
|
2023-06-30 14:05:57 +00:00
|
|
|
>
|
2023-07-07 10:58:58 +00:00
|
|
|
New Chat
|
2023-06-30 14:05:57 +00:00
|
|
|
<AddIcon
|
2023-07-07 10:58:58 +00:00
|
|
|
class={tw`w-[1.5rem] h-[1.5rem]`}
|
2023-06-30 14:05:57 +00:00
|
|
|
style={{
|
2023-07-07 10:58:58 +00:00
|
|
|
fill: PrimaryTextColor,
|
2023-06-30 14:05:57 +00:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<ul class={tw`bg-[#36393F] w-full flex h-[3rem] border-b border-[#36393F]`}>
|
|
|
|
<li
|
|
|
|
class={tw`h-full flex-1 cursor-pointer hover:text-[#F7F7F7] text-[#96989D] bg-[#2F3136] hover:bg-[#42464D] ${CenterClass} ${
|
|
|
|
props.selectedContactGroup == "Contacts"
|
|
|
|
? "border-b-2 border-[#54D48C] bg-[#42464D] text-[#F7F7F7]"
|
|
|
|
: ""
|
|
|
|
}`}
|
|
|
|
onClick={() => {
|
2023-09-10 17:56:37 +00:00
|
|
|
props.emit({
|
2023-09-23 21:33:30 +00:00
|
|
|
type: "SelectConversationGroup",
|
2023-06-30 14:05:57 +00:00
|
|
|
group: "Contacts",
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Contacts: {contacts.length}
|
|
|
|
</li>
|
|
|
|
<li class={tw`w-[0.05rem] h-full bg-[#2F3136]`}></li>
|
|
|
|
<li
|
|
|
|
class={tw`h-full flex-1 cursor-pointer hover:text-[#F7F7F7] text-[#96989D] bg-[#2F3136] hover:bg-[#42464D] ${CenterClass} ${
|
|
|
|
props.selectedContactGroup == "Strangers"
|
|
|
|
? "border-b-2 border-[#54D48C] bg-[#42464D] text-[#F7F7F7]"
|
|
|
|
: ""
|
|
|
|
}`}
|
|
|
|
onClick={() => {
|
2023-09-10 17:56:37 +00:00
|
|
|
props.emit({
|
2023-09-23 21:33:30 +00:00
|
|
|
type: "SelectConversationGroup",
|
2023-06-30 14:05:57 +00:00
|
|
|
group: "Strangers",
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Strangers: {strangers.length}
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
<ContactGroup
|
|
|
|
contacts={Array.from(contactsToRender.values())}
|
|
|
|
currentSelected={props.currentSelected}
|
2023-09-10 17:56:37 +00:00
|
|
|
emit={props.emit}
|
2023-06-30 14:05:57 +00:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConversationListProps = {
|
2023-09-23 21:54:13 +00:00
|
|
|
contacts: { userInfo: ConversationSummary; isMarked: boolean }[];
|
2023-06-30 14:05:57 +00:00
|
|
|
currentSelected: PublicKey | undefined;
|
2023-09-10 17:56:37 +00:00
|
|
|
emit: emitFunc<ContactUpdate>;
|
2023-06-30 14:05:57 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
function ContactGroup(props: ConversationListProps) {
|
|
|
|
const t = Date.now();
|
|
|
|
props.contacts.sort((a, b) => {
|
|
|
|
return sortUserInfo(a.userInfo, b.userInfo);
|
|
|
|
});
|
|
|
|
const pinned = [];
|
|
|
|
const unpinned = [];
|
|
|
|
for (const contact of props.contacts) {
|
|
|
|
if (contact.userInfo.pinEvent && contact.userInfo.pinEvent.content.type == "PinContact") {
|
|
|
|
pinned.push(contact);
|
|
|
|
} else {
|
|
|
|
unpinned.push(contact);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// console.log("ContactGroup", Date.now() - t);
|
|
|
|
return (
|
|
|
|
<ul class={tw`overflow-auto flex-1 p-2 text-[#96989D]`}>
|
|
|
|
{pinned.map((contact) => {
|
|
|
|
return (
|
|
|
|
<li
|
|
|
|
class={tw`${
|
|
|
|
props.currentSelected && contact.userInfo.pubkey.hex ===
|
|
|
|
props.currentSelected.hex
|
|
|
|
? "bg-[#42464D] text-[#FFFFFF]"
|
|
|
|
: "bg-[#42464D] text-[#96989D]"
|
|
|
|
} cursor-pointer p-2 hover:bg-[#3C3F45] my-2 rounded-lg flex items-center w-full relative group`}
|
|
|
|
onClick={() => {
|
2023-09-10 17:56:37 +00:00
|
|
|
props.emit({
|
2023-09-23 21:33:30 +00:00
|
|
|
type: "SelectConversation",
|
2023-06-30 14:05:57 +00:00
|
|
|
pubkey: contact.userInfo.pubkey,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<ConversationListItem
|
|
|
|
userInfo={contact.userInfo}
|
|
|
|
isMarked={contact.isMarked}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<button
|
|
|
|
class={tw`w-6 h-6 absolute hidden group-hover:flex top-[-0.75rem] right-[0.75rem] ${IconButtonClass} bg-[#42464D] hover:bg-[#2F3136]`}
|
|
|
|
style={{
|
|
|
|
boxShadow: "2px 2px 5px 0 black",
|
|
|
|
}}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.stopPropagation();
|
2023-09-10 17:56:37 +00:00
|
|
|
props.emit({
|
2023-06-30 14:05:57 +00:00
|
|
|
type: "UnpinContact",
|
|
|
|
pubkey: contact.userInfo.pubkey.hex,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<UnpinIcon
|
|
|
|
class={tw`w-4 h-4`}
|
|
|
|
style={{
|
|
|
|
fill: "#ED4245",
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
|
|
|
|
{unpinned.map((contact) => {
|
|
|
|
return (
|
|
|
|
<li
|
|
|
|
class={tw`${
|
|
|
|
props.currentSelected && contact.userInfo?.pubkey.hex ===
|
|
|
|
props.currentSelected.hex
|
|
|
|
? "bg-[#42464D] text-[#FFFFFF]"
|
|
|
|
: "bg-transparent text-[#96989D]"
|
|
|
|
} cursor-pointer p-2 hover:bg-[#3C3F45] my-2 rounded-lg flex items-center w-full relative group`}
|
|
|
|
onClick={() => {
|
2023-09-10 17:56:37 +00:00
|
|
|
props.emit({
|
2023-09-23 21:33:30 +00:00
|
|
|
type: "SelectConversation",
|
2023-06-30 14:05:57 +00:00
|
|
|
pubkey: contact.userInfo.pubkey,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<ConversationListItem
|
|
|
|
userInfo={contact.userInfo}
|
|
|
|
isMarked={contact.isMarked}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<button
|
|
|
|
class={tw`w-6 h-6 absolute hidden group-hover:flex top-[-0.75rem] right-[0.75rem] ${IconButtonClass} bg-[#42464D] hover:bg-[#2F3136]`}
|
|
|
|
style={{
|
|
|
|
boxShadow: "2px 2px 5px 0 black",
|
|
|
|
}}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.stopPropagation();
|
2023-09-10 17:56:37 +00:00
|
|
|
props.emit({
|
2023-06-30 14:05:57 +00:00
|
|
|
type: "PinContact",
|
|
|
|
pubkey: contact.userInfo.pubkey.hex,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<PinIcon
|
|
|
|
class={tw`w-4 h-4`}
|
|
|
|
style={{
|
|
|
|
fill: "rgb(185, 187, 190)",
|
|
|
|
stroke: "rgb(185, 187, 190)",
|
|
|
|
strokeWidth: 2,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</ul>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
type ListItemProps = {
|
2023-09-23 21:54:13 +00:00
|
|
|
userInfo: ConversationSummary;
|
2023-06-30 14:05:57 +00:00
|
|
|
isMarked: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
function ConversationListItem(props: ListItemProps) {
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
<Avatar
|
|
|
|
class={tw`w-8 h-8 mr-2`}
|
2023-07-14 10:59:25 +00:00
|
|
|
picture={props.userInfo.profile?.profile.picture}
|
2023-06-30 14:05:57 +00:00
|
|
|
/>
|
|
|
|
<div
|
|
|
|
class={tw`flex-1 overflow-hidden relative`}
|
|
|
|
>
|
|
|
|
<p class={tw`truncate w-full`}>
|
2023-07-14 10:59:25 +00:00
|
|
|
{props.userInfo.profile?.profile.name || props.userInfo.pubkey.bech32()}
|
2023-06-30 14:05:57 +00:00
|
|
|
</p>
|
|
|
|
{props.userInfo.newestEventReceivedByMe !== undefined
|
|
|
|
? (
|
|
|
|
<p
|
|
|
|
class={tw`text-[#78828B] text-[0.8rem] truncate`}
|
|
|
|
>
|
|
|
|
{new Date(
|
|
|
|
props.userInfo.newestEventReceivedByMe
|
|
|
|
.created_at * 1000,
|
|
|
|
).toLocaleString()}
|
|
|
|
</p>
|
|
|
|
)
|
|
|
|
: undefined}
|
|
|
|
|
|
|
|
{props.isMarked
|
|
|
|
? (
|
|
|
|
<span
|
|
|
|
class={tw`absolute rounded-full h-2 w-2 bottom-2 right-2 bg-[#54D48C]`}
|
|
|
|
>
|
|
|
|
</span>
|
|
|
|
)
|
|
|
|
: undefined}
|
|
|
|
{props.userInfo.pinEvent != undefined && props.userInfo.pinEvent.content.type == "PinContact"
|
|
|
|
? (
|
|
|
|
<PinIcon
|
|
|
|
class={tw`w-3 h-3 absolute top-0 right-0`}
|
|
|
|
style={{
|
|
|
|
fill: "rgb(185, 187, 190)",
|
|
|
|
stroke: "rgb(185, 187, 190)",
|
|
|
|
strokeWidth: 2,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
: undefined}
|
|
|
|
</div>
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|