import { orderBy } from "lodash"; import moment from "moment"; import * as numeral from "numeral"; import * as React from "react"; import { connect } from "react-redux"; import { Dispatch } from "redux"; import sanitize from "sanitize-html"; import { AnchorButton, Button, Callout, Card, Classes, Code, Divider, Elevation, H2, H4, HTMLTable, Icon, NonIdealState, Position, Tab, Tabs, Tooltip } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; import { selectAndLoadInstance } from "../redux/actions"; import { IAppState, IGraph, IInstanceDetails } from "../redux/types"; import FullDiv from "./atoms/FullDiv"; import { ErrorState } from "./ErrorState"; interface ISidebarProps { graph?: IGraph; instanceName: string | null; instanceLoadError: boolean; instanceDetails: IInstanceDetails | null; isLoadingInstanceDetails: boolean; selectAndLoadInstance: (instanceName: string) => void; } interface ISidebarState { isOpen: boolean; neighbors?: string[]; isProcessingNeighbors: boolean; } class SidebarImpl extends React.Component { constructor(props: ISidebarProps) { super(props); const isOpen = window.innerWidth >= 900 ? true : false; this.state = { isOpen, isProcessingNeighbors: false }; } public componentDidMount() { this.processEdgesToFindNeighbors(); } public componentDidUpdate(prevProps: ISidebarProps, prevState: ISidebarState) { if (prevProps.instanceName !== this.props.instanceName) { this.processEdgesToFindNeighbors(); } } public render() { const closedClass = this.state.isOpen ? "" : " closed"; const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT; return (
); } private handleToggle = () => { this.setState({ isOpen: !this.state.isOpen }); }; private processEdgesToFindNeighbors = () => { const { graph, instanceName } = this.props; if (!graph || !instanceName) { return; } this.setState({ isProcessingNeighbors: true }); const edges = graph.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1); const neighbors: any[] = []; edges.forEach(e => { if (e.data.source === instanceName) { neighbors.push({ neighbor: e.data.target, weight: e.data.weight }); } else { neighbors.push({ neighbor: e.data.source, weight: e.data.weight }); } }); this.setState({ neighbors, isProcessingNeighbors: false }); }; private renderSidebarContents = () => { let content; if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) { content = this.renderLoadingState(); } else if (!this.props.instanceDetails) { return this.renderEmptyState(); } else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) { content = this.renderPersonalInstanceErrorState(); } else if (this.props.instanceDetails.status !== "success") { content = this.renderMissingDataState(); } else if (this.props.instanceLoadError) { return (content = ); } else { content = this.renderTabs(); } return ( {this.renderHeading()} {content} ); }; private renderTabs = () => { const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0; const insularCallout = hasNeighbors ? ( undefined ) : (

This instance doesn't have any neighbors that we know of, so it's hidden from the graph.

); return (
{insularCallout} {this.props.instanceDetails!.description && ( )} {this.shouldRenderStats() && }
); }; private shouldRenderStats = () => { const details = this.props.instanceDetails; return details && (details.version || details.userCount || details.statusCount || details.domainCount); }; private renderHeading = () => { let content: JSX.Element; if (!this.props.instanceName) { return; } else { content = ( {this.props.instanceName + " "} ); } return (

{content}

); }; private renderDescription = () => { const description = this.props.instanceDetails!.description; if (!description) { return; } return

; }; private renderVersionAndCounts = () => { if (!this.props.instanceDetails) { throw new Error("Did not receive instance details as expected!"); } const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails; return (

Version {{version} || "Unknown"} Users {(userCount && numeral.default(userCount).format("0,0")) || "Unknown"} Statuses {(statusCount && numeral.default(statusCount).format("0,0")) || "Unknown"} Insularity{" "} The percentage of mentions that are directed
toward users on the same instance. } position={Position.TOP} className={Classes.DARK} >
{(insularity && numeral.default(insularity).format("0.0%")) || "Unknown"} Known peers {(domainCount && numeral.default(domainCount).format("0,0")) || "Unknown"} Last updated {moment(lastUpdated + "Z").fromNow() || "Unknown"}
); }; private renderNeighbors = () => { if (!this.props.graph || !this.props.instanceName) { return; } const edges = this.props.graph.edges.filter( e => [e.data.source, e.data.target].indexOf(this.props.instanceName!) > -1 ); const neighbors: any[] = []; edges.forEach(e => { if (e.data.source === this.props.instanceName) { neighbors.push({ neighbor: e.data.target, weight: e.data.weight }); } else { neighbors.push({ neighbor: e.data.source, weight: e.data.weight }); } }); const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => ( {neighborDetails.neighbor} {neighborDetails.weight.toFixed(4)} )); return (

The mention ratio is the average of how many times the two instances mention each other per status. A mention ratio of 1 would mean that every single status contained a mention of a user on the other instance.

Instance Mention ratio {neighborRows}
); }; private renderPeers = () => { const peers = this.props.instanceDetails!.peers; if (!peers || peers.length === 0) { return; } const peerRows = peers.map(instance => ( {instance.name} )); return (

All the instances, past and present, that {this.props.instanceName} knows about.

{peerRows}
); }; private renderEmptyState = () => { return ( ); }; private renderLoadingState = () => { return (

Description

Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur necessitatibus tempora. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur necessitatibus tempora.

Version

Eaque rerum sequi unde omnis voluptatibus non quia fugit.

Stats

Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit.

); }; private renderPersonalInstanceErrorState = () => { return ( Message @fediversespace to opt in } /> ); }; private renderMissingDataState = () => { return ( {this.props.instanceDetails && this.props.instanceDetails.status} ); }; private openInstanceLink = () => { window.open("https://" + this.props.instanceName, "_blank"); }; private selectInstance = (e: any) => { this.props.selectAndLoadInstance(e.target.innerText); }; } const mapStateToProps = (state: IAppState) => ({ graph: state.data.graph, instanceDetails: state.currentInstance.currentInstanceDetails, instanceLoadError: state.currentInstance.error, instanceName: state.currentInstance.currentInstanceName, isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails }); const mapDispatchToProps = (dispatch: Dispatch) => ({ selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any) }); export const Sidebar = connect( mapStateToProps, mapDispatchToProps )(SidebarImpl);