From bf61bc1b96e63a848e7ec7984be54cb508b4bfe7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 11:48:25 +0200 Subject: Fix drag & drop overlay flickering --- .../javascripts/components/features/ui/index.jsx | 51 ++++++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index b7e8f32a4..4b7e4bb46 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -36,15 +36,31 @@ const UI = React.createClass({ this.setState({ width: window.innerWidth }); }, + handleDragEnter (e) { + e.preventDefault(); + + if (!this.dragTargets) { + this.dragTargets = []; + } + + if (this.dragTargets.indexOf(e.target) === -1) { + this.dragTargets.push(e.target); + } + + this.setState({ draggingOver: true }); + }, + handleDragOver (e) { e.preventDefault(); e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; + try { + e.dataTransfer.dropEffect = 'copy'; + } catch (err) { - if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') { - this.setState({ draggingOver: true }); } + + return false; }, handleDrop (e) { @@ -57,14 +73,25 @@ const UI = React.createClass({ } }, - handleDragLeave () { + handleDragLeave (e) { + e.preventDefault(); + e.stopPropagation(); + + this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)); + + if (this.dragTargets.length > 0) { + return; + } + this.setState({ draggingOver: false }); }, componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); - window.addEventListener('dragover', this.handleDragOver); - window.addEventListener('drop', this.handleDrop); + document.addEventListener('dragenter', this.handleDragEnter, false); + document.addEventListener('dragover', this.handleDragOver, false); + document.addEventListener('drop', this.handleDrop, false); + document.addEventListener('dragleave', this.handleDragLeave, false); this.props.dispatch(refreshTimeline('home')); this.props.dispatch(refreshNotifications()); @@ -72,8 +99,14 @@ const UI = React.createClass({ componentWillUnmount () { window.removeEventListener('resize', this.handleResize); - window.removeEventListener('dragover', this.handleDragOver); - window.removeEventListener('drop', this.handleDrop); + document.removeEventListener('dragenter', this.handleDragEnter); + document.removeEventListener('dragover', this.handleDragOver); + document.removeEventListener('drop', this.handleDrop); + document.removeEventListener('dragleave', this.handleDragLeave); + }, + + setRef (c) { + this.node = c; }, render () { @@ -100,7 +133,7 @@ const UI = React.createClass({ } return ( -
+
{mountedColumns} -- cgit From 680f9efe9c4aa7fce1f4dd6a35ef4aca7a80c1f3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 14:23:44 +0200 Subject: Fix web UI profile clickable area overlapping with follow button area --- .../features/account/components/header.jsx | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index e1aae3c77..a359963c4 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -4,6 +4,7 @@ import emojify from '../../../emoji'; import escapeTextContentForBrowser from 'escape-html'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from '../../../components/icon_button'; +import { Motion, spring } from 'react-motion'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -11,6 +12,47 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } }); +const Avatar = React.createClass({ + + propTypes: { + account: ImmutablePropTypes.map.isRequired + }, + + getInitialState () { + return { + isHovered: false + }; + }, + + mixins: [PureRenderMixin], + + handleMouseOver () { + if (this.state.isHovered) return; + this.setState({ isHovered: true }); + }, + + handleMouseOut () { + if (!this.state.isHovered) return; + this.setState({ isHovered: false }); + }, + + render () { + const { account } = this.props; + const { isHovered } = this.state; + + return ( + + {({ radius }) => + + {account.get('acct')} + + } + + ); + } + +}); + const Header = React.createClass({ propTypes: { @@ -68,14 +110,9 @@ const Header = React.createClass({ return (
- -
- -
- - -
+ + @{account.get('acct')} {lockedIcon}
-- cgit From b4046c5957f16437910cdfe1ab45ee818118e7d7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 19:59:54 +0200 Subject: Rework search --- .../javascripts/components/actions/search.jsx | 66 ++++++---- .../features/compose/components/drawer.jsx | 44 ------- .../features/compose/components/search.jsx | 114 +++++------------ .../features/compose/components/search_results.jsx | 68 ++++++++++ .../compose/components/sensitive_toggle.jsx | 31 ----- .../features/compose/components/spoiler_toggle.jsx | 27 ---- .../compose/containers/search_container.jsx | 17 ++- .../containers/search_results_container.jsx | 8 ++ .../components/features/compose/index.jsx | 64 ++++++++-- .../javascripts/components/reducers/accounts.jsx | 4 +- .../javascripts/components/reducers/search.jsx | 31 +++-- .../javascripts/components/reducers/statuses.jsx | 4 +- app/assets/stylesheets/components.scss | 137 +++++++++++++++++---- 13 files changed, 351 insertions(+), 264 deletions(-) delete mode 100644 app/assets/javascripts/components/features/compose/components/drawer.jsx create mode 100644 app/assets/javascripts/components/features/compose/components/search_results.jsx delete mode 100644 app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx delete mode 100644 app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx create mode 100644 app/assets/javascripts/components/features/compose/containers/search_results_container.jsx (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx index e4af716ee..9d28ed11e 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() { - return { - type: SEARCH_SUGGESTIONS_CLEAR - }; -}; - -export function readySearchSuggestions(value, { accounts, hashtags, statuses }) { +export function clearSearch() { return { - type: SEARCH_SUGGESTIONS_READY, - value, - accounts, - hashtags, - statuses + type: SEARCH_CLEAR }; }; -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_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_RESET + 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 ab67c86ea..000000000 --- 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 a0e8f82fb..8e86f053e 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()); + handleKeyDown (e) { + if (e.key === 'Enter') { + e.preventDefault(); + this.props.onSubmit(); + } }, - 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}`); - } + 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 000000000..fd05e7f7e --- /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 97cc9487e..000000000 --- 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 1c59e4393..000000000 --- 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 17a68f2fc..96709215a 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 000000000..e5911fd38 --- /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 15e2c5809..d21e7a9bc 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 6ce41670d..df9440093 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 e95f9ed79..b3fe6c7be 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 1669b8c65..ca8fa7a01 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 1c1e8bffc..9c138e495 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; + } +} -- cgit From d93d6f5124d7e120ad1542a65b72792e31f86b22 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 22:44:12 +0200 Subject: Fix reworked search --- .../javascripts/components/actions/search.jsx | 4 ++++ .../features/compose/components/search.jsx | 5 +++-- .../compose/containers/search_container.jsx | 3 ++- .../components/features/compose/index.jsx | 4 ++-- .../components/features/getting_started/index.jsx | 2 -- .../components/reducers/relationships.jsx | 22 +++++++++++----------- app/assets/stylesheets/components.scss | 9 ++++++--- app/services/search_service.rb | 4 ++-- 8 files changed, 30 insertions(+), 23 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx index 9d28ed11e..df3ae0db1 100644 --- a/app/assets/javascripts/components/actions/search.jsx +++ b/app/assets/javascripts/components/actions/search.jsx @@ -25,6 +25,10 @@ export function submitSearch() { return (dispatch, getState) => { const value = getState().getIn(['search', 'value']); + if (value.length === 0) { + return; + } + dispatch(fetchSearchRequest()); api(getState).get('/api/v1/search', { diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx index 8e86f053e..936e003f2 100644 --- a/app/assets/javascripts/components/features/compose/components/search.jsx +++ b/app/assets/javascripts/components/features/compose/components/search.jsx @@ -10,6 +10,7 @@ const Search = React.createClass({ propTypes: { value: React.PropTypes.string.isRequired, + submitted: React.PropTypes.bool, onChange: React.PropTypes.func.isRequired, onSubmit: React.PropTypes.func.isRequired, onClear: React.PropTypes.func.isRequired, @@ -40,8 +41,8 @@ const Search = React.createClass({ }, render () { - const { intl, value } = this.props; - const hasValue = value.length > 0; + const { intl, value, submitted } = this.props; + const hasValue = value.length > 0 || submitted; return (
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 96709215a..906c0c28c 100644 --- a/app/assets/javascripts/components/features/compose/containers/search_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/search_container.jsx @@ -8,7 +8,8 @@ import { import Search from '../components/search'; const mapStateToProps = state => ({ - value: state.getIn(['search', 'value']) + value: state.getIn(['search', 'value']), + submitted: state.getIn(['search', 'submitted']) }); const mapDispatchToProps = dispatch => ({ diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index d21e7a9bc..d4df259dc 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -70,9 +70,9 @@ const Compose = React.createClass({
- + {({ x }) => -
+
} diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 6f9e988ba..8253ad017 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -43,8 +43,6 @@ const GettingStarted = ({ intl, me }) => {
-

-

tootsuite/mastodon, apps: }} />

diff --git a/app/assets/javascripts/components/reducers/relationships.jsx b/app/assets/javascripts/components/reducers/relationships.jsx index 591f8034b..c65c48b43 100644 --- a/app/assets/javascripts/components/reducers/relationships.jsx +++ b/app/assets/javascripts/components/reducers/relationships.jsx @@ -23,16 +23,16 @@ const initialState = Immutable.Map(); export default function relationships(state = initialState, action) { switch(action.type) { - case ACCOUNT_FOLLOW_SUCCESS: - case ACCOUNT_UNFOLLOW_SUCCESS: - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_UNBLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - case ACCOUNT_UNMUTE_SUCCESS: - return normalizeRelationship(state, action.relationship); - case RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - default: - return state; + case ACCOUNT_FOLLOW_SUCCESS: + case ACCOUNT_UNFOLLOW_SUCCESS: + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_UNBLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + case ACCOUNT_UNMUTE_SUCCESS: + return normalizeRelationship(state, action.relationship); + case RELATIONSHIPS_FETCH_SUCCESS: + return normalizeRelationships(state, action.relationships); + default: + return state; } }; diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 9c138e495..d7589d9b0 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1120,9 +1120,8 @@ a.status__content__spoiler-link { box-sizing: border-box; overflow-y: auto; padding-bottom: 235px; - background: image-url('mastodon-getting-started.png') no-repeat bottom left; - height: auto; - min-height: 100%; + background: image-url('mastodon-getting-started.png') no-repeat 0 100% local; + height: 100%; p { color: $color2; @@ -1793,6 +1792,10 @@ button.active i.fa-retweet { &.active { transform: translateZ(0) rotate(90deg); } + + &:hover { + color: $color5; + } } } diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 159c03713..e9745010b 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,10 +2,10 @@ class SearchService < BaseService def call(query, limit, resolve = false, account = nil) - return if query.blank? - results = { accounts: [], hashtags: [], statuses: [] } + return results if query.blank? + if query =~ /\Ahttps?:\/\// resource = FetchRemoteResourceService.new.call(query) -- cgit From 13dfd8d109442ffdd90dbd533a426b04b68e5119 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 15:17:35 +0200 Subject: Improve mobile tabs a little --- .../components/features/ui/components/tabs_bar.jsx | 28 ++++++++++++------- app/assets/stylesheets/components.scss | 31 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 11 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx index 225a6a5fc..6cdb29dbf 100644 --- a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx +++ b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx @@ -1,15 +1,23 @@ import { Link } from 'react-router'; import { FormattedMessage } from 'react-intl'; -const TabsBar = () => { - return ( -
- - - - -
- ); -}; +const TabsBar = React.createClass({ + + render () { + return ( +
+ + + + + + + + +
+ ); + } + +}); export default TabsBar; diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index d7589d9b0..a4dce7f18 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -858,11 +858,25 @@ a.status__content__spoiler-link { font-size:12px; font-weight: 500; border-bottom: 2px solid lighten($color1, 8%); + transition: all 200ms linear; + + .fa { + font-weight: 400; + } &.active { border-bottom: 2px solid $color4; color: $color4; } + + &:hover, &:focus, &:active { + background: lighten($color1, 14%); + transition: all 100ms linear; + } + + span { + display: none; + } } @media screen and (min-width: 360px) { @@ -870,6 +884,22 @@ a.status__content__spoiler-link { margin: 10px; margin-bottom: 0; } + + .search { + margin-bottom: 10px; + } +} + +@media screen and (min-width: 600px) { + .tabs-bar__link { + .fa { + margin-right: 5px; + } + + span { + display: inline; + } + } } @media screen and (min-width: 1025px) { @@ -1721,7 +1751,6 @@ button.active i.fa-retweet { .search { position: relative; - margin-bottom: 10px; } .search__input { -- cgit From 60ebfa182f944d5803dd6f3d54aa5e9ef24fc922 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 22:11:28 +0200 Subject: Made modal system more generic --- .../javascripts/components/actions/modal.jsx | 25 +--- .../javascripts/components/components/lightbox.jsx | 82 ---------- .../components/containers/status_container.jsx | 4 +- .../components/features/status/index.jsx | 4 +- .../features/ui/components/media_modal.jsx | 134 +++++++++++++++++ .../features/ui/components/modal_root.jsx | 80 ++++++++++ .../features/ui/containers/modal_container.jsx | 166 +-------------------- .../javascripts/components/features/ui/index.jsx | 4 +- .../javascripts/components/reducers/modal.jsx | 30 +--- app/assets/stylesheets/components.scss | 46 +++++- 10 files changed, 285 insertions(+), 290 deletions(-) delete mode 100644 app/assets/javascripts/components/components/lightbox.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/media_modal.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/modal_root.jsx (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx index d19218c48..615cd6bfe 100644 --- a/app/assets/javascripts/components/actions/modal.jsx +++ b/app/assets/javascripts/components/actions/modal.jsx @@ -1,14 +1,11 @@ -export const MEDIA_OPEN = 'MEDIA_OPEN'; +export const MODAL_OPEN = 'MODAL_OPEN'; export const MODAL_CLOSE = 'MODAL_CLOSE'; -export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE'; -export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE'; - -export function openMedia(media, index) { +export function openModal(type, props) { return { - type: MEDIA_OPEN, - media, - index + type: MODAL_OPEN, + modalType: type, + modalProps: props }; }; @@ -17,15 +14,3 @@ export function closeModal() { type: MODAL_CLOSE }; }; - -export function decreaseIndexInModal() { - return { - type: MODAL_INDEX_DECREASE - }; -}; - -export function increaseIndexInModal() { - return { - type: MODAL_INDEX_INCREASE - }; -}; diff --git a/app/assets/javascripts/components/components/lightbox.jsx b/app/assets/javascripts/components/components/lightbox.jsx deleted file mode 100644 index f04ca47ba..000000000 --- a/app/assets/javascripts/components/components/lightbox.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import IconButton from './icon_button'; -import { Motion, spring } from 'react-motion'; -import { injectIntl } from 'react-intl'; - -const overlayStyle = { - position: 'fixed', - top: '0', - left: '0', - width: '100%', - height: '100%', - background: 'rgba(0, 0, 0, 0.5)', - display: 'flex', - justifyContent: 'center', - alignContent: 'center', - flexDirection: 'row', - zIndex: '9999' -}; - -const dialogStyle = { - color: '#282c37', - boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)', - margin: 'auto', - position: 'relative' -}; - -const closeStyle = { - position: 'absolute', - top: '4px', - right: '4px' -}; - -const Lightbox = React.createClass({ - - propTypes: { - isVisible: React.PropTypes.bool, - onOverlayClicked: React.PropTypes.func, - onCloseClicked: React.PropTypes.func, - intl: React.PropTypes.object.isRequired, - children: React.PropTypes.node - }, - - mixins: [PureRenderMixin], - - componentDidMount () { - this._listener = e => { - if (this.props.isVisible && e.key === 'Escape') { - this.props.onCloseClicked(); - } - }; - - window.addEventListener('keyup', this._listener); - }, - - componentWillUnmount () { - window.removeEventListener('keyup', this._listener); - }, - - stopPropagation (e) { - e.stopPropagation(); - }, - - render () { - const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props; - - return ( - - {({ backgroundOpacity, opacity, y }) => -
-
- - {children} -
-
- } -
- ); - } - -}); - -export default injectIntl(Lightbox); diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index e7543bc39..fd3fbe4c3 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -17,7 +17,7 @@ import { } from '../actions/accounts'; import { deleteStatus } from '../actions/statuses'; import { initReport } from '../actions/reports'; -import { openMedia } from '../actions/modal'; +import { openModal } from '../actions/modal'; import { createSelector } from 'reselect' import { isMobile } from '../is_mobile' @@ -63,7 +63,7 @@ const mapDispatchToProps = (dispatch) => ({ }, onOpenMedia (media, index) { - dispatch(openMedia(media, index)); + dispatch(openModal('MEDIA', { media, index })); }, onBlock (account) { diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index 6a7635cc6..f98fe1b01 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -28,7 +28,7 @@ import { import { ScrollContainer } from 'react-router-scroll'; import ColumnBackButton from '../../components/column_back_button'; import StatusContainer from '../../containers/status_container'; -import { openMedia } from '../../actions/modal'; +import { openModal } from '../../actions/modal'; import { isMobile } from '../../is_mobile' const makeMapStateToProps = () => { @@ -99,7 +99,7 @@ const Status = React.createClass({ }, handleOpenMedia (media, index) { - this.props.dispatch(openMedia(media, index)); + this.props.dispatch(openModal('MEDIA', { media, index })); }, handleReport (status) { diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx new file mode 100644 index 000000000..e8b718094 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -0,0 +1,134 @@ +import Lightbox from '../../../components/lightbox'; +import LoadingIndicator from '../../../components/loading_indicator'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import ImageLoader from 'react-imageloader'; +import { defineMessages, injectIntl } from 'react-intl'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' } +}); + +const leftNavStyle = { + position: 'absolute', + background: 'rgba(0, 0, 0, 0.5)', + padding: '30px 15px', + cursor: 'pointer', + fontSize: '24px', + top: '0', + left: '-61px', + boxSizing: 'border-box', + height: '100%', + display: 'flex', + alignItems: 'center' +}; + +const rightNavStyle = { + position: 'absolute', + background: 'rgba(0, 0, 0, 0.5)', + padding: '30px 15px', + cursor: 'pointer', + fontSize: '24px', + top: '0', + right: '-61px', + boxSizing: 'border-box', + height: '100%', + display: 'flex', + alignItems: 'center' +}; + +const closeStyle = { + position: 'absolute', + top: '4px', + right: '4px' +}; + +const MediaModal = React.createClass({ + + propTypes: { + media: ImmutablePropTypes.list.isRequired, + index: React.PropTypes.number.isRequired, + onClose: React.PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired + }, + + getInitialState () { + return { + index: null + }; + }, + + mixins: [PureRenderMixin], + + handleNextClick () { + this.setState({ index: (this.getIndex() + 1) % this.props.media.size}); + }, + + handlePrevClick () { + this.setState({ index: (this.getIndex() - 1) % this.props.media.size}); + }, + + handleKeyUp (e) { + switch(e.key) { + case 'ArrowLeft': + this.handlePrevClick(); + break; + case 'ArrowRight': + this.handleNextClick(); + break; + } + }, + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + }, + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + }, + + getIndex () { + return this.state.index !== null ? this.state.index : this.props.index; + }, + + render () { + const { media, intl, onClose } = this.props; + + const index = this.getIndex(); + const attachment = media.get(index); + const url = attachment.get('url'); + + let leftNav, rightNav, content; + + leftNav = rightNav = content = ''; + + if (media.size > 1) { + leftNav =
; + rightNav =
; + } + + if (attachment.get('type') === 'image') { + content = ; + } else if (attachment.get('type') === 'gifv') { + content = ; + } + + return ( +
+ {leftNav} + +
+ + {content} +
+ + {rightNav} +
+ ); + } + +}); + +export default injectIntl(MediaModal); diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx new file mode 100644 index 000000000..d2ae5e145 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -0,0 +1,80 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import MediaModal from './media_modal'; +import { TransitionMotion, spring } from 'react-motion'; + +const MODAL_COMPONENTS = { + 'MEDIA': MediaModal +}; + +const ModalRoot = React.createClass({ + + propTypes: { + type: React.PropTypes.string, + props: React.PropTypes.object, + onClose: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleKeyUp (e) { + if (e.key === 'Escape' && !!this.props.type) { + this.props.onClose(); + } + }, + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + }, + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + }, + + willEnter () { + return { opacity: 0, scale: 0.98 }; + }, + + willLeave () { + return { opacity: spring(0), scale: spring(0.98) }; + }, + + render () { + const { type, props, onClose } = this.props; + const items = []; + + if (!!type) { + items.push({ + key: type, + data: { type, props }, + style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) } + }); + } + + return ( + + {interpolatedStyles => +
+ {interpolatedStyles.map(({ key, data: { type, props }, style }) => { + const SpecificComponent = MODAL_COMPONENTS[type]; + + return ( +
+
+
+ +
+
+ ); + })} +
+ } + + ); + } + +}); + +export default ModalRoot; diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index e3c4281b9..26d77818c 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -1,170 +1,16 @@ import { connect } from 'react-redux'; -import { - closeModal, - decreaseIndexInModal, - increaseIndexInModal -} from '../../../actions/modal'; -import Lightbox from '../../../components/lightbox'; -import ImageLoader from 'react-imageloader'; -import LoadingIndicator from '../../../components/loading_indicator'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import { closeModal } from '../../../actions/modal'; +import ModalRoot from '../components/modal_root'; const mapStateToProps = state => ({ - media: state.getIn(['modal', 'media']), - index: state.getIn(['modal', 'index']), - isVisible: state.getIn(['modal', 'open']) + type: state.get('modal').modalType, + props: state.get('modal').modalProps }); const mapDispatchToProps = dispatch => ({ - onCloseClicked () { + onClose () { dispatch(closeModal()); }, - - onOverlayClicked () { - dispatch(closeModal()); - }, - - onNextClicked () { - dispatch(increaseIndexInModal()); - }, - - onPrevClicked () { - dispatch(decreaseIndexInModal()); - } -}); - -const imageStyle = { - display: 'block', - maxWidth: '80vw', - maxHeight: '80vh' -}; - -const loadingStyle = { - width: '400px', - paddingBottom: '120px' -}; - -const preloader = () => ( -
- -
-); - -const leftNavStyle = { - position: 'absolute', - background: 'rgba(0, 0, 0, 0.5)', - padding: '30px 15px', - cursor: 'pointer', - fontSize: '24px', - top: '0', - left: '-61px', - boxSizing: 'border-box', - height: '100%', - display: 'flex', - alignItems: 'center' -}; - -const rightNavStyle = { - position: 'absolute', - background: 'rgba(0, 0, 0, 0.5)', - padding: '30px 15px', - cursor: 'pointer', - fontSize: '24px', - top: '0', - right: '-61px', - boxSizing: 'border-box', - height: '100%', - display: 'flex', - alignItems: 'center' -}; - -const Modal = React.createClass({ - - propTypes: { - media: ImmutablePropTypes.list, - index: React.PropTypes.number.isRequired, - isVisible: React.PropTypes.bool, - onCloseClicked: React.PropTypes.func, - onOverlayClicked: React.PropTypes.func, - onNextClicked: React.PropTypes.func, - onPrevClicked: React.PropTypes.func - }, - - mixins: [PureRenderMixin], - - handleNextClick () { - this.props.onNextClicked(); - }, - - handlePrevClick () { - this.props.onPrevClicked(); - }, - - componentDidMount () { - this._listener = e => { - if (!this.props.isVisible) { - return; - } - - switch(e.key) { - case 'ArrowLeft': - this.props.onPrevClicked(); - break; - case 'ArrowRight': - this.props.onNextClicked(); - break; - } - }; - - window.addEventListener('keyup', this._listener); - }, - - componentWillUnmount () { - window.removeEventListener('keyup', this._listener); - }, - - render () { - const { media, index, ...other } = this.props; - - if (!media) { - return null; - } - - const attachment = media.get(index); - const url = attachment.get('url'); - - let leftNav, rightNav, content; - - leftNav = rightNav = content = ''; - - if (media.size > 1) { - leftNav =
; - rightNav =
; - } - - if (attachment.get('type') === 'image') { - content = ( - - ); - } else if (attachment.get('type') === 'gifv') { - content = ; - } - - return ( - - {leftNav} - {content} - {rightNav} - - ); - } - }); -export default connect(mapStateToProps, mapDispatchToProps)(Modal); +export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 4b7e4bb46..89fb82568 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -47,7 +47,9 @@ const UI = React.createClass({ this.dragTargets.push(e.target); } - this.setState({ draggingOver: true }); + if (e.dataTransfer && e.dataTransfer.files.length > 0) { + this.setState({ draggingOver: true }); + } }, handleDragOver (e) { diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx index 37ffbc62b..3566820ef 100644 --- a/app/assets/javascripts/components/reducers/modal.jsx +++ b/app/assets/javascripts/components/reducers/modal.jsx @@ -1,31 +1,17 @@ -import { - MEDIA_OPEN, - MODAL_CLOSE, - MODAL_INDEX_DECREASE, - MODAL_INDEX_INCREASE -} from '../actions/modal'; +import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; import Immutable from 'immutable'; -const initialState = Immutable.Map({ - media: null, - index: 0, - open: false -}); +const initialState = { + modalType: null, + modalProps: {} +}; export default function modal(state = initialState, action) { switch(action.type) { - case MEDIA_OPEN: - return state.withMutations(map => { - map.set('media', action.media); - map.set('index', action.index); - map.set('open', true); - }); + case MODAL_OPEN: + return { modalType: action.modalType, modalProps: action.modalProps }; case MODAL_CLOSE: - return state.set('open', false); - case MODAL_INDEX_DECREASE: - return state.update('index', index => (index - 1) % state.get('media').size); - case MODAL_INDEX_INCREASE: - return state.update('index', index => (index + 1) % state.get('media').size); + return initialState; default: return state; } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index a4dce7f18..d87e09453 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1311,7 +1311,7 @@ button.active i.fa-retweet { color: $color3; } -.modal-container--nav { +.modal-container__nav { color: $color5; } @@ -1848,3 +1848,47 @@ button.active i.fa-retweet { text-decoration: underline; } } + +.modal-root__overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + opacity: 0; + background: rgba($color8, 0.7); +} + +.modal-root__container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + align-content: space-around; + z-index: 9999; + opacity: 0; + pointer-events: none; + user-select: none; +} + +.modal-root__modal { + pointer-events: auto; + display: flex; +} + +.media-modal { + max-width: 80vw; + max-height: 80vh; + position: relative; + + img, video { + max-width: 80vw; + max-height: 80vh; + } +} -- cgit From a8c2e44fee65484f43dd9c58e9e519c375a76fd5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 22:29:20 +0200 Subject: Fix broken reference --- app/assets/javascripts/components/features/ui/components/media_modal.jsx | 1 - 1 file changed, 1 deletion(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx index e8b718094..35eb2cb0c 100644 --- a/app/assets/javascripts/components/features/ui/components/media_modal.jsx +++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -1,4 +1,3 @@ -import Lightbox from '../../../components/lightbox'; import LoadingIndicator from '../../../components/loading_indicator'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; -- cgit From e809caa0e1633cede15b2578d1582d9878eae291 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 15:46:31 +0200 Subject: Fix feed regeneration bug --- app/assets/javascripts/components/features/compose/index.jsx | 2 +- app/services/fan_out_on_write_service.rb | 6 +++++- app/services/precompute_feed_service.rb | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index d4df259dc..9421de3ff 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -72,7 +72,7 @@ const Compose = React.createClass({ {({ x }) => -
+
} diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 0cacfd7cd..402b84b2f 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -6,7 +6,11 @@ class FanOutOnWriteService < BaseService def call(status) deliver_to_self(status) if status.account.local? - status.direct_visibility? ? deliver_to_mentioned_followers(status) : deliver_to_followers(status) + if status.direct_visibility? + deliver_to_mentioned_followers(status) + else + deliver_to_followers(status) + end return if status.account.silenced? || !status.public_visibility? || status.reblog? diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 54d11b631..984eb8e86 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -4,10 +4,10 @@ class PrecomputeFeedService < BaseService # Fill up a user's home/mentions feed from DB and return a subset # @param [Symbol] type :home or :mentions # @param [Account] account - def call(type, account) - Status.send("as_#{type}_timeline", account).limit(FeedManager::MAX_ITEMS).each do |status| - next if FeedManager.instance.filter?(type, status, account) - redis.zadd(FeedManager.instance.key(type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) + def call(_, account) + Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status| + next if (status.direct_visibility? && !status.permitted?(account)) || FeedManager.instance.filter?(:home, status, account) + redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) end end -- cgit From 4b7dca47130b97e2f213cb28def741c6dee46ff1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 16:45:49 +0200 Subject: Fix wording "show reblogs" -> "show boosts", order reports chronologically in admin UI --- .../components/features/home_timeline/components/column_settings.jsx | 4 ++-- app/controllers/admin/reports_controller.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx index 3317210bf..92e700874 100644 --- a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx +++ b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx @@ -6,7 +6,7 @@ import SettingToggle from '../../notifications/components/setting_toggle'; import SettingText from './setting_text'; const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter by regular expressions' } + filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' } }); const outerStyle = { @@ -44,7 +44,7 @@ const ColumnSettings = React.createClass({
- } /> + } />
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 67d57e4eb..0117a18ee 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -7,7 +7,7 @@ class Admin::ReportsController < ApplicationController layout 'admin' def index - @reports = Report.includes(:account, :target_account).paginate(page: params[:page], per_page: 40) + @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40) @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved end -- cgit From aaa4d1b0fb896f0d5f607cca3760106399caf41b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 21:44:06 +0200 Subject: Keep track of which timelines are connected live to avoid redundant refreshes on navigation --- .../javascripts/components/actions/timelines.jsx | 22 ++++++++++++++++++++++ .../javascripts/components/containers/mastodon.jsx | 13 ++++++++++++- .../features/community_timeline/index.jsx | 16 +++++++++++++++- .../components/features/public_timeline/index.jsx | 16 +++++++++++++++- .../javascripts/components/reducers/timelines.jsx | 11 ++++++++++- 5 files changed, 74 insertions(+), 4 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 3e2d4ff43..6cd1f04b3 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -14,6 +14,9 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; + export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { return { type: TIMELINE_REFRESH_SUCCESS, @@ -76,6 +79,11 @@ export function refreshTimeline(timeline, id = null) { let skipLoading = false; if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) { + if (id === null && getState().getIn(['timelines', timeline, 'online'])) { + // Skip refreshing when timeline is live anyway + return; + } + params = { ...params, since_id: newestId }; skipLoading = true; } @@ -162,3 +170,17 @@ export function scrollTopTimeline(timeline, top) { top }; }; + +export function connectTimeline(timeline) { + return { + type: TIMELINE_CONNECT, + timeline + }; +}; + +export function disconnectTimeline(timeline) { + return { + type: TIMELINE_DISCONNECT, + timeline + }; +}; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 40fbac525..6dc08bb4c 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -4,7 +4,9 @@ import { refreshTimelineSuccess, updateTimeline, deleteFromTimelines, - refreshTimeline + refreshTimeline, + connectTimeline, + disconnectTimeline } from '../actions/timelines'; import { updateNotifications, refreshNotifications } from '../actions/notifications'; import createBrowserHistory from 'history/lib/createBrowserHistory'; @@ -70,6 +72,14 @@ const Mastodon = React.createClass({ this.subscription = createStream(accessToken, 'user', { + connected () { + store.dispatch(connectTimeline('home')); + }, + + disconnected () { + store.dispatch(disconnectTimeline('home')); + }, + received (data) { switch(data.event) { case 'update': @@ -85,6 +95,7 @@ const Mastodon = React.createClass({ }, reconnected () { + store.dispatch(connectTimeline('home')); store.dispatch(refreshTimeline('home')); store.dispatch(refreshNotifications()); } diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx index 2cfd7b2fe..0957338cf 100644 --- a/app/assets/javascripts/components/features/community_timeline/index.jsx +++ b/app/assets/javascripts/components/features/community_timeline/index.jsx @@ -5,7 +5,9 @@ import Column from '../ui/components/column'; import { refreshTimeline, updateTimeline, - deleteFromTimelines + deleteFromTimelines, + connectTimeline, + disconnectTimeline } from '../../actions/timelines'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; @@ -44,6 +46,18 @@ const CommunityTimeline = React.createClass({ subscription = createStream(accessToken, 'public:local', { + connected () { + dispatch(connectTimeline('community')); + }, + + reconnected () { + dispatch(connectTimeline('community')); + }, + + disconnected () { + dispatch(disconnectTimeline('community')); + }, + received (data) { switch(data.event) { case 'update': diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index b2342abbd..6d766a83b 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -5,7 +5,9 @@ import Column from '../ui/components/column'; import { refreshTimeline, updateTimeline, - deleteFromTimelines + deleteFromTimelines, + connectTimeline, + disconnectTimeline } from '../../actions/timelines'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; @@ -44,6 +46,18 @@ const PublicTimeline = React.createClass({ subscription = createStream(accessToken, 'public', { + connected () { + dispatch(connectTimeline('public')); + }, + + reconnected () { + dispatch(connectTimeline('public')); + }, + + disconnected () { + dispatch(disconnectTimeline('public')); + }, + received (data) { switch(data.event) { case 'update': diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index c67d05423..675a52759 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -7,7 +7,9 @@ import { TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_FAIL, - TIMELINE_SCROLL_TOP + TIMELINE_SCROLL_TOP, + TIMELINE_CONNECT, + TIMELINE_DISCONNECT } from '../actions/timelines'; import { REBLOG_SUCCESS, @@ -35,6 +37,7 @@ const initialState = Immutable.Map({ path: () => '/api/v1/timelines/home', next: null, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -45,6 +48,7 @@ const initialState = Immutable.Map({ path: () => '/api/v1/timelines/public', next: null, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -56,6 +60,7 @@ const initialState = Immutable.Map({ next: null, params: { local: true }, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -300,6 +305,10 @@ export default function timelines(state = initialState, action) { return filterTimelines(state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); + case TIMELINE_CONNECT: + return state.setIn([action.timeline, 'online'], true); + case TIMELINE_DISCONNECT: + return state.setIn([action.timeline, 'online'], false); default: return state; } -- cgit From 5652f00d81aa18dd4fa6046c22282c000635e032 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Apr 2017 11:44:11 -0400 Subject: GitHub should be capitalized --- app/assets/javascripts/components/features/getting_started/index.jsx | 2 +- app/assets/javascripts/components/locales/en.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 8253ad017..d7a78d9cc 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -43,7 +43,7 @@ const GettingStarted = ({ intl, me }) => {
-

tootsuite/mastodon, apps: }} />

+

tootsuite/mastodon, apps: }} />

diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 2d3360b6b..53e2898eb 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -25,7 +25,7 @@ const en = { "getting_started.heading": "Getting started", "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.", "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", "column.home": "Home", "column.community": "Local timeline", "column.public": "Federated timeline", -- cgit