From 5f0d3e8bad361acf80633479c89b52efddc14a27 Mon Sep 17 00:00:00 2001 From: Isatis <515462+Reverite@users.noreply.github.com> Date: Sat, 15 Dec 2018 20:50:09 -0800 Subject: [PATCH 01/23] Dockerfile: Nodejs 8.12 -> 8.14 (#9532) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9c53b4145..11fc17d36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8.12.0-alpine as node +FROM node:8.14.0-alpine as node FROM ruby:2.4.5-alpine3.8 LABEL maintainer="https://github.com/tootsuite/mastodon" \ From 13dce126655f856f23d02373fa2e333e74bdc36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ngei?= Date: Sun, 16 Dec 2018 05:56:41 +0100 Subject: [PATCH 02/23] Add notification quick-filter bar in the frontend app (#9399) * create FilterBar componer and its container, unstyled * introduce basic styling for FilterBar * add selection css * allow FilterBar to display active CSS with js * connect the FilterBar to the Redux state * change getNotifications to use filter * remove temporary comments * add an option to turn the FilterBar off in settings * fix showFilterBar data type to boolean * fix eslint errors * add English and Polish translations * allowed filter bar overflow to accomodate for longer languages * fix mispelled translation key * add unified CSS look * replace text in FilterBar with icons * add tooltips * replace text @ with an icon * introduce simple and advanced filtering view * add ability to toggle the advanced view * add Polish translations * change Advanced View description to be more clear * make each filter flush notifications and load new ones, fixing pagination * simplify getNotifications once frontend filtering is not needed for FilterBar * add a semicolon * Revert "simplify getNotifications once frontend filtering is not needed for FilterBar" This reverts commit 9f4be7857135b0327814bd22a3e8a4e7b546f7cc. * reset filter to 'all' when turning off FilterBar --- .../mastodon/actions/notifications.js | 24 ++++- .../components/column_settings.js | 18 +++- .../notifications/components/filter_bar.js | 93 +++++++++++++++++++ .../containers/column_settings_container.js | 4 + .../containers/filter_bar_container.js | 16 ++++ .../mastodon/features/notifications/index.js | 23 ++++- app/javascript/mastodon/locales/en.json | 8 ++ app/javascript/mastodon/locales/pl.json | 8 ++ .../mastodon/reducers/notifications.js | 3 + app/javascript/mastodon/reducers/settings.js | 8 ++ .../styles/mastodon/components.scss | 46 +++++++++ 11 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications/components/filter_bar.js create mode 100644 app/javascript/mastodon/features/notifications/containers/filter_bar_container.js diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index d24f39ad2..4c145febc 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -8,6 +8,7 @@ import { importFetchedStatuses, } from './importer'; import { defineMessages } from 'react-intl'; +import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFilters, regexFromFilters } from '../selectors'; @@ -18,6 +19,8 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; +export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; + export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; @@ -88,10 +91,16 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); +const excludeTypesFromFilter = filter => { + const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']); + return allTypes.filterNot(item => item === filter).toJS(); +}; + const noOp = () => {}; export function expandNotifications({ maxId } = {}, done = noOp) { return (dispatch, getState) => { + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); const notifications = getState().get('notifications'); const isLoadingMore = !!maxId; @@ -102,7 +111,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) { const params = { max_id: maxId, - exclude_types: excludeTypesFromSettings(getState()), + exclude_types: activeFilter === 'all' + ? excludeTypesFromSettings(getState()) + : excludeTypesFromFilter(activeFilter), }; if (!maxId && notifications.get('items').size > 0) { @@ -167,3 +178,14 @@ export function scrollTopNotifications(top) { top, }; }; + +export function setFilter (filterType) { + return dispatch => { + dispatch({ + type: NOTIFICATIONS_FILTER_SET, + path: ['notifications', 'quickFilter', 'active'], + value: filterType, + }); + dispatch(expandNotifications()); + }; +}; diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index fcdf5c6e6..a334fd63c 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -21,9 +21,11 @@ export default class ColumnSettings extends React.PureComponent { render () { const { settings, pushSettings, onChange, onClear } = this.props; - const alertStr = ; - const showStr = ; - const soundStr = ; + const filterShowStr = ; + const filterAdvancedStr = ; + const alertStr = ; + const showStr = ; + const soundStr = ; const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); const pushStr = showPushSettings && ; @@ -34,6 +36,16 @@ export default class ColumnSettings extends React.PureComponent { +
+ + + +
+ + +
+
+
diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.js b/app/javascript/mastodon/features/notifications/components/filter_bar.js new file mode 100644 index 000000000..f95a2c9de --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +const tooltips = defineMessages({ + mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, + favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' }, + boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, + follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, +}); + +export default @injectIntl +class FilterBar extends React.PureComponent { + + static propTypes = { + selectFilter: PropTypes.func.isRequired, + selectedFilter: PropTypes.string.isRequired, + advancedMode: PropTypes.bool.isRequired, + intl: PropTypes.object.isRequired, + }; + + onClick (notificationType) { + return () => this.props.selectFilter(notificationType); + } + + render () { + const { selectedFilter, advancedMode, intl } = this.props; + const renderedElement = !advancedMode ? ( +
+ + +
+ ) : ( +
+ + + + + +
+ ); + return renderedElement; + } + +} diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index e9cef0a7b..a67f26295 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; import ColumnSettings from '../components/column_settings'; import { changeSetting } from '../../../actions/settings'; +import { setFilter } from '../../../actions/notifications'; import { clearNotifications } from '../../../actions/notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { openModal } from '../../../actions/modal'; @@ -21,6 +22,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onChange (path, checked) { if (path[0] === 'push') { dispatch(changePushNotifications(path.slice(1), checked)); + } else if (path[0] === 'quickFilter') { + dispatch(changeSetting(['notifications', ...path], checked)); + dispatch(setFilter('all')); } else { dispatch(changeSetting(['notifications', ...path], checked)); } diff --git a/app/javascript/mastodon/features/notifications/containers/filter_bar_container.js b/app/javascript/mastodon/features/notifications/containers/filter_bar_container.js new file mode 100644 index 000000000..4d495c290 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/containers/filter_bar_container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import FilterBar from '../components/filter_bar'; +import { setFilter } from '../../../actions/notifications'; + +const makeMapStateToProps = state => ({ + selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']), + advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']), +}); + +const mapDispatchToProps = (dispatch) => ({ + selectFilter (newActiveFilter) { + dispatch(setFilter(newActiveFilter)); + }, +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(FilterBar); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index aa82dbbb9..9430b2050 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from './containers/notification_container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; +import FilterBarContainer from './containers/filter_bar_container'; import { createSelector } from 'reselect'; import { List as ImmutableList } from 'immutable'; import { debounce } from 'lodash'; @@ -20,11 +21,22 @@ const messages = defineMessages({ }); const getNotifications = createSelector([ + state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']), + state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']), state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), state => state.getIn(['notifications', 'items']), -], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')))); +], (showFilterBar, allowedType, excludedTypes, notifications) => { + if (!showFilterBar || allowedType === 'all') { + // used if user changed the notification settings after loading the notifications from the server + // otherwise a list of notifications will come pre-filtered from the backend + // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category + return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); + } + return notifications.filter(item => item !== null && allowedType === item.get('type')); +}); const mapStateToProps = state => ({ + showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']), notifications: getNotifications(state), isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, @@ -38,6 +50,7 @@ class Notifications extends React.PureComponent { static propTypes = { columnId: PropTypes.string, notifications: ImmutablePropTypes.list.isRequired, + showFilterBar: PropTypes.bool.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, intl: PropTypes.object.isRequired, @@ -117,12 +130,16 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; const pinned = !!columnId; const emptyMessage = ; let scrollableContent = null; + const filterBarContainer = showFilterBar + ? () + : null; + if (isLoading && this.scrollableContent) { scrollableContent = this.scrollableContent; } else if (notifications.size > 0 || hasMore) { @@ -179,7 +196,7 @@ class Notifications extends React.PureComponent { > - + {filterBarContainer} {scrollContainer} ); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 9a15d84b7..414b9def3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -223,6 +223,14 @@ "notification.reblog": "{name} boosted your status", "notifications.clear": "Clear notifications", "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.filter.all": "All", + "notifications.filter.mentions": "Mentions", + "notifications.filter.favourites": "Favourites", + "notifications.filter.boosts": "Boosts", + "notifications.filter.follows": "Follows", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Display all categories", "notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index ae673cf9f..0589b06f5 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -223,6 +223,14 @@ "notification.reblog": "{name} podbił(a) Twój wpis", "notifications.clear": "Wyczyść powiadomienia", "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?", + "notifications.filter.all": "Wszystkie", + "notifications.filter.mentions": "Wspomnienia", + "notifications.filter.favourites": "Ulubione", + "notifications.filter.boosts": "Podbicia", + "notifications.filter.follows": "Śledzenia", + "notifications.column_settings.filter_bar.category": "Szybkie filtrowanie", + "notifications.column_settings.filter_bar.show": "Pokaż", + "notifications.column_settings.filter_bar.advanced": "Wyświetl wszystkie kategorie", "notifications.column_settings.alert": "Powiadomienia na pulpicie", "notifications.column_settings.favourite": "Dodanie do ulubionych:", "notifications.column_settings.follow": "Nowi śledzący:", diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index d71ae00ae..19a02f5b1 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -3,6 +3,7 @@ import { NOTIFICATIONS_EXPAND_SUCCESS, NOTIFICATIONS_EXPAND_REQUEST, NOTIFICATIONS_EXPAND_FAIL, + NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, } from '../actions/notifications'; @@ -98,6 +99,8 @@ export default function notifications(state = initialState, action) { return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); + case NOTIFICATIONS_FILTER_SET: + return state.set('items', ImmutableList()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 12bcc2583..2e1878cf7 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -1,4 +1,5 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'; +import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; 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'; @@ -32,6 +33,12 @@ const initialState = ImmutableMap({ mention: true, }), + quickFilter: ImmutableMap({ + active: 'all', + show: true, + advanced: false, + }), + shows: ImmutableMap({ follow: true, favourite: true, @@ -112,6 +119,7 @@ export default function settings(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: return hydrate(state, action.state.get('settings')); + case NOTIFICATIONS_FILTER_SET: case SETTING_CHANGE: return state .setIn(action.path, action.value) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c880e99a9..1c1b8c506 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1484,6 +1484,52 @@ a.account__display-name { } } +.notification__filter-bar { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + background: $ui-base-color; + + & > button { + position: relative; + flex-grow: 1; + color: $primary-text-color; + padding: 10px 5px 12px; + text-decoration: none; + font-weight: 400; + font-size: 15px; + line-height: 18px; + background: darken($ui-base-color, 4%); + border: 0; + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: default; + + &.active { + color: $secondary-text-color; + + &::before, + &::after { + display: block; + content: ""; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + transform: translateX(-50%); + border-style: solid; + border-width: 0 10px 10px; + border-color: transparent transparent lighten($ui-base-color, 8%); + } + + &::after { + bottom: -1px; + border-color: transparent transparent $ui-base-color; + } + } + } +} + .notification__message { margin: 0 10px 0 68px; padding: 8px 0 0; From 32d7d617031a3cbd20387a8f02278b4734651671 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 16 Dec 2018 21:17:15 +0100 Subject: [PATCH 03/23] Remove PostgreSQL statement timeout (#9537) Revert #9382 --- config/database.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/database.yml b/config/database.yml index 90133881a..82e560515 100644 --- a/config/database.yml +++ b/config/database.yml @@ -3,8 +3,6 @@ default: &default pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %> timeout: 5000 encoding: unicode - variables: - statement_timeout: 60000 development: <<: *default From 4297de34cfe705622d53d9688c1ad9abb24ced76 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 16 Dec 2018 21:17:56 +0100 Subject: [PATCH 04/23] Split out is_changing_upload from is_submitting (#9536) There is no reason to disable the composer textarea when some media metadata is being modified, nor is there any reason to focus the textarea when some media metadata has been modified (prevents clicking one image's description field right after having modified another). --- .../features/compose/components/compose_form.js | 7 ++++--- .../compose/containers/compose_form_container.js | 1 + app/javascript/mastodon/reducers/compose.js | 10 +++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 0625ab223..ac458fd25 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -46,6 +46,7 @@ class ComposeForm extends ImmutablePureComponent { caretPosition: PropTypes.number, preselectDate: PropTypes.instanceOf(Date), is_submitting: PropTypes.bool, + is_changing_upload: PropTypes.bool, is_uploading: PropTypes.bool, onChange: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, @@ -81,10 +82,10 @@ class ComposeForm extends ImmutablePureComponent { } // Submit disabled: - const { is_submitting, is_uploading, anyMedia } = this.props; + const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props; const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join(''); - if (is_submitting || is_uploading || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) { + if (is_submitting || is_uploading || is_changing_upload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) { return; } @@ -160,7 +161,7 @@ class ComposeForm extends ImmutablePureComponent { const { intl, onPaste, showSearch, anyMedia } = this.props; const disabled = this.props.is_submitting; const text = [this.props.spoiler_text, countableText(this.props.text)].join(''); - const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia); + const disabledButton = disabled || this.props.is_uploading || this.props.is_changing_upload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia); let publishText = ''; if (this.props.privacy === 'private' || this.props.privacy === 'direct') { 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 5d7fb8852..b4a1c4b44 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -22,6 +22,7 @@ const mapStateToProps = state => ({ caretPosition: state.getIn(['compose', 'caretPosition']), preselectDate: state.getIn(['compose', 'preselectDate']), is_submitting: state.getIn(['compose', 'is_submitting']), + is_changing_upload: state.getIn(['compose', 'is_changing_upload']), is_uploading: state.getIn(['compose', 'is_uploading']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 67d55f66f..1622871b8 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -51,6 +51,7 @@ const initialState = ImmutableMap({ in_reply_to: null, is_composing: false, is_submitting: false, + is_changing_upload: false, is_uploading: false, progress: 0, media_attachments: ImmutableList(), @@ -79,6 +80,7 @@ function clearAll(state) { map.set('spoiler', false); map.set('spoiler_text', ''); map.set('is_submitting', false); + map.set('is_changing_upload', false); map.set('in_reply_to', null); map.set('privacy', state.get('default_privacy')); map.set('sensitive', false); @@ -248,13 +250,15 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); }); case COMPOSE_SUBMIT_REQUEST: - case COMPOSE_UPLOAD_CHANGE_REQUEST: return state.set('is_submitting', true); + case COMPOSE_UPLOAD_CHANGE_REQUEST: + return state.set('is_changing_upload', true); case COMPOSE_SUBMIT_SUCCESS: return clearAll(state); case COMPOSE_SUBMIT_FAIL: - case COMPOSE_UPLOAD_CHANGE_FAIL: return state.set('is_submitting', false); + case COMPOSE_UPLOAD_CHANGE_FAIL: + return state.set('is_changing_upload', false); case COMPOSE_UPLOAD_REQUEST: return state.set('is_uploading', true); case COMPOSE_UPLOAD_SUCCESS: @@ -300,7 +304,7 @@ export default function compose(state = initialState, action) { return insertEmoji(state, action.position, action.emoji, action.needsSpace); case COMPOSE_UPLOAD_CHANGE_SUCCESS: return state - .set('is_submitting', false) + .set('is_changing_upload', false) .update('media_attachments', list => list.map(item => { if (item.get('id') === action.media.id) { return fromJS(action.media); From 628da11e38b0580a074268f32d09791ed6278def Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 17 Dec 2018 03:14:13 +0100 Subject: [PATCH 05/23] Do no retry web push workers if the server returns a 4xx response (#9434) Add timeout of 10s to web push requests --- app/models/web/push_subscription.rb | 3 +++ app/workers/web/push_notification_worker.rb | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index d19b20c48..b57807d1c 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -68,6 +68,9 @@ class Web::PushSubscription < ApplicationRecord p256dh: key_p256dh, auth: key_auth, ttl: ttl, + ssl_timeout: 10, + open_timeout: 10, + read_timeout: 10, vapid: { subject: "mailto:#{::Setting.site_contact_email}", private_key: Rails.configuration.x.vapid_private_key, diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index 4a40e5c8b..8e8a35973 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -10,8 +10,8 @@ class Web::PushNotificationWorker notification = Notification.find(notification_id) subscription.push(notification) unless notification.activity.nil? - rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription - subscription.destroy! + rescue Webpush::ResponseError => e + subscription.destroy! if (400..499).cover?(e.response.code.to_i) rescue ActiveRecord::RecordNotFound true end From 087e11897137dc1f2811c21c3ccc6cec3ccdedb3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 17 Dec 2018 03:14:28 +0100 Subject: [PATCH 06/23] Remove "most popular" tab from profile directory, add responsive design (#9539) * Remove "most popular" tab from profile directory, add responsive design * Remove unused translations --- app/controllers/directories_controller.rb | 12 +----- .../styles/mastodon/containers.scss | 6 +++ app/javascript/styles/mastodon/widgets.scss | 43 +++++++++++++------ app/models/account.rb | 3 +- app/views/directories/index.html.haml | 8 +--- app/views/layouts/public.html.haml | 6 +-- config/locales/ar.yml | 1 - config/locales/co.yml | 2 - config/locales/cs.yml | 2 - config/locales/el.yml | 2 - config/locales/en.yml | 2 - config/locales/eu.yml | 2 - config/locales/fr.yml | 2 - config/locales/gl.yml | 2 - config/locales/ja.yml | 2 - config/locales/nl.yml | 2 - config/locales/oc.yml | 2 - config/locales/pl.yml | 2 - config/locales/sk.yml | 2 - config/routes.rb | 2 - 20 files changed, 43 insertions(+), 62 deletions(-) diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index b8565af4b..df012657a 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -32,22 +32,12 @@ class DirectoriesController < ApplicationController end def set_accounts - @accounts = Account.searchable.discoverable.page(params[:page]).per(50).tap do |query| + @accounts = Account.discoverable.page(params[:page]).per(30).tap do |query| query.merge!(Account.tagged_with(@tag.id)) if @tag - - if popular_requested? - query.merge!(Account.popular) - else - query.merge!(Account.by_recent_status) - end end end def set_instance_presenter @instance_presenter = InstancePresenter.new end - - def popular_requested? - request.path.ends_with?('/popular') - end end diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 44fc1e538..8de53ca98 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -294,6 +294,12 @@ text-decoration: underline; color: $primary-text-color; } + + @media screen and (max-width: $no-gap-breakpoint) { + &.optional { + display: none; + } + } } .nav-button { diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index c863e3b4f..87e633c70 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -229,18 +229,6 @@ margin-bottom: 10px; } -.moved-account-widget, -.memoriam-widget, -.box-widget, -.contact-widget, -.landing-page__information.contact-widget { - @media screen and (max-width: $no-gap-breakpoint) { - margin-bottom: 0; - box-shadow: none; - border-radius: 0; - } -} - .page-header { background: lighten($ui-base-color, 8%); box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); @@ -261,11 +249,20 @@ font-size: 15px; color: $darker-text-color; } + + @media screen and (max-width: $no-gap-breakpoint) { + margin-top: 0; + background: lighten($ui-base-color, 4%); + + h1 { + font-size: 24px; + } + } } .directory { background: $ui-base-color; - border-radius: 0 0 4px 4px; + border-radius: 4px; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); &__tag { @@ -407,4 +404,24 @@ font-size: 14px; } } + + @media screen and (max-width: $no-gap-breakpoint) { + tbody td.optional { + display: none; + } + } +} + +.moved-account-widget, +.memoriam-widget, +.box-widget, +.contact-widget, +.landing-page__information.contact-widget, +.directory, +.page-header { + @media screen and (max-width: $no-gap-breakpoint) { + margin-bottom: 0; + box-shadow: none; + border-radius: 0; + } } diff --git a/app/models/account.rb b/app/models/account.rb index 9767e3767..a47741611 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -91,9 +91,8 @@ class Account < ApplicationRecord scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) } - scope :discoverable, -> { where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) } + scope :discoverable, -> { searchable.where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } - scope :popular, -> { order('account_stats.followers_count desc') } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } delegate :email, diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index f70eb964a..88706def7 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -16,10 +16,6 @@ .grid .column-0 - .account__section-headline - = active_link_to t('directories.most_recently_active'), @tag ? explore_hashtag_path(@tag) : explore_path - = active_link_to t('directories.most_popular'), @tag ? explore_hashtag_popular_path(@tag) : explore_popular_path - - if @accounts.empty? = nothing_here - else @@ -29,10 +25,10 @@ - @accounts.each do |account| %tr %td= account_link_to account - %td.accounts-table__count + %td.accounts-table__count.optional = number_to_human account.statuses_count, strip_insignificant_zeros: true %small= t('accounts.posts', count: account.statuses_count).downcase - %td.accounts-table__count + %td.accounts-table__count.optional = number_to_human account.followers_count, strip_insignificant_zeros: true %small= t('accounts.followers', count: account.followers_count).downcase %td.accounts-table__count diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 93ed12f18..caccd5bb6 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -10,9 +10,9 @@ = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - if Setting.profile_directory - = link_to t('directories.directory'), explore_path, class: 'nav-link' - = link_to t('about.about_this'), about_more_path, class: 'nav-link' - = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link' + = link_to t('directories.directory'), explore_path, class: 'nav-link optional' + = link_to t('about.about_this'), about_more_path, class: 'nav-link optional' + = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' .nav-center .nav-right - if user_signed_in? diff --git a/config/locales/ar.yml b/config/locales/ar.yml index eda99e24c..4de1e4e26 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -541,7 +541,6 @@ ar: warning_title: توافر المحتوى المنشور و المبعثَر directories: explore_mastodon: استكشف %{title} - most_popular: المشهورة errors: '403': ليس لك الصلاحيات الكافية لعرض هذه الصفحة. '404': إنّ الصفحة التي تبحث عنها لا وجود لها أصلا. diff --git a/config/locales/co.yml b/config/locales/co.yml index d2dcef9a4..80d2decd3 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -531,8 +531,6 @@ co: directory: Annuariu di i prufili explanation: Scopre utilizatori à partesi di i so centri d'interessu explore_mastodon: Scopre à %{title} - most_popular: I più pupulari - most_recently_active: Attività a più fresca people: one: "%{count} persona" other: "%{count} persone" diff --git a/config/locales/cs.yml b/config/locales/cs.yml index a5a3c0184..1bba55f0f 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -536,8 +536,6 @@ cs: directory: Adresář profilů explanation: Objevujte uživatele podle jejich zájmů explore_mastodon: Prozkoumejte %{title} - most_popular: Nejpopulárnější - most_recently_active: Naposledy aktivní people: few: "%{count} lidé" one: "%{count} člověk" diff --git a/config/locales/el.yml b/config/locales/el.yml index 342cad91c..9d41f353f 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -531,8 +531,6 @@ el: directory: Κατάλογος λογαριασμών explanation: Βρες χρήστες βάσει των ενδιαφερόντων τους explore_mastodon: Εξερεύνησε %{title} - most_popular: Δημοφιλείς - most_recently_active: Πρόσφατα ενεργοί people: one: "%{count} άτομο" other: "%{count} άτομα" diff --git a/config/locales/en.yml b/config/locales/en.yml index 314787acd..c8bfccdf7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -535,8 +535,6 @@ en: directory: Profile directory explanation: Discover users based on their interests explore_mastodon: Explore %{title} - most_popular: Most popular - most_recently_active: Most recently active people: one: "%{count} person" other: "%{count} people" diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 15307c76e..c96438bc3 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -531,8 +531,6 @@ eu: directory: Profilen direktorioa explanation: Deskubritu erabiltzaileak interesen arabera explore_mastodon: Esploratu %{title} - most_popular: Puri-purian - most_recently_active: Azkenaldian aktibo people: one: pertsona %{count} other: "%{count} pertsona" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index de3070e8a..c171d9342 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -531,8 +531,6 @@ fr: directory: Annuaire des profils explanation: Découvrir des utilisateurs en se basant sur leurs centres d'intérêt explore_mastodon: Explorer %{title} - most_popular: Les plus populaires - most_recently_active: Les actifs les plus récents people: one: "%{count} personne" other: "%{count} personne" diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 8f12587d6..5f4e420cb 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -531,8 +531,6 @@ gl: directory: Directorio de perfil explanation: Descubra usuarias según o seu interese explore_mastodon: Explorar %{title} - most_popular: Máis popular - most_recently_active: Máis activa recentemente people: one: "%{count} persoa" other: "%{count} persoas" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 50e9522bc..9c8d7f5b9 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -530,8 +530,6 @@ ja: directories: directory: ディレクトリ explore_mastodon: "%{title}を探索" - most_popular: 人気順 - most_recently_active: 直近の活動順 people: one: "%{count} 人" other: "%{count} 人" diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 30af6562a..b5229d241 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -531,8 +531,6 @@ nl: directory: Gebruikersgids explanation: Ontdek gebruikers aan de hand van hun interesses explore_mastodon: "%{title} verkennen" - most_popular: Meest populair - most_recently_active: Recentelijk actief people: one: "%{count} gebruikers" other: "%{count} gebruikers" diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 8fe3e350a..9015997fc 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -587,8 +587,6 @@ oc: directory: Annuari de perfils explanation: Trobar d’utilizaires segon lor interèsses explore_mastodon: Explorar %{title} - most_popular: Mai populars - most_recently_active: Mai actius recentament people: one: "%{count} persona" other: "%{count} personas" diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 4a0d65440..79ba6f9fb 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -541,8 +541,6 @@ pl: directory: Katalog profilów explanation: Poznaj profile na podstawie zainteresowań explore_mastodon: Odkrywaj %{title} - most_popular: Napopularniejsi - most_recently_active: Ostatnio aktywni people: few: "%{count} osoby" many: "%{count} osób" diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 5f49a2d0e..bea4ac334 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -536,8 +536,6 @@ sk: directory: Databáza profilov explanation: Pátraj po užívateľoch podľa ich záujmov explore_mastodon: Prebádaj %{title} - most_popular: Najpopulárnejšie - most_recently_active: Naposledy aktívni people: few: "%{count} ľudia" one: "%{count} človek" diff --git a/config/routes.rb b/config/routes.rb index 4a0289465..0aba433e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -81,9 +81,7 @@ Rails.application.routes.draw do post '/interact/:id', to: 'remote_interaction#create' get '/explore', to: 'directories#index', as: :explore - get '/explore/popular', to: 'directories#index', as: :explore_popular get '/explore/:id', to: 'directories#show', as: :explore_hashtag - get '/explore/:id/popular', to: 'directories#show', as: :explore_hashtag_popular namespace :settings do resource :profile, only: [:show, :update] From adaf249700a5384b817de12bc43ca67fcdc6f257 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 17 Dec 2018 04:32:36 +0100 Subject: [PATCH 07/23] Fix regression in #9539 (#9541) --- app/models/account.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/account.rb b/app/models/account.rb index a47741611..5a7a9c580 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -94,6 +94,7 @@ class Account < ApplicationRecord scope :discoverable, -> { searchable.where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } + scope :popular, -> { order('account_stats.followers_count desc') } delegate :email, :unconfirmed_email, From a3dcbfddd6869f6bdc28f348c07ba70a764b94cc Mon Sep 17 00:00:00 2001 From: ysksn Date: Mon, 17 Dec 2018 14:03:51 +0900 Subject: [PATCH 08/23] Add specs for Accounts::PinsController (#9542) --- .../api/v1/accounts/pins_controller_spec.rb | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/controllers/api/v1/accounts/pins_controller_spec.rb diff --git a/spec/controllers/api/v1/accounts/pins_controller_spec.rb b/spec/controllers/api/v1/accounts/pins_controller_spec.rb new file mode 100644 index 000000000..c71935df2 --- /dev/null +++ b/spec/controllers/api/v1/accounts/pins_controller_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::Accounts::PinsController, type: :controller do + let(:john) { Fabricate(:user, account: Fabricate(:account, username: 'john')) } + let(:kevin) { Fabricate(:user, account: Fabricate(:account, username: 'kevin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: john.id, scopes: 'write:accounts') } + + before do + kevin.account.followers << john.account + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + subject { post :create, params: { account_id: kevin.account.id } } + + it 'returns 200' do + expect(response).to have_http_status(200) + end + + it 'creates account_pin' do + expect do + subject + end.to change { AccountPin.where(account: john.account, target_account: kevin.account).count }.by(1) + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { account_id: kevin.account.id } } + + before do + Fabricate(:account_pin, account: john.account, target_account: kevin.account) + end + + it 'returns 200' do + expect(response).to have_http_status(200) + end + + it 'destroys account_pin' do + expect do + subject + end.to change { AccountPin.where(account: john.account, target_account: kevin.account).count }.by(-1) + end + end +end From bfd0ebf92593d048d16a3882ddf44f83fa28cee2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Dec 2018 10:15:14 +0100 Subject: [PATCH 09/23] Bump omniauth from 1.8.1 to 1.9.0 (#9544) Bumps [omniauth](https://github.com/omniauth/omniauth) from 1.8.1 to 1.9.0. - [Release notes](https://github.com/omniauth/omniauth/releases) - [Commits](https://github.com/omniauth/omniauth/compare/v1.8.1...v1.9.0) Signed-off-by: dependabot[bot] --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index feaa75439..6be26f1ff 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ end gem 'net-ldap', '~> 0.10' gem 'omniauth-cas', '~> 1.1' gem 'omniauth-saml', '~> 1.10' -gem 'omniauth', '~> 1.2' +gem 'omniauth', '~> 1.9' gem 'doorkeeper', '~> 5.0' gem 'fast_blank', '~> 1.0' diff --git a/Gemfile.lock b/Gemfile.lock index d9fc1c6b6..c24128590 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -251,7 +251,7 @@ GEM hamster (3.0.0) concurrent-ruby (~> 1.0) hashdiff (0.3.7) - hashie (3.5.7) + hashie (3.6.0) heapy (0.1.4) highline (2.0.0) hiredis (0.6.3) @@ -364,8 +364,8 @@ GEM sidekiq (>= 3.5.0) statsd-ruby (~> 1.2.0) oj (3.7.4) - omniauth (1.8.1) - hashie (>= 3.4.6, < 3.6.0) + omniauth (1.9.0) + hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) omniauth-cas (1.1.1) addressable (~> 2.3) @@ -712,7 +712,7 @@ DEPENDENCIES nokogiri (~> 1.8) nsa (~> 0.2) oj (~> 3.7) - omniauth (~> 1.2) + omniauth (~> 1.9) omniauth-cas (~> 1.1) omniauth-saml (~> 1.10) ostatus2 (~> 2.0) From 9cb26bb56b6b61e4e8577519347ada40a7751cd6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 17 Dec 2018 11:07:17 +0100 Subject: [PATCH 10/23] Add new first-time tutorial (#9531) * Prepare to load onboarding as a full page * Update the first-time introduction * Improve responsive design * Replace speech bubble with logo * Increase text size and reword first paragraph --- app/javascript/images/screen_federation.svg | 1 + app/javascript/images/screen_hello.svg | 1 + app/javascript/images/screen_interactions.svg | 1 + app/javascript/mastodon/actions/onboarding.js | 14 +- .../mastodon/containers/mastodon.js | 51 ++- .../mastodon/features/introduction/index.js | 196 +++++++++++ .../features/ui/components/modal_root.js | 2 - .../ui/components/onboarding_modal.js | 324 ------------------ app/javascript/mastodon/features/ui/index.js | 6 + .../features/ui/util/async-components.js | 4 - app/javascript/styles/application.scss | 1 + .../styles/mastodon/components.scss | 239 ------------- .../styles/mastodon/introduction.scss | 153 +++++++++ 13 files changed, 397 insertions(+), 596 deletions(-) create mode 100644 app/javascript/images/screen_federation.svg create mode 100644 app/javascript/images/screen_hello.svg create mode 100644 app/javascript/images/screen_interactions.svg create mode 100644 app/javascript/mastodon/features/introduction/index.js delete mode 100644 app/javascript/mastodon/features/ui/components/onboarding_modal.js create mode 100644 app/javascript/styles/mastodon/introduction.scss diff --git a/app/javascript/images/screen_federation.svg b/app/javascript/images/screen_federation.svg new file mode 100644 index 000000000..7019a7356 --- /dev/null +++ b/app/javascript/images/screen_federation.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/screen_hello.svg b/app/javascript/images/screen_hello.svg new file mode 100644 index 000000000..7bcdd0afd --- /dev/null +++ b/app/javascript/images/screen_hello.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/screen_interactions.svg b/app/javascript/images/screen_interactions.svg new file mode 100644 index 000000000..41873371a --- /dev/null +++ b/app/javascript/images/screen_interactions.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js index a161c50ef..a1dd3a731 100644 --- a/app/javascript/mastodon/actions/onboarding.js +++ b/app/javascript/mastodon/actions/onboarding.js @@ -1,14 +1,8 @@ -import { openModal } from './modal'; import { changeSetting, saveSettings } from './settings'; -export function showOnboardingOnce() { - return (dispatch, getState) => { - const alreadySeen = getState().getIn(['settings', 'onboarded']); +export const INTRODUCTION_VERSION = 20181216044202; - if (!alreadySeen) { - dispatch(openModal('ONBOARDING')); - dispatch(changeSetting(['onboarded'], true)); - dispatch(saveSettings()); - } - }; +export const closeOnboarding = () => dispatch => { + dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION)); + dispatch(saveSettings()); }; diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index b2b0265aa..2912540a0 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -1,11 +1,12 @@ import React from 'react'; -import { Provider } from 'react-redux'; +import { Provider, connect } from 'react-redux'; import PropTypes from 'prop-types'; import configureStore from '../store/configureStore'; -import { showOnboardingOnce } from '../actions/onboarding'; +import { INTRODUCTION_VERSION } from '../actions/onboarding'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; import UI from '../features/ui'; +import Introduction from '../features/introduction'; import { fetchCustomEmojis } from '../actions/custom_emojis'; import { hydrateStore } from '../actions/store'; import { connectUserStream } from '../actions/streaming'; @@ -18,11 +19,39 @@ addLocaleData(localeData); export const store = configureStore(); const hydrateAction = hydrateStore(initialState); -store.dispatch(hydrateAction); -// load custom emojis +store.dispatch(hydrateAction); store.dispatch(fetchCustomEmojis()); +const mapStateToProps = state => ({ + showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, +}); + +@connect(mapStateToProps) +class MastodonMount extends React.PureComponent { + + static propTypes = { + showIntroduction: PropTypes.bool, + }; + + render () { + const { showIntroduction } = this.props; + + if (showIntroduction) { + return ; + } + + return ( + + + + + + ); + } + +} + export default class Mastodon extends React.PureComponent { static propTypes = { @@ -31,14 +60,6 @@ export default class Mastodon extends React.PureComponent { componentDidMount() { this.disconnect = store.dispatch(connectUserStream()); - - // Desktop notifications - // Ask after 1 minute - if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { - window.setTimeout(() => Notification.requestPermission(), 60 * 1000); - } - - store.dispatch(showOnboardingOnce()); } componentWillUnmount () { @@ -54,11 +75,7 @@ export default class Mastodon extends React.PureComponent { return ( - - - - - + ); diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js new file mode 100644 index 000000000..6e0617f72 --- /dev/null +++ b/app/javascript/mastodon/features/introduction/index.js @@ -0,0 +1,196 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactSwipeableViews from 'react-swipeable-views'; +import classNames from 'classnames'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { closeOnboarding } from '../../actions/onboarding'; +import screenHello from '../../../images/screen_hello.svg'; +import screenFederation from '../../../images/screen_federation.svg'; +import screenInteractions from '../../../images/screen_interactions.svg'; +import logoTransparent from '../../../images/logo_transparent.svg'; + +const FrameWelcome = ({ domain, onNext }) => ( +
+
+ +
+ +
+

+

{domain} }} />

+
+ +
+ +
+
+); + +FrameWelcome.propTypes = { + domain: PropTypes.string.isRequired, + onNext: PropTypes.func.isRequired, +}; + +const FrameFederation = ({ onNext }) => ( +
+
+ +
+ +
+
+

+

+
+ +
+

+

+
+ +
+

+

+
+
+ +
+ +
+
+); + +FrameFederation.propTypes = { + onNext: PropTypes.func.isRequired, +}; + +const FrameInteractions = ({ onNext }) => ( +
+
+ +
+ +
+
+

+

+
+ +
+

+

+
+ +
+

+

+
+
+ +
+ +
+
+); + +FrameInteractions.propTypes = { + onNext: PropTypes.func.isRequired, +}; + +@connect(state => ({ domain: state.getIn(['meta', 'domain']) })) +export default class Introduction extends React.PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + }; + + state = { + currentIndex: 0, + }; + + componentWillMount () { + this.pages = [ + , + , + , + ]; + } + + componentDidMount() { + window.addEventListener('keyup', this.handleKeyUp); + } + + componentWillUnmount() { + window.addEventListener('keyup', this.handleKeyUp); + } + + handleDot = (e) => { + const i = Number(e.currentTarget.getAttribute('data-index')); + e.preventDefault(); + this.setState({ currentIndex: i }); + } + + handlePrev = () => { + this.setState(({ currentIndex }) => ({ + currentIndex: Math.max(0, currentIndex - 1), + })); + } + + handleNext = () => { + const { pages } = this; + + this.setState(({ currentIndex }) => ({ + currentIndex: Math.min(currentIndex + 1, pages.length - 1), + })); + } + + handleSwipe = (index) => { + this.setState({ currentIndex: index }); + } + + handleFinish = () => { + this.props.dispatch(closeOnboarding()); + } + + handleKeyUp = ({ key }) => { + switch (key) { + case 'ArrowLeft': + this.handlePrev(); + break; + case 'ArrowRight': + this.handleNext(); + break; + } + } + + render () { + const { currentIndex } = this.state; + const { pages } = this; + + return ( +
+ + {pages.map((page, i) => ( +
{page}
+ ))} +
+ +
+ {pages.map((_, i) => ( +
+ ))} +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index b3b1ea862..cc2ab6c8c 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -11,7 +11,6 @@ import BoostModal from './boost_modal'; import ConfirmationModal from './confirmation_modal'; import FocalPointModal from './focal_point_modal'; import { - OnboardingModal, MuteModal, ReportModal, EmbedModal, @@ -21,7 +20,6 @@ import { const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), - 'ONBOARDING': OnboardingModal, 'VIDEO': () => Promise.resolve({ default: VideoModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js deleted file mode 100644 index 4a5b249c9..000000000 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ /dev/null @@ -1,324 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ReactSwipeableViews from 'react-swipeable-views'; -import classNames from 'classnames'; -import Permalink from '../../../components/permalink'; -import ComposeForm from '../../compose/components/compose_form'; -import Search from '../../compose/components/search'; -import NavigationBar from '../../compose/components/navigation_bar'; -import ColumnHeader from './column_header'; -import { List as ImmutableList } from 'immutable'; -import { me } from '../../../initial_state'; - -const noop = () => { }; - -const messages = defineMessages({ - home_title: { id: 'column.home', defaultMessage: 'Home' }, - notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - local_title: { id: 'column.community', defaultMessage: 'Local timeline' }, - federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }, -}); - -const PageOne = ({ acct, domain }) => ( -
-
-

-

-
- -
-
-
- -
- -
- @{acct}@{domain} -
-
- -

-
-
-); - -PageOne.propTypes = { - acct: PropTypes.string.isRequired, - domain: PropTypes.string.isRequired, -}; - -const PageTwo = ({ myAccount }) => ( -
-
-
- - - -
-
- -

-
-); - -PageTwo.propTypes = { - myAccount: ImmutablePropTypes.map.isRequired, -}; - -const PageThree = ({ myAccount }) => ( -
-
- - -
- -
-
- -

#illustration, introductions: #introductions }} />

-

-
-); - -PageThree.propTypes = { - myAccount: ImmutablePropTypes.map.isRequired, -}; - -const PageFour = ({ domain, intl }) => ( -
-
-
-
-
-

-
- -
-
-

-
-
- -
-
-
-
- -
-
-
-
- -

-
-
-); - -PageFour.propTypes = { - domain: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired, -}; - -const PageSix = ({ admin, domain }) => { - let adminSection = ''; - - if (admin) { - adminSection = ( -

- @{admin.get('acct')} }} /> -
- }} /> -

- ); - } - - return ( -
-

- {adminSection} -

GitHub }} />

-

}} />

-

-
- ); -}; - -PageSix.propTypes = { - admin: ImmutablePropTypes.map, - domain: PropTypes.string.isRequired, -}; - -const mapStateToProps = state => ({ - myAccount: state.getIn(['accounts', me]), - admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), - domain: state.getIn(['meta', 'domain']), -}); - -export default @connect(mapStateToProps) -@injectIntl -class OnboardingModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - myAccount: ImmutablePropTypes.map.isRequired, - domain: PropTypes.string.isRequired, - admin: ImmutablePropTypes.map, - }; - - state = { - currentIndex: 0, - }; - - componentWillMount() { - const { myAccount, admin, domain, intl } = this.props; - this.pages = [ - , - , - , - , - , - ]; - }; - - componentDidMount() { - window.addEventListener('keyup', this.handleKeyUp); - } - - componentWillUnmount() { - window.addEventListener('keyup', this.handleKeyUp); - } - - handleSkip = (e) => { - e.preventDefault(); - this.props.onClose(); - } - - handleDot = (e) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); - this.setState({ currentIndex: i }); - } - - handlePrev = () => { - this.setState(({ currentIndex }) => ({ - currentIndex: Math.max(0, currentIndex - 1), - })); - } - - handleNext = () => { - const { pages } = this; - this.setState(({ currentIndex }) => ({ - currentIndex: Math.min(currentIndex + 1, pages.length - 1), - })); - } - - handleSwipe = (index) => { - this.setState({ currentIndex: index }); - } - - handleKeyUp = ({ key }) => { - switch (key) { - case 'ArrowLeft': - this.handlePrev(); - break; - case 'ArrowRight': - this.handleNext(); - break; - } - } - - handleClose = () => { - this.props.onClose(); - } - - render () { - const { pages } = this; - const { currentIndex } = this.state; - const hasMore = currentIndex < pages.length - 1; - - const nextOrDoneBtn = hasMore ? ( - - ) : ( - - ); - - return ( -
- - {pages.map((page, i) => { - const className = classNames('onboarding-modal__page__wrapper', `onboarding-modal__page__wrapper-${i}`, { - 'onboarding-modal__page__wrapper--active': i === currentIndex, - }); - - return ( -
{page}
- ); - })} -
- -
-
- -
- -
- {pages.map((_, i) => { - const className = classNames('onboarding-modal__dot', { - active: i === currentIndex, - }); - - return ( -
- ); - })} -
- -
- {nextOrDoneBtn} -
-
-
- ); - } - -} diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 662375a76..e11235a81 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -294,6 +294,7 @@ class UI extends React.PureComponent { componentWillMount () { window.addEventListener('beforeunload', this.handleBeforeUnload, false); + document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -304,8 +305,13 @@ class UI extends React.PureComponent { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); } + if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { + window.setTimeout(() => Notification.requestPermission(), 120 * 1000); + } + this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); + setTimeout(() => this.props.dispatch(fetchFilters()), 500); } diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 2a15c052f..235fd2a07 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -102,10 +102,6 @@ export function Mutes () { return import(/* webpackChunkName: "features/mutes" */'../../mutes'); } -export function OnboardingModal () { - return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'); -} - export function MuteModal () { return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal'); } diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 0990a4f25..4bce74187 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -16,6 +16,7 @@ @import 'mastodon/stream_entries'; @import 'mastodon/boost'; @import 'mastodon/components'; +@import 'mastodon/introduction'; @import 'mastodon/modal'; @import 'mastodon/emoji_picker'; @import 'mastodon/about'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1c1b8c506..d2b3baaf0 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3835,25 +3835,6 @@ a.status-card.compact:hover { flex-direction: column; } -.onboarding-modal__pager { - height: 80vh; - width: 80vw; - max-width: 520px; - max-height: 470px; - - .react-swipeable-view-container > div { - width: 100%; - height: 100%; - box-sizing: border-box; - display: none; - flex-direction: column; - align-items: center; - justify-content: center; - display: flex; - user-select: text; - } -} - .error-modal__body { height: 80vh; width: 80vw; @@ -3887,22 +3868,6 @@ a.status-card.compact:hover { text-align: center; } -@media screen and (max-width: 550px) { - .onboarding-modal { - width: 100%; - height: 100%; - border-radius: 0; - } - - .onboarding-modal__pager { - width: 100%; - height: auto; - max-width: none; - max-height: none; - flex: 1 1 auto; - } -} - .onboarding-modal__paginator, .error-modal__footer { flex: 0 0 auto; @@ -3951,124 +3916,6 @@ a.status-card.compact:hover { justify-content: center; } -.onboarding-modal__dots { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; -} - -.onboarding-modal__dot { - width: 14px; - height: 14px; - border-radius: 14px; - background: darken($ui-secondary-color, 16%); - margin: 0 3px; - cursor: pointer; - - &:hover { - background: darken($ui-secondary-color, 18%); - } - - &.active { - cursor: default; - background: darken($ui-secondary-color, 24%); - } -} - -.onboarding-modal__page__wrapper { - pointer-events: none; - padding: 25px; - padding-bottom: 0; - - &.onboarding-modal__page__wrapper--active { - pointer-events: auto; - } -} - -.onboarding-modal__page { - cursor: default; - line-height: 21px; - - h1 { - font-size: 18px; - font-weight: 500; - color: $inverted-text-color; - margin-bottom: 20px; - } - - a { - color: $highlight-text-color; - - &:hover, - &:focus, - &:active { - color: lighten($highlight-text-color, 4%); - } - } - - .navigation-bar a { - color: inherit; - } - - p { - font-size: 16px; - color: $lighter-text-color; - margin-top: 10px; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - - strong { - font-weight: 500; - background: $ui-base-color; - color: $secondary-text-color; - border-radius: 4px; - font-size: 14px; - padding: 3px 6px; - - @each $lang in $cjk-langs { - &:lang(#{$lang}) { - font-weight: 700; - } - } - } - } -} - -.onboarding-modal__page__wrapper-0 { - background: url('../images/elephant_ui_greeting.svg') no-repeat left bottom / auto 250px; - height: 100%; - padding: 0; -} - -.onboarding-modal__page-one { - &__lead { - padding: 65px; - padding-top: 45px; - padding-bottom: 0; - margin-bottom: 10px; - - h1 { - font-size: 26px; - line-height: 36px; - margin-bottom: 8px; - } - - p { - margin-bottom: 0; - } - } - - &__extra { - padding-right: 65px; - padding-left: 185px; - text-align: center; - } -} - .display-case { text-align: center; font-size: 15px; @@ -4091,92 +3938,6 @@ a.status-card.compact:hover { } } -.onboarding-modal__page-two, -.onboarding-modal__page-three, -.onboarding-modal__page-four, -.onboarding-modal__page-five { - p { - text-align: left; - } - - .figure { - background: darken($ui-base-color, 8%); - color: $secondary-text-color; - margin-bottom: 20px; - border-radius: 4px; - padding: 10px; - text-align: center; - font-size: 14px; - box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.3); - - .onboarding-modal__image { - border-radius: 4px; - margin-bottom: 10px; - } - - &.non-interactive { - pointer-events: none; - text-align: left; - } - } -} - -.onboarding-modal__page-four__columns { - .row { - display: flex; - margin-bottom: 20px; - - & > div { - flex: 1 1 0; - margin: 0 10px; - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - - p { - text-align: center; - } - } - - &:last-child { - margin-bottom: 0; - } - } - - .column-header { - color: $primary-text-color; - } -} - -@media screen and (max-width: 320px) and (max-height: 600px) { - .onboarding-modal__page p { - font-size: 14px; - line-height: 20px; - } - - .onboarding-modal__page-two .figure, - .onboarding-modal__page-three .figure, - .onboarding-modal__page-four .figure, - .onboarding-modal__page-five .figure { - font-size: 12px; - margin-bottom: 10px; - } - - .onboarding-modal__page-four__columns .row { - margin-bottom: 10px; - } - - .onboarding-modal__page-four__columns .column-header { - padding: 5px; - font-size: 12px; - } -} - .onboard-sliders { display: inline-block; max-width: 30px; diff --git a/app/javascript/styles/mastodon/introduction.scss b/app/javascript/styles/mastodon/introduction.scss new file mode 100644 index 000000000..222d8f60e --- /dev/null +++ b/app/javascript/styles/mastodon/introduction.scss @@ -0,0 +1,153 @@ +.introduction { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + @media screen and (max-width: 920px) { + background: darken($ui-base-color, 8%); + display: block !important; + } + + &__pager { + background: darken($ui-base-color, 8%); + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + overflow: hidden; + } + + &__pager, + &__frame { + border-radius: 10px; + width: 50vw; + min-width: 920px; + + @media screen and (max-width: 920px) { + min-width: 0; + width: 100%; + border-radius: 0; + box-shadow: none; + } + } + + &__frame-wrapper { + opacity: 0; + transition: opacity 500ms linear; + + &.active { + opacity: 1; + transition: opacity 50ms linear; + } + } + + &__frame { + overflow: hidden; + } + + &__illustration { + height: 50vh; + + @media screen and (max-width: 630px) { + height: auto; + } + + img { + object-fit: cover; + display: block; + margin: 0; + width: 100%; + height: 100%; + } + } + + &__text { + border-top: 2px solid $ui-highlight-color; + + &--columnized { + display: flex; + + & > div { + flex: 1 1 33.33%; + text-align: center; + padding: 25px; + padding-bottom: 30px; + } + + @media screen and (max-width: 630px) { + display: block; + padding: 15px 0; + padding-bottom: 20px; + + & > div { + padding: 10px 25px; + } + } + } + + h3 { + font-size: 24px; + line-height: 1.5; + font-weight: 700; + margin-bottom: 10px; + } + + p { + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: $darker-text-color; + + code { + display: inline-block; + background: darken($ui-base-color, 8%); + font-size: 15px; + border: 1px solid lighten($ui-base-color, 8%); + border-radius: 2px; + padding: 1px 3px; + } + } + + &--centered { + padding: 25px; + padding-bottom: 30px; + text-align: center; + } + } + + &__dots { + display: flex; + align-items: center; + justify-content: center; + padding: 25px; + + @media screen and (max-width: 630px) { + display: none; + } + } + + &__dot { + width: 14px; + height: 14px; + border-radius: 14px; + border: 1px solid $ui-highlight-color; + background: transparent; + margin: 0 3px; + cursor: pointer; + + &:hover { + background: lighten($ui-base-color, 8%); + } + + &.active { + cursor: default; + background: $ui-highlight-color; + } + } + + &__action { + padding: 25px; + padding-top: 0; + display: flex; + align-items: center; + justify-content: center; + } +} From 3fa9615cb3050a34824f6450ae9de76d6e0e8f6c Mon Sep 17 00:00:00 2001 From: ysksn Date: Mon, 17 Dec 2018 19:32:24 +0900 Subject: [PATCH 11/23] Add spec for Api::V1::Instances::ActivityController (#9545) --- .../v1/instances/activity_controller_spec.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/controllers/api/v1/instances/activity_controller_spec.rb diff --git a/spec/controllers/api/v1/instances/activity_controller_spec.rb b/spec/controllers/api/v1/instances/activity_controller_spec.rb new file mode 100644 index 000000000..159792ee0 --- /dev/null +++ b/spec/controllers/api/v1/instances/activity_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::Instances::ActivityController, type: :controller do + describe 'GET #show' do + it 'returns 200' do + get :show + expect(response).to have_http_status(200) + end + + context '!Setting.activity_api_enabled' do + it 'returns 404' do + Setting.activity_api_enabled = false + + get :show + expect(response).to have_http_status(404) + end + end + end +end From 2d871feb10e42becb9248e44e108ebcc93b671fe Mon Sep 17 00:00:00 2001 From: ysksn Date: Mon, 17 Dec 2018 19:32:44 +0900 Subject: [PATCH 12/23] Add spec for Api::V1::EndorsementsController (#9543) --- .../api/v1/endorsements_controller_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 spec/controllers/api/v1/endorsements_controller_spec.rb diff --git a/spec/controllers/api/v1/endorsements_controller_spec.rb b/spec/controllers/api/v1/endorsements_controller_spec.rb new file mode 100644 index 000000000..ad5ff400f --- /dev/null +++ b/spec/controllers/api/v1/endorsements_controller_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::EndorsementsController, type: :controller do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + + describe 'GET #index' do + it 'returns 200' do + allow(controller).to receive(:doorkeeper_token) { token } + get :index + + expect(response).to have_http_status(200) + end + end +end From 351938520d5e5e8792772fd5f8ad30ba3e11639c Mon Sep 17 00:00:00 2001 From: ysksn Date: Mon, 17 Dec 2018 19:35:55 +0900 Subject: [PATCH 13/23] Add specs for Api::V1::Instances::PeersController (#9546) --- .../api/v1/instances/peers_controller_spec.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/controllers/api/v1/instances/peers_controller_spec.rb diff --git a/spec/controllers/api/v1/instances/peers_controller_spec.rb b/spec/controllers/api/v1/instances/peers_controller_spec.rb new file mode 100644 index 000000000..12a214a83 --- /dev/null +++ b/spec/controllers/api/v1/instances/peers_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::Instances::PeersController, type: :controller do + describe 'GET #index' do + it 'returns 200' do + get :index + expect(response).to have_http_status(200) + end + + context '!Setting.peers_api_enabled' do + it 'returns 404' do + Setting.peers_api_enabled = false + + get :index + expect(response).to have_http_status(404) + end + end + end +end From 0c8071523592fc4ce73aa5c39822ca5d7f5f71d7 Mon Sep 17 00:00:00 2001 From: ysksn Date: Mon, 17 Dec 2018 19:36:20 +0900 Subject: [PATCH 14/23] Add spec for Api::V1::Timelines::DirectController (#9547) --- .../api/v1/timelines/direct_controller_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 spec/controllers/api/v1/timelines/direct_controller_spec.rb diff --git a/spec/controllers/api/v1/timelines/direct_controller_spec.rb b/spec/controllers/api/v1/timelines/direct_controller_spec.rb new file mode 100644 index 000000000..a22c2cbea --- /dev/null +++ b/spec/controllers/api/v1/timelines/direct_controller_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::Timelines::DirectController, type: :controller do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } + + describe 'GET #show' do + it 'returns 200' do + allow(controller).to receive(:doorkeeper_token) { token } + get :show + + expect(response).to have_http_status(200) + end + end +end From 3281df0df1eb83e77d5c3028537be2669eebd69c Mon Sep 17 00:00:00 2001 From: ysksn Date: Mon, 17 Dec 2018 19:40:51 +0900 Subject: [PATCH 15/23] Move #set_user to Admin::BaseController (#9470) * Move #set_user to Admin::BaseController * Rename Admin::TwoFactorAuthenticationsController from `#set_user` to `#set_target_user` . --- app/controllers/admin/base_controller.rb | 4 ++++ app/controllers/admin/confirmations_controller.rb | 4 ---- app/controllers/admin/resets_controller.rb | 6 ------ app/controllers/admin/roles_controller.rb | 6 ------ .../admin/two_factor_authentications_controller.rb | 4 ++-- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 8593b582a..7b81a2b01 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -15,5 +15,9 @@ module Admin def set_body_classes @body_classes = 'admin' end + + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) + end end end diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 8d3477e66..efe7dcbd4 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -25,10 +25,6 @@ module Admin private - def set_user - @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) - end - def check_confirmation if @user.confirmed? flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb index 3e27d01ac..db8f61d64 100644 --- a/app/controllers/admin/resets_controller.rb +++ b/app/controllers/admin/resets_controller.rb @@ -10,11 +10,5 @@ module Admin log_action :reset_password, @user redirect_to admin_accounts_path end - - private - - def set_user - @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) - end end end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index af7ec0740..13f56e9be 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -17,11 +17,5 @@ module Admin log_action :demote, @user redirect_to admin_account_path(@user.account_id) end - - private - - def set_user - @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) - end end end diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb index 022107203..2577a4b17 100644 --- a/app/controllers/admin/two_factor_authentications_controller.rb +++ b/app/controllers/admin/two_factor_authentications_controller.rb @@ -2,7 +2,7 @@ module Admin class TwoFactorAuthenticationsController < BaseController - before_action :set_user + before_action :set_target_user def destroy authorize @user, :disable_2fa? @@ -13,7 +13,7 @@ module Admin private - def set_user + def set_target_user @user = User.find(params[:user_id]) end end From 4ede51743e5b9121a49e9131f91cf012fab410f8 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 17 Dec 2018 17:02:59 +0100 Subject: [PATCH 16/23] Minor scrollable list fixes (#9551) * Make sure loading indicator has enough vertical space * Respect reduce_motion setting for loading indicator --- .../mastodon/features/account_gallery/index.js | 2 +- app/javascript/styles/mastodon/components.scss | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 0d66868ed..96051818b 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -103,7 +103,7 @@ class AccountGallery extends ImmutablePureComponent { ); } - if (hasMore) { + if (hasMore && !(isLoading && medias.size === 0)) { loadOlder = ; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d2b3baaf0..595472263 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2153,6 +2153,7 @@ a.account__display-name { &__append { flex: 1 1 auto; position: relative; + min-height: 120px; } } @@ -2946,7 +2947,6 @@ a.status-card.compact:hover { transform: translateX(-50%); margin: 82px 0 0 50%; white-space: nowrap; - animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); } } @@ -2955,11 +2955,20 @@ a.status-card.compact:hover { top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 0; - height: 0; + width: 42px; + height: 42px; box-sizing: border-box; + background-color: transparent; border: 0 solid lighten($ui-base-color, 26%); + border-width: 6px; border-radius: 50%; +} + +.no-reduce-motion .loading-indicator span { + animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); +} + +.no-reduce-motion .loading-indicator__figure { animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); } From e709b8da0d685d3cc48d430a9761896094f67d72 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 17 Dec 2018 19:19:45 +0100 Subject: [PATCH 17/23] Ignore low-confidence CharlockHolmes guesses when parsing link cards (#9510) * Add failing test for windows-1251 link cards * Ignore low-confidence CharlockHolmes guesses Fixes #9466 * Fix no method error when charlock holmes cannot detect charset --- app/services/fetch_link_card_service.rb | 3 ++- spec/fixtures/requests/windows-1251.txt | 17 +++++++++++++++++ spec/services/fetch_link_card_service_spec.rb | 11 +++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/requests/windows-1251.txt diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 38c578de2..7979c312e 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -137,7 +137,8 @@ class FetchLinkCardService < BaseService detector.strip_tags = true guess = detector.detect(@html, @html_charset) - page = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil)) + encoding = guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil + page = Nokogiri::HTML(@html, nil, encoding) player_url = meta_property(page, 'twitter:player') if player_url && !bad_url?(Addressable::URI.parse(player_url)) diff --git a/spec/fixtures/requests/windows-1251.txt b/spec/fixtures/requests/windows-1251.txt new file mode 100644 index 000000000..f573e28b2 --- /dev/null +++ b/spec/fixtures/requests/windows-1251.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +server: nginx +date: Wed, 12 Dec 2018 13:14:03 GMT +content-type: text/html +content-length: 190 +accept-ranges: bytes + + + + + + + + +

+ + diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 88c5339db..50c60aafd 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -17,6 +17,8 @@ RSpec.describe FetchLinkCardService, type: :service do stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) stub_request(:head, 'http://example.com/test-').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt')) + stub_request(:head, 'http://example.com/windows-1251').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt')) subject.call(status) end @@ -57,6 +59,15 @@ RSpec.describe FetchLinkCardService, type: :service do end end + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/windows-1251') } + + it 'works with windows-1251' do + expect(a_request(:get, 'http://example.com/windows-1251')).to have_been_made.at_least_once + expect(status.preview_cards.first.title).to eq('сэмпл текст') + end + end + context do let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') } From 12ab15e584e78d209b59a893405a0cde83f49035 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 17 Dec 2018 21:08:40 +0100 Subject: [PATCH 18/23] Make notifications quick-filter use consistent style with profile tabs (#9554) --- .../styles/mastodon/components.scss | 53 +++---------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 595472263..40a1e3fae 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1484,52 +1484,6 @@ a.account__display-name { } } -.notification__filter-bar { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - background: $ui-base-color; - - & > button { - position: relative; - flex-grow: 1; - color: $primary-text-color; - padding: 10px 5px 12px; - text-decoration: none; - font-weight: 400; - font-size: 15px; - line-height: 18px; - background: darken($ui-base-color, 4%); - border: 0; - border-bottom: 1px solid lighten($ui-base-color, 8%); - cursor: default; - - &.active { - color: $secondary-text-color; - - &::before, - &::after { - display: block; - content: ""; - position: absolute; - bottom: 0; - left: 50%; - width: 0; - height: 0; - transform: translateX(-50%); - border-style: solid; - border-width: 0 10px 10px; - border-color: transparent transparent lighten($ui-base-color, 8%); - } - - &::after { - bottom: -1px; - border-color: transparent transparent $ui-base-color; - } - } - } -} - .notification__message { margin: 0 10px 0 68px; padding: 8px 0 0; @@ -4846,12 +4800,19 @@ a.status-card.compact:hover { } } +.notification__filter-bar, .account__section-headline { background: darken($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 8%); cursor: default; display: flex; + button { + background: darken($ui-base-color, 4%); + border: 0; + } + + button, a { display: block; flex: 1 1 auto; From 857e8eb312bc1767d6d04c5490c2acb3b787cf9a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 18 Dec 2018 01:22:29 +0100 Subject: [PATCH 19/23] Fix tootctl accounts rotate not updating public keys (#9556) This allowed you to brick your system when running that command, because the accounts would continue to advertise the old public key, but sign things with the new one --- lib/mastodon/accounts_cli.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 9f7870bcd..b21968223 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -309,8 +309,8 @@ module Mastodon end old_key = account.private_key - new_key = OpenSSL::PKey::RSA.new(2048).to_pem - account.update(private_key: new_key) + new_key = OpenSSL::PKey::RSA.new(2048) + account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem) ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key) end end From 2c1a6f746fdce3654590cb2cb6703db24148cf59 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 18 Dec 2018 16:40:30 +0100 Subject: [PATCH 20/23] fix CSP / X-Frame-Options for media embeds (#9558) --- app/controllers/media_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 88c7232dd..8e1624ce1 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -6,12 +6,17 @@ class MediaController < ApplicationController before_action :set_media_attachment before_action :verify_permitted_status! + content_security_policy only: :player do |p| + p.frame_ancestors(false) + end + def show redirect_to @media_attachment.file.url(:original) end def player @body_classes = 'player' + response.headers['X-Frame-Options'] = 'ALLOWALL' raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv? end From 071eb0e2022a49ced8a0fa808fb54e6f81fcb43e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Tue, 18 Dec 2018 16:41:41 +0100 Subject: [PATCH 21/23] Bump nokogiri from 1.8.5 to 1.9.1 (#9557) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.8.5 to 1.9.1. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.8.5...v1.9.1) Signed-off-by: dependabot[bot] --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 6be26f1ff..3d78b1dfe 100644 --- a/Gemfile +++ b/Gemfile @@ -57,7 +57,7 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' -gem 'nokogiri', '~> 1.8' +gem 'nokogiri', '~> 1.9' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.7' gem 'ostatus2', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index c24128590..36a3a65f6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -343,7 +343,7 @@ GEM mime-types-data (3.2018.0812) mimemagic (0.3.2) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) msgpack (1.2.4) multi_json (1.13.1) @@ -354,8 +354,8 @@ GEM net-ssh (>= 2.6.5) net-ssh (5.0.2) nio4r (2.3.1) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.9.1) + mini_portile2 (~> 2.4.0) nokogumbo (2.0.0) nokogiri (~> 1.8, >= 1.8.4) nsa (0.2.4) @@ -709,7 +709,7 @@ DEPENDENCIES microformats (~> 4.0) mime-types (~> 3.2) net-ldap (~> 0.10) - nokogiri (~> 1.8) + nokogiri (~> 1.9) nsa (~> 0.2) oj (~> 3.7) omniauth (~> 1.9) From dd85700a3e06ecec424ffc9f623f9407b007b229 Mon Sep 17 00:00:00 2001 From: ysksn Date: Wed, 19 Dec 2018 00:43:03 +0900 Subject: [PATCH 22/23] Add spec for AccountableConcern#log_action (#9559) --- .../concerns/accountable_concern_spec.rb | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 spec/controllers/concerns/accountable_concern_spec.rb diff --git a/spec/controllers/concerns/accountable_concern_spec.rb b/spec/controllers/concerns/accountable_concern_spec.rb new file mode 100644 index 000000000..e3c06b494 --- /dev/null +++ b/spec/controllers/concerns/accountable_concern_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AccountableConcern do + class Hoge + include AccountableConcern + attr_reader :current_account + + def initialize(current_account) + @current_account = current_account + end + end + + let(:user) { Fabricate(:user, account: Fabricate(:account)) } + let(:target) { Fabricate(:user, account: Fabricate(:account)) } + let(:hoge) { Hoge.new(user.account) } + + describe '#log_action' do + it 'creates Admin::ActionLog' do + expect do + hoge.log_action(:create, target.account) + end.to change { Admin::ActionLog.count }.by(1) + end + end +end From 5bf100f87be571e86305f3ab244183fc46f1ede2 Mon Sep 17 00:00:00 2001 From: kedama Date: Wed, 19 Dec 2018 00:43:50 +0900 Subject: [PATCH 23/23] Back to the getting-started when pins the timeline. (#9561) --- .../mastodon/components/column_header.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index 457508d13..f68e4155e 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -37,6 +37,14 @@ class ColumnHeader extends React.PureComponent { animating: false, }; + historyBack = () => { + if (window.history && window.history.length === 1) { + this.context.router.history.push('/'); + } else { + this.context.router.history.goBack(); + } + } + handleToggleClick = (e) => { e.stopPropagation(); this.setState({ collapsed: !this.state.collapsed, animating: true }); @@ -55,16 +63,22 @@ class ColumnHeader extends React.PureComponent { } handleBackClick = () => { - if (window.history && window.history.length === 1) this.context.router.history.push('/'); - else this.context.router.history.goBack(); + this.historyBack(); } handleTransitionEnd = () => { this.setState({ animating: false }); } + handlePin = () => { + if (!this.props.pinned) { + this.historyBack(); + } + this.props.onPin(); + } + render () { - const { title, icon, active, children, pinned, onPin, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props; + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -95,7 +109,7 @@ class ColumnHeader extends React.PureComponent { } if (multiColumn && pinned) { - pinButton = ; + pinButton = ; moveButtons = (
@@ -104,7 +118,7 @@ class ColumnHeader extends React.PureComponent {
); } else if (multiColumn) { - pinButton = ; + pinButton = ; } if (!pinned && (multiColumn || showBackButton)) {