diff options
Diffstat (limited to 'app')
13 files changed, 172 insertions, 57 deletions
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 2e9f73bb8..70818610c 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -9,7 +9,7 @@ class InvitesController < ApplicationController before_action :set_pack def index - authorize :invite, :index? + authorize :invite, :create? @invites = invites @invite = Invite.new(expires_in: 1.day.to_i) diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 35a6f7f9e..03c890a50 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -6,7 +6,7 @@ class Settings::ApplicationsController < Settings::BaseController before_action :prepare_scopes, only: [:create, :update] def index - @applications = current_user.applications.page(params[:page]) + @applications = current_user.applications.order(id: :desc).page(params[:page]) end def new diff --git a/app/javascript/mastodon/actions/columns.js b/app/javascript/mastodon/actions/columns.js index bcb0cdf98..f550e6c48 100644 --- a/app/javascript/mastodon/actions/columns.js +++ b/app/javascript/mastodon/actions/columns.js @@ -1,8 +1,9 @@ import { saveSettings } from './settings'; -export const COLUMN_ADD = 'COLUMN_ADD'; -export const COLUMN_REMOVE = 'COLUMN_REMOVE'; -export const COLUMN_MOVE = 'COLUMN_MOVE'; +export const COLUMN_ADD = 'COLUMN_ADD'; +export const COLUMN_REMOVE = 'COLUMN_REMOVE'; +export const COLUMN_MOVE = 'COLUMN_MOVE'; +export const COLUMN_PARAMS_CHANGE = 'COLUMN_PARAMS_CHANGE'; export function addColumn(id, params) { return dispatch => { @@ -38,3 +39,15 @@ export function moveColumn(uuid, direction) { dispatch(saveSettings()); }; }; + +export function changeColumnParams(uuid, params) { + return dispatch => { + dispatch({ + type: COLUMN_PARAMS_CHANGE, + uuid, + params, + }); + + dispatch(saveSettings()); + }; +} diff --git a/app/javascript/mastodon/features/community_timeline/components/section_headline.js b/app/javascript/mastodon/features/community_timeline/components/section_headline.js new file mode 100644 index 000000000..c7176d04b --- /dev/null +++ b/app/javascript/mastodon/features/community_timeline/components/section_headline.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { NavLink } from 'react-router-dom'; + +export default class SectionHeadline extends Component { + + static propTypes = { + timelineId: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + pinned: PropTypes.bool.isRequired, + onlyMedia: PropTypes.bool.isRequired, + onClick: PropTypes.func, + }; + + shouldComponentUpdate (nextProps) { + return ( + this.props.onlyMedia !== nextProps.onlyMedia || + this.props.pinned !== nextProps.pinned || + this.props.to !== nextProps.to || + this.props.timelineId !== nextProps.timelineId + ); + } + + handleClick = e => { + const { onClick } = this.props; + + if (typeof onClick === 'function') { + e.preventDefault(); + + onClick.call(this, e); + } + } + + render () { + const { timelineId, to, pinned, onlyMedia } = this.props; + + return ( + <div className={`${timelineId}-timeline__section-headline`}> + {pinned ? ( + <Fragment> + <a href={to} className={!onlyMedia ? 'active' : undefined} onClick={this.handleClick}> + <FormattedMessage id='timeline.posts' defaultMessage='Toots' /> + </a> + <a href={`${to}/media`} className={onlyMedia ? 'active' : undefined} onClick={this.handleClick}> + <FormattedMessage id='timeline.media' defaultMessage='Media' /> + </a> + </Fragment> + ) : ( + <Fragment> + <NavLink exact to={to} replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink> + <NavLink exact to={`${to}/media`} replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink> + </Fragment> + )} + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index b6489432f..58b8a8b73 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -1,14 +1,14 @@ import React from 'react'; import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { NavLink, Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { expandCommunityTimeline } from '../../actions/timelines'; -import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; +// import SectionHeadline from './components/section_headline'; import { connectCommunityStream } from '../../actions/streaming'; const messages = defineMessages({ @@ -62,6 +62,16 @@ export default class CommunityTimeline extends React.PureComponent { this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); } + componentDidUpdate (prevProps) { + if (prevProps.onlyMedia !== this.props.onlyMedia) { + const { dispatch, onlyMedia } = this.props; + + this.disconnect(); + dispatch(expandCommunityTimeline({ onlyMedia })); + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + } + componentWillUnmount () { if (this.disconnect) { this.disconnect(); @@ -79,21 +89,28 @@ export default class CommunityTimeline extends React.PureComponent { dispatch(expandCommunityTimeline({ maxId, onlyMedia })); } + handleHeadlineLinkClick = e => { + const { columnId, dispatch } = this.props; + const onlyMedia = /\/media$/.test(e.currentTarget.href); + + dispatch(changeColumnParams(columnId, { other: { onlyMedia } })); + } + render () { const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; - const headline = pinned ? ( - <div className='community-timeline__section-headline'> - <Link to='/timelines/public/local' replace className={!onlyMedia ? 'active' : undefined}><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></Link> - <Link to='/timelines/public/local/media' replace className={onlyMedia ? 'active' : undefined}><FormattedMessage id='timeline.media' defaultMessage='Media' /></Link> - </div> - ) : ( - <div className='community-timeline__section-headline'> - <NavLink exact to='/timelines/public/local' replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink> - <NavLink exact to='/timelines/public/local/media' replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink> - </div> - ); + // pending + // + // const headline = ( + // <SectionHeadline + // timelineId='community' + // to='/timelines/public/local' + // pinned={pinned} + // onlyMedia={onlyMedia} + // onClick={this.handleHeadlineLinkClick} + // /> + // ); return ( <Column ref={this.setRef}> @@ -111,7 +128,7 @@ export default class CommunityTimeline extends React.PureComponent { </ColumnHeader> <StatusListContainer - prepend={headline} + // prepend={headline} trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId={`community${onlyMedia ? ':media' : ''}`} diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 39eb02362..6cc594c88 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -40,6 +40,7 @@ export default class ComposeForm extends ImmutablePureComponent { privacy: PropTypes.string, spoiler_text: PropTypes.string, focusDate: PropTypes.instanceOf(Date), + caretPosition: PropTypes.number, preselectDate: PropTypes.instanceOf(Date), is_submitting: PropTypes.bool, is_uploading: PropTypes.bool, @@ -96,7 +97,6 @@ export default class ComposeForm extends ImmutablePureComponent { } onSuggestionSelected = (tokenStart, token, value) => { - this._restoreCaret = null; this.props.onSuggestionSelected(tokenStart, token, value); } @@ -104,31 +104,21 @@ export default class ComposeForm extends ImmutablePureComponent { this.props.onChangeSpoilerText(e.target.value); } - componentWillReceiveProps (nextProps) { - // If this is the update where we've finished uploading, - // save the last caret position so we can restore it below! - if (!nextProps.is_uploading && this.props.is_uploading) { - this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; - } - } - componentDidUpdate (prevProps) { // This statement does several things: // - If we're beginning a reply, and, // - Replying to zero or one users, places the cursor at the end of the textbox. // - Replying to more than one user, selects any usernames past the first; // this provides a convenient shortcut to drop everyone else from the conversation. - // - If we've just finished uploading an image, and have a saved caret position, - // restores the cursor to that position after the text changes! - if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { + if (this.props.focusDate !== prevProps.focusDate) { let selectionEnd, selectionStart; if (this.props.preselectDate !== prevProps.preselectDate) { selectionEnd = this.props.text.length; selectionStart = this.props.text.search(/\s/) + 1; - } else if (typeof this._restoreCaret === 'number') { - selectionStart = this._restoreCaret; - selectionEnd = this._restoreCaret; + } else if (typeof this.props.caretPosition === 'number') { + selectionStart = this.props.caretPosition; + selectionEnd = this.props.caretPosition; } else { selectionEnd = this.props.text.length; selectionStart = selectionEnd; @@ -148,10 +138,8 @@ export default class ComposeForm extends ImmutablePureComponent { handleEmojiPick = (data) => { const { text } = this.props; const position = this.autosuggestTextarea.textarea.selectionStart; - const emojiChar = data.native; const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); - this._restoreCaret = position + emojiChar.length + 1 + (needsSpace ? 1 : 0); this.props.onPickEmoji(position, data, needsSpace); } 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 c3aa580ee..3822dd711 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -19,6 +19,7 @@ const mapStateToProps = state => ({ spoiler_text: state.getIn(['compose', 'spoiler_text']), privacy: state.getIn(['compose', 'privacy']), focusDate: state.getIn(['compose', 'focusDate']), + caretPosition: state.getIn(['compose', 'caretPosition']), preselectDate: state.getIn(['compose', 'preselectDate']), is_submitting: state.getIn(['compose', 'is_submitting']), is_uploading: state.getIn(['compose', 'is_uploading']), diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index da59b7167..2fce4d581 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -1,14 +1,14 @@ import React from 'react'; import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { NavLink, Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { expandPublicTimeline } from '../../actions/timelines'; -import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; +// import SectionHeadline from '../community_timeline/components/section_headline'; import { connectPublicStream } from '../../actions/streaming'; const messages = defineMessages({ @@ -62,6 +62,16 @@ export default class PublicTimeline extends React.PureComponent { this.disconnect = dispatch(connectPublicStream({ onlyMedia })); } + componentDidUpdate (prevProps) { + if (prevProps.onlyMedia !== this.props.onlyMedia) { + const { dispatch, onlyMedia } = this.props; + + this.disconnect(); + dispatch(expandPublicTimeline({ onlyMedia })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + } + } + componentWillUnmount () { if (this.disconnect) { this.disconnect(); @@ -79,21 +89,28 @@ export default class PublicTimeline extends React.PureComponent { dispatch(expandPublicTimeline({ maxId, onlyMedia })); } + handleHeadlineLinkClick = e => { + const { columnId, dispatch } = this.props; + const onlyMedia = /\/media$/.test(e.currentTarget.href); + + dispatch(changeColumnParams(columnId, { other: { onlyMedia } })); + } + render () { const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; - const headline = pinned ? ( - <div className='public-timeline__section-headline'> - <Link to='/timelines/public' replace className={!onlyMedia ? 'active' : undefined}><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></Link> - <Link to='/timelines/public/media' replace className={onlyMedia ? 'active' : undefined}><FormattedMessage id='timeline.media' defaultMessage='Media' /></Link> - </div> - ) : ( - <div className='public-timeline__section-headline'> - <NavLink exact to='/timelines/public' replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink> - <NavLink exact to='/timelines/public/media' replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink> - </div> - ); + // pending + // + // const headline = ( + // <SectionHeadline + // timelineId='public' + // to='/timelines/public' + // pinned={pinned} + // onlyMedia={onlyMedia} + // onClick={this.handleHeadlineLinkClick} + // /> + // ); return ( <Column ref={this.setRef}> @@ -111,7 +128,7 @@ export default class PublicTimeline extends React.PureComponent { </ColumnHeader> <StatusListContainer - prepend={headline} + // prepend={headline} timelineId={`public${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index b920bc19e..b67afd29f 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -39,7 +39,7 @@ "bundle_modal_error.close": "关闭", "bundle_modal_error.message": "载入这个组件时发生了错误。", "bundle_modal_error.retry": "重试", - "column.blocks": "屏蔽用户", + "column.blocks": "已屏蔽的用户", "column.community": "本站时间轴", "column.direct": "私信", "column.domain_blocks": "已屏蔽的网站", @@ -47,7 +47,7 @@ "column.follow_requests": "关注请求", "column.home": "主页", "column.lists": "列表", - "column.mutes": "被隐藏的用户", + "column.mutes": "已隐藏的用户", "column.notifications": "通知", "column.pins": "置顶嘟文", "column.public": "跨站公共时间轴", @@ -157,7 +157,7 @@ "missing_indicator.label": "找不到内容", "missing_indicator.sublabel": "无法找到此资源", "mute_modal.hide_notifications": "同时隐藏来自这个用户的通知", - "navigation_bar.blocks": "被屏蔽的用户", + "navigation_bar.blocks": "已屏蔽的用户", "navigation_bar.community_timeline": "本站时间轴", "navigation_bar.direct": "私信", "navigation_bar.domain_blocks": "已屏蔽的网站", @@ -168,7 +168,7 @@ "navigation_bar.keyboard_shortcuts": "快捷键列表", "navigation_bar.lists": "列表", "navigation_bar.logout": "注销", - "navigation_bar.mutes": "被隐藏的用户", + "navigation_bar.mutes": "已隐藏的用户", "navigation_bar.pins": "置顶嘟文", "navigation_bar.preferences": "首选项", "navigation_bar.public_timeline": "跨站公共时间轴", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 46d9d6c8e..62461d1a7 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -44,6 +44,7 @@ const initialState = ImmutableMap({ privacy: null, text: '', focusDate: null, + caretPosition: null, preselectDate: null, in_reply_to: null, is_composing: false, @@ -91,7 +92,6 @@ function appendMedia(state, media) { map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); - map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { @@ -119,6 +119,7 @@ const insertSuggestion = (state, position, token, completion) => { 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('idempotencyKey', uuid()); }); }; @@ -142,6 +143,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => { return state.merge({ text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`, focusDate: new Date(), + caretPosition: position + emoji.length + 1, idempotencyKey: uuid(), }); }; @@ -216,6 +218,7 @@ export default function compose(state = initialState, action) { map.set('text', statusToTextMentions(state, action.status)); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); @@ -259,6 +262,7 @@ export default function compose(state = initialState, action) { return state.withMutations(map => { map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('idempotencyKey', uuid()); }); case COMPOSE_DIRECT: @@ -266,6 +270,7 @@ export default function compose(state = initialState, action) { map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); map.set('privacy', 'direct'); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('idempotencyKey', uuid()); }); case COMPOSE_SUGGESTIONS_CLEAR: diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 9ec52a7fa..de8865e43 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -1,5 +1,5 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'; -import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; +import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns'; import { STORE_HYDRATE } from '../actions/store'; import { EMOJI_USE } from '../actions/emojis'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; @@ -89,6 +89,17 @@ const moveColumn = (state, uuid, direction) => { .set('saved', false); }; +const changeColumnParams = (state, uuid, params) => { + const columns = state.get('columns'); + const index = columns.findIndex(item => item.get('uuid') === uuid); + + const newColumns = columns.update(index, column => column.update('params', () => fromJS(params))); + + return state + .set('columns', newColumns) + .set('saved', false); +}; + const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false); const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId)); @@ -111,6 +122,8 @@ export default function settings(state = initialState, action) { .set('saved', false); case COLUMN_MOVE: return moveColumn(state, action.uuid, action.direction); + case COLUMN_PARAMS_CHANGE: + return changeColumnParams(state, action.uuid, action.params); case EMOJI_USE: return updateFrequentEmojis(state, action.emoji); case SETTING_SAVE: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2967c2d68..f067898bc 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1984,6 +1984,7 @@ a.account__display-name { padding: 15px; margin: 0; z-index: 3; + outline: 0; &:hover { text-decoration: underline; diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb index f3c4ffc47..43ba4d92a 100644 --- a/app/serializers/web/notification_serializer.rb +++ b/app/serializers/web/notification_serializer.rb @@ -33,6 +33,7 @@ class Web::NotificationSerializer < ActiveModel::Serializer end def body - truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140) + str = truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140) + HTMLEntities.new.decode(str.to_str) # Do not encode entities, since this value will not be used in HTML end end |