diff options
author | Thibaut Girka <thib@sitedethib.com> | 2018-05-23 16:05:19 +0200 |
---|---|---|
committer | Thibaut Girka <thib@sitedethib.com> | 2018-05-23 16:05:19 +0200 |
commit | e0ef89c73f6b380a2841b9dcde07725d515340a4 (patch) | |
tree | 3e96aef309d0ce02649fc115abb4eb4f3020beb2 | |
parent | 2289250f69e78a8888f8fc4db7ec24a1049c161e (diff) | |
parent | d8864b9e9d4fd56606a3b809bedeb1216b6620ae (diff) |
Merge branch 'master' into glitch-soc/merge-upstream
21 files changed, 295 insertions, 70 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 diff --git a/config/locales/en.yml b/config/locales/en.yml index a14b5b9d6..68d5f8358 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -283,7 +283,7 @@ en: create_and_resolve: Resolve with note create_and_unresolve: Reopen with note delete: Delete - placeholder: Describe what actions have been taken, or any other updates to this report… + placeholder: Describe what actions have been taken, or any other related updates... reopen: Reopen report report: 'Report #%{id}' report_contents: Contents @@ -559,7 +559,7 @@ en: subject: one: "1 new notification since your last visit \U0001F418" other: "%{count} new notifications since your last visit \U0001F418" - title: In your absence… + title: In your absence... favourite: body: 'Your status was favourited by %{name}:' subject: "%{name} favourited your status" diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 0f4565bbc..89aefc1cd 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -795,6 +795,7 @@ ru: themes: contrast: Высококонтрастная default: Mastodon + mastodon-light: Mastodon (светлая) time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index b12538b35..5b3b25ab1 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -13,7 +13,7 @@ zh-CN: note: 还能输入 <span class="note-counter">%{count}</span> 个字符 setting_hide_network: 你关注的人和关注你的人将不会在你的个人资料页上展示 setting_noindex: 此设置会影响到你的公开个人资料以及嘟文页面 - setting_theme: 此设置会影响到你从任意设备登录时 Mastodon 的显示样式 + setting_theme: 此设置会影响到所有已登录设备上 Mastodon 的显示样式 imports: data: 请上传从其他 Mastodon 实例导出的 CSV 文件 sessions: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index f7fc1a7a9..357575a3b 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -26,9 +26,9 @@ zh-CN: within_reach_title: 始终触手可及 generic_description: "%{domain} 是这个庞大网络中的一台服务器" hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例 - learn_more: 详细了解 + learn_more: 了解详情 other_instances: 其他实例 - source_code: 源码 + source_code: 源代码 status_count_after: 条嘟文 status_count_before: 他们共嘟出了 user_count_after: 位用户 @@ -281,7 +281,7 @@ zh-CN: create_and_resolve: 添加记录并标记为“已处理” create_and_unresolve: 添加记录并重开 delete: 删除 - placeholder: 描述已经执行的操作,或其他任何与此条举报相关的跟进情况 + placeholder: 描述已经执行的操作,或其他任何相关的跟进情况 reopen: 重开举报 report: '举报 #%{id}' report_contents: 内容 @@ -405,6 +405,9 @@ zh-CN: migrate_account_html: 如果你希望引导他人关注另一个帐户,请<a href="%{path}">点击这里进行设置</a>。 or: 或者 or_log_in_with: 或通过其他方式登录 + providers: + cas: CAS + saml: SAML register: 注册 register_elsewhere: 前往其他实例注册 resend_confirmation: 重新发送确认邮件 @@ -460,7 +463,7 @@ zh-CN: archive_takeout: date: 日期 download: 下载你的存档 - hint_html: 你可以请求一份包含你的<strong>嘟文和已上传的媒体文件</strong>的存档。导出的数据为 ActivityPub 格式,因而可以通过相应的软件读取。每次可以请求存档的间隔至少为 7 天。 + hint_html: 你可以请求一份帐户数据存档,其中包含你的<strong>嘟文和已上传的媒体文件</strong>。导出的数据为 ActivityPub 格式,因而可以被兼容的软件读取。每次允许请求存档的间隔至少为 7 天。 in_progress: 正在准备你的存档…… request: 请求你的存档 size: 大小 @@ -574,7 +577,9 @@ zh-CN: trillion: T unit: '' pagination: + newer: 更新 next: 下一页 + older: 更早 prev: 上一页 truncate: "…" preferences: @@ -599,12 +604,14 @@ zh-CN: blackberry: Blackberry chrome: Chrome edge: Microsoft Edge + electron: Electron firefox: Firefox generic: 未知浏览器 ie: Internet Explorer micro_messenger: 微信 nokia: Nokia S40 Ovi 浏览器 opera: Opera + otter: Otter phantom_js: PhantomJS qq: QQ浏览器 safari: Safari @@ -632,7 +639,7 @@ zh-CN: title: 会话 settings: authorized_apps: 已授权的应用 - back: 回到 Mastodon + back: 返回 Mastodon delete: 删除帐户 development: 开发 edit_profile: 更改个人资料 @@ -681,6 +688,7 @@ zh-CN: themes: contrast: 高对比度 default: Mastodon + mastodon-light: Mastodon(亮色主题) time: formats: default: "%Y年%-m月%d日 %H:%M" diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 73f7105af..9a7d49674 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -21,7 +21,7 @@ module Mastodon end def flags - 'rc5' + '' end def to_a diff --git a/spec/controllers/admin/report_notes_controller_spec.rb b/spec/controllers/admin/report_notes_controller_spec.rb new file mode 100644 index 000000000..2c32303fb --- /dev/null +++ b/spec/controllers/admin/report_notes_controller_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' + +describe Admin::ReportNotesController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + subject { post :create, params: params } + + let(:report) { Fabricate(:report, action_taken: action_taken, action_taken_by_account_id: account_id) } + + context 'when parameter is valid' do + + context 'when report is unsolved' do + let(:action_taken) { false } + let(:account_id) { nil } + + context 'when create_and_resolve flag is on' do + let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_resolve: nil } } + + it 'creates a report note and resolves report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).to be_action_taken + expect(subject).to redirect_to admin_reports_path + end + end + + context 'when create_and_resolve flag is false' do + let(:params) { { report_note: { content: 'test content', report_id: report.id } } } + + it 'creates a report note and does not resolve report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).not_to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + end + + context 'when report is resolved' do + let(:action_taken) { true } + let(:account_id) { user.account.id } + + context 'when create_and_unresolve flag is on' do + let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_unresolve: nil } } + + it 'creates a report note and unresolves report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).not_to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + + context 'when create_and_unresolve flag is false' do + let(:params) { { report_note: { content: 'test content', report_id: report.id } } } + + it 'creates a report note and does not unresolve report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + end + end + + context 'when parameter is invalid' do + let(:params) { { report_note: { content: '', report_id: report.id } } } + let(:action_taken) { false } + let(:account_id) { nil } + + it 'renders admin/reports/show' do + expect(subject).to render_template 'admin/reports/show' + end + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { id: report_note.id } } + + let!(:report_note) { Fabricate(:report_note) } + + it 'deletes note' do + expect{ subject }.to change{ ReportNote.count }.by(-1) + expect(subject).to redirect_to admin_report_path(report_note.report) + end + end +end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index c5c6cb651..9f5ab67c3 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -7,15 +7,21 @@ describe InvitesController do sign_in user end + around do |example| + min_invite_role = Setting.min_invite_role + example.run + Setting.min_invite_role = min_invite_role + end + describe 'GET #index' do subject { get :index } + let(:user) { Fabricate(:user, moderator: false, admin: false) } let!(:invite) { Fabricate(:invite, user: user) } context 'when user is a staff' do - let(:user) { Fabricate(:user, moderator: true, admin: false) } - it 'renders index page' do + Setting.min_invite_role = 'user' expect(subject).to render_template :index expect(assigns(:invites)).to include invite expect(assigns(:invites).count).to eq 1 @@ -23,9 +29,8 @@ describe InvitesController do end context 'when user is not a staff' do - let(:user) { Fabricate(:user, moderator: false, admin: false) } - it 'returns 403' do + Setting.min_invite_role = 'modelator' expect(subject).to have_http_status 403 end end diff --git a/spec/fabricators/report_note_fabricator.rb b/spec/fabricators/report_note_fabricator.rb new file mode 100644 index 000000000..e139efffb --- /dev/null +++ b/spec/fabricators/report_note_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:report_note) do + report + account { Fabricate(:account) } + content "Test Content" +end |