diff options
Diffstat (limited to 'app')
39 files changed, 569 insertions, 98 deletions
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index f9506971a..b0e134554 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -65,7 +65,7 @@ class Api::V1::StatusesController < Api::BaseController RemovalWorker.perform_async(@status.id) - render_empty + render json: @status, serializer: REST::StatusSerializer, source_requested: true end private diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 53cf1c4ca..28eebda28 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -28,7 +28,11 @@ class StatusesController < ApplicationController respond_to do |format| format.html do use_pack 'public' - mark_cacheable! unless user_signed_in? + + unless user_signed_in? + skip_session! + expires_in 10.seconds, public: true + end @body_classes = 'with-modals' diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 8cb54a148..1e16c5157 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -16,6 +16,12 @@ class StreamEntriesController < ApplicationController respond_to do |format| format.html do use_pack 'public' + + unless user_signed_in? + skip_session! + expires_in 5.minutes, public: true + end + redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status' end diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 4eabc4be0..550fe510f 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -71,10 +71,11 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status) { +export function redraft(status, raw_text) { return { type: REDRAFT, status, + raw_text, }; }; @@ -88,12 +89,12 @@ export function deleteStatus(id, router, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api(getState).delete(`/api/v1/statuses/${id}`).then(() => { + api(getState).delete(`/api/v1/statuses/${id}`).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); if (withRedraft) { - dispatch(redraft(status)); + dispatch(redraft(status, response.data.text)); if (!getState().getIn(['compose', 'mounted'])) { router.push('/statuses/new'); diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 60636feb4..98dc5bb87 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -88,18 +88,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onModalReblog (status) { - dispatch(reblog(status)); + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else { + dispatch(reblog(status)); + } }, onReblog (status, e) { - if (status.get('reblogged')) { - dispatch(unreblog(status)); + if (e.shiftKey || !boostModal) { + this.onModalReblog(status); } else { - if (e.shiftKey || !boostModal) { - this.onModalReblog(status); - } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); - } + dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); } }, diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js index 8f163979f..fa1ae8821 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, @@ -42,9 +41,19 @@ class SensitiveButton extends React.PureComponent { return ( <div className='compose-form__sensitive-button'> - <button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> - <Icon icon='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> - </button> + <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <input + name='mark-sensitive' + type='checkbox' + checked={active} + onChange={onClick} + disabled={disabled} + /> + + <span className={classNames('checkbox', { active })} /> + + <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + </label> </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js index 0a914dce2..ce7ec2479 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js @@ -10,6 +10,7 @@ import DisplayName from 'flavours/glitch/components/display_name'; import ImmutablePureComponent from 'react-immutable-pure-component'; const messages = defineMessages({ + cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, }); @@ -52,6 +53,7 @@ export default class BoostModal extends ImmutablePureComponent { render () { const { status, intl } = this.props; + const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; return ( <div className='modal-root__modal boost-modal'> @@ -77,7 +79,7 @@ export default class BoostModal extends ImmutablePureComponent { <div className='boost-modal__action-bar'> <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div> - <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} /> + <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} /> </div> </div> ); diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 009e1fee7..bc1785a48 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -426,7 +426,7 @@ export default function compose(state = initialState, action) { return state.mergeIn(['doodle'], action.options); case REDRAFT: return state.withMutations(map => { - map.set('text', unescapeHTML(expandMentions(action.status))); + map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status))); map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('privacy', action.status.get('visibility')); map.set('media_attachments', action.status.get('media_attachments')); diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 86041da20..bb333d35f 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -61,9 +61,35 @@ padding: 10px; padding-top: 0; - .icon-button { - font-size: 14px; - font-weight: 500; + font-size: 14px; + font-weight: 500; + + &.active { + color: $highlight-text-color; + } + + input[type=checkbox] { + display: none; + } + + .checkbox { + display: inline-block; + position: relative; + border: 1px solid $ui-primary-color; + box-sizing: border-box; + width: 18px; + height: 18px; + flex: 0 0 auto; + margin-left: 5px; + margin-right: 10px; + top: -1px; + border-radius: 4px; + vertical-align: middle; + + &.active { + border-color: $highlight-text-color; + background: $highlight-text-color; + } } } diff --git a/app/javascript/flavours/glitch/styles/contrast/diff.scss b/app/javascript/flavours/glitch/styles/contrast/diff.scss index 8429103b8..f78e60597 100644 --- a/app/javascript/flavours/glitch/styles/contrast/diff.scss +++ b/app/javascript/flavours/glitch/styles/contrast/diff.scss @@ -67,3 +67,11 @@ text-decoration: none; } } + +.nothing-here { + color: $darker-text-color; +} + +.public-layout .public-account-header__tabs__tabs .counter.active::after { + border-bottom: 4px solid $ui-highlight-color; +} diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 315fd5782..50bb45e7c 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -118,11 +118,14 @@ text-decoration: underline; font-size: inherit; - &:hover, - &:focus, - &:active { + &:hover { text-decoration: none; } + + &:active, + &:focus { + background-color: rgba($dark-text-color, .1); + } } .button { diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 0ee663766..94062f2be 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -383,7 +383,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) { }; }; -export function selectComposeSuggestion(position, token, suggestion) { +export function selectComposeSuggestion(position, token, suggestion, path) { return (dispatch, getState) => { let completion, startPosition; @@ -405,6 +405,7 @@ export function selectComposeSuggestion(position, token, suggestion) { position: startPosition, token, completion, + path, }); }; }; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 1794538e2..3916b9ac1 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -131,10 +131,11 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status) { +export function redraft(status, raw_text) { return { type: REDRAFT, status, + raw_text, }; }; @@ -148,13 +149,13 @@ export function deleteStatus(id, router, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api(getState).delete(`/api/v1/statuses/${id}`).then(() => { + api(getState).delete(`/api/v1/statuses/${id}`).then(response => { evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); if (withRedraft) { - dispatch(redraft(status)); + dispatch(redraft(status, response.data.text)); if (!getState().getIn(['compose', 'mounted'])) { router.push('/statuses/new'); diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js new file mode 100644 index 000000000..bb8ab60db --- /dev/null +++ b/app/javascript/mastodon/components/autosuggest_input.js @@ -0,0 +1,229 @@ +import React from 'react'; +import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; +import AutosuggestEmoji from './autosuggest_emoji'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { isRtl } from '../rtl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import classNames from 'classnames'; +import { List as ImmutableList } from 'immutable'; + +const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { + let word; + + let left = str.slice(0, caretPosition).search(/\S+$/); + let right = str.slice(caretPosition).search(/\s/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) { + return [null, null]; + } + + word = word.trim().toLowerCase(); + + if (word.length > 0) { + return [left + 1, word]; + } else { + return [null, null]; + } +}; + +export default class AutosuggestInput extends ImmutablePureComponent { + + static propTypes = { + value: PropTypes.string, + suggestions: ImmutablePropTypes.list, + disabled: PropTypes.bool, + placeholder: PropTypes.string, + onSuggestionSelected: PropTypes.func.isRequired, + onSuggestionsClearRequested: PropTypes.func.isRequired, + onSuggestionsFetchRequested: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyUp: PropTypes.func, + onKeyDown: PropTypes.func, + autoFocus: PropTypes.bool, + className: PropTypes.string, + id: PropTypes.string, + searchTokens: PropTypes.list, + maxLength: PropTypes.number, + }; + + static defaultProps = { + autoFocus: true, + searchTokens: ImmutableList(['@', ':', '#']), + }; + + state = { + suggestionsHidden: true, + focused: false, + selectedSuggestion: 0, + lastToken: null, + tokenStart: 0, + }; + + onChange = (e) => { + const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens); + + if (token !== null && this.state.lastToken !== token) { + this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); + this.props.onSuggestionsFetchRequested(token); + } else if (token === null) { + this.setState({ lastToken: null }); + this.props.onSuggestionsClearRequested(); + } + + this.props.onChange(e); + } + + onKeyDown = (e) => { + const { suggestions, disabled } = this.props; + const { selectedSuggestion, suggestionsHidden } = this.state; + + if (disabled) { + e.preventDefault(); + return; + } + + if (e.which === 229 || e.isComposing) { + // Ignore key events during text composition + // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) + return; + } + + switch(e.key) { + case 'Escape': + if (suggestions.size === 0 || suggestionsHidden) { + document.querySelector('.ui').parentElement.focus(); + } else { + e.preventDefault(); + this.setState({ suggestionsHidden: true }); + } + + break; + case 'ArrowDown': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); + } + + break; + case 'ArrowUp': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); + } + + break; + case 'Enter': + case 'Tab': + // Select suggestion + if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + e.stopPropagation(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + } + + break; + } + + if (e.defaultPrevented || !this.props.onKeyDown) { + return; + } + + this.props.onKeyDown(e); + } + + onBlur = () => { + this.setState({ suggestionsHidden: true, focused: false }); + } + + onFocus = () => { + this.setState({ focused: true }); + } + + onSuggestionClick = (e) => { + const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); + e.preventDefault(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); + this.input.focus(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { + this.setState({ suggestionsHidden: false }); + } + } + + setInput = (c) => { + this.input = c; + } + + renderSuggestion = (suggestion, i) => { + const { selectedSuggestion } = this.state; + let inner, key; + + if (typeof suggestion === 'object') { + inner = <AutosuggestEmoji emoji={suggestion} />; + key = suggestion.id; + } else if (suggestion[0] === '#') { + inner = suggestion; + key = suggestion; + } else { + inner = <AutosuggestAccountContainer id={suggestion} />; + key = suggestion; + } + + return ( + <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}> + {inner} + </div> + ); + } + + render () { + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; + const { suggestionsHidden } = this.state; + const style = { direction: 'ltr' }; + + if (isRtl(value)) { + style.direction = 'rtl'; + } + + return ( + <div className='autosuggest-input'> + <label> + <span style={{ display: 'none' }}>{placeholder}</span> + + <input + type='text' + ref={this.setInput} + disabled={disabled} + placeholder={placeholder} + autoFocus={autoFocus} + value={value} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyUp={onKeyUp} + onFocus={this.onFocus} + onBlur={this.onBlur} + style={style} + aria-autocomplete='list' + id={id} + className={className} + maxLength={maxLength} + /> + </label> + + <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> + {suggestions.map(this.renderSuggestion)} + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index a4f5cf50c..f3fb7fa8b 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -55,7 +55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { }; state = { - suggestionsHidden: false, + suggestionsHidden: true, + focused: false, selectedSuggestion: 0, lastToken: null, tokenStart: 0, @@ -134,7 +135,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } onBlur = () => { - this.setState({ suggestionsHidden: true }); + this.setState({ suggestionsHidden: true, focused: false }); + } + + onFocus = () => { + this.setState({ focused: true }); } onSuggestionClick = (e) => { @@ -145,7 +150,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } } @@ -207,6 +212,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onChange={this.onChange} onKeyDown={this.onKeyDown} onKeyUp={onKeyUp} + onFocus={this.onFocus} onBlur={this.onBlur} onPaste={this.onPaste} style={style} diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 0fce674e2..86324b846 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -69,18 +69,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onModalReblog (status) { - dispatch(reblog(status)); + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else { + dispatch(reblog(status)); + } }, onReblog (status, e) { - if (status.get('reblogged')) { - dispatch(unreblog(status)); + if (e.shiftKey || !boostModal) { + this.onModalReblog(status); } else { - if (e.shiftKey || !boostModal) { - this.onModalReblog(status); - } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); - } + dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); } }, diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index 5643e6449..2609b96ff 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; import { autoPlayGif, displayMedia } from 'mastodon/initial_state'; import classNames from 'classnames'; import { decode } from 'blurhash'; @@ -88,8 +89,10 @@ export default class MediaItem extends ImmutablePureComponent { const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; const height = width; const status = attachment.get('status'); + const title = status.get('spoiler_text') || attachment.get('description'); let thumbnail = ''; + let icon; if (attachment.get('type') === 'unknown') { // Skip @@ -131,11 +134,20 @@ export default class MediaItem extends ImmutablePureComponent { ); } + if (!visible) { + icon = ( + <span className='account-gallery__item__icons'> + <Icon id='eye-slash' /> + </span> + ); + } + return ( <div className='account-gallery__item' style={{ width, height }}> - <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> {visible && thumbnail} + {!visible && icon} </a> </div> ); diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 03738f1de..7e8b38580 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; +import AutosuggestInput from '../../../components/autosuggest_input'; import PollButtonContainer from '../containers/poll_button_container'; import UploadButtonContainer from '../containers/upload_button_container'; import { defineMessages, injectIntl } from 'react-intl'; @@ -103,7 +104,11 @@ class ComposeForm extends ImmutablePureComponent { } onSuggestionSelected = (tokenStart, token, value) => { - this.props.onSuggestionSelected(tokenStart, token, value); + this.props.onSuggestionSelected(tokenStart, token, value, ['text']); + } + + onSpoilerSuggestionSelected = (tokenStart, token, value) => { + this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); } handleChangeSpoilerText = (e) => { @@ -136,7 +141,7 @@ class ComposeForm extends ImmutablePureComponent { this.autosuggestTextarea.textarea.focus(); } else if (this.props.spoiler !== prevProps.spoiler) { if (this.props.spoiler) { - this.spoilerText.focus(); + this.spoilerText.input.focus(); } else { this.autosuggestTextarea.textarea.focus(); } @@ -179,10 +184,21 @@ class ComposeForm extends ImmutablePureComponent { <ReplyIndicatorContainer /> <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> - <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} /> - </label> + <AutosuggestInput + placeholder={intl.formatMessage(messages.spoiler_placeholder)} + value={this.props.spoilerText} + onChange={this.handleChangeSpoilerText} + onKeyDown={this.handleKeyDown} + disabled={!this.props.spoiler} + ref={this.setSpoilerText} + suggestions={this.props.suggestions} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSpoilerSuggestionSelected} + searchTokens={[':']} + id='cw-spoiler-input' + className='spoiler-input__input' + /> </div> <div className='compose-form__autosuggest-wrapper'> diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js index 383e37eb6..211601d52 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from 'mastodon/components/icon_button'; import Icon from 'mastodon/components/icon'; +import AutosuggestInput from 'mastodon/components/autosuggest_input'; import classNames from 'classnames'; const messages = defineMessages({ @@ -27,6 +28,10 @@ class Option extends React.PureComponent { onChange: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired, onToggleMultiple: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + onClearSuggestions: PropTypes.func.isRequired, + onFetchSuggestions: PropTypes.func.isRequired, + onSuggestionSelected: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -38,12 +43,25 @@ class Option extends React.PureComponent { this.props.onRemove(this.props.index); }; + handleToggleMultiple = e => { this.props.onToggleMultiple(); e.preventDefault(); e.stopPropagation(); }; + onSuggestionsClearRequested = () => { + this.props.onClearSuggestions(); + } + + onSuggestionsFetchRequested = (token) => { + this.props.onFetchSuggestions(token); + } + + onSuggestionSelected = (tokenStart, token, value) => { + this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); + } + render () { const { isPollMultiple, title, index, intl } = this.props; @@ -57,12 +75,16 @@ class Option extends React.PureComponent { tabIndex='0' /> - <input - type='text' + <AutosuggestInput placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} maxLength={25} value={title} onChange={this.handleOptionTitleChange} + suggestions={this.props.suggestions} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSuggestionSelected} + searchTokens={[':']} /> </label> @@ -87,6 +109,10 @@ class PollForm extends ImmutablePureComponent { onAddOption: PropTypes.func.isRequired, onRemoveOption: PropTypes.func.isRequired, onChangeSettings: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + onClearSuggestions: PropTypes.func.isRequired, + onFetchSuggestions: PropTypes.func.isRequired, + onSuggestionSelected: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -103,7 +129,7 @@ class PollForm extends ImmutablePureComponent { }; render () { - const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props; + const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; if (!options) { return null; @@ -112,7 +138,7 @@ class PollForm extends ImmutablePureComponent { return ( <div className='compose-form__poll-wrapper'> <ul> - {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)} + {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)} </ul> <div className='poll__footer'> diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index f9f1fba36..93a468388 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(fetchComposeSuggestions(token)); }, - onSuggestionSelected (position, token, suggestion) { - dispatch(selectComposeSuggestion(position, token, suggestion)); + onSuggestionSelected (position, token, suggestion, path) { + dispatch(selectComposeSuggestion(position, token, suggestion, path)); }, onChangeSpoilerText (checked) { diff --git a/app/javascript/mastodon/features/compose/containers/poll_form_container.js b/app/javascript/mastodon/features/compose/containers/poll_form_container.js index da795a291..1401371d0 100644 --- a/app/javascript/mastodon/features/compose/containers/poll_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/poll_form_container.js @@ -1,8 +1,14 @@ import { connect } from 'react-redux'; import PollForm from '../components/poll_form'; import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose'; +import { + clearComposeSuggestions, + fetchComposeSuggestions, + selectComposeSuggestion, +} from '../../../actions/compose'; const mapStateToProps = state => ({ + suggestions: state.getIn(['compose', 'suggestions']), options: state.getIn(['compose', 'poll', 'options']), expiresIn: state.getIn(['compose', 'poll', 'expires_in']), isMultiple: state.getIn(['compose', 'poll', 'multiple']), @@ -24,6 +30,19 @@ const mapDispatchToProps = dispatch => ({ onChangeSettings(expiresIn, isMultiple) { dispatch(changePollSettings(expiresIn, isMultiple)); }, + + onClearSuggestions () { + dispatch(clearComposeSuggestions()); + }, + + onFetchSuggestions (token) { + dispatch(fetchComposeSuggestions(token)); + }, + + onSuggestionSelected (position, token, accountId, path) { + dispatch(selectComposeSuggestion(position, token, accountId, path)); + }, + }); export default connect(mapStateToProps, mapDispatchToProps)(PollForm); diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js index 50612b086..7073f76c2 100644 --- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { changeComposeSensitivity } from 'mastodon/actions/compose'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import Icon from 'mastodon/components/icon'; const messages = defineMessages({ marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, @@ -38,9 +37,19 @@ class SensitiveButton extends React.PureComponent { return ( <div className='compose-form__sensitive-button'> - <button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> - <Icon id='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> - </button> + <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <input + name='mark-sensitive' + type='checkbox' + checked={active} + onChange={onClick} + disabled={disabled} + /> + + <span className={classNames('checkbox', { active })} /> + + <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + </label> </div> ); } diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 920e93d40..4c39a60e7 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -11,6 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Icon from 'mastodon/components/icon'; const messages = defineMessages({ + cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, }); @@ -51,6 +52,7 @@ class BoostModal extends ImmutablePureComponent { render () { const { status, intl } = this.props; + const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; return ( <div className='modal-root__modal boost-modal'> @@ -76,7 +78,7 @@ class BoostModal extends ImmutablePureComponent { <div className='boost-modal__action-bar'> <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div> - <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} /> + <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} /> </div> </div> ); diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 18dd56d0d..0cafb1120 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -17,7 +17,7 @@ "account.hide_reblogs": "Amaga els impulsos de @{name}", "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}", "account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.", - "account.media": "Media", + "account.media": "Mèdia", "account.mention": "Esmentar @{name}", "account.moved_to": "{name} s'ha mogut a:", "account.mute": "Silencia @{name}", @@ -77,6 +77,7 @@ "compose_form.poll.remove_option": "Elimina aquesta opció", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Marcar mèdia com a sensible", "compose_form.sensitive.marked": "Mèdia marcat com a sensible", "compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible", "compose_form.spoiler.marked": "Text es ocult sota l'avís", @@ -85,7 +86,7 @@ "confirmation_modal.cancel": "Cancel·la", "confirmations.block.block_and_report": "Block & Report", "confirmations.block.confirm": "Bloca", - "confirmations.block.message": "Estàs segur que vols blocar {name}?", + "confirmations.block.message": "Estàs segur que vols bloquejar a {name}?", "confirmations.delete.confirm": "Suprimeix", "confirmations.delete.message": "Estàs segur que vols suprimir aquest estat?", "confirmations.delete_list.confirm": "Suprimeix", @@ -125,7 +126,7 @@ "empty_column.favourited_statuses": "Encara no tens cap toot favorit. Quan en tinguis, apareixerà aquí.", "empty_column.favourites": "Encara ningú ha marcat aquest toot com a favorit. Quan algú ho faci, apareixera aquí.", "empty_column.follow_requests": "Encara no teniu cap petició de seguiment. Quan rebeu una, apareixerà aquí.", - "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.", + "empty_column.hashtag": "Encara no hi ha res en aquesta etiqueta.", "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.", "empty_column.home.public_timeline": "la línia de temps pública", "empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres d'aquesta llista publiquin nous estats, apareixeran aquí.", @@ -209,6 +210,7 @@ "lightbox.close": "Tancar", "lightbox.next": "Següent", "lightbox.previous": "Anterior", + "lightbox.view_context": "Veure el context", "lists.account.add": "Afegir a la llista", "lists.account.remove": "Treure de la llista", "lists.delete": "Delete list", @@ -340,7 +342,6 @@ "status.reply": "Respondre", "status.replyAll": "Respondre al tema", "status.report": "Informar sobre @{name}", - "status.sensitive_toggle": "Clic per veure", "status.sensitive_warning": "Contingut sensible", "status.share": "Compartir", "status.show_less": "Mostra menys", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index 016be39b3..335706af7 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -77,6 +77,7 @@ "compose_form.poll.remove_option": "Toglie sta scelta", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Indicà u media cum'è sensibile", "compose_form.sensitive.marked": "Media indicatu cum'è sensibile", "compose_form.sensitive.unmarked": "Media micca indicatu cum'è sensibile", "compose_form.spoiler.marked": "Testu piattatu daret'à un'avertimentu", @@ -209,6 +210,7 @@ "lightbox.close": "Chjudà", "lightbox.next": "Siguente", "lightbox.previous": "Pricidente", + "lightbox.view_context": "Vede u cuntestu", "lists.account.add": "Aghjunghje à a lista", "lists.account.remove": "Toglie di a lista", "lists.delete": "Supprime a lista", @@ -340,7 +342,6 @@ "status.reply": "Risponde", "status.replyAll": "Risponde à tutti", "status.report": "Palisà @{name}", - "status.sensitive_toggle": "Cliccate per vede", "status.sensitive_warning": "Cuntinutu sensibile", "status.share": "Sparte", "status.show_less": "Ripiegà", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 76d4351d0..8e7ed4210 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -180,10 +180,6 @@ { "defaultMessage": "Media hidden", "id": "status.media_hidden" - }, - { - "defaultMessage": "Click to view", - "id": "status.sensitive_toggle" } ], "path": "app/javascript/mastodon/components/media_gallery.json" @@ -1096,6 +1092,10 @@ { "defaultMessage": "Media is not marked as sensitive", "id": "compose_form.sensitive.unmarked" + }, + { + "defaultMessage": "Mark media as sensitive", + "id": "compose_form.sensitive.hide" } ], "path": "app/javascript/mastodon/features/compose/containers/sensitive_button_container.json" @@ -2262,6 +2262,10 @@ { "defaultMessage": "Next", "id": "lightbox.next" + }, + { + "defaultMessage": "View context", + "id": "lightbox.view_context" } ], "path": "app/javascript/mastodon/features/ui/components/media_modal.json" @@ -2357,6 +2361,15 @@ { "descriptors": [ { + "defaultMessage": "View context", + "id": "lightbox.view_context" + } + ], + "path": "app/javascript/mastodon/features/ui/components/video_modal.json" + }, + { + "descriptors": [ + { "defaultMessage": "Your draft will be lost if you leave Mastodon.", "id": "ui.beforeunload" } @@ -2408,10 +2421,6 @@ { "defaultMessage": "Media hidden", "id": "status.media_hidden" - }, - { - "defaultMessage": "Click to view", - "id": "status.sensitive_toggle" } ], "path": "app/javascript/mastodon/features/video/index.json" diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 79e0c504b..b1eb814fc 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -81,6 +81,7 @@ "compose_form.poll.remove_option": "Remove this choice", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Mark media as sensitive", "compose_form.sensitive.marked": "Media is marked as sensitive", "compose_form.sensitive.unmarked": "Media is not marked as sensitive", "compose_form.spoiler.marked": "Text is hidden behind warning", @@ -213,6 +214,7 @@ "lightbox.close": "Close", "lightbox.next": "Next", "lightbox.previous": "Previous", + "lightbox.view_context": "View context", "lists.account.add": "Add to list", "lists.account.remove": "Remove from list", "lists.delete": "Delete list", @@ -345,7 +347,6 @@ "status.reply": "Reply", "status.replyAll": "Reply to thread", "status.report": "Report @{name}", - "status.sensitive_toggle": "Click to view", "status.sensitive_warning": "Sensitive content", "status.share": "Share", "status.show_less": "Show less", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 58f3ce147..090f15bea 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -77,6 +77,7 @@ "compose_form.poll.remove_option": "Supprimer ce choix", "compose_form.publish": "Pouet", "compose_form.publish_loud": "{publish} !", + "compose_form.sensitive.hide": "Marquer le média comme sensible", "compose_form.sensitive.marked": "Média marqué comme sensible", "compose_form.sensitive.unmarked": "Média non marqué comme sensible", "compose_form.spoiler.marked": "Le texte est caché derrière un avertissement", @@ -209,6 +210,7 @@ "lightbox.close": "Fermer", "lightbox.next": "Suivant", "lightbox.previous": "Précédent", + "lightbox.view_context": "Voir le contexte", "lists.account.add": "Ajouter à la liste", "lists.account.remove": "Supprimer de la liste", "lists.delete": "Effacer la liste", @@ -340,7 +342,6 @@ "status.reply": "Répondre", "status.replyAll": "Répondre au fil", "status.report": "Signaler @{name}", - "status.sensitive_toggle": "Cliquer pour afficher", "status.sensitive_warning": "Contenu sensible", "status.share": "Partager", "status.show_less": "Replier", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 0bf9a22c4..509d0c2e8 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -81,6 +81,7 @@ "compose_form.poll.remove_option": "この項目を削除", "compose_form.publish": "トゥート", "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "メディアを閲覧注意にする", "compose_form.sensitive.marked": "メディアに閲覧注意が設定されています", "compose_form.sensitive.unmarked": "メディアに閲覧注意が設定されていません", "compose_form.spoiler.marked": "閲覧注意が設定されています", @@ -213,6 +214,7 @@ "lightbox.close": "閉じる", "lightbox.next": "次", "lightbox.previous": "前", + "lightbox.view_context": "トゥートを表示", "lists.account.add": "リストに追加", "lists.account.remove": "リストから外す", "lists.delete": "リストを削除", @@ -345,7 +347,6 @@ "status.reply": "返信", "status.replyAll": "全員に返信", "status.report": "@{name}さんを通報", - "status.sensitive_toggle": "クリックして表示", "status.sensitive_warning": "閲覧注意", "status.share": "共有", "status.show_less": "隠す", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 96e39356b..497a11d5c 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -77,6 +77,7 @@ "compose_form.poll.remove_option": "Deze keuze verwijderen", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Media als gevoelig markeren", "compose_form.sensitive.marked": "Media is als gevoelig gemarkeerd", "compose_form.sensitive.unmarked": "Media is niet als gevoelig gemarkeerd", "compose_form.spoiler.marked": "Tekst is achter een waarschuwing verborgen", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index b45def281..85cbdfb17 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -131,13 +131,15 @@ function removeMedia(state, mediaId) { }); }; -const insertSuggestion = (state, position, token, completion) => { +const insertSuggestion = (state, position, token, completion, path) => { return state.withMutations(map => { - map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); + map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); map.set('suggestion_token', null); - map.update('suggestions', ImmutableList(), list => list.clear()); - map.set('focusDate', new Date()); - map.set('caretPosition', position + completion.length + 1); + map.set('suggestions', ImmutableList()); + if (path.length === 1 && path[0] === 'text') { + map.set('focusDate', new Date()); + map.set('caretPosition', position + completion.length + 1); + } map.set('idempotencyKey', uuid()); }); }; @@ -304,7 +306,7 @@ export default function compose(state = initialState, action) { case COMPOSE_SUGGESTIONS_READY: return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: - return insertSuggestion(state, action.position, action.token, action.completion); + return insertSuggestion(state, action.position, action.token, action.completion, action.path); case COMPOSE_SUGGESTION_TAGS_UPDATE: return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: @@ -329,7 +331,7 @@ export default function compose(state = initialState, action) { })); case REDRAFT: return state.withMutations(map => { - map.set('text', unescapeHTML(expandMentions(action.status))); + map.set('text', action.raw_content || unescapeHTML(expandMentions(action.status))); map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('privacy', action.status.get('visibility')); map.set('media_attachments', action.status.get('media_attachments')); diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index 8429103b8..f78e60597 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -67,3 +67,11 @@ text-decoration: none; } } + +.nothing-here { + color: $darker-text-color; +} + +.public-layout .public-account-header__tabs__tabs .counter.active::after { + border-bottom: 4px solid $ui-highlight-color; +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1ba6fa5b1..f8f64bdd6 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -268,9 +268,34 @@ padding: 10px; padding-top: 0; - .icon-button { - font-size: 14px; - font-weight: 500; + font-size: 14px; + font-weight: 500; + + &.active { + color: $highlight-text-color; + } + + input[type=checkbox] { + display: none; + } + + .checkbox { + display: inline-block; + position: relative; + border: 1px solid $ui-primary-color; + box-sizing: border-box; + width: 18px; + height: 18px; + flex: 0 0 auto; + margin-right: 10px; + top: -1px; + border-radius: 4px; + vertical-align: middle; + + &.active { + border-color: $highlight-text-color; + background: $highlight-text-color; + } } } @@ -319,6 +344,7 @@ } .autosuggest-textarea, + .autosuggest-input, .spoiler-input { position: relative; } @@ -4829,6 +4855,14 @@ a.status-card.compact:hover { border-radius: 4px; overflow: hidden; margin: 2px; + + &__icons { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + } } .notification__filter-bar, diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index d8bc5473a..0d55afda4 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -37,11 +37,14 @@ display: none; } + .autossugest-input { + flex: 1 1 auto; + } + input[type=text] { display: block; box-sizing: border-box; - flex: 1 1 auto; - width: 20px; + width: 100%; font-size: 14px; color: $inverted-text-color; display: block; @@ -64,6 +67,7 @@ &.editable { display: flex; align-items: center; + overflow: visible; } } @@ -114,11 +118,14 @@ text-decoration: underline; font-size: inherit; - &:hover, - &:focus, - &:active { + &:hover { text-decoration: none; } + + &:active, + &:focus { + background-color: rgba($dark-text-color, .1); + } } .button { diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 892bb9974..595291342 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -65,7 +65,14 @@ class ActivityPub::TagManager when 'unlisted', 'private' [account_followers_url(status.account)] when 'direct', 'limited' - status.active_mentions.map { |mention| uri_for(mention.account) } + if status.account.silenced? + # Only notify followers if the account is locally silenced + account_ids = status.active_mentions.pluck(:account_id) + to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) } + to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) + else + status.active_mentions.map { |mention| uri_for(mention.account) } + end end end @@ -86,7 +93,16 @@ class ActivityPub::TagManager cc << COLLECTIONS[:public] end - cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility? || status.limited_visibility? + unless status.direct_visibility? || status.limited_visibility? + if status.account.silenced? + # Only notify followers if the account is locally silenced + account_ids = status.active_mentions.pluck(:account_id) + cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) }) + cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) + else + cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) + end + end cc end diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb index 898728067..933dfdaca 100644 --- a/app/models/form/status_batch.rb +++ b/app/models/form/status_batch.rb @@ -35,6 +35,7 @@ class Form::StatusBatch def delete_statuses Status.where(id: status_ids).reorder(nil).find_each do |status| RemovalWorker.perform_async(status.id) + Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) log_action :destroy, status end diff --git a/app/models/tombstone.rb b/app/models/tombstone.rb index 997bb65fd..bf666c43a 100644 --- a/app/models/tombstone.rb +++ b/app/models/tombstone.rb @@ -4,11 +4,12 @@ # # Table name: tombstones # -# id :bigint(8) not null, primary key -# account_id :bigint(8) -# uri :string not null -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# uri :string not null +# created_at :datetime not null +# updated_at :datetime not null +# by_moderator :boolean # class Tombstone < ApplicationRecord diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index a7b797368..906f489db 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -3,7 +3,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility, :language, - :uri, :content, :url, :replies_count, :reblogs_count, + :uri, :url, :replies_count, :reblogs_count, :favourites_count attribute :favourited, if: :current_user? @@ -13,6 +13,9 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :pinned, if: :pinnable? attribute :local_only if :local? + attribute :content, unless: :source_requested? + attribute :text, if: :source_requested? + belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application, if: :show_application? belongs_to :account, serializer: REST::AccountSerializer @@ -115,6 +118,10 @@ class REST::StatusSerializer < ActiveModel::Serializer %w(public unlisted).include?(object.visibility) end + def source_requested? + instance_options[:source_requested] + end + def ordered_mentions object.active_mentions.to_a.sort_by(&:id) end diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml index f059814bd..a545e189e 100644 --- a/app/views/admin/action_logs/_action_log.html.haml +++ b/app/views/admin/action_logs/_action_log.html.haml @@ -6,7 +6,7 @@ .log-entry__title = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe .log-entry__timestamp - %time= l action_log.created_at + %time.formatted{ datetime: action_log.created_at.iso8601 } .spacer .log-entry__icon = fa_icon icon_for_log(action_log) |