blowater/app/UI/relay-detail.tsx

351 lines
13 KiB
TypeScript
Raw Permalink Normal View History

2023-11-21 07:20:39 +00:00
/** @jsx h */
import { Component, Fragment, h } from "preact";
2023-11-21 07:20:39 +00:00
import { CopyButton } from "./components/copy-button.tsx";
import { CenterClass, InputClass } from "./components/tw.ts";
import { ErrorColor, HintTextColor, PrimaryTextColor } from "./style/colors.ts";
2023-11-21 07:20:39 +00:00
import { Avatar } from "./components/avatar.tsx";
2024-06-28 08:28:53 +00:00
import { func_GetProfileByPublicKey } from "./search.tsx";
2023-11-21 07:20:39 +00:00
import { Loading } from "./components/loading.tsx";
import { Profile_Nostr_Event } from "../nostr.ts";
import { emitFunc } from "../event-bus.ts";
import { SelectConversation } from "./search_model.ts";
import { PublicKey, RelayInformation, robohash } from "@blowater/nostr-sdk";
2024-06-17 07:42:51 +00:00
import { setState } from "./_helper.ts";
import { Channel } from "@blowater/csp";
2024-06-17 07:42:51 +00:00
import { ViewUserDetail } from "./message-panel.tsx";
import { CloseIcon } from "./icons/close-icon.tsx";
import { HideModal } from "./components/modal.tsx";
import { MembersIcon } from "./icons/members-icon.tsx";
import { GeneralIcon } from "./icons/general-icon.tsx";
2023-11-21 07:20:39 +00:00
2024-06-28 08:28:53 +00:00
type SpaceSettingProps = {
spaceUrl: URL;
emit: emitFunc<SelectConversation | ViewUserDetail | HideModal>;
2024-06-28 08:28:53 +00:00
getProfileByPublicKey: func_GetProfileByPublicKey;
getSpaceInformationChan: func_GetSpaceInformationChan;
getMemberSet: func_GetMemberSet;
};
2024-06-17 07:42:51 +00:00
type SpaceSettingState = {
tab: tabs;
info: RelayInformation | undefined;
2023-11-21 07:20:39 +00:00
};
2024-06-28 08:28:53 +00:00
2024-06-17 07:42:51 +00:00
type tabs = "general" | "members";
// return a set of public keys that participates in this relay
2024-06-28 08:28:53 +00:00
export type func_GetMemberSet = (space_url: URL) => Set<string>;
export type func_GetSpaceInformationChan = () => Channel<RelayInformation | Error>;
2024-06-17 07:42:51 +00:00
2024-06-28 08:28:53 +00:00
export class SpaceSetting extends Component<SpaceSettingProps, SpaceSettingState> {
2024-06-17 07:42:51 +00:00
infoStream: Channel<RelayInformation | Error> | undefined;
state: SpaceSettingState = {
tab: "general",
info: undefined,
};
async componentDidMount() {
2024-06-28 08:28:53 +00:00
this.infoStream = this.props.getSpaceInformationChan();
2024-06-17 07:42:51 +00:00
for await (const info of this.infoStream) {
if (info instanceof Error) {
console.error(info.message, info.cause);
} else {
2024-06-28 08:28:53 +00:00
await setState(this, { info });
2024-06-17 07:42:51 +00:00
}
}
}
async componentWillUnmount() {
2024-06-28 08:28:53 +00:00
await this.infoStream?.close();
2024-06-17 07:42:51 +00:00
}
render(props: SpaceSettingProps, state: SpaceSettingState) {
2024-06-17 07:42:51 +00:00
return (
<div class="h-[60dvh] w-[95dvw] sm:w-[80dvw] md:w-[60dvw] bg-neutral-700 rounded-xl text-[#fff] text-sm font-sans font-medium leading-5">
<div class="w-full h-full flex flex-col p-[1rem]">
<div class="flex flex-row grow">
<div class="text-xl font-semibold leading-7 flex-1">Space Settings</div>
<button
class="w-6 min-w-[1.5rem] h-6 focus:outline-none focus-visible:outline-none rounded-full hover:bg-neutral-500 z-10 flex items-center justify-center "
onClick={async () => {
await props.emit({
type: "HideModal",
});
}}
2024-06-17 07:42:51 +00:00
>
<CloseIcon
class={`w-4 h-4`}
style={{
stroke: "rgb(185, 187, 190)",
}}
/>
</button>
</div>
<div class="flex flex-row h-[90%]">
<div class="h-full flex flex-col pr-[1rem]">
<div
class={`flex flex-row items-center gap-2 p-2 hover:bg-neutral-500 rounded-lg mt-2 hover:cursor-pointer ${
state.tab == "general" ? "bg-neutral-600" : "text-neutral-300"
}`}
onClick={this.changeTab("general")}
2024-06-17 07:42:51 +00:00
>
<GeneralIcon class="w-6 h-6" />
<div>General</div>
</div>
<div
class={`flex flex-row items-center gap-2 p-2 hover:bg-neutral-500 rounded-lg mt-2 hover:cursor-pointer ${
state.tab == "members" ? "bg-neutral-600" : "text-neutral-300"
}`}
onClick={this.changeTab("members")}
2024-06-17 07:42:51 +00:00
>
<MembersIcon class="w-6 h-6" />
<div>Members</div>
</div>
</div>
<div class="overflow-y-auto grow">
{this.state.tab == "general"
? this.state.info == undefined ? <Loading /> : (
<RelayInformationComponent
{...this.props}
info={{
...this.state.info,
url: this.props.spaceUrl,
}}
/>
)
: (
<MemberList
getProfileByPublicKey={this.props.getProfileByPublicKey}
emit={this.props.emit}
getMemberSet={this.props.getMemberSet}
space_url={this.props.spaceUrl}
/>
)}
2024-06-17 07:42:51 +00:00
</div>
</div>
</div>
</div>
);
}
changeTab = (tab: tabs) => () => {
this.setState({
tab,
});
};
}
2023-11-21 07:20:39 +00:00
type State = {
error: string;
};
type Props = {
2024-06-28 08:28:53 +00:00
getProfileByPublicKey: func_GetProfileByPublicKey;
emit: emitFunc<SelectConversation>;
info: RelayInformation & { url: URL };
2023-11-21 07:20:39 +00:00
};
export class RelayInformationComponent extends Component<Props, State> {
2023-11-21 07:20:39 +00:00
styles = {
container: `p-8`,
2023-12-18 10:23:15 +00:00
title: `pt-8 text-[${PrimaryTextColor}]`,
error: `text-[${ErrorColor}] ${CenterClass}`,
2023-11-21 07:20:39 +00:00
header: {
2023-12-18 10:23:15 +00:00
container: `text-lg flex text-[${PrimaryTextColor}] pb-4`,
2023-11-21 07:20:39 +00:00
},
};
state: State = {
error: "",
};
render() {
2024-06-17 07:42:51 +00:00
const info = this.props.info;
2023-11-21 07:20:39 +00:00
let vNode;
2024-06-17 07:42:51 +00:00
if (this.state.error) {
2023-11-21 07:20:39 +00:00
vNode = <p class={this.styles.error}>{this.state.error}</p>;
} else {
const nodes = [];
2024-06-17 07:42:51 +00:00
if (info.pubkey) {
const pubkey = PublicKey.FromString(info.pubkey);
if (pubkey instanceof Error) {
2024-06-17 07:42:51 +00:00
// todo make a UI
2024-06-21 17:05:39 +00:00
console.log(info);
2024-06-17 07:42:51 +00:00
console.error(pubkey);
} else {
nodes.push(
2023-11-21 07:20:39 +00:00
<Fragment>
<p class={this.styles.title}>Admin</p>
<AuthorField
publicKey={pubkey}
profileData={this.props.getProfileByPublicKey(pubkey, this.props.info.url)}
emit={this.props.emit}
/>
</Fragment>,
2023-11-21 07:20:39 +00:00
);
}
}
2024-06-17 07:42:51 +00:00
if (info.contact) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Contact</p>
2024-06-17 07:42:51 +00:00
<TextField text={info.contact} />
</Fragment>,
);
}
2024-06-17 07:42:51 +00:00
if (info.description) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Description</p>
2024-06-17 07:42:51 +00:00
<TextField text={info.description} />
</Fragment>,
);
}
2024-06-17 07:42:51 +00:00
if (info.software) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Software</p>
2024-06-17 07:42:51 +00:00
<TextField text={info.software} />
</Fragment>,
);
}
2024-06-17 07:42:51 +00:00
if (info.version) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Version</p>
2024-06-17 07:42:51 +00:00
<TextField text={info.version} />
</Fragment>,
);
}
2024-06-17 07:42:51 +00:00
if (info.supported_nips) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Supported NIPs</p>
2024-06-17 07:42:51 +00:00
<TextField text={info.supported_nips.join(", ")} />
</Fragment>,
);
}
vNode = (
<div>
{nodes}
</div>
);
2023-11-21 07:20:39 +00:00
}
return (
<div class={this.styles.container}>
<p class={this.styles.header.container}>
2024-06-17 07:42:51 +00:00
{info.icon
? (
<Avatar
class="w-8 h-8 mr-2"
2024-06-17 07:42:51 +00:00
picture={info.icon || robohash(this.props.info.url)}
/>
)
: undefined}
2024-06-17 07:42:51 +00:00
{info.name}
<div class="mx-2"></div>
2024-06-17 07:42:51 +00:00
{this.props.info.url}
2023-11-21 07:20:39 +00:00
</p>
{vNode}
</div>
);
}
}
2024-06-28 08:28:53 +00:00
type MemberListProps = {
getProfileByPublicKey: func_GetProfileByPublicKey;
emit: emitFunc<SelectConversation | ViewUserDetail>;
getMemberSet: func_GetMemberSet;
space_url: URL;
};
export class MemberList extends Component<MemberListProps> {
clickSpaceMember = (pubkey: string) => () => {
const p = PublicKey.FromString(pubkey);
if (p instanceof Error) {
return console.error(p);
}
this.props.emit({
type: "ViewUserDetail",
pubkey: p,
});
};
render(props: MemberListProps) {
const members = props.getMemberSet(props.space_url);
return (
<>
{Array.from(members).map((member) => {
const profile = props.getProfileByPublicKey(member, props.space_url);
2024-06-28 08:28:53 +00:00
return (
<div
class="w-full flex items-center px-4 py-2 hover:bg-neutral-500 rounded-lg cursor-pointer"
2024-06-28 08:28:53 +00:00
onClick={this.clickSpaceMember(member)}
>
<Avatar
class={`flex-shrink-0 w-8 h-8 mr-2 bg-[#fff] rounded-full`}
2024-06-28 08:28:53 +00:00
picture={profile?.profile.picture || robohash(member)}
>
</Avatar>
<div class="truncate">
{profile?.profile.name || profile?.profile.display_name ||
member}
</div>
</div>
);
})}
</>
);
}
}
2023-11-21 07:20:39 +00:00
function AuthorField(props: {
publicKey: PublicKey;
profileData: Profile_Nostr_Event | undefined;
emit: emitFunc<SelectConversation>;
2023-11-21 07:20:39 +00:00
}) {
const styles = {
2023-12-18 10:23:15 +00:00
avatar: `h-8 w-8 mr-2`,
icon: `w-4 h-4 text-[${HintTextColor}] fill-current rotate-180`,
name: `overflow-x-auto flex-1`,
2023-11-21 07:20:39 +00:00
};
const { profileData, publicKey } = props;
2023-11-21 07:20:39 +00:00
return (
<Fragment>
<div
class={`flex items-center ${InputClass} border-neutral-600 mt-4 hover:bg-neutral-500 hover:cursor-pointer`}
onClick={() => props.emit({ type: "SelectConversation", pubkey: props.publicKey })}
>
<Avatar
picture={profileData?.profile.picture || robohash(publicKey.bech32())}
class={styles.avatar}
/>
<p class={styles.name}>{profileData?.profile.name || publicKey.bech32()}</p>
2023-11-21 07:20:39 +00:00
</div>
<TextField text={publicKey.bech32()} />
2023-11-21 07:20:39 +00:00
</Fragment>
);
}
function TextField(props: {
text: string;
}) {
const styles = {
container: `relative ${InputClass} border-neutral-600 resize-none flex p-0 mt-4`,
2023-12-18 10:23:15 +00:00
pre: `whitespace-pre flex-1 overflow-x-auto px-4 py-3`,
copyButton: `w-14 ${CenterClass}`,
2023-11-21 07:20:39 +00:00
};
return (
<div class={styles.container}>
<pre class={styles.pre}>{props.text}</pre>
<div class={styles.copyButton}>
<CopyButton text={props.text} />
</div>
</div>
);
}