commit: bbbb8e40f52cd05e8bbeec1901990144b87fea55
parent: 5b1c13505f9750c819505028a21bd02d98e8e2f9
Author: Morgan Bazalgette <the@howl.moe>
Date: Mon, 9 Apr 2018 21:54:59 +0200
Merge branch 'master' of github.com:tootsuite/mastodon
Diffstat:
46 files changed, 410 insertions(+), 88 deletions(-)
diff --git a/.dockerignore b/.dockerignore
@@ -11,3 +11,4 @@ vendor/bundle
*~
postgres
redis
+elasticsearch
diff --git a/.gitignore b/.gitignore
@@ -36,9 +36,10 @@ config/deploy/*
.vscode/
.idea/
-# Ignore postgres + redis volume optionally created by docker-compose
+# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
postgres
redis
+elasticsearch
# Ignore Apple files
.DS_Store
diff --git a/Gemfile b/Gemfile
@@ -35,6 +35,7 @@ gem 'devise-two-factor', '~> 3.0'
group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.0'
end
+
gem 'net-ldap', '~> 0.10'
gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
@@ -79,6 +80,7 @@ gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 3.4'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
+gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations'
gem 'tty-command'
gem 'tty-prompt'
diff --git a/Gemfile.lock b/Gemfile.lock
@@ -550,6 +550,7 @@ GEM
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-ruby (1.2.1)
+ stoplight (2.1.3)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.1.9)
@@ -716,6 +717,7 @@ DEPENDENCIES
simple_form (~> 3.4)
simplecov (~> 0.14)
sprockets-rails (~> 3.2)
+ stoplight (~> 2.1.3)
streamio-ffmpeg (~> 3.0)
strong_migrations
tty-command
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
@@ -12,7 +12,7 @@ module Admin
def index
authorize :status, :index?
- @statuses = @account.statuses
+ @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -13,6 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
def update
@account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
+ UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
@@ -22,4 +23,15 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
def account_params
params.permit(:display_name, :note, :avatar, :header, :locked)
end
+
+ def user_settings_params
+ return nil unless params.key?(:source)
+
+ source_params = params.require(:source)
+
+ {
+ 'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy),
+ 'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
+ }
+ end
end
diff --git a/app/controllers/concerns/remote_account_controller_concern.rb b/app/controllers/concerns/remote_account_controller_concern.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module RemoteAccountControllerConcern
+ extend ActiveSupport::Concern
+
+ included do
+ layout 'public'
+ before_action :set_account
+ before_action :check_account_suspension
+ end
+
+ private
+
+ def set_account
+ @account = Account.find_remote!(params[:acct])
+ end
+
+ def check_account_suspension
+ gone if @account.suspended?
+ end
+end
diff --git a/app/controllers/remote_unfollows.rb b/app/controllers/remote_unfollows.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class RemoteUnfollowsController < ApplicationController
+ layout 'modal'
+
+ before_action :authenticate_user!
+ before_action :set_body_classes
+
+ def create
+ @account = unfollow_attempt.try(:target_account)
+
+ if @account.nil?
+ render :error
+ else
+ render :success
+ end
+ rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
+ render :error
+ end
+
+ private
+
+ def unfollow_attempt
+ username, domain = acct_without_prefix.split('@')
+ UnfollowService.new.call(current_account, Account.find_remote!(username, domain))
+ end
+
+ def acct_without_prefix
+ acct_params.gsub(/\Aacct:/, '')
+ end
+
+ def acct_params
+ params.fetch(:acct, '')
+ end
+
+ def set_body_classes
+ @body_classes = 'modal-layout'
+ end
+end
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
@@ -1,5 +1,5 @@
import api, { getLinks } from '../api';
-import asyncDB from '../storage/db';
+import openDB from '../storage/db';
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
@@ -94,12 +94,15 @@ export function fetchAccount(id) {
dispatch(fetchAccountRequest(id));
- asyncDB.then(db => getFromDB(
+ openDB().then(db => getFromDB(
dispatch,
getState,
db.transaction('accounts', 'read').objectStore('accounts').index('id'),
id
- )).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
+ ).then(() => db.close(), error => {
+ db.close();
+ throw error;
+ })).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data));
})).then(() => {
dispatch(fetchAccountSuccess());
diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
@@ -1,3 +1,4 @@
+import { autoPlayGif } from '../../initial_state';
import { putAccounts, putStatuses } from '../../storage/modifier';
import { normalizeAccount, normalizeStatus } from './normalizer';
@@ -44,7 +45,7 @@ export function importFetchedAccounts(accounts) {
}
accounts.forEach(processAccount);
- putAccounts(normalAccounts);
+ putAccounts(normalAccounts, !autoPlayGif);
return importAccounts(normalAccounts);
}
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
@@ -1,5 +1,5 @@
import api from '../api';
-import asyncDB from '../storage/db';
+import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
@@ -92,12 +92,17 @@ export function fetchStatus(id) {
dispatch(fetchStatusRequest(id, skipLoading));
- asyncDB.then(db => {
+ openDB().then(db => {
const transaction = db.transaction(['accounts', 'statuses'], 'read');
const accountIndex = transaction.objectStore('accounts').index('id');
const index = transaction.objectStore('statuses').index('id');
- return getFromDB(dispatch, getState, accountIndex, index, id);
+ return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
+ db.close();
+ }, error => {
+ db.close();
+ throw error;
+ });
}).then(() => {
dispatch(fetchStatusSuccess(skipLoading));
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
@@ -31,6 +31,8 @@ export default class Status extends ImmutablePureComponent {
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
+ onDirect: PropTypes.func,
+ onMention: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
@@ -9,6 +9,7 @@ import { me } from '../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
+ direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
@@ -36,6 +37,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
+ onDirect: PropTypes.func,
onMention: PropTypes.func,
onBlock: PropTypes.func,
onMuteConversation: PropTypes.func,
@@ -84,6 +86,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
+ handleDirectClick = () => {
+ this.props.onDirect(this.props.status.get('account'), this.context.router.history);
+ }
+
handleBlockClick = () => {
this.props.onBlock(this.props.status.get('account'));
}
@@ -121,6 +127,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+ menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
}
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
@@ -5,6 +5,7 @@ import { makeGetStatus } from '../selectors';
import {
replyCompose,
mentionCompose,
+ directCompose,
} from '../actions/compose';
import {
reblog,
@@ -85,6 +86,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
+ onDirect (account, router) {
+ dispatch(directCompose(account, router));
+ },
+
onMention (account, router) {
dispatch(mentionCompose(account, router));
},
diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
@@ -38,7 +38,8 @@ const getFrequentlyUsedEmojis = createSelector([
.toArray();
if (emojis.length < DEFAULTS.length) {
- emojis = emojis.concat(DEFAULTS.slice(0, DEFAULTS.length - emojis.length));
+ let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
+ emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
}
return emojis;
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
@@ -23,9 +23,9 @@ const messages = defineMessages({
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
});
-const mapStateToProps = state => ({
+const mapStateToProps = (state, ownProps) => ({
columns: state.getIn(['settings', 'columns']),
- showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
+ showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
});
@connect(mapStateToProps)
@@ -37,6 +37,7 @@ export default class Compose extends React.PureComponent {
columns: ImmutablePropTypes.list.isRequired,
multiColumn: PropTypes.bool,
showSearch: PropTypes.bool,
+ isSearchPage: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -57,7 +58,7 @@ export default class Compose extends React.PureComponent {
}
render () {
- const { multiColumn, showSearch, intl } = this.props;
+ const { multiColumn, showSearch, isSearchPage, intl } = this.props;
let header = '';
@@ -88,7 +89,7 @@ export default class Compose extends React.PureComponent {
<div className='drawer'>
{header}
- <SearchContainer />
+ {(multiColumn || isSearchPage) && <SearchContainer /> }
<div className='drawer__pager'>
<div className='drawer__inner' onFocus={this.onFocus}>
@@ -100,7 +101,7 @@ export default class Compose extends React.PureComponent {
)}
</div>
- <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+ <Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
{({ x }) => (
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
<SearchResultsContainer />
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -8,6 +8,7 @@ import { me } from '../../../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
+ direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
@@ -32,6 +33,7 @@ export default class ActionBar extends React.PureComponent {
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
+ onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMuteConversation: PropTypes.func,
onBlock: PropTypes.func,
@@ -55,6 +57,10 @@ export default class ActionBar extends React.PureComponent {
this.props.onDelete(this.props.status);
}
+ handleDirectClick = () => {
+ this.props.onDirect(this.props.status.get('account'), this.context.router.history);
+ }
+
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
@@ -92,6 +98,7 @@ export default class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+ menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
}
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
@@ -17,6 +17,7 @@ import {
import {
replyCompose,
mentionCompose,
+ directCompose,
} from '../../actions/compose';
import { blockAccount } from '../../actions/accounts';
import {
@@ -138,6 +139,10 @@ export default class Status extends ImmutablePureComponent {
}
}
+ handleDirectClick = (account, router) => {
+ this.props.dispatch(directCompose(account, router));
+ }
+
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
}
@@ -365,6 +370,7 @@ export default class Status extends ImmutablePureComponent {
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onDelete={this.handleDeleteClick}
+ onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
onMuteConversation={this.handleConversationMuteClick}
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -8,6 +8,7 @@ import { isUserTouching } from '../../../is_mobile';
export const links = [
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
+ <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
@@ -139,6 +139,8 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
+ <WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
+
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
@@ -198,6 +198,10 @@
"id": "status.delete"
},
{
+ "defaultMessage": "Direct message @{name}",
+ "id": "status.direct"
+ },
+ {
"defaultMessage": "Mention @{name}",
"id": "status.mention"
},
@@ -1371,6 +1375,10 @@
"id": "status.delete"
},
{
+ "defaultMessage": "Direct message @{name}",
+ "id": "status.direct"
+ },
+ {
"defaultMessage": "Mention @{name}",
"id": "status.mention"
},
@@ -1718,6 +1726,10 @@
"id": "tabs_bar.notifications"
},
{
+ "defaultMessage": "Search",
+ "id": "tabs_bar.search"
+ },
+ {
"defaultMessage": "Local",
"id": "tabs_bar.local_timeline"
},
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
@@ -240,6 +240,7 @@
"status.block": "Block @{name}",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Delete",
+ "status.direct": "Direct message @{name}",
"status.embed": "Embed",
"status.favourite": "Favourite",
"status.load_more": "Load more",
@@ -269,6 +270,7 @@
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
+ "tabs_bar.search": "Search",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
@@ -269,6 +269,7 @@
"tabs_bar.home": "Strona główna",
"tabs_bar.local_timeline": "Lokalne",
"tabs_bar.notifications": "Powiadomienia",
+ "tabs_bar.search": "Szukaj",
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
"upload_area.title": "Przeciągnij i upuść aby wysłać",
"upload_button.label": "Dodaj zawartość multimedialną",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
@@ -189,7 +189,7 @@
"onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes que se juntam para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.",
"onboarding.page_one.full_handle": "Seu nome de usuário completo",
"onboarding.page_one.handle_hint": "Isso é o que você diz aos seus amigos para que eles possam te mandar mensagens ou te seguir a partir de outra instância.",
- "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
+ "onboarding.page_one.welcome": "Boas-vindas ao Mastodon!",
"onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
"onboarding.page_six.almost_done": "Quase acabando...",
"onboarding.page_six.appetoot": "Bom Apepost!",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
@@ -189,7 +189,7 @@
"onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes ligados entre si para fazer uma grande rede social. Nós chamamos instâncias a estes servidores.",
"onboarding.page_one.full_handle": "O teu nome de utilizador completo",
"onboarding.page_one.handle_hint": "Isto é o que dizes aos teus amigos para pesquisar.",
- "onboarding.page_one.welcome": "Bem-vindo(a) ao Mastodon!",
+ "onboarding.page_one.welcome": "Boas-vindas ao Mastodon!",
"onboarding.page_six.admin": "O administrador da tua instância é {admin}.",
"onboarding.page_six.almost_done": "Quase pronto...",
"onboarding.page_six.appetoot": "Have fun!",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
@@ -259,16 +259,18 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_PROGRESS:
return state.set('progress', Math.round((action.loaded / action.total) * 100));
case COMPOSE_MENTION:
- return state
- .update('text', text => `${text}@${action.account.get('acct')} `)
- .set('focusDate', new Date())
- .set('idempotencyKey', uuid());
+ return state.withMutations(map => {
+ map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
+ map.set('focusDate', new Date());
+ map.set('idempotencyKey', uuid());
+ });
case COMPOSE_DIRECT:
- return state
- .update('text', text => `@${action.account.get('acct')} `)
- .set('privacy', 'direct')
- .set('focusDate', new Date())
- .set('idempotencyKey', uuid());
+ return state.withMutations(map => {
+ map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
+ map.set('privacy', 'direct');
+ map.set('focusDate', new Date());
+ map.set('idempotencyKey', uuid());
+ });
case COMPOSE_SUGGESTIONS_CLEAR:
return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
case COMPOSE_SUGGESTIONS_READY:
diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js
@@ -1,3 +1,4 @@
+import { freeStorage } from '../storage/modifier';
import './web_push_notifications';
function openSystemCache() {
@@ -51,8 +52,10 @@ self.addEventListener('fetch', function(event) {
event.respondWith(asyncResponse.then(async response => {
if (response.ok || response.type === 'opaqueredirect') {
- const cache = await asyncCache;
- await cache.delete('/web');
+ await Promise.all([
+ asyncCache.then(cache => cache.delete('/web')),
+ indexedDB.deleteDatabase('mastodon'),
+ ]);
}
return response;
@@ -66,7 +69,11 @@ self.addEventListener('fetch', function(event) {
const fetched = await fetch(event.request);
if (fetched.ok) {
- await cache.put(event.request.url, fetched.clone());
+ try {
+ await cache.put(event.request.url, fetched.clone());
+ } finally {
+ freeStorage();
+ }
}
return fetched;
diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js
@@ -1,15 +1,14 @@
-import { me } from '../initial_state';
-
-export default new Promise((resolve, reject) => {
+export default () => new Promise((resolve, reject) => {
+ // ServiceWorker is required to synchronize the login state.
// Microsoft Edge 17 does not support getAll according to:
// Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
// https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
- if (!me || !('getAll' in IDBObjectStore.prototype)) {
+ if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) {
reject();
return;
}
- const request = indexedDB.open('mastodon:' + me);
+ const request = indexedDB.open('mastodon');
request.onerror = reject;
request.onsuccess = ({ target }) => resolve(target.result);
diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js
@@ -1,13 +1,14 @@
-import asyncDB from './db';
-import { autoPlayGif } from '../initial_state';
+import openDB from './db';
const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
-const avatarKey = autoPlayGif ? 'avatar' : 'avatar_static';
-const limit = 1024;
+const storageMargin = 8388608;
+const storeLimit = 1024;
-// ServiceWorker and Cache API is not available on iOS 11
-// https://webkit.org/status/#specification-service-workers
-const asyncCache = window.caches ? caches.open('mastodon-system') : Promise.reject();
+function openCache() {
+ // ServiceWorker and Cache API is not available on iOS 11
+ // https://webkit.org/status/#specification-service-workers
+ return self.caches ? caches.open('mastodon-system') : Promise.reject();
+}
function printErrorIfAvailable(error) {
if (error) {
@@ -16,7 +17,7 @@ function printErrorIfAvailable(error) {
}
function put(name, objects, onupdate, oncreate) {
- return asyncDB.then(db => new Promise((resolve, reject) => {
+ return openDB().then(db => (new Promise((resolve, reject) => {
const putTransaction = db.transaction(name, 'readwrite');
const putStore = putTransaction.objectStore(name);
const putIndex = putStore.index('id');
@@ -53,7 +54,7 @@ function put(name, objects, onupdate, oncreate) {
const count = readStore.count();
count.onsuccess = () => {
- const excess = count.result - limit;
+ const excess = count.result - storeLimit;
if (excess > 0) {
const retrieval = readStore.getAll(null, excess);
@@ -69,11 +70,17 @@ function put(name, objects, onupdate, oncreate) {
};
putTransaction.onerror = reject;
+ })).then(resolved => {
+ db.close();
+ return resolved;
+ }, error => {
+ db.close();
+ throw error;
}));
}
function evictAccountsByRecords(records) {
- asyncDB.then(db => {
+ return openDB().then(db => {
const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
const accounts = transaction.objectStore('accounts');
const accountsIdIndex = accounts.index('id');
@@ -83,7 +90,7 @@ function evictAccountsByRecords(records) {
function evict(toEvict) {
toEvict.forEach(record => {
- asyncCache
+ openCache()
.then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
.catch(printErrorIfAvailable);
@@ -98,6 +105,8 @@ function evictAccountsByRecords(records) {
}
evict(records);
+
+ db.close();
}).catch(printErrorIfAvailable);
}
@@ -106,8 +115,9 @@ export function evictStatus(id) {
}
export function evictStatuses(ids) {
- asyncDB.then(db => {
- const store = db.transaction('statuses', 'readwrite').objectStore('statuses');
+ return openDB().then(db => {
+ const transaction = db.transaction('statuses', 'readwrite');
+ const store = transaction.objectStore('statuses');
const idIndex = store.index('id');
const reblogIndex = store.index('reblog');
@@ -118,14 +128,17 @@ export function evictStatuses(ids) {
idIndex.getKey(id).onsuccess =
({ target }) => target.result && store.delete(target.result);
});
+
+ db.close();
}).catch(printErrorIfAvailable);
}
function evictStatusesByRecords(records) {
- evictStatuses(records.map(({ id }) => id));
+ return evictStatuses(records.map(({ id }) => id));
}
-export function putAccounts(records) {
+export function putAccounts(records, avatarStatic) {
+ const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
const newURLs = [];
put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
@@ -135,7 +148,7 @@ export function putAccounts(records) {
const oldURL = target.result[key];
if (newURL !== oldURL) {
- asyncCache
+ openCache()
.then(cache => cache.delete(oldURL))
.catch(printErrorIfAvailable);
}
@@ -153,11 +166,12 @@ export function putAccounts(records) {
}, (newRecord, oncomplete) => {
newURLs.push(newRecord[avatarKey]);
oncomplete();
- }).then(records => {
- evictAccountsByRecords(records);
- asyncCache
- .then(cache => cache.addAll(newURLs))
- .catch(printErrorIfAvailable);
+ }).then(records => Promise.all([
+ evictAccountsByRecords(records),
+ openCache().then(cache => cache.addAll(newURLs)),
+ ])).then(freeStorage, error => {
+ freeStorage();
+ throw error;
}).catch(printErrorIfAvailable);
}
@@ -166,3 +180,27 @@ export function putStatuses(records) {
.then(evictStatusesByRecords)
.catch(printErrorIfAvailable);
}
+
+export function freeStorage() {
+ return navigator.storage.estimate().then(({ quota, usage }) => {
+ if (usage + storageMargin < quota) {
+ return null;
+ }
+
+ return openDB().then(db => new Promise((resolve, reject) => {
+ const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
+
+ retrieval.onsuccess = () => {
+ if (retrieval.result.length > 0) {
+ resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
+ } else {
+ resolve(caches.delete('mastodon-system'));
+ }
+ };
+
+ retrieval.onerror = reject;
+
+ db.close();
+ }));
+ });
+}
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
@@ -17,21 +17,25 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
end
def delete_note
- status = Status.find_by(uri: object_uri, account: @account)
- status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
+ @status = Status.find_by(uri: object_uri, account: @account)
+ @status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
delete_later!(object_uri)
- return if status.nil?
+ return if @status.nil?
- forward_for_reblogs(status)
- delete_now!(status)
+ if @status.public_visibility? || @status.unlisted_visibility?
+ forward_for_reply
+ forward_for_reblogs
+ end
+
+ delete_now!
end
- def forward_for_reblogs(status)
+ def forward_for_reblogs
return if @json['signature'].blank?
- rebloggers_ids = status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
+ rebloggers_ids = @status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
inboxes = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url]
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
@@ -39,8 +43,22 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
end
end
- def delete_now!(status)
- RemoveStatusService.new.call(status)
+ def replied_to_status
+ return @replied_to_status if defined?(@replied_to_status)
+ @replied_to_status = @status.thread
+ end
+
+ def reply_to_local?
+ !replied_to_status.nil? && replied_to_status.account.local?
+ end
+
+ def forward_for_reply
+ return unless @json['signature'].present? && reply_to_local?
+ ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
+ end
+
+ def delete_now!
+ RemoveStatusService.new.call(@status)
end
def payload
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
@@ -83,7 +83,7 @@ class UserSettingsDecorator
end
def boolean_cast_setting(key)
- settings[key] == '1'
+ ActiveModel::Type::Boolean.new.cast(settings[key])
end
def coerced_settings(key)
@@ -91,7 +91,7 @@ class UserSettingsDecorator
end
def coerce_values(params_hash)
- params_hash.transform_values { |x| x == '1' }
+ params_hash.transform_values { |x| ActiveModel::Type::Boolean.new.cast(x) }
end
def change?(key)
diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb
@@ -15,16 +15,12 @@ module StatusThreadingConcern
def ancestor_ids
Rails.cache.fetch("ancestors:#{id}") do
- ancestors_without_self.pluck(:id)
+ ancestor_statuses.pluck(:id)
end
end
- def ancestors_without_self
- ancestor_statuses - [self]
- end
-
def ancestor_statuses
- Status.find_by_sql([<<-SQL.squish, id: id])
+ Status.find_by_sql([<<-SQL.squish, id: in_reply_to_id])
WITH RECURSIVE search_tree(id, in_reply_to_id, path)
AS (
SELECT id, in_reply_to_id, ARRAY[id]
@@ -43,11 +39,7 @@ module StatusThreadingConcern
end
def descendant_ids
- descendants_without_self.pluck(:id)
- end
-
- def descendants_without_self
- descendant_statuses - [self]
+ descendant_statuses.pluck(:id)
end
def descendant_statuses
@@ -56,7 +48,7 @@ module StatusThreadingConcern
AS (
SELECT id, ARRAY[id]
FROM statuses
- WHERE id = :id
+ WHERE in_reply_to_id = :id
UNION ALL
SELECT statuses.id, path || statuses.id
FROM search_tree
diff --git a/app/views/accounts/_follow_button.html.haml b/app/views/accounts/_follow_button.html.haml
@@ -8,16 +8,16 @@
- if user_signed_in? && current_account.id != account.id && !requested
.controls
- if following
- = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do
+ = link_to (account.local? ? account_unfollow_path(account) : remote_unfollow_path(acct: account.acct)), data: { method: :post }, class: 'icon-button' do
= fa_icon 'user-times'
= t('accounts.unfollow')
- else
- = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
+ = link_to (account.local? ? account_follow_path(account) : authorize_follow_path(acct: account.acct)), data: { method: :post }, class: 'icon-button' do
= fa_icon 'user-plus'
= t('accounts.follow')
- elsif !user_signed_in?
.controls
.remote-follow
- = link_to account_remote_follow_path(account), class: 'icon-button' do
+ = link_to (account.local? ? account_remote_follow_path(account) : "web+mastodon://follow?uri=#{account.uri}"), class: 'icon-button' do
= fa_icon 'user-plus'
= t('accounts.remote_follow')
diff --git a/app/views/accounts/_follow_grid.html.haml b/app/views/accounts/_follow_grid.html.haml
@@ -2,6 +2,6 @@
- if accounts.empty?
= render partial: 'accounts/nothing_here'
- else
- = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: true
+ = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in?
= paginate follows
diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml
@@ -0,0 +1,13 @@
+.account-card
+ .detailed-status__display-name
+ %div
+ = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar'
+
+ %span.display-name
+ - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account)
+ = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
+ %strong.emojify= display_name(account)
+ %span @#{account.acct}
+
+ - if account.note?
+ .account__header__content.emojify= Formatter.instance.simplified_format(account)
diff --git a/app/views/remote_unfollows/_post_follow_actions.html.haml b/app/views/remote_unfollows/_post_follow_actions.html.haml
@@ -0,0 +1,4 @@
+.post-follow-actions
+ %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block'
+ %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@account), class: 'button button--block'
+ %div= t('authorize_follow.post_follow.close')
diff --git a/app/views/remote_unfollows/error.html.haml b/app/views/remote_unfollows/error.html.haml
@@ -0,0 +1,3 @@
+.form-container
+ .flash-message#error_explanation
+ = t('remote_unfollow.error')
diff --git a/app/views/remote_unfollows/success.html.haml b/app/views/remote_unfollows/success.html.haml
@@ -0,0 +1,10 @@
+- content_for :page_title do
+ = t('remote_unfollow.title', acct: @account.acct)
+
+.form-container
+ .follow-prompt
+ %h2= t('remote_unfollow.unfollowed')
+
+ = render 'card', account: @account
+
+ = render 'post_follow_actions'
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
@@ -12,9 +12,7 @@ class ActivityPub::DeliveryWorker
@source_account = Account.find(source_account_id)
@inbox_url = inbox_url
- perform_request do |response|
- raise Mastodon::UnexpectedResponseError, response unless response_successful? response
- end
+ perform_request
failure_tracker.track_success!
rescue => e
@@ -30,8 +28,14 @@ class ActivityPub::DeliveryWorker
request.add_headers(HEADERS)
end
- def perform_request(&block)
- build_request.perform(&block)
+ def perform_request
+ light = Stoplight(@inbox_url) do
+ build_request.perform do |response|
+ raise Mastodon::UnexpectedResponseError, response unless response_successful?(response)
+ end
+ end
+
+ light.run
end
def response_successful?(response)
diff --git a/config/initializers/stoplight.rb b/config/initializers/stoplight.rb
@@ -0,0 +1,3 @@
+require 'stoplight'
+
+Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(Redis.current)
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
@@ -700,6 +700,83 @@ pl:
reblogged: podbił
sensitive_content: Wrażliwa zawartość
terms:
+ body_html: |
+ <h2>Polityka prywatności</h2>
+ <h3 id="collect">Jakie informacje zbieramy?</h3>
+
+ <ul>
+ <li><em>Podstawowe informacje o koncie</em>: Podczas rejestracji na tym serwerze, możesz zostać poproszony o wprowadzenie nazwy użytkownika, adresu e-mail i hasła. Możesz także wprowadzić dodatkowe informacje o profilu, takie jak nazwa wyświetlana i biografia oraz wysłać awatar i obraz nagłówka. Nazwa użytkownika, nazwa wyświetlana, biografia, awatar i obraz nagłówka są zawsze widoczne dla wszystkich.</li>
+ <li><em>Wpisy, śledzenie i inne publiczne informacje</em>: Lista osób które śledzisz jest widoczna publicznie, tak jak lista osób, które Cię śledzą. Jeżeli dodasz wpis, data i czas jego utworzenia i aplikacja, z której go wysłano są przechowywane. Wiadomości mogą zawierać załączniki multimedialne, takie jak zdjęcia i filmy. Publiczne i niewidoczne wpisy są dostępne publicznie. Udostępniony wpis również jest widoczny publicznie. Twoje wpisy są dostarczane obserwującym, co oznacza że jego kopie mogą zostać dostarczone i być przechowywane na innych serwerach. Kiedy usuniesz wpis, przestaje być widoczny również dla osób śledzących Cię. „Podbijanie” i dodanie do ulubionych jest zawsze publiczne.</li>
+ <li><em>Wpisy bezpośrednie i tylko dla śledzących</em>: Wszystkie wpisy są przechowywane i przetwarzane na serwerze. Wpisy przeznaczone tylko dla śledzących są widoczne tylko dla nich i osób wspomnianych we wpisie, a wpisy bezpośrednie tylko dla wspimnianych. W wielu przypadkach oznacza to, że ich kopie są dostarczane i przechowywane na innych serwerach. Staramy się ograniczać zasięg tych wpisów wyłącznie do właściwych odbiorców, ale inne serwery mogą tego nie robić. Ważne jest, aby sprawdzać jakich serwerów używają osoby, które Cię śledzą. Możesz aktywować opcję pozwalającą na ręczne akceptowanie i odrzucanie nowych śledzących. <em>Pamiętaj, że właściciele serwerów mogą zobaczyć te wiadomości</em>, a odbiorcy mogą wykonać zrzut ekranu, skopiować lub udostępniać ten wpis. <em>Nie udostępniaj wrażliwych danych z użyciem Mastodona.</em></li>
+ <li><em>Adresy IP i inne metadane</em>: Kiedy zalogujesz się, przechowujemy adres IP użyty w trakcie logowania wraz z nazwą używanej przeglądarki. Wszystkie aktywne sesje możesz zobaczyć (i wygasić) w ustawieniach. Ostatnio używany adres IP jest przechowywany przez nas do 12 miesięcy. Możemy również przechowywać adresy IP wykorzystywane przy każdym działaniu na serwerze.</li>
+ </ul>
+
+ <hr class="spacer" />
+
+ <h3 id="use">W jakim celu wykorzystujecie informacje?</h3>
+
+ <p>Zebrane informacje mogą zostać użyte w następujące sposoby:</p>
+
+ <ul>
+ <li>Aby dostarczyć podstawową funkcjonalność Mastodona. Możesz wchodzić w interakcje z zawartością tworzoną przez innych tylko gdy jesteś zalogowany. Na przykład, możesz śledzić innych, aby widzieć ich wpisy w dostosowanej osi czasu.</li>
+ <li>Aby wspomóc moderację społeczności, na przykład porównując Twój adres IP ze znanymi, aby rozpoznać próbę obejścia blokady i inne naruszenia.</li>
+ <li>Adres e-mail może zostać wykorzystany, aby wysyłać Ci informacje, powiadomienia o osobach wchodzących w interakcje z tworzoną przez Ciebie zawartością, wysyłających Ci wiadomości, odpowiadać na zgłoszenia i inne żądania lub zapytania.</li>
+ </ul>
+
+ <hr class="spacer" />
+
+ <h3 id="protect">W jaki sposób chronimy Twoje dane?</h3>
+
+ <p>Wykorzystujemy różne zabezpieczenia, aby zapewnić bezpieczeństwo informacji, które wprowadzasz, wysyłasz lub do których uzyskujesz dostęp. Poza tym, sesja przeglądarki oraz ruch pomiędzy aplikacją a API jest zabezpieczany z użyciem SSL, a hasło jest hashowane z użyciem silnego algorytmu. Możesz też aktywować uwierzytelnianie dwustopniowe, aby lepiej zabezpieczyć dostęp do konta.</p>
+
+ <hr class="spacer" />
+
+ <h3 id="data-retention">Jaka jest nasza polityka przechowywania danych?</h3>
+
+ <p>Staramy się:</p>
+
+ <ul>
+ <li>Przechowywać logi zawierające adresy IP używane przy każdym żądaniu do serwera przez nie dłużej niż 90 dni.</li>
+ <li>Przechowywać adresy IP przypisane do użytkowników przez nie dłużej niż 12 miesięcy.</li>
+ </ul>
+
+ <p>Możesz zażądać i pobrać archiwum tworzonej zawartości, wliczając Twoje wpisy, załączniki multimedialne, awatar i zdjęcie nagłówka.</p>
+
+ <p>Możesz nieodwracalnie usunąć konto w każdej chwili.</p>
+
+ <hr class="spacer"/>
+
+ <h3 id="cookies">Czy używany plików cookies?</h3>
+
+ <p>Tak. Pliki cookies są małymi plikami, które strona lub dostawca jej usługi dostarcza na dysk twardy komputera z użyciem przeglądarki internetowej (jeżeli na to pozwoli). Pliki cookies pozwalają na rozpoznanie przeglądarki i – jeśli jesteś zarejestrowany – przypisanie jej do konta.</p>
+
+ <p>Wykorzystujemy pliki cookies, aby przechowywać preferencję użytkowników na przyszłe wizyty.</p>
+
+ <hr class="spacer" />
+
+ <h3 id="disclose">Czy przekazujemy informacje osobom trzecim?</h3>
+
+ <p>Nie sprzedajemy, nie wymieniamy i nie przekazujemy osobom trzecim informacji pozwalających na identyfikację Ciebie. Nie dotyczy to zaufanym dostawcom pomagającym w prowadzeniu lub obsługiwaniu użytkowników, jeżeli zgadzają się, aby nie przekazywać dalej tych informacji. Możemy również udostępnić informacje, jeżeli uważany to za wymagane przez prawo, konieczne do wypełnienia polityki strony, przestrzegania naszych lub cudzych praw, własności i bezpieczeństwa.</p>
+
+ <p>Twoja publiczna zawartość może zostać pobrana przez inne serwery w sieci. Wpisy publiczne i tylko dla śledzących są dostarczane na serwery, na których znajdują się śledzący Cię, a wiadomości bezpośrednie trafiają na serwery adresatów, jeżeli są oni użytkownikami innego serwera.</p>
+
+ <p>Kiedy pozwolisz aplikacji na dostęp do Twojego konta, w zależności od nadanych jej pozwoleń, może uzyskać dostęp do publicznych informacji, listy śledzonych, Twoich list, wszystkich wpisów i ulubionych. Aplikacje nie mogą uzyskać dostępu do Twojego adresu e-mail i hasła.</p>
+
+ <hr class="spacer" />
+
+ <h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3>
+
+ <p>Ta strona, produkty i usługi są przeznaczone dla osób, które ukończyły 13 lat. Jeżeli serwer znajduje się w USA, a nie ukończyłeś 13 roku życia, zgodnie z wymogami COPPA (<a href="https://pl.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Prawo o Ochronie Prywatności Dzieci w Internecie</a>), nie używaj tej strony.</p>
+
+ <hr class="spacer" />
+
+ <h3 id="changes">Zmiany w naszej polityce prywatności</h3>
+
+ <p>Jeżeli zdecydujemy się na zmiany w polityce prywatności, pojawią się na tej stronie.</p>
+
+ <p>Dokument jest dostępny na licencji CC-BY-SA. Ostatnio zmodyfikowano go 7 marca 2018, przetłumaczono 9 kwietnia 2018. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.</p>
+
+ <p>Bazowano na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.</p>
title: Zasady korzystania i polityka prywatności %{instance}
themes:
default: Mastodon
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
@@ -709,7 +709,7 @@ pt-BR:
tip_local_timeline: A timeline local é uma visão contínua das pessoas que estão em %{instance}. Esses são seus vizinhos próximos!
tip_mobile_webapp: Se o seu navegador móvel oferecer a opção de adicionar Mastodon à tela inicial, você pode receber notificações push. Vai funcionar quase como um aplicativo nativo!
tips: Dicas
- title: Boas-vindas à bordo, %{name}!
+ title: Boas-vindas a bordo, %{name}!
users:
invalid_email: O endereço de e-mail é inválido
invalid_otp_token: Código de autenticação inválido
diff --git a/config/routes.rb b/config/routes.rb
@@ -116,6 +116,7 @@ Rails.application.routes.draw do
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy
# Remote follow
+ resource :remote_unfollow, only: [:create]
resource :authorize_follow, only: [:show, :create]
resource :share, only: [:show, :create]
diff --git a/public/headers/original/missing.png b/public/headers/original/missing.png
Binary files differ.
diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
@@ -28,6 +28,10 @@ describe Api::V1::Accounts::CredentialsController do
note: "Hi!\n\nToot toot!",
avatar: fixture_file_upload('files/avatar.gif', 'image/gif'),
header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'),
+ source: {
+ privacy: 'unlisted',
+ sensitive: true,
+ }
}
end
@@ -42,6 +46,8 @@ describe Api::V1::Accounts::CredentialsController do
expect(user.account.note).to eq("Hi!\n\nToot toot!")
expect(user.account.avatar).to exist
expect(user.account.header).to exist
+ expect(user.setting_default_privacy).to eq('unlisted')
+ expect(user.setting_default_sensitive).to eq(true)
end
it 'queues up an account update distribution' do
diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb
@@ -69,5 +69,16 @@ describe UserSettingsDecorator do
settings.update(values)
expect(user.settings['system_font_ui']).to eq false
end
+
+ it 'decoerces setting values before applying' do
+ values = {
+ 'setting_delete_modal' => 'false',
+ 'setting_boost_modal' => 'true',
+ }
+
+ settings.update(values)
+ expect(user.settings['delete_modal']).to eq false
+ expect(user.settings['boost_modal']).to eq true
+ end
end
end