blowater/app/UI/relay-detail.tsx

256 lines
7.9 KiB
TypeScript
Raw Normal View History

2023-11-21 07:20:39 +00:00
/** @jsx h */
import { Component, ComponentChildren, Fragment, h } from "https://esm.sh/preact@10.17.1";
import { CopyButton } from "./components/copy-button.tsx";
import { CenterClass, InputClass } from "./components/tw.ts";
import {
BackgroundColor_HoverButton,
2023-11-21 07:20:39 +00:00
ErrorColor,
HintTextColor,
PrimaryTextColor,
SecondaryBackgroundColor,
TitleIconColor,
} from "./style/colors.ts";
import { Avatar } from "./components/avatar.tsx";
import { ProfileGetter } from "./search.tsx";
2024-01-01 17:28:10 +00:00
import { PublicKey } from "../../libs/nostr.ts/key.ts";
2023-11-21 07:20:39 +00:00
import { Loading } from "./components/loading.tsx";
import { RelayIcon } from "./icons/relay-icon.tsx";
import { Profile_Nostr_Event } from "../nostr.ts";
import { emitFunc } from "../event-bus.ts";
import { SelectConversation } from "./search_model.ts";
2023-11-21 07:20:39 +00:00
export type RelayInformation = {
2023-11-21 07:20:39 +00:00
name?: string;
description?: string;
pubkey?: string;
contact?: string;
supported_nips?: number[];
software?: string;
version?: string;
icon?: string;
2023-11-21 07:20:39 +00:00
};
type State = {
relayInfo: RelayInformation;
2023-11-21 07:20:39 +00:00
error: string;
isLoading: boolean;
};
type Props = {
relayUrl: string;
profileGetter: ProfileGetter;
emit: emitFunc<SelectConversation>;
2023-11-21 07:20:39 +00:00
};
export class RelayInformationComponent extends Component<Props, State> {
2023-11-21 07:20:39 +00:00
styles = {
2023-12-18 10:23:15 +00:00
container: `bg-[${SecondaryBackgroundColor}] p-8`,
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 = {
relayInfo: {
2023-11-21 07:20:39 +00:00
name: undefined,
description: undefined,
pubkey: undefined,
contact: undefined,
supported_nips: undefined,
software: undefined,
version: undefined,
},
error: "",
isLoading: false,
};
async componentWillMount() {
this.setState({
isLoading: true,
});
const res = await getRelayInformation(this.props.relayUrl);
2023-11-21 07:20:39 +00:00
if (res instanceof Error) {
this.setState({
isLoading: false,
error: res.message,
});
} else {
this.setState({
isLoading: false,
relayInfo: res,
2023-11-21 07:20:39 +00:00
});
}
}
render() {
let vNode;
if (this.state.isLoading) {
vNode = <Loading />;
} else if (this.state.error) {
vNode = <p class={this.styles.error}>{this.state.error}</p>;
} else {
const nodes = [];
if (this.state.relayInfo.pubkey) {
const pubkey = PublicKey.FromString(this.state.relayInfo.pubkey);
if (pubkey instanceof Error) {
} 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.profileGetter.getProfilesByPublicKey(pubkey)}
emit={this.props.emit}
/>
</Fragment>,
2023-11-21 07:20:39 +00:00
);
}
}
if (this.state.relayInfo.contact) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Contact</p>
<TextField text={this.state.relayInfo.contact} />
</Fragment>,
);
}
if (this.state.relayInfo.description) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Description</p>
<TextField text={this.state.relayInfo.description} />
</Fragment>,
);
}
if (this.state.relayInfo.software) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Software</p>
<TextField text={this.state.relayInfo.software} />
</Fragment>,
);
}
if (this.state.relayInfo.version) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Version</p>
<TextField text={this.state.relayInfo.version} />
</Fragment>,
);
}
if (this.state.relayInfo.supported_nips) {
nodes.push(
<Fragment>
<p class={this.styles.title}>Supported NIPs</p>
<TextField text={this.state.relayInfo.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}>
{this.state.relayInfo.icon
? (
<Avatar
class="w-8 h-8 mr-2"
picture={this.state.relayInfo.icon || robohash(this.props.relayUrl)}
/>
)
: undefined}
{this.state.relayInfo.name}
<div class="mx-2"></div>
{this.props.relayUrl}
2023-11-21 07:20:39 +00:00
</p>
{vNode}
</div>
);
}
}
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} mt-4 hover:${BackgroundColor_HoverButton} 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 = {
2023-12-18 10:23:15 +00:00
container: `relative ${InputClass} resize-none flex p-0 mt-4`,
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>
);
}
export async function getRelayInformation(url: string) {
try {
const httpURL = new URL(url);
2024-03-22 08:56:32 +00:00
httpURL.protocol = httpURL.protocol == "wss:" ? "https" : "http";
const res = await fetch(httpURL, {
headers: {
"accept": "application/nostr+json",
},
});
if (!res.ok) {
return new Error(`Faild to get detail, ${res.status}: ${await res.text()}`);
}
const detail = await res.text();
const info = JSON.parse(detail) as RelayInformation;
if (!info.icon) {
info.icon = robohash(url);
}
return info;
} catch (e) {
return e as Error;
}
}
export function robohash(url: string | URL) {
return `https://robohash.org/${url}`;
}