diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx
index e4af716eef8..9d28ed11eab 100644
--- a/app/assets/javascripts/components/actions/search.jsx
+++ b/app/assets/javascripts/components/actions/search.jsx
@@ -1,9 +1,12 @@
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 const SEARCH_CHANGE = 'SEARCH_CHANGE';
+export const SEARCH_CLEAR = 'SEARCH_CLEAR';
+export const SEARCH_SHOW = 'SEARCH_SHOW';
+
+export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
+export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
+export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
export function changeSearch(value) {
return {
@@ -12,42 +15,55 @@ export function changeSearch(value) {
};
};
-export function clearSearchSuggestions() {
+export function clearSearch() {
return {
- type: SEARCH_SUGGESTIONS_CLEAR
+ type: SEARCH_CLEAR
};
};
-export function readySearchSuggestions(value, { accounts, hashtags, statuses }) {
- return {
- type: SEARCH_SUGGESTIONS_READY,
- value,
- accounts,
- hashtags,
- statuses
- };
-};
-
-export function fetchSearchSuggestions(value) {
+export function submitSearch() {
return (dispatch, getState) => {
- if (getState().getIn(['search', 'loaded_value']) === value) {
- return;
- }
+ const value = getState().getIn(['search', 'value']);
+
+ dispatch(fetchSearchRequest());
api(getState).get('/api/v1/search', {
params: {
q: value,
- resolve: true,
- limit: 4
+ resolve: true
}
}).then(response => {
- dispatch(readySearchSuggestions(value, response.data));
+ dispatch(fetchSearchSuccess(response.data));
+ }).catch(error => {
+ dispatch(fetchSearchFail(error));
});
};
};
-export function resetSearch() {
+export function fetchSearchRequest() {
return {
- type: SEARCH_RESET
+ type: SEARCH_FETCH_REQUEST
+ };
+};
+
+export function fetchSearchSuccess(results) {
+ return {
+ type: SEARCH_FETCH_SUCCESS,
+ results,
+ accounts: results.accounts,
+ statuses: results.statuses
+ };
+};
+
+export function fetchSearchFail(error) {
+ return {
+ type: SEARCH_FETCH_FAIL,
+ error
+ };
+};
+
+export function showSearch() {
+ return {
+ type: SEARCH_SHOW
};
};
diff --git a/app/assets/javascripts/components/features/compose/components/drawer.jsx b/app/assets/javascripts/components/features/compose/components/drawer.jsx
deleted file mode 100644
index ab67c86ea5b..00000000000
--- a/app/assets/javascripts/components/features/compose/components/drawer.jsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Link } from 'react-router';
-import { injectIntl, defineMessages } from 'react-intl';
-
-const messages = defineMessages({
- start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
- public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
- community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
- preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
- logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
-});
-
-const Drawer = ({ children, withHeader, intl }) => {
- let header = '';
-
- if (withHeader) {
- header = (
-
- );
- }
-
- return (
-
- {header}
-
-
- {children}
-
-
- );
-};
-
-Drawer.propTypes = {
- withHeader: React.PropTypes.bool,
- children: React.PropTypes.node,
- intl: React.PropTypes.object
-};
-
-export default injectIntl(Drawer);
diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx
index a0e8f82fbc7..8e86f053e4d 100644
--- a/app/assets/javascripts/components/features/compose/components/search.jsx
+++ b/app/assets/javascripts/components/features/compose/components/search.jsx
@@ -1,123 +1,67 @@
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';
-import AutosuggestStatusContainer from '../containers/autosuggest_status_container';
-import { debounce } from 'react-decoration';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
});
-const getSuggestionValue = suggestion => suggestion.value;
-
-const renderSuggestion = suggestion => {
- if (suggestion.type === 'account') {
- return ;
- } else if (suggestion.type === 'hashtag') {
- return #{suggestion.id};
- } else {
- return ;
- }
-};
-
-const renderSectionTitle = section => (
-
-);
-
-const getSectionSuggestions = section => section.items;
-
-const outerStyle = {
- padding: '10px',
- lineHeight: '20px',
- position: 'relative'
-};
-
-const iconStyle = {
- position: 'absolute',
- top: '18px',
- right: '20px',
- 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,
+ onSubmit: React.PropTypes.func.isRequired,
onClear: React.PropTypes.func.isRequired,
- onFetch: React.PropTypes.func.isRequired,
- onReset: React.PropTypes.func.isRequired,
+ onShow: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
- onChange (_, { newValue }) {
- if (typeof newValue !== 'string') {
- return;
- }
-
- this.props.onChange(newValue);
+ handleChange (e) {
+ this.props.onChange(e.target.value);
},
- onSuggestionsClearRequested () {
+ handleClear (e) {
+ e.preventDefault();
this.props.onClear();
},
- @debounce(500)
- onSuggestionsFetchRequested ({ value }) {
- value = value.replace('#', '');
- this.props.onFetch(value.trim());
- },
-
- onSuggestionSelected (_, { suggestion }) {
- if (suggestion.type === 'account') {
- this.context.router.push(`/accounts/${suggestion.id}`);
- } else if(suggestion.type === 'hashtag') {
- this.context.router.push(`/timelines/tag/${suggestion.id}`);
- } else {
- this.context.router.push(`/statuses/${suggestion.id}`);
+ handleKeyDown (e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.props.onSubmit();
}
},
+ handleFocus () {
+ this.props.onShow();
+ },
+
render () {
- const inputProps = {
- placeholder: this.props.intl.formatMessage(messages.placeholder),
- value: this.props.value,
- onChange: this.onChange,
- className: 'search__input'
- };
+ const { intl, value } = this.props;
+ const hasValue = value.length > 0;
return (
-
);
- },
+ }
});
diff --git a/app/assets/javascripts/components/features/compose/components/search_results.jsx b/app/assets/javascripts/components/features/compose/components/search_results.jsx
new file mode 100644
index 00000000000..fd05e7f7e49
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/search_results.jsx
@@ -0,0 +1,68 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import AccountContainer from '../../../containers/account_container';
+import StatusContainer from '../../../containers/status_container';
+import { Link } from 'react-router';
+
+const SearchResults = React.createClass({
+
+ propTypes: {
+ results: ImmutablePropTypes.map.isRequired
+ },
+
+ mixins: [PureRenderMixin],
+
+ render () {
+ const { results } = this.props;
+
+ let accounts, statuses, hashtags;
+ let count = 0;
+
+ if (results.get('accounts') && results.get('accounts').size > 0) {
+ count += results.get('accounts').size;
+ accounts = (
+
+ {results.get('accounts').map(accountId =>
)}
+
+ );
+ }
+
+ if (results.get('statuses') && results.get('statuses').size > 0) {
+ count += results.get('statuses').size;
+ statuses = (
+
+ {results.get('statuses').map(statusId => )}
+
+ );
+ }
+
+ if (results.get('hashtags') && results.get('hashtags').size > 0) {
+ count += results.get('hashtags').size;
+ hashtags = (
+
+ {results.get('hashtags').map(hashtag =>
+
+ #{hashtag}
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {accounts}
+ {statuses}
+ {hashtags}
+
+ );
+ }
+
+});
+
+export default SearchResults;
diff --git a/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx b/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx
deleted file mode 100644
index 97cc9487e75..00000000000
--- a/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import { FormattedMessage } from 'react-intl';
-import Toggle from 'react-toggle';
-import Collapsable from '../../../components/collapsable';
-
-const SensitiveToggle = React.createClass({
-
- propTypes: {
- hasMedia: React.PropTypes.bool,
- isSensitive: React.PropTypes.bool,
- onChange: React.PropTypes.func.isRequired
- },
-
- mixins: [PureRenderMixin],
-
- render () {
- const { hasMedia, isSensitive, onChange } = this.props;
-
- return (
-
-
-
- );
- }
-
-});
-
-export default SensitiveToggle;
diff --git a/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx b/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx
deleted file mode 100644
index 1c59e439355..00000000000
--- a/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import { FormattedMessage } from 'react-intl';
-import Toggle from 'react-toggle';
-
-const SpoilerToggle = React.createClass({
-
- propTypes: {
- isSpoiler: React.PropTypes.bool,
- onChange: React.PropTypes.func.isRequired
- },
-
- mixins: [PureRenderMixin],
-
- render () {
- const { isSpoiler, onChange } = this.props;
-
- return (
-
- );
- }
-
-});
-
-export default SpoilerToggle;
diff --git a/app/assets/javascripts/components/features/compose/containers/search_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_container.jsx
index 17a68f2fc8a..96709215af3 100644
--- a/app/assets/javascripts/components/features/compose/containers/search_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/search_container.jsx
@@ -1,14 +1,13 @@
import { connect } from 'react-redux';
import {
changeSearch,
- clearSearchSuggestions,
- fetchSearchSuggestions,
- resetSearch
+ clearSearch,
+ submitSearch,
+ showSearch
} from '../../../actions/search';
import Search from '../components/search';
const mapStateToProps = state => ({
- suggestions: state.getIn(['search', 'suggestions']),
value: state.getIn(['search', 'value'])
});
@@ -19,15 +18,15 @@ const mapDispatchToProps = dispatch => ({
},
onClear () {
- dispatch(clearSearchSuggestions());
+ dispatch(clearSearch());
},
- onFetch (value) {
- dispatch(fetchSearchSuggestions(value));
+ onSubmit () {
+ dispatch(submitSearch());
},
- onReset () {
- dispatch(resetSearch());
+ onShow () {
+ dispatch(showSearch());
}
});
diff --git a/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx
new file mode 100644
index 00000000000..e5911fd3883
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx
@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import SearchResults from '../components/search_results';
+
+const mapStateToProps = state => ({
+ results: state.getIn(['search', 'results'])
+});
+
+export default connect(mapStateToProps)(SearchResults);
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
index 15e2c580946..d21e7a9bc1d 100644
--- a/app/assets/javascripts/components/features/compose/index.jsx
+++ b/app/assets/javascripts/components/features/compose/index.jsx
@@ -1,17 +1,34 @@
-import Drawer from './components/drawer';
import ComposeFormContainer from './containers/compose_form_container';
import UploadFormContainer from './containers/upload_form_container';
import NavigationContainer from './containers/navigation_container';
import PureRenderMixin from 'react-addons-pure-render-mixin';
-import SearchContainer from './containers/search_container';
import { connect } from 'react-redux';
import { mountCompose, unmountCompose } from '../../actions/compose';
+import { Link } from 'react-router';
+import { injectIntl, defineMessages } from 'react-intl';
+import SearchContainer from './containers/search_container';
+import { Motion, spring } from 'react-motion';
+import SearchResultsContainer from './containers/search_results_container';
+
+const messages = defineMessages({
+ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+ public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
+ community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
+ preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+ logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
+});
+
+const mapStateToProps = state => ({
+ showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
+});
const Compose = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
- withHeader: React.PropTypes.bool
+ withHeader: React.PropTypes.bool,
+ showSearch: React.PropTypes.bool,
+ intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
@@ -25,15 +42,46 @@ const Compose = React.createClass({
},
render () {
+ const { withHeader, showSearch, intl } = this.props;
+
+ let header = '';
+
+ if (withHeader) {
+ header = (
+
+ );
+ }
+
return (
-
+
+ {header}
+
-
-
-
+
+
+
+
+
+
+
+
+ {({ x }) =>
+
+
+
+ }
+
+
+
);
}
});
-export default connect()(Compose);
+export default connect(mapStateToProps)(injectIntl(Compose));
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index 6ce41670d60..df944009395 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -33,7 +33,7 @@ import {
STATUS_FETCH_SUCCESS,
CONTEXT_FETCH_SUCCESS
} from '../actions/statuses';
-import { SEARCH_SUGGESTIONS_READY } from '../actions/search';
+import { SEARCH_FETCH_SUCCESS } from '../actions/search';
import {
NOTIFICATIONS_UPDATE,
NOTIFICATIONS_REFRESH_SUCCESS,
@@ -97,7 +97,7 @@ export default function accounts(state = initialState, action) {
return normalizeAccounts(state, action.accounts);
case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS:
- case SEARCH_SUGGESTIONS_READY:
+ case SEARCH_FETCH_SUCCESS:
return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
case TIMELINE_REFRESH_SUCCESS:
case TIMELINE_EXPAND_SUCCESS:
diff --git a/app/assets/javascripts/components/reducers/search.jsx b/app/assets/javascripts/components/reducers/search.jsx
index e95f9ed79f6..b3fe6c7bea2 100644
--- a/app/assets/javascripts/components/reducers/search.jsx
+++ b/app/assets/javascripts/components/reducers/search.jsx
@@ -1,14 +1,17 @@
import {
SEARCH_CHANGE,
- SEARCH_SUGGESTIONS_READY,
- SEARCH_RESET
+ SEARCH_CLEAR,
+ SEARCH_FETCH_SUCCESS,
+ SEARCH_SHOW
} from '../actions/search';
+import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose';
import Immutable from 'immutable';
const initialState = Immutable.Map({
value: '',
- loaded_value: '',
- suggestions: []
+ submitted: false,
+ hidden: false,
+ results: Immutable.Map()
});
const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => {
@@ -69,14 +72,24 @@ 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, action.hashtags, action.statuses);
- case SEARCH_RESET:
+ case SEARCH_CLEAR:
return state.withMutations(map => {
- map.set('suggestions', []);
map.set('value', '');
- map.set('loaded_value', '');
+ map.set('results', Immutable.Map());
+ map.set('submitted', false);
+ map.set('hidden', false);
});
+ case SEARCH_SHOW:
+ return state.set('hidden', false);
+ case COMPOSE_REPLY:
+ case COMPOSE_MENTION:
+ return state.set('hidden', true);
+ case SEARCH_FETCH_SUCCESS:
+ return state.set('results', Immutable.Map({
+ accounts: Immutable.List(action.results.accounts.map(item => item.id)),
+ statuses: Immutable.List(action.results.statuses.map(item => item.id)),
+ hashtags: Immutable.List(action.results.hashtags)
+ })).set('submitted', true);
default:
return state;
}
diff --git a/app/assets/javascripts/components/reducers/statuses.jsx b/app/assets/javascripts/components/reducers/statuses.jsx
index 1669b8c65e8..ca8fa7a015b 100644
--- a/app/assets/javascripts/components/reducers/statuses.jsx
+++ b/app/assets/javascripts/components/reducers/statuses.jsx
@@ -32,7 +32,7 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS
} from '../actions/favourites';
-import { SEARCH_SUGGESTIONS_READY } from '../actions/search';
+import { SEARCH_FETCH_SUCCESS } from '../actions/search';
import Immutable from 'immutable';
const normalizeStatus = (state, status) => {
@@ -109,7 +109,7 @@ export default function statuses(state = initialState, action) {
case NOTIFICATIONS_EXPAND_SUCCESS:
case FAVOURITED_STATUSES_FETCH_SUCCESS:
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
- case SEARCH_SUGGESTIONS_READY:
+ case SEARCH_FETCH_SUCCESS:
return normalizeStatuses(state, action.statuses);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 1c1e8bffcbc..9c138e4958e 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -764,8 +764,19 @@ a.status__content__spoiler-link {
}
}
+.drawer__pager {
+ box-sizing: border-box;
+ padding: 0;
+ flex-grow: 1;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+}
+
.drawer__inner {
- //background: linear-gradient(rgba(lighten($color1, 13%), 1), rgba(lighten($color1, 13%), 0.65));
+ position: absolute;
+ top: 0;
+ left: 0;
background: lighten($color1, 13%);
box-sizing: border-box;
padding: 0;
@@ -773,7 +784,12 @@ a.status__content__spoiler-link {
flex-direction: column;
overflow: hidden;
overflow-y: auto;
- flex-grow: 1;
+ width: 100%;
+ height: 100%;
+
+ &.darker {
+ background: $color1;
+ }
}
.drawer__header {
@@ -1224,26 +1240,6 @@ button.active i.fa-retweet {
}
}
-.search {
- .fa {
- color: $color3;
- }
-}
-
-.search__input {
- box-sizing: border-box;
- display: block;
- width: 100%;
- border: none;
- padding: 10px;
- padding-right: 30px;
- font-family: inherit;
- background: $color1;
- color: $color3;
- font-size: 14px;
- margin: 0;
-}
-
.loading-indicator {
color: $color2;
}
@@ -1723,3 +1719,100 @@ button.active i.fa-retweet {
box-shadow: 2px 4px 6px rgba($color8, 0.1);
}
}
+
+.search {
+ position: relative;
+ margin-bottom: 10px;
+}
+
+.search__input {
+ padding-right: 30px;
+ color: $color2;
+ outline: 0;
+ box-sizing: border-box;
+ display: block;
+ width: 100%;
+ border: none;
+ padding: 10px;
+ padding-right: 30px;
+ font-family: inherit;
+ background: $color1;
+ color: $color3;
+ font-size: 14px;
+ margin: 0;
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+
+ &::-moz-focus-inner, &:focus, &:active {
+ outline: 0 !important;
+ }
+
+ &:focus {
+ background: lighten($color1, 4%);
+ }
+}
+
+.search__icon {
+ .fa {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 2;
+ display: inline-block;
+ opacity: 0;
+ transition: all 100ms linear;
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ color: $color2;
+ cursor: default;
+ pointer-events: none;
+
+ &.active {
+ pointer-events: auto;
+ opacity: 0.3;
+ }
+ }
+
+ .fa-search {
+ transform: translateZ(0) rotate(90deg);
+
+ &.active {
+ pointer-events: none;
+ transform: translateZ(0) rotate(0deg);
+ }
+ }
+
+ .fa-times-circle {
+ top: 11px;
+ transform: translateZ(0) rotate(0deg);
+ cursor: pointer;
+
+ &.active {
+ transform: translateZ(0) rotate(90deg);
+ }
+ }
+}
+
+.search-results__header {
+ color: lighten($color1, 26%);
+ background: lighten($color1, 2%);
+ border-bottom: 1px solid darken($color1, 4%);
+ padding: 15px 10px;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.search-results__hashtag {
+ display: block;
+ padding: 10px;
+ color: $color2;
+ text-decoration: none;
+
+ &:hover, &:active, &:focus {
+ color: lighten($color2, 4%);
+ text-decoration: underline;
+ }
+}