Get basic search working

This commit is contained in:
Marquis Kurt 2019-04-02 17:28:04 -04:00
parent d4580b0cb5
commit d32c0730fe
6 changed files with 225 additions and 4 deletions

23
package-lock.json generated
View File

@ -14787,6 +14787,17 @@
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"query-string": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.4.2.tgz",
"integrity": "sha512-DfJqAen17LfLA3rQ+H5S4uXphrF+ANU1lT2ijds4V/Tj4gZxA3gx5/tg1bz7kYCmwna7LyJNCYqO7jNRzo3aLw==",
"dev": true,
"requires": {
"decode-uri-component": "^0.2.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
}
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@ -17114,6 +17125,12 @@
"wbuf": "^1.7.3"
}
},
"split-on-first": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.0.0.tgz",
"integrity": "sha512-mjA57TQtdWztVZ9THAjGNpgbuIrNfsNrGa5IyK94NoPaT4N14M+GI4jD7t4arLjFkYRQWdETC5RxFzLWouoB3A==",
"dev": true
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -17291,6 +17308,12 @@
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
},
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=",
"dev": true
},
"string-length": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",

View File

@ -20,7 +20,8 @@
"react-swipeable-views": "^0.13.1",
"typescript": "3.3.4000",
"notistack": "^0.5.1",
"react-web-share-api": "^0.0.2"
"react-web-share-api": "^0.0.2",
"query-string": "^6.4.2"
},
"scripts": {
"start": "BROWSER='Safari Technology Preview' react-scripts start",

View File

@ -13,6 +13,8 @@ import LocalPage from './pages/Local';
import PublicPage from './pages/Public';
import Conversation from './pages/Conversation';
import NotificationsPage from './pages/Notifications';
import SearchPage from './pages/Search';
import {withSnackbar} from 'notistack';
let theme = setHyperspaceTheme(getUserDefaultTheme());
@ -48,6 +50,7 @@ class App extends Component<any, any> {
<Route path="/notifications" component={NotificationsPage}/>
<Route path="/profile/:profileId" render={props => <ProfilePage {...props}></ProfilePage>}/>
<Route path="/conversation/:conversationId" component={Conversation}/>
<Route path="/search" component={SearchPage}/>
<Route path="/settings" component={Settings}/>
<Route path="/about" component={AboutPage}/>
</MuiThemeProvider>

View File

@ -52,10 +52,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
componentDidMount() {
this.streamListener = this.client.stream('/streaming/user');
this.streamListener.on('connect', () => { console.log ('Streaming notifs!')});
this.streamListener.on('notification', (notif: Notification) => {
console.log("received notif");
const notificationCount = this.state.notificationCount + 1;
this.setState({ notificationCount });
if (!document.hasFocus()) {
@ -107,6 +104,11 @@ export class AppLayout extends Component<any, IAppLayoutState> {
})
}
searchForQuery(what: string) {
window.location.href = "/#/search?query=" + what;
window.location.reload;
}
titlebar() {
const { classes } = this.props;
if (process.env.NODE_ENV === "development") {
@ -216,6 +218,11 @@ export class AppLayout extends Component<any, IAppLayoutState> {
root: classes.appBarSearchInputRoot,
input: classes.appBarSearchInputInput
}}
onKeyUp={(event) => {
if (event.keyCode === 13) {
this.searchForQuery(event.currentTarget.value);
}
}}
/>
</div>
<div className={classes.appBarFlexGrow}/>

178
src/pages/Search.tsx Normal file
View File

@ -0,0 +1,178 @@
import React, { Component } from 'react';
import {
List,
ListItem,
ListItemText,
ListSubheader,
ListItemSecondaryAction,
ListItemAvatar,
Avatar,
Paper,
IconButton,
withStyles,
Typography,
Link,
CircularProgress
} from '@material-ui/core';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import DomainIcon from '@material-ui/icons/Domain';
import ChatIcon from '@material-ui/icons/Chat';
import PersonIcon from '@material-ui/icons/Person';
import {styles} from './PageLayout.styles';
import {LinkableIconButton} from '../interfaces/overrides';
import Mastodon from 'megalodon';
import {parse as parseParams, ParsedQuery} from 'query-string';
import { Results } from '../types/Search';
import { withSnackbar } from 'notistack';
import Post from '../components/Post';
import { Status } from '../types/Status';
import { Account } from '../types/Account';
interface ISearchPageState {
query: string[] | string;
type?: string[] | string;
results?: Results;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: string;
}
class SearchPage extends Component<any, ISearchPageState> {
client: Mastodon;
constructor(props: any) {
super(props);
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v2");
let search = this.runQueryCheck();
let query;
let type;
if (search.query) {
query = search.query;
} else {
query = "";
}
if (search.type && search.type !== undefined) {
type = search.type;
}
this.state = {
viewIsLoading: true,
query,
type
}
this.searchQuery(query);
}
componentWillReceiveProps(props: any) {
this.setState({ viewDidLoad: false, viewIsLoading: true, viewDidError: false, viewDidErrorCode: '', results: undefined});
let newSearch = this.runQueryCheck(props.location);
let query;
let type;
if (newSearch.query) {
query = newSearch.query;
} else {
query = "";
}
if (newSearch.type && newSearch.type !== undefined) {
type = newSearch.type;
}
this.setState({ query, type });
this.searchQuery(query);
}
runQueryCheck(newLocation?: string): ParsedQuery {
let searchParams = "";
if (newLocation !== undefined && typeof(newLocation) === "string") {
searchParams = newLocation.replace("#/search", "");
} else {
searchParams = location.hash.replace("#/search", "");
}
return parseParams(searchParams);
}
searchQuery(query: string | string[]) {
this.client.get('/search', {q: query}).then((resp: any) => {
let results: Results = resp.data;
this.setState({
results,
viewDidLoad: true,
viewIsLoading: false
});
}).catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
})
this.props.enqueueSnackbar(`Couldn't search for ${this.state.query}: ${err.name}`, { variant: 'error' });
});
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
<ListSubheader>Accounts</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
{
this.state.results?
this.state.results.accounts.map((acct: Account) => {
return (
<ListItem key={acct.id}>
<ListItemAvatar>
<Avatar alt={acct.username} src={acct.avatar_static}/>
</ListItemAvatar>
<ListItemText primary={acct.display_name || acct.acct} secondary={acct.acct}/>
<ListItemSecondaryAction>
<LinkableIconButton to={`/profile/${acct.id}`}>
<PersonIcon/>
</LinkableIconButton>
</ListItemSecondaryAction>
</ListItem>
);
}): null
}
</List>
</Paper>
<br/>
<ListSubheader>Posts</ListSubheader>
{
this.state.results?
this.state.results.statuses.length > 0?
this.state.results.statuses.map((post: Status) => {
return <Post key={post.id} post={post} client={this.client}/>
}): <Typography variant="caption">No results found.</Typography>: null
}
{
this.state.viewDidError?
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
</Paper>:
<span/>
}
{
this.state.viewIsLoading?
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
<span/>
}
</div>
);
}
}
export default withStyles(styles)(withSnackbar(SearchPage));

9
src/types/Search.tsx Normal file
View File

@ -0,0 +1,9 @@
import { Account } from "./Account";
import { Status } from "./Status";
import { Tag } from "./Tag";
export type Results = {
accounts: [Account];
statuses: [Status];
hashtags: [Tag];
}