diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx
new file mode 100644
index 000000000..ceb0e4a0c
--- /dev/null
+++ b/app/assets/javascripts/components/actions/search.jsx
@@ -0,0 +1,51 @@
+import api from '../api'
+
+export const SEARCH_CHANGE = 'SEARCH_CHANGE';
+export const SEARCH_SUGGESTIONS_CLEAR = 'SEARCH_SUGGESTIONS_CLEAR';
+export const SEARCH_SUGGESTIONS_READY = 'SEARCH_SUGGESTIONS_READY';
+export const SEARCH_RESET = 'SEARCH_RESET';
+
+export function changeSearch(value) {
+ return {
+ type: SEARCH_CHANGE,
+ value
+ };
+};
+
+export function clearSearchSuggestions() {
+ return {
+ type: SEARCH_SUGGESTIONS_CLEAR
+ };
+};
+
+export function readySearchSuggestions(value, accounts) {
+ return {
+ type: SEARCH_SUGGESTIONS_READY,
+ value,
+ accounts
+ };
+};
+
+export function fetchSearchSuggestions(value) {
+ return (dispatch, getState) => {
+ if (getState().getIn(['search', 'loaded_value']) === value) {
+ return;
+ }
+
+ api(getState).get('/api/v1/accounts/search', {
+ params: {
+ q: value,
+ resolve: true,
+ limit: 4
+ }
+ }).then(response => {
+ dispatch(readySearchSuggestions(value, response.data));
+ });
+ };
+};
+
+export function resetSearch() {
+ return {
+ type: SEARCH_RESET
+ };
+};
diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx
new file mode 100644
index 000000000..e81771e6a
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/search.jsx
@@ -0,0 +1,126 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Autosuggest from 'react-autosuggest';
+import AutosuggestAccountContainer from '../containers/autosuggest_account_container';
+
+const getSuggestionValue = suggestion => suggestion.value;
+
+const renderSuggestion = suggestion => {
+ if (suggestion.type === 'account') {
+ return ;
+ } else {
+ return #{suggestion.id}
+ }
+};
+
+const renderSectionTitle = section => (
+ {section.title}
+);
+
+const getSectionSuggestions = section => section.items;
+
+const outerStyle = {
+ padding: '10px',
+ lineHeight: '20px',
+ position: 'relative'
+};
+
+const inputStyle = {
+ boxSizing: 'border-box',
+ display: 'block',
+ width: '100%',
+ border: 'none',
+ padding: '10px',
+ paddingRight: '30px',
+ fontFamily: 'Roboto',
+ background: '#282c37',
+ color: '#9baec8',
+ fontSize: '14px',
+ margin: '0'
+};
+
+const iconStyle = {
+ position: 'absolute',
+ top: '18px',
+ right: '20px',
+ color: '#9baec8',
+ fontSize: '18px',
+ pointerEvents: 'none'
+};
+
+const Search = React.createClass({
+
+ contextTypes: {
+ router: React.PropTypes.object
+ },
+
+ propTypes: {
+ suggestions: React.PropTypes.array.isRequired,
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ onClear: React.PropTypes.func.isRequired,
+ onFetch: React.PropTypes.func.isRequired,
+ onReset: React.PropTypes.func.isRequired
+ },
+
+ mixins: [PureRenderMixin],
+
+ onChange (_, { newValue }) {
+ if (typeof newValue !== 'string') {
+ return;
+ }
+
+ this.props.onChange(newValue);
+ },
+
+ onSuggestionsClearRequested () {
+ this.props.onClear();
+ },
+
+ onSuggestionsFetchRequested ({ value }) {
+ value = value.replace('#', '');
+ this.props.onFetch(value.trim());
+ },
+
+ onSuggestionSelected (_, { suggestion }) {
+ if (suggestion.type === 'account') {
+ this.context.router.push(`/accounts/${suggestion.id}`);
+ } else {
+ this.context.router.push(`/statuses/tag/${suggestion.id}`);
+ }
+ },
+
+ render () {
+ const inputProps = {
+ placeholder: 'Search',
+ value: this.props.value,
+ onChange: this.onChange,
+ style: inputStyle
+ };
+
+ return (
+
+ );
+ },
+
+});
+
+export default Search;
diff --git a/app/assets/javascripts/components/features/compose/containers/search_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_container.jsx
new file mode 100644
index 000000000..17a68f2fc
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/search_container.jsx
@@ -0,0 +1,35 @@
+import { connect } from 'react-redux';
+import {
+ changeSearch,
+ clearSearchSuggestions,
+ fetchSearchSuggestions,
+ resetSearch
+} from '../../../actions/search';
+import Search from '../components/search';
+
+const mapStateToProps = state => ({
+ suggestions: state.getIn(['search', 'suggestions']),
+ value: state.getIn(['search', 'value'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+ onChange (value) {
+ dispatch(changeSearch(value));
+ },
+
+ onClear () {
+ dispatch(clearSearchSuggestions());
+ },
+
+ onFetch (value) {
+ dispatch(fetchSearchSuggestions(value));
+ },
+
+ onReset () {
+ dispatch(resetSearch());
+ }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Search);
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
index d76afc437..260f67034 100644
--- a/app/assets/javascripts/components/features/compose/index.jsx
+++ b/app/assets/javascripts/components/features/compose/index.jsx
@@ -5,6 +5,7 @@ import UploadFormContainer from '../ui/containers/upload_form_container';
import NavigationContainer from '../ui/containers/navigation_container';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import SuggestionsContainer from './containers/suggestions_container';
+import SearchContainer from './containers/search_container';
import { fetchSuggestions } from '../../actions/suggestions';
import { connect } from 'react-redux';
@@ -24,13 +25,13 @@ const Compose = React.createClass({
return (
+
-
);
}
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index 471e1b0aa..b20b3d0c5 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -26,6 +26,7 @@ import {
STATUS_FETCH_SUCCESS,
CONTEXT_FETCH_SUCCESS
} from '../actions/statuses';
+import { SEARCH_SUGGESTIONS_READY } from '../actions/search';
import Immutable from 'immutable';
const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account));
@@ -70,6 +71,7 @@ export default function accounts(state = initialState, action) {
case REBLOGS_FETCH_SUCCESS:
case FAVOURITES_FETCH_SUCCESS:
case COMPOSE_SUGGESTIONS_READY:
+ case SEARCH_SUGGESTIONS_READY:
return normalizeAccounts(state, action.accounts);
case TIMELINE_REFRESH_SUCCESS:
case TIMELINE_EXPAND_SUCCESS:
diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx
index ccc9e8e8e..e2203cc1a 100644
--- a/app/assets/javascripts/components/reducers/index.jsx
+++ b/app/assets/javascripts/components/reducers/index.jsx
@@ -10,6 +10,7 @@ import user_lists from './user_lists';
import accounts from './accounts';
import statuses from './statuses';
import relationships from './relationships';
+import search from './search';
export default combineReducers({
timelines,
@@ -22,5 +23,6 @@ export default combineReducers({
user_lists,
accounts,
statuses,
- relationships
+ relationships,
+ search
});
diff --git a/app/assets/javascripts/components/reducers/search.jsx b/app/assets/javascripts/components/reducers/search.jsx
new file mode 100644
index 000000000..f3ee17f60
--- /dev/null
+++ b/app/assets/javascripts/components/reducers/search.jsx
@@ -0,0 +1,60 @@
+import {
+ SEARCH_CHANGE,
+ SEARCH_SUGGESTIONS_READY,
+ SEARCH_RESET
+} from '../actions/search';
+import Immutable from 'immutable';
+
+const initialState = Immutable.Map({
+ value: '',
+ loaded_value: '',
+ suggestions: []
+});
+
+const normalizeSuggestions = (state, value, accounts) => {
+ let newSuggestions = [
+ {
+ title: 'Account',
+ items: accounts.map(item => ({
+ type: 'account',
+ id: item.id,
+ value: item.acct
+ }))
+ }
+ ];
+
+ if (value.indexOf('@') === -1) {
+ newSuggestions.push({
+ title: 'Hashtag',
+ items: [
+ {
+ type: 'hashtag',
+ id: value,
+ value: `#${value}`
+ }
+ ]
+ });
+ }
+
+ return state.withMutations(map => {
+ map.set('suggestions', newSuggestions);
+ map.set('loaded_value', value);
+ });
+};
+
+export default function search(state = initialState, action) {
+ switch(action.type) {
+ case SEARCH_CHANGE:
+ return state.set('value', action.value);
+ case SEARCH_SUGGESTIONS_READY:
+ return normalizeSuggestions(state, action.value, action.accounts);
+ case SEARCH_RESET:
+ return state.withMutations(map => {
+ map.set('suggestions', []);
+ map.set('value', '');
+ map.set('loaded_value', '');
+ });
+ default:
+ return state;
+ }
+};
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 89397a96d..2cd58bb2b 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -325,12 +325,22 @@
top: 100%;
width: 100%;
z-index: 99;
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
+}
+
+.react-autosuggest__section-title {
+ background: #9baec8;
+ padding: 4px 10px;
+ font-weight: 500;
+ cursor: default;
+ color: #282c37;
+ text-transform: uppercase;
+ font-size: 11px;
}
.react-autosuggest__suggestions-list {
- background: #9baec8;
+ background: #d9e1e8;
color: #282c37;
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
font-size: 14px;
}