diff --git a/src/menu/menu_item.tsx b/src/menu/menu_item.tsx
new file mode 100644
index 0000000..e313b5a
--- /dev/null
+++ b/src/menu/menu_item.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react';
+import {
+ Menu,
+ Dropdown,
+ MenuItemProps,
+ DropdownItemProps,
+} from 'semantic-ui-react';
+
+export enum MenuType {
+ Menu,
+ Dropdown,
+}
+
+interface Props {
+ menuType: MenuType;
+}
+
+export class MenuItem extends React.Component<
+ Props & MenuItemProps & DropdownItemProps
+> {
+ render() {
+ return (
+ <>
+ {this.props.menuType === MenuType.Menu ? (
+
{this.props.children}
+ ) : (
+ {this.props.children}
+ )}
+ >
+ );
+ }
+}
diff --git a/src/menu/top_bar.tsx b/src/menu/top_bar.tsx
index 1fae588..c68eb43 100644
--- a/src/menu/top_bar.tsx
+++ b/src/menu/top_bar.tsx
@@ -1,24 +1,20 @@
import * as queryString from 'query-string';
import * as React from 'react';
import debounce from 'debounce';
-import md5 from 'md5';
-import wikitreeLogo from './wikitree.png';
import {analyticsEvent} from '../util/analytics';
import {buildSearchIndex, SearchIndex} from './search_index';
import {displaySearchResult} from './search_util';
import {FormattedMessage, intlShape} from 'react-intl';
-import {getLoggedInUserName} from '../datasource/wikitree';
import {IndiInfo, JsonGedcomData} from 'topola';
import {Link} from 'react-router-dom';
+import {MenuType} from './menu_item';
import {RouteComponentProps} from 'react-router-dom';
+import {UploadMenu} from './upload_menu';
+import {UrlMenu} from './url_menu';
+import {WikiTreeLoginMenu, WikiTreeMenu} from './wikitree_menu';
import {
- Header,
- Button,
Icon,
Menu,
- Modal,
- Input,
- Form,
Dropdown,
Search,
SearchProps,
@@ -26,12 +22,6 @@ import {
Responsive,
} from 'semantic-ui-react';
-enum WikiTreeLoginState {
- UNKNOWN,
- NOT_LOGGED_IN,
- LOGGED_IN,
-}
-
enum ScreenSize {
LARGE,
SMALL,
@@ -39,12 +29,6 @@ enum ScreenSize {
/** Menus and dialogs state. */
interface State {
- loadUrlDialogOpen: boolean;
- wikiTreeIdDialogOpen: boolean;
- url?: string;
- wikiTreeId?: string;
- wikiTreeLoginState: WikiTreeLoginState;
- wikiTreeLoginUsername?: string;
searchResults: SearchResultProps[];
}
@@ -69,179 +53,21 @@ interface Props {
showWikiTreeMenus: boolean;
}
-function loadFileAsText(file: File): Promise {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = (evt: ProgressEvent) => {
- resolve((evt.target as FileReader).result as string);
- };
- reader.readAsText(file);
- });
-}
-
-function isImageFileName(fileName: string) {
- const lower = fileName.toLowerCase();
- return lower.endsWith('.jpg') || lower.endsWith('.png');
-}
-
export class TopBar extends React.Component<
RouteComponentProps & Props,
State
> {
state: State = {
- loadUrlDialogOpen: false,
- wikiTreeIdDialogOpen: false,
searchResults: [],
- wikiTreeLoginState: WikiTreeLoginState.UNKNOWN,
};
/** Make intl appear in this.context. */
static contextTypes = {
intl: intlShape,
};
- urlInputRef: React.RefObject = React.createRef();
- wikiTreeIdInputRef: React.RefObject = React.createRef();
- wikiTreeLoginFormRef: React.RefObject = React.createRef();
- wikiTreeReturnUrlRef: React.RefObject = React.createRef();
searchRef?: {setValue(value: string): void};
searchIndex?: SearchIndex;
- /** Handles the "Upload file" button. */
- private async handleUpload(event: React.SyntheticEvent) {
- const files = (event.target as HTMLInputElement).files;
- if (!files || !files.length) {
- return;
- }
- const filesArray = Array.from(files);
- (event.target as HTMLInputElement).value = ''; // Reset the file input.
- analyticsEvent('upload_files_selected', {
- event_value: files.length,
- });
-
- const gedcomFile =
- filesArray.length === 1
- ? filesArray[0]
- : filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) ||
- filesArray[0];
-
- // Convert uploaded images to object URLs.
- const images = filesArray
- .filter(
- (file) => file.name !== gedcomFile.name && isImageFileName(file.name),
- )
- .map((file) => ({
- name: file.name,
- url: URL.createObjectURL(file),
- }));
- const imageMap = new Map(
- images.map((entry) => [entry.name, entry.url] as [string, string]),
- );
-
- const data = await loadFileAsText(gedcomFile);
- const imageFileNames = images
- .map((image) => image.name)
- .sort()
- .join('|');
- // Hash GEDCOM contents with uploaded image file names.
- const hash = md5(md5(data) + imageFileNames);
-
- // Use history.replace() when reuploading the same file and history.push() when loading
- // a new file.
- const search = queryString.parse(this.props.location.search);
- const historyPush =
- search.file === hash
- ? this.props.history.replace
- : this.props.history.push;
-
- historyPush({
- pathname: '/view',
- search: queryString.stringify({file: hash}),
- state: {data, images: imageMap},
- });
- }
-
- /** Opens the "Load from URL" dialog. */
- private openLoadUrlDialog() {
- this.setState(
- Object.assign({}, this.state, {loadUrlDialogOpen: true}),
- () => this.urlInputRef.current!.focus(),
- );
- }
-
- private openWikiTreeIdDialog() {
- this.setState(
- Object.assign({}, this.state, {wikiTreeIdDialogOpen: true}),
- () => this.wikiTreeIdInputRef.current!.focus(),
- );
- }
-
- /** Cancels any of the open dialogs. */
- private handleClose() {
- this.setState(
- Object.assign({}, this.state, {
- loadUrlDialogOpen: false,
- wikiTreeIdDialogOpen: false,
- }),
- );
- }
-
- /** Load button clicked in the "Load from URL" dialog. */
- private handleLoad() {
- this.setState(
- Object.assign({}, this.state, {
- loadUrlDialogOpen: false,
- }),
- );
- if (this.state.url) {
- analyticsEvent('url_selected');
- this.props.history.push({
- pathname: '/view',
- search: queryString.stringify({url: this.state.url}),
- });
- }
- }
-
- /** Select button clicked in the "Select WikiTree ID" dialog. */
- private handleSelectWikiTreeId() {
- this.setState(
- Object.assign({}, this.state, {
- wikiTreeIdDialogOpen: false,
- }),
- );
- if (this.state.wikiTreeId) {
- analyticsEvent('wikitree_id_selected');
- const search = queryString.parse(this.props.location.search);
- const standalone =
- search.standalone !== undefined ? search.standalone : true;
- this.props.history.push({
- pathname: '/view',
- search: queryString.stringify({
- indi: this.state.wikiTreeId,
- source: 'wikitree',
- standalone,
- }),
- });
- }
- }
-
- /** Called when the URL input is typed into. */
- private handleUrlChange(value: string) {
- this.setState(
- Object.assign({}, this.state, {
- url: value,
- }),
- );
- }
-
- /** Called when the URL input is typed into. */
- private handleWikiTreeIdChange(value: string) {
- this.setState(
- Object.assign({}, this.state, {
- wikiTreeId: value,
- }),
- );
- }
-
/** On search input change. */
private handleSearch(input: string | undefined) {
if (!input) {
@@ -266,7 +92,7 @@ export class TopBar extends React.Component<
}
}
- changeView(view: string) {
+ private changeView(view: string) {
const location = this.props.location;
const search = queryString.parse(location.search);
if (search.view !== view) {
@@ -276,192 +102,16 @@ export class TopBar extends React.Component<
}
}
- /**
- * Redirect to the WikiTree Apps login page with a return URL pointing to
- * Topola Viewer hosted on apps.wikitree.com.
- */
- private wikiTreeLogin() {
- const wikiTreeTopolaUrl =
- 'https://apps.wikitree.com/apps/wiech13/topola-viewer';
- // Append '&' because the login page appends '?authcode=...' to this URL.
- // TODO: remove ?authcode if it is in the current URL.
- const returnUrl = `${wikiTreeTopolaUrl}${window.location.hash}&`;
- this.wikiTreeReturnUrlRef.current!.value = returnUrl;
- this.wikiTreeLoginFormRef.current!.submit();
- }
-
- private checkWikiTreeLoginState() {
- const wikiTreeLoginUsername = getLoggedInUserName();
- const wikiTreeLoginState = wikiTreeLoginUsername
- ? WikiTreeLoginState.LOGGED_IN
- : WikiTreeLoginState.NOT_LOGGED_IN;
- if (this.state.wikiTreeLoginState !== wikiTreeLoginState) {
- this.setState(
- Object.assign({}, this.state, {
- wikiTreeLoginState,
- wikiTreeLoginUsername,
- }),
- );
- }
- }
-
- async componentDidMount() {
- this.checkWikiTreeLoginState();
+ componentDidMount() {
this.initializeSearchIndex();
}
componentDidUpdate(prevProps: Props) {
- this.checkWikiTreeLoginState();
if (prevProps.data !== this.props.data) {
this.initializeSearchIndex();
}
}
- private loadFromUrlModal() {
- return (
- this.handleClose()}
- centered={false}
- >
-
-
-
-
-
-
-
-
-
- );
- }
-
- private enterWikiTreeId(event: React.MouseEvent, id: string) {
- event.preventDefault(); // Do not follow link in href.
- ((this.wikiTreeIdInputRef.current as unknown) as {
- inputRef: HTMLInputElement;
- }).inputRef.value = id;
- this.handleWikiTreeIdChange(id);
- this.wikiTreeIdInputRef.current!.focus();
- }
-
- private wikiTreeIdModal() {
- return (
- this.handleClose()}
- centered={false}
- >
-
-
- txt}
- />
-
-
-
-
-
-
-
-
-
- );
- }
-
private search() {
return (
-
-
- >
- );
-
// In standalone WikiTree mode, show only the "Select WikiTree ID" menu.
if (!this.props.standalone && this.props.showWikiTreeMenus) {
switch (screenSize) {
case ScreenSize.LARGE:
- return (
- <>
- this.openWikiTreeIdDialog()}>
- {loadWikiTreeItem}
-
- {this.wikiTreeIdModal()}
- >
- );
+ return ;
case ScreenSize.SMALL:
return (
<>
- this.openWikiTreeIdDialog()}>
- {loadWikiTreeItem}
-
+
- {this.wikiTreeIdModal()}
>
);
}
@@ -681,35 +311,6 @@ export class TopBar extends React.Component<
return null;
}
- const openFileItem = (
- <>
-
-
- >
- );
- const loadUrlItem = (
- <>
-
-
- >
- );
- const commonElements = (
- <>
- {this.loadFromUrlModal()}
- {this.wikiTreeIdModal()}
- this.handleUpload(e)}
- />
- >
- );
switch (screenSize) {
case ScreenSize.LARGE:
// Show dropdown if chart is shown, otherwise show individual menu
@@ -725,51 +326,27 @@ export class TopBar extends React.Component<
className="item"
>
-
- {openFileItem}
-
- this.openLoadUrlDialog()}>
- {loadUrlItem}
-
- this.openWikiTreeIdDialog()}>
- {loadWikiTreeItem}
-
+
+
+
) : (
<>
-
- this.openLoadUrlDialog()}>
- {loadUrlItem}
-
- this.openWikiTreeIdDialog()}>
- {loadWikiTreeItem}
-
- >
- );
- return (
- <>
- {menus}
- {commonElements}
+
+
+
>
);
+ return menus;
case ScreenSize.SMALL:
return (
<>
-
- {openFileItem}
-
- this.openLoadUrlDialog()}>
- {loadUrlItem}
-
- this.openWikiTreeIdDialog()}>
- {loadWikiTreeItem}
-
+
+
+
- {commonElements}
>
);
}
@@ -779,112 +356,17 @@ export class TopBar extends React.Component<
if (!this.props.showWikiTreeMenus) {
return null;
}
- switch (this.state.wikiTreeLoginState) {
- case WikiTreeLoginState.NOT_LOGGED_IN:
- const loginForm = (
-
- );
- switch (screenSize) {
- case ScreenSize.LARGE:
- return (
- this.wikiTreeLogin()}>
-
-
- {loginForm}
-
- );
-
- case ScreenSize.SMALL:
- return (
- <>
- this.wikiTreeLogin()}>
-
-
- {loginForm}
-
-
- >
- );
- }
- break;
-
- case WikiTreeLoginState.LOGGED_IN:
- const tooltip = this.state.wikiTreeLoginUsername
- ? this.context.intl.formatMessage(
- {
- id: 'menu.wikitree_popup_username',
- defaultMessage: 'Logged in to WikiTree as {username}',
- },
- {username: this.state.wikiTreeLoginUsername},
- )
- : this.context.intl.formatMessage({
- id: 'menu.wikitree_popup',
- defaultMessage: 'Logged in to WikiTree',
- });
- switch (screenSize) {
- case ScreenSize.LARGE:
- return (
-
-
-
-
- );
-
- case ScreenSize.SMALL:
- return (
- <>
-
-
-
-
-
- >
- );
-
- default:
- return null;
- }
- }
+ return (
+ <>
+
+ {screenSize === ScreenSize.SMALL ? : null}
+ >
+ );
}
private mobileMenus() {
diff --git a/src/menu/upload_menu.tsx b/src/menu/upload_menu.tsx
new file mode 100644
index 0000000..91b9387
--- /dev/null
+++ b/src/menu/upload_menu.tsx
@@ -0,0 +1,113 @@
+import * as queryString from 'query-string';
+import * as React from 'react';
+import md5 from 'md5';
+import {analyticsEvent} from '../util/analytics';
+import {Dropdown, Icon, Menu} from 'semantic-ui-react';
+import {FormattedMessage} from 'react-intl';
+import {MenuType} from './menu_item';
+import {RouteComponentProps} from 'react-router-dom';
+
+function loadFileAsText(file: File): Promise {
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = (evt: ProgressEvent) => {
+ resolve((evt.target as FileReader).result as string);
+ };
+ reader.readAsText(file);
+ });
+}
+
+function isImageFileName(fileName: string) {
+ const lower = fileName.toLowerCase();
+ return lower.endsWith('.jpg') || lower.endsWith('.png');
+}
+
+interface Props {
+ menuType: MenuType;
+}
+
+/** Displays and handles the "Open file" menu. */
+export class UploadMenu extends React.Component {
+ private async handleUpload(event: React.SyntheticEvent) {
+ const files = (event.target as HTMLInputElement).files;
+ if (!files || !files.length) {
+ return;
+ }
+ const filesArray = Array.from(files);
+ (event.target as HTMLInputElement).value = ''; // Reset the file input.
+ analyticsEvent('upload_files_selected', {
+ event_value: files.length,
+ });
+
+ const gedcomFile =
+ filesArray.length === 1
+ ? filesArray[0]
+ : filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) ||
+ filesArray[0];
+
+ // Convert uploaded images to object URLs.
+ const images = filesArray
+ .filter(
+ (file) => file.name !== gedcomFile.name && isImageFileName(file.name),
+ )
+ .map((file) => ({
+ name: file.name,
+ url: URL.createObjectURL(file),
+ }));
+ const imageMap = new Map(
+ images.map((entry) => [entry.name, entry.url] as [string, string]),
+ );
+
+ const data = await loadFileAsText(gedcomFile);
+ const imageFileNames = images
+ .map((image) => image.name)
+ .sort()
+ .join('|');
+ // Hash GEDCOM contents with uploaded image file names.
+ const hash = md5(md5(data) + imageFileNames);
+
+ // Use history.replace() when reuploading the same file and history.push() when loading
+ // a new file.
+ const search = queryString.parse(this.props.location.search);
+ const historyPush =
+ search.file === hash
+ ? this.props.history.replace
+ : this.props.history.push;
+
+ historyPush({
+ pathname: '/view',
+ search: queryString.stringify({file: hash}),
+ state: {data, images: imageMap},
+ });
+ }
+
+ render() {
+ const content = (
+ <>
+
+
+ >
+ );
+ return (
+ <>
+ {this.props.menuType === MenuType.Menu ? (
+
+ ) : (
+
+ {content}
+
+ )}
+ this.handleUpload(e)}
+ />
+ >
+ );
+ }
+}
diff --git a/src/menu/url_menu.tsx b/src/menu/url_menu.tsx
new file mode 100644
index 0000000..dc7e3a5
--- /dev/null
+++ b/src/menu/url_menu.tsx
@@ -0,0 +1,140 @@
+import * as queryString from 'query-string';
+import * as React from 'react';
+import {analyticsEvent} from '../util/analytics';
+import {Button, Form, Header, Icon, Input, Modal} from 'semantic-ui-react';
+import {FormattedMessage} from 'react-intl';
+import {MenuItem, MenuType} from './menu_item';
+import {RouteComponentProps} from 'react-router-dom';
+
+interface Props {
+ menuType: MenuType;
+}
+
+interface State {
+ dialogOpen: boolean;
+ url?: string;
+}
+
+/** Displays and handles the "Open URL" menu. */
+export class UrlMenu extends React.Component<
+ RouteComponentProps & Props,
+ State
+> {
+ state: State = {dialogOpen: false};
+
+ inputRef: React.RefObject = React.createRef();
+
+ /** Opens the "Load from URL" dialog. */
+ private openDialog() {
+ this.setState(Object.assign({}, this.state, {dialogOpen: true}), () =>
+ this.inputRef.current!.focus(),
+ );
+ }
+
+ /** Cancels any of the open dialogs. */
+ private handleClose() {
+ this.setState(
+ Object.assign({}, this.state, {
+ dialogOpen: false,
+ }),
+ );
+ }
+
+ /** Load button clicked in the "Load from URL" dialog. */
+ private handleLoad() {
+ this.setState(
+ Object.assign({}, this.state, {
+ dialogOpen: false,
+ }),
+ );
+ if (this.state.url) {
+ analyticsEvent('url_selected');
+ this.props.history.push({
+ pathname: '/view',
+ search: queryString.stringify({url: this.state.url}),
+ });
+ }
+ }
+
+ /** Called when the URL input is typed into. */
+ private handleUrlChange(value: string) {
+ this.setState(
+ Object.assign({}, this.state, {
+ url: value,
+ }),
+ );
+ }
+
+ private loadFromUrlModal() {
+ return (
+ this.handleClose()}
+ centered={false}
+ >
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+ <>
+
+ {this.loadFromUrlModal()}
+ >
+ );
+ }
+}
diff --git a/src/menu/wikitree_menu.tsx b/src/menu/wikitree_menu.tsx
new file mode 100644
index 0000000..abedaa1
--- /dev/null
+++ b/src/menu/wikitree_menu.tsx
@@ -0,0 +1,311 @@
+import * as queryString from 'query-string';
+import * as React from 'react';
+import wikitreeLogo from './wikitree.png';
+import {analyticsEvent} from '../util/analytics';
+import {FormattedMessage, intlShape} from 'react-intl';
+import {getLoggedInUserName} from '../datasource/wikitree';
+import {MenuItem, MenuType} from './menu_item';
+import {RouteComponentProps} from 'react-router-dom';
+import {Header, Button, Modal, Input, Form} from 'semantic-ui-react';
+
+enum WikiTreeLoginState {
+ UNKNOWN,
+ NOT_LOGGED_IN,
+ LOGGED_IN,
+}
+
+interface Props {
+ menuType: MenuType;
+}
+
+interface State {
+ dialogOpen: boolean;
+ wikiTreeId?: string;
+}
+
+/** Displays and handles the "Select WikiTree ID" menu. */
+export class WikiTreeMenu extends React.Component<
+ RouteComponentProps & Props,
+ State
+> {
+ state: State = {
+ dialogOpen: false,
+ };
+
+ inputRef: React.RefObject = React.createRef();
+
+ private openDialog() {
+ this.setState(Object.assign({}, this.state, {dialogOpen: true}), () =>
+ this.inputRef.current!.focus(),
+ );
+ }
+
+ /** Cancels any of the open dialogs. */
+ private handleClose() {
+ this.setState(
+ Object.assign({}, this.state, {
+ dialogOpen: false,
+ }),
+ );
+ }
+
+ /** Select button clicked in the "Select WikiTree ID" dialog. */
+ private handleSelectId() {
+ this.setState(
+ Object.assign({}, this.state, {
+ dialogOpen: false,
+ }),
+ );
+ if (this.state.wikiTreeId) {
+ analyticsEvent('wikitree_id_selected');
+ const search = queryString.parse(this.props.location.search);
+ const standalone =
+ search.standalone !== undefined ? search.standalone : true;
+ this.props.history.push({
+ pathname: '/view',
+ search: queryString.stringify({
+ indi: this.state.wikiTreeId,
+ source: 'wikitree',
+ standalone,
+ }),
+ });
+ }
+ }
+
+ /** Called when the WikiTree ID input is typed into. */
+ private handleIdChange(value: string) {
+ this.setState(
+ Object.assign({}, this.state, {
+ wikiTreeId: value,
+ }),
+ );
+ }
+
+ private enterId(event: React.MouseEvent, id: string) {
+ event.preventDefault(); // Do not follow link in href.
+ ((this.inputRef.current as unknown) as {
+ inputRef: HTMLInputElement;
+ }).inputRef.value = id;
+ this.handleIdChange(id);
+ this.inputRef.current!.focus();
+ }
+
+ private wikiTreeIdModal() {
+ return (
+ this.handleClose()}
+ centered={false}
+ >
+
+
+ txt}
+ />
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+ <>
+
+ {this.wikiTreeIdModal()}
+ >
+ );
+ }
+}
+
+interface LoginState {
+ wikiTreeLoginState: WikiTreeLoginState;
+ wikiTreeLoginUsername?: string;
+}
+
+/** Displays and handles the "Log in to WikiTree" menu. */
+export class WikiTreeLoginMenu extends React.Component<
+ RouteComponentProps & Props,
+ LoginState
+> {
+ state: LoginState = {
+ wikiTreeLoginState: WikiTreeLoginState.UNKNOWN,
+ };
+ /** Make intl appear in this.context. */
+ static contextTypes = {
+ intl: intlShape,
+ };
+
+ wikiTreeLoginFormRef: React.RefObject = React.createRef();
+ wikiTreeReturnUrlRef: React.RefObject = React.createRef();
+
+ /**
+ * Redirect to the WikiTree Apps login page with a return URL pointing to
+ * Topola Viewer hosted on apps.wikitree.com.
+ */
+ private wikiTreeLogin() {
+ const wikiTreeTopolaUrl =
+ 'https://apps.wikitree.com/apps/wiech13/topola-viewer';
+ // Append '&' because the login page appends '?authcode=...' to this URL.
+ // TODO: remove ?authcode if it is in the current URL.
+ const returnUrl = `${wikiTreeTopolaUrl}${window.location.hash}&`;
+ this.wikiTreeReturnUrlRef.current!.value = returnUrl;
+ this.wikiTreeLoginFormRef.current!.submit();
+ }
+
+ private checkWikiTreeLoginState() {
+ const wikiTreeLoginUsername = getLoggedInUserName();
+ const wikiTreeLoginState = wikiTreeLoginUsername
+ ? WikiTreeLoginState.LOGGED_IN
+ : WikiTreeLoginState.NOT_LOGGED_IN;
+ if (this.state.wikiTreeLoginState !== wikiTreeLoginState) {
+ this.setState(
+ Object.assign({}, this.state, {
+ wikiTreeLoginState,
+ wikiTreeLoginUsername,
+ }),
+ );
+ }
+ }
+
+ componentDidMount() {
+ this.checkWikiTreeLoginState();
+ }
+
+ componentDidUpdate() {
+ this.checkWikiTreeLoginState();
+ }
+
+ render() {
+ switch (this.state.wikiTreeLoginState) {
+ case WikiTreeLoginState.NOT_LOGGED_IN:
+ return (
+ <>
+
+
+ >
+ );
+
+ case WikiTreeLoginState.LOGGED_IN:
+ const tooltip = this.state.wikiTreeLoginUsername
+ ? this.context.intl.formatMessage(
+ {
+ id: 'menu.wikitree_popup_username',
+ defaultMessage: 'Logged in to WikiTree as {username}',
+ },
+ {username: this.state.wikiTreeLoginUsername},
+ )
+ : this.context.intl.formatMessage({
+ id: 'menu.wikitree_popup',
+ defaultMessage: 'Logged in to WikiTree',
+ });
+ return (
+
+ );
+ }
+ return null;
+ }
+}