logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe
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:

M.dockerignore1+
M.gitignore3++-
MGemfile2++
MGemfile.lock2++
Mapp/controllers/admin/statuses_controller.rb2+-
Mapp/controllers/api/v1/accounts/credentials_controller.rb12++++++++++++
Aapp/controllers/concerns/remote_account_controller_concern.rb21+++++++++++++++++++++
Aapp/controllers/remote_unfollows.rb39+++++++++++++++++++++++++++++++++++++++
Mapp/javascript/mastodon/actions/accounts.js9++++++---
Mapp/javascript/mastodon/actions/importer/index.js3++-
Mapp/javascript/mastodon/actions/statuses.js11++++++++---
Mapp/javascript/mastodon/components/status.js2++
Mapp/javascript/mastodon/components/status_action_bar.js7+++++++
Mapp/javascript/mastodon/containers/status_container.js5+++++
Mapp/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js3++-
Mapp/javascript/mastodon/features/compose/index.js11++++++-----
Mapp/javascript/mastodon/features/status/components/action_bar.js7+++++++
Mapp/javascript/mastodon/features/status/index.js6++++++
Mapp/javascript/mastodon/features/ui/components/tabs_bar.js1+
Mapp/javascript/mastodon/features/ui/index.js2++
Mapp/javascript/mastodon/locales/defaultMessages.json12++++++++++++
Mapp/javascript/mastodon/locales/en.json2++
Mapp/javascript/mastodon/locales/pl.json1+
Mapp/javascript/mastodon/locales/pt-BR.json2+-
Mapp/javascript/mastodon/locales/pt.json2+-
Mapp/javascript/mastodon/reducers/compose.js20+++++++++++---------
Mapp/javascript/mastodon/service_worker/entry.js13++++++++++---
Mapp/javascript/mastodon/storage/db.js9++++-----
Mapp/javascript/mastodon/storage/modifier.js80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mapp/lib/activitypub/activity/delete.rb36+++++++++++++++++++++++++++---------
Mapp/lib/user_settings_decorator.rb4++--
Mapp/models/concerns/status_threading_concern.rb16++++------------
Mapp/views/accounts/_follow_button.html.haml6+++---
Mapp/views/accounts/_follow_grid.html.haml2+-
Aapp/views/remote_unfollows/_card.html.haml13+++++++++++++
Aapp/views/remote_unfollows/_post_follow_actions.html.haml4++++
Aapp/views/remote_unfollows/error.html.haml3+++
Aapp/views/remote_unfollows/success.html.haml10++++++++++
Mapp/workers/activitypub/delivery_worker.rb14+++++++++-----
Aconfig/initializers/stoplight.rb3+++
Mconfig/locales/pl.yml77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mconfig/locales/pt-BR.yml2+-
Mconfig/routes.rb1+
Mpublic/headers/original/missing.png0
Mspec/controllers/api/v1/accounts/credentials_controller_spec.rb6++++++
Mspec/lib/user_settings_decorator_spec.rb11+++++++++++
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