diff --git a/Dockerfile b/Dockerfile
index 3e54b6555e..826e237c4f 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" \
diff --git a/Gemfile b/Gemfile
index ee2de58c97..63ea35a655 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'
@@ -58,7 +58,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 a5dac694a4..99db896939 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)
@@ -345,7 +345,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)
@@ -356,8 +356,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)
@@ -366,8 +366,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,10 +712,10 @@ 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.2)
+ omniauth (~> 1.9)
omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10)
ostatus2 (~> 2.0)
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index f2190ddf9b..cc6cd51f0b 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -20,5 +20,9 @@ module Admin
def set_pack
use_pack '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 8d3477e660..efe7dcbd4b 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 3e27d01ac2..db8f61d64c 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 af7ec0740d..13f56e9beb 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 0221072032..2577a4b17f 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
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
index 1c8ebdac9d..e91e784a54 100644
--- a/app/controllers/directories_controller.rb
+++ b/app/controllers/directories_controller.rb
@@ -37,22 +37,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/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 88c7232dd8..8e1624ce1b 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
diff --git a/app/javascript/images/screen_federation.svg b/app/javascript/images/screen_federation.svg
new file mode 100644
index 0000000000..7019a7356a
--- /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 0000000000..7bcdd0afd5
--- /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 0000000000..41873371aa
--- /dev/null
+++ b/app/javascript/images/screen_interactions.svg
@@ -0,0 +1 @@
+
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index d24f39ad2b..4c145febc4 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/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js
index a161c50efe..a1dd3a731e 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/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index 457508d138..f68e4155eb 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 = (
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 0000000000..f95a2c9dea
--- /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 e9cef0a7bc..a67f262953 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 0000000000..4d495c2908
--- /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 aa82dbbb97..9430b20505 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/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index b3b1ea8623..cc2ab6c8ce 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 4a5b249c9a..0000000000
--- 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 662375a769..e11235a814 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 2a15c052f1..235fd2a073 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/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index ca5abcb059..d20ac1f881 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -228,6 +228,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 237ffd33a7..356697024d 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -228,6 +228,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/compose.js b/app/javascript/mastodon/reducers/compose.js
index 67d55f66f0..1622871b8f 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);
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index d71ae00aec..19a02f5b15 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 12bcc2583f..2e1878cf78 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/application.scss b/app/javascript/styles/application.scss
index 0990a4f259..4bce741876 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 3084295738..61e330a262 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2107,6 +2107,7 @@ a.account__display-name {
&__append {
flex: 1 1 auto;
position: relative;
+ min-height: 120px;
}
}
@@ -2900,7 +2901,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);
}
}
@@ -2909,11 +2909,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);
}
@@ -3789,25 +3798,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;
@@ -3841,22 +3831,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;
@@ -3905,124 +3879,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;
@@ -4045,92 +3901,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;
@@ -5030,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;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 44fc1e5386..8de53ca986 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/introduction.scss b/app/javascript/styles/mastodon/introduction.scss
new file mode 100644
index 0000000000..222d8f60e8
--- /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;
+ }
+}
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index c863e3b4fe..87e633c704 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 71264bc9f1..60ad0788ea 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -95,10 +95,10 @@ 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')) }
+ scope :popular, -> { order('account_stats.followers_count desc') }
delegate :email,
:unconfirmed_email,
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index d19b20c483..b57807d1c2 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/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 38c578de29..7979c312e5 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/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index f70eb964a6..88706def7b 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 5545df54c3..c1c0f4b871 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -7,9 +7,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/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb
index 4a40e5c8bd..8e8a359735 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
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index eda99e24ca..4de1e4e266 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 d2dcef9a41..80d2decd3a 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 a5a3c01845..1bba55f0fb 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 342cad91ce..9d41f353f7 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 ca139a35e9..d6f071c789 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 15307c76eb..c96438bc33 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 de3070e8ab..c171d93425 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 29d03859da..7c4b6e5ed4 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 06c8d28d74..21e4236a82 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 b1a34efbda..8c654034c2 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 9e798067f2..b65e02210d 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 e0bf082b21..0e4498eee4 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 135fee51fa..25d157155d 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 c0cfbf4659..918c69cc26 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]
diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb
index 9f7870bcd3..b219682232 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
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 0000000000..c71935df21
--- /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
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 0000000000..ad5ff400f5
--- /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
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 0000000000..159792ee01
--- /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
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 0000000000..12a214a83a
--- /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
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 0000000000..a22c2cbea5
--- /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
diff --git a/spec/controllers/concerns/accountable_concern_spec.rb b/spec/controllers/concerns/accountable_concern_spec.rb
new file mode 100644
index 0000000000..e3c06b4947
--- /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
diff --git a/spec/fixtures/requests/windows-1251.txt b/spec/fixtures/requests/windows-1251.txt
new file mode 100644
index 0000000000..f573e28b24
--- /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 88c5339db4..50c60aafd1 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/日本語') }