From eb31f13030877574ec3e5cb55419e7ca66678a96 Mon Sep 17 00:00:00 2001 From: Przemek Wiech Date: Mon, 4 May 2020 21:37:09 +0200 Subject: [PATCH] Refactoring: extracted load menus to separate files --- src/menu/menu_item.tsx | 32 +++ src/menu/top_bar.tsx | 576 ++----------------------------------- src/menu/upload_menu.tsx | 113 ++++++++ src/menu/url_menu.tsx | 140 +++++++++ src/menu/wikitree_menu.tsx | 311 ++++++++++++++++++++ 5 files changed, 625 insertions(+), 547 deletions(-) create mode 100644 src/menu/menu_item.tsx create mode 100644 src/menu/upload_menu.tsx create mode 100644 src/menu/url_menu.tsx create mode 100644 src/menu/wikitree_menu.tsx 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} - > -
- - txt} - /> -
- -
this.handleLoad()}> - this.handleUrlChange(data.value)} - ref={this.urlInputRef} - /> -

- - cors-anywhere.herokuapp.com - - ), - }} - /> -

-
-
- - - - -
- ); - } - - 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} - > -
- WikiTree logo - txt} - /> -
- -
this.handleSelectWikiTreeId()}> -

- - WikiTree - - ), - example1: ( - this.enterWikiTreeId(e, 'Wojtyla-13')} - className="link-span" - > - Wojtyla-13 - - ), - example2: ( - this.enterWikiTreeId(e, 'Skłodowska-2')} - className="link-span" - > - Skłodowska-2 - - ), - }} - /> -

- this.handleWikiTreeIdChange(data.value)} - ref={this.wikiTreeIdInputRef} - /> -
-
- - - - -
- ); - } - private search() { return ( - WikiTree logo - - - ); - // 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()}> - WikiTree logo - - {loginForm} - - ); - - case ScreenSize.SMALL: - return ( - <> - this.wikiTreeLogin()}> - WikiTree logo - - {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 ( - - WikiTree logo - - - ); - - case ScreenSize.SMALL: - return ( - <> - - WikiTree logo - - - - - ); - - 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} + > +
+ + txt} + /> +
+ +
this.handleLoad()}> + this.handleUrlChange(data.value)} + ref={this.inputRef} + /> +

+ + cors-anywhere.herokuapp.com + + ), + }} + /> +

+
+
+ + + + +
+ ); + } + + render() { + return ( + <> + this.openDialog()} + menuType={this.props.menuType} + > + + + + {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} + > +
+ WikiTree logo + txt} + /> +
+ +
this.handleSelectId()}> +

+ + WikiTree + + ), + example1: ( + this.enterId(e, 'Wojtyla-13')} + className="link-span" + > + Wojtyla-13 + + ), + example2: ( + this.enterId(e, 'Skłodowska-2')} + className="link-span" + > + Skłodowska-2 + + ), + }} + /> +

+ this.handleIdChange(data.value)} + ref={this.inputRef} + /> +
+
+ + + + +
+ ); + } + + render() { + return ( + <> + this.openDialog()} + > + WikiTree logo + + + {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 ( + <> + this.wikiTreeLogin()} + > + WikiTree logo + + +
+ + +
+ + ); + + 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 ( + + WikiTree logo + + + ); + } + return null; + } +}