mirror of
https://github.com/BlowaterNostr/blowater.git
synced 2024-10-18 07:33:22 +00:00
new conversation list (#496)
Co-authored-by: BlowaterNostr <blowater.nostr@proton.me>
This commit is contained in:
parent
f037c3adbc
commit
9b7373de99
@ -2,7 +2,7 @@
|
|||||||
import { h, render } from "preact";
|
import { h, render } from "preact";
|
||||||
import { NewNav } from "./new-nav.tsx";
|
import { NewNav } from "./new-nav.tsx";
|
||||||
import { prepareProfileEvent, testEventBus } from "./_setup.test.ts";
|
import { prepareProfileEvent, testEventBus } from "./_setup.test.ts";
|
||||||
import { ConnectionPool, InMemoryAccountContext } from "@blowater/nostr-sdk";
|
import { ConnectionPool, InMemoryAccountContext, PublicKey } from "@blowater/nostr-sdk";
|
||||||
|
|
||||||
const pool = new ConnectionPool();
|
const pool = new ConnectionPool();
|
||||||
await pool.addRelayURLs(
|
await pool.addRelayURLs(
|
||||||
@ -14,22 +14,22 @@ await pool.addRelayURLs(
|
|||||||
"wss://relay.nostr.wirednet.jp",
|
"wss://relay.nostr.wirednet.jp",
|
||||||
"wss://relay.nostr.moctane.com",
|
"wss://relay.nostr.moctane.com",
|
||||||
"wss://remnant.cloud",
|
"wss://remnant.cloud",
|
||||||
// "wss://nostr.cahlen.org",
|
"wss://nostr.cahlen.org",
|
||||||
// "wss://fog.dedyn.io",
|
"wss://fog.dedyn.io",
|
||||||
// "wss://global-relay.cesc.trade",
|
"wss://global-relay.cesc.trade",
|
||||||
// "wss://nostr.dakukitsune.ca",
|
"wss://nostr.dakukitsune.ca",
|
||||||
// "wss://africa.nostr.joburg",
|
"wss://africa.nostr.joburg",
|
||||||
// "wss://nostr-relay.ktwo.io",
|
"wss://nostr-relay.ktwo.io",
|
||||||
// "wss://bevo.nostr1.com",
|
"wss://bevo.nostr1.com",
|
||||||
// "wss://relay.corpum.com",
|
"wss://relay.corpum.com",
|
||||||
// "wss://relay.nostr.directory",
|
"wss://relay.nostr.directory",
|
||||||
// "wss://nostr.1f52b.xyz",
|
"wss://nostr.1f52b.xyz",
|
||||||
// "wss://lnbits.eldamar.icu/nostrrelay/relay",
|
"wss://lnbits.eldamar.icu/nostrrelay/relay",
|
||||||
// "wss://relay.cosmicbolt.net",
|
"wss://relay.cosmicbolt.net",
|
||||||
// "wss://island.nostr1.com",
|
"wss://island.nostr1.com",
|
||||||
// "wss://nostr.codingarena.de",
|
"wss://nostr.codingarena.de",
|
||||||
// "wss://nostr.madco.me",
|
"wss://nostr.madco.me",
|
||||||
// "wss://nostr-relay.bitcoin.ninja",
|
"wss://nostr-relay.bitcoin.ninja",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const ctx = InMemoryAccountContext.Generate();
|
const ctx = InMemoryAccountContext.Generate();
|
||||||
@ -42,13 +42,29 @@ const profileEvent = await prepareProfileEvent(ctx, {
|
|||||||
picture: "https://image.nostr.build/655007ae74f24ea1c611889f48b25cb485b83ab67408daddd98f95782f47e1b5.jpg",
|
picture: "https://image.nostr.build/655007ae74f24ea1c611889f48b25cb485b83ab67408daddd98f95782f47e1b5.jpg",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let currentConversation: PublicKey | undefined;
|
||||||
|
const convoList = new Set<PublicKey>();
|
||||||
|
const pinList = new Set<PublicKey>();
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const pubkey = InMemoryAccountContext.Generate().publicKey;
|
||||||
|
if (i % 4 == 0) pinList.add(pubkey);
|
||||||
|
if (i == 5) currentConversation = pubkey;
|
||||||
|
convoList.add(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<NewNav
|
<NewNav
|
||||||
currentSpace="wss://blowater.nostr1.com"
|
currentSpace="wss://blowater.nostr1.com"
|
||||||
emit={testEventBus.emit}
|
emit={testEventBus.emit}
|
||||||
pool={pool}
|
pool={pool}
|
||||||
activeNav="DM"
|
activeNav={"Public"}
|
||||||
profile={profileEvent}
|
profile={profileEvent}
|
||||||
|
currentConversation={currentConversation}
|
||||||
|
getters={{
|
||||||
|
getProfileByPublicKey: () => profileEvent,
|
||||||
|
getConversationList: () => convoList,
|
||||||
|
getPinList: () => pinList,
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
|
@ -1,35 +1,38 @@
|
|||||||
/** @jsx h */
|
/** @jsx h */
|
||||||
import { Component, Fragment, h } from "preact";
|
import { Component, h } from "preact";
|
||||||
import { emitFunc, EventSubscriber } from "../event-bus.ts";
|
import { emitFunc } from "../event-bus.ts";
|
||||||
import { NavigationUpdate, NavTabID, SelectSpace, ShowProfileSetting } from "./nav.tsx";
|
import { NavigationUpdate, NavTabID, SelectSpace, ShowProfileSetting } from "./nav.tsx";
|
||||||
import { ViewSpaceSettings } from "./setting.tsx";
|
import { ViewSpaceSettings } from "./setting.tsx";
|
||||||
import { ConnectionPool, getRelayInformation, RelayInformation, robohash } from "@blowater/nostr-sdk";
|
import {
|
||||||
|
ConnectionPool,
|
||||||
|
getRelayInformation,
|
||||||
|
PublicKey,
|
||||||
|
RelayInformation,
|
||||||
|
robohash,
|
||||||
|
} from "@blowater/nostr-sdk";
|
||||||
import { setState } from "./_helper.ts";
|
import { setState } from "./_helper.ts";
|
||||||
import { Avatar, RelayAvatar } from "./components/avatar.tsx";
|
import { Avatar, RelayAvatar } from "./components/avatar.tsx";
|
||||||
import { CaretDownIcon } from "./icons/caret-down-icon.tsx";
|
import { CaretDownIcon } from "./icons/caret-down-icon.tsx";
|
||||||
import { PoundIcon } from "./icons/pound-icon.tsx";
|
import { PoundIcon } from "./icons/pound-icon.tsx";
|
||||||
import { Profile_Nostr_Event } from "../nostr.ts";
|
import { Profile_Nostr_Event } from "../nostr.ts";
|
||||||
import { ConversationSummary } from "./conversation-list.ts";
|
import { ContactUpdate } from "./conversation-list.tsx";
|
||||||
import { ProfileData } from "../features/profile.ts";
|
|
||||||
import { PinIcon } from "./icons/pin-icon.tsx";
|
|
||||||
import { PrimaryTextColor } from "./style/colors.ts";
|
|
||||||
import {
|
|
||||||
ContactUpdate,
|
|
||||||
ConversationListRetriever,
|
|
||||||
ConversationType,
|
|
||||||
NewMessageChecker,
|
|
||||||
PinListGetter,
|
|
||||||
} from "./conversation-list.tsx";
|
|
||||||
import { SearchUpdate, SelectConversation } from "./search_model.ts";
|
|
||||||
import { TagSelected } from "./contact-tags.tsx";
|
import { TagSelected } from "./contact-tags.tsx";
|
||||||
import { ViewUserDetail } from "./message-panel.tsx";
|
import { ViewUserDetail } from "./message-panel.tsx";
|
||||||
import { UI_Interaction_Event, UserBlocker } from "./app_update.tsx";
|
import { func_GetProfileByPublicKey } from "./search.tsx";
|
||||||
import { func_GetProfileByPublicKey, func_GetProfilesByText } from "./search.tsx";
|
import { PinIcon } from "./icons/pin-icon.tsx";
|
||||||
|
import { SelectConversation } from "./search_model.ts";
|
||||||
|
|
||||||
type NewNavProps = {
|
type NewNavProps = {
|
||||||
pool: ConnectionPool;
|
pool: ConnectionPool;
|
||||||
activeNav: NavTabID;
|
activeNav: NavTabID;
|
||||||
currentSpace: string;
|
currentSpace: string;
|
||||||
|
profile: Profile_Nostr_Event | undefined;
|
||||||
|
currentConversation: PublicKey | undefined;
|
||||||
|
getters: {
|
||||||
|
getProfileByPublicKey: func_GetProfileByPublicKey;
|
||||||
|
getConversationList: func_GetConversationList;
|
||||||
|
getPinList: func_GetPinList;
|
||||||
|
};
|
||||||
emit: emitFunc<
|
emit: emitFunc<
|
||||||
| SelectSpace
|
| SelectSpace
|
||||||
| NavigationUpdate
|
| NavigationUpdate
|
||||||
@ -39,22 +42,26 @@ type NewNavProps = {
|
|||||||
| TagSelected
|
| TagSelected
|
||||||
| ViewUserDetail
|
| ViewUserDetail
|
||||||
>;
|
>;
|
||||||
profile: Profile_Nostr_Event | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class NewNav extends Component<NewNavProps> {
|
export class NewNav extends Component<NewNavProps> {
|
||||||
render(props: NewNavProps) {
|
render(props: NewNavProps) {
|
||||||
return (
|
return (
|
||||||
<div class="h-screen w-64 flex flex-col gap-y-4 overflow-y-auto bg-neutral-900 p-2 items-center">
|
<div class="h-screen w-64 flex flex-col gap-y-4 overflow-y-auto bg-neutral-900 p-2 items-center select-none">
|
||||||
<SpaceDropDownPanel
|
<SpaceDropDownPanel
|
||||||
currentSpace={props.currentSpace}
|
currentSpace={props.currentSpace}
|
||||||
spaceList={new Set(Array.from(props.pool.getRelays()).map((r) => r.url))}
|
spaceList={new Set(Array.from(props.pool.getRelays()).map((r) => r.url))}
|
||||||
emit={props.emit}
|
emit={props.emit}
|
||||||
/>
|
/>
|
||||||
{/* <GlobalSearch /> */}
|
{/* <GlobalSearch /> */}
|
||||||
<GroupChatList />
|
<GroupChatList emit={props.emit} activeNav={props.activeNav} />
|
||||||
<DirectMessageList profile={props.profile} />
|
<DirectMessageList
|
||||||
<ProfileMenu profile={props.profile} emit={props.emit} />
|
emit={props.emit}
|
||||||
|
currentSpace={props.currentSpace}
|
||||||
|
currentConversation={props.currentConversation}
|
||||||
|
getters={props.getters}
|
||||||
|
/>
|
||||||
|
<UserIndicator profile={props.profile} emit={props.emit} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -111,13 +118,13 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
{this.TopIconButton()}
|
{this.CurrentSpaceIndicator()}
|
||||||
{this.state.showDropDown ? this.DropDown(spaceList) : undefined}
|
{this.state.showDropDown ? this.DropDown(spaceList) : undefined}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TopIconButton = () => {
|
CurrentSpaceIndicator = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center gap-1 p-1 rounded cursor-pointer hover:bg-neutral-500"
|
class="flex flex-row items-center gap-1 p-1 rounded cursor-pointer hover:bg-neutral-500"
|
||||||
@ -139,7 +146,9 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
|
|||||||
: <div>current space url</div>}
|
: <div>current space url</div>}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-8 h-8 flex justify-center items-center rounded">
|
<div class="w-8 h-8 flex justify-center items-center rounded">
|
||||||
<CaretDownIcon class="w-6 h-6 text-neutral-600" />
|
{this.state.showDropDown
|
||||||
|
? <CaretDownIcon class="w-6 h-6 text-neutral-600 transition-transform -rotate-180" />
|
||||||
|
: <CaretDownIcon class="w-6 h-6 text-neutral-600 transition-transform" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -147,7 +156,7 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
|
|||||||
|
|
||||||
DropDown = (spaceList: h.JSX.Element[]) => {
|
DropDown = (spaceList: h.JSX.Element[]) => {
|
||||||
return (
|
return (
|
||||||
<div class="absolute z-10 min-w-64 rounded-lg bg-neutral-700 text-white p-4">
|
<div class="absolute z-10 min-w-64 rounded-lg bg-neutral-700 text-white p-4 mt-1">
|
||||||
{this.SettingsButton()}
|
{this.SettingsButton()}
|
||||||
{/* {this.InviteButton()} */}
|
{/* {this.InviteButton()} */}
|
||||||
<div class="border border-neutral-600 my-3"></div>
|
<div class="border border-neutral-600 my-3"></div>
|
||||||
@ -296,67 +305,148 @@ function GlobalSearch() {
|
|||||||
return <div>Search</div>;
|
return <div>Search</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupChatList() {
|
type GroupChatListProps = {
|
||||||
|
activeNav: NavTabID;
|
||||||
|
emit: emitFunc<NavigationUpdate>;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GroupChatList extends Component<GroupChatListProps> {
|
||||||
|
render(props: GroupChatListProps) {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center justify-start gap-1 w-full p-2">
|
<div
|
||||||
|
class={`flex items-center justify-start gap-1 w-full p-2 rounded-md px-2 py-1 hover:bg-neutral-950 cursor-pointer ${
|
||||||
|
props.activeNav == "Public" ? "bg-neutral-800" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
props.emit({
|
||||||
|
type: "ChangeNavigation",
|
||||||
|
id: "Public",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PoundIcon class="w-4 h-4 text-neutral-500" />
|
<PoundIcon class="w-4 h-4 text-neutral-500" />
|
||||||
<div class="text-white text-sm font-medium font-sans leading-5">Public</div>
|
<div class="text-white text-sm font-medium font-sans leading-5">Public</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type func_GetConversationList = () => Set<PublicKey>;
|
||||||
|
type func_GetPinList = () => Set<PublicKey>;
|
||||||
|
|
||||||
type DirectMessageListProps = {
|
type DirectMessageListProps = {
|
||||||
//TODO: The list is based on private messages both received and sent, as well as other sources.
|
emit: emitFunc<ContactUpdate>;
|
||||||
profile: Profile_Nostr_Event | undefined;
|
currentSpace: string;
|
||||||
|
currentConversation: PublicKey | undefined;
|
||||||
|
getters: {
|
||||||
|
getProfileByPublicKey: func_GetProfileByPublicKey;
|
||||||
|
getConversationList: func_GetConversationList;
|
||||||
|
getPinList: func_GetPinList;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// type DirectMessageListState = {};
|
|
||||||
|
|
||||||
class DirectMessageList extends Component<DirectMessageListProps> {
|
class DirectMessageList extends Component<DirectMessageListProps> {
|
||||||
render(props: DirectMessageListProps) {
|
render(props: DirectMessageListProps) {
|
||||||
|
const pinList = props.getters.getPinList();
|
||||||
|
const pinned = [];
|
||||||
|
const unpinned = [];
|
||||||
|
for (const pubkey of props.getters.getConversationList()) {
|
||||||
|
if (pinList.has(pubkey)) {
|
||||||
|
pinned.push(pubkey);
|
||||||
|
} else {
|
||||||
|
unpinned.push(pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 w-full">
|
<div class="flex-1 w-full overflow-y-auto">
|
||||||
<div class="flex flex-col gap-2 w-full justify-start items-center">
|
<div class="flex flex-col gap-1 w-full justify-start items-center">
|
||||||
<div class="flex gap-2 rounded-md px-2 py-1 bg-neutral-800 w-full">
|
{pinned.map((pubkey: PublicKey) => (
|
||||||
<div class="w-4 flex justify-center items-center">
|
<DireactMessageItem
|
||||||
<Avatar picture={props.profile?.profile.picture || "./logo.webp"} />
|
emit={props.emit}
|
||||||
</div>
|
pubkey={pubkey}
|
||||||
<div class="text-white text-sm font-medium font-sans leading-5">
|
isPined={true}
|
||||||
{props.profile?.profile.name || props.profile?.profile.display_name ||
|
currentConversation={props.currentConversation}
|
||||||
props.profile?.pubkey}
|
currentSpace={props.currentSpace}
|
||||||
</div>
|
getters={props.getters}
|
||||||
</div>
|
/>
|
||||||
<div class="flex gap-2 rounded-md px-2 py-1 w-full">
|
))}
|
||||||
<div class="w-4 flex justify-center items-center">
|
{unpinned.map((pubkey: PublicKey) => (
|
||||||
<Avatar picture={props.profile?.profile.picture || "./logo.webp"} />
|
<DireactMessageItem
|
||||||
</div>
|
emit={props.emit}
|
||||||
<div class="text-white text-sm font-medium font-sans leading-5">
|
pubkey={pubkey}
|
||||||
{props.profile?.profile.name || props.profile?.profile.display_name ||
|
isPined={false}
|
||||||
props.profile?.pubkey}
|
currentConversation={props.currentConversation}
|
||||||
</div>
|
currentSpace={props.currentSpace}
|
||||||
</div>
|
getters={props.getters}
|
||||||
<div class="flex gap-2 rounded-md px-2 py-1 w-full">
|
/>
|
||||||
<div class="w-4 flex justify-center items-center">
|
))}
|
||||||
<Avatar picture={props.profile?.profile.picture || "./logo.webp"} />
|
|
||||||
</div>
|
|
||||||
<div class="text-white text-sm font-medium font-sans leading-5">
|
|
||||||
{props.profile?.profile.name || props.profile?.profile.display_name ||
|
|
||||||
props.profile?.pubkey}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileMenuProps = {
|
type DireactMessageItemProps = {
|
||||||
|
pubkey: PublicKey;
|
||||||
|
emit: emitFunc<SelectConversation>;
|
||||||
|
isPined: boolean;
|
||||||
|
currentSpace: string;
|
||||||
|
currentConversation: PublicKey | undefined;
|
||||||
|
getters: {
|
||||||
|
getProfileByPublicKey: func_GetProfileByPublicKey;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class DireactMessageItem extends Component<DireactMessageItemProps> {
|
||||||
|
render(props: DireactMessageItemProps) {
|
||||||
|
const profile = props.getters.getProfileByPublicKey(props.pubkey, new URL(props.currentSpace))
|
||||||
|
?.profile;
|
||||||
|
const picture = profile?.picture || "./logo.webp";
|
||||||
|
const name = profile?.name || profile?.display_name || profile?.pubkey;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`flex gap-2 rounded-md px-2 py-1 w-full hover:bg-neutral-950 cursor-pointer ${
|
||||||
|
props.pubkey.hex == props.currentConversation?.hex ? "bg-neutral-800" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
props.emit({
|
||||||
|
type: "SelectConversation",
|
||||||
|
pubkey: props.pubkey,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="w-4 flex justify-center items-center">
|
||||||
|
<Avatar picture={picture} />
|
||||||
|
</div>
|
||||||
|
<div class="text-white text-sm font-medium font-sans leading-5 flex-1">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
{props.isPined
|
||||||
|
? (
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<PinIcon
|
||||||
|
class={`w-3 h-3`}
|
||||||
|
style={{
|
||||||
|
fill: "rgb(185, 187, 190)",
|
||||||
|
stroke: "rgb(185, 187, 190)",
|
||||||
|
strokeWidth: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserIndicatorProps = {
|
||||||
profile: Profile_Nostr_Event | undefined;
|
profile: Profile_Nostr_Event | undefined;
|
||||||
emit: emitFunc<ShowProfileSetting>;
|
emit: emitFunc<ShowProfileSetting>;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ProfileMenu extends Component<ProfileMenuProps> {
|
class UserIndicator extends Component<UserIndicatorProps> {
|
||||||
render(props: ProfileMenuProps) {
|
render(props: UserIndicatorProps) {
|
||||||
return (
|
return (
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div
|
<div
|
||||||
|
Loading…
Reference in New Issue
Block a user