diff options
83 files changed, 748 insertions, 232 deletions
diff --git a/Dockerfile b/Dockerfile index aefcb44bd..eca90094c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM node:8.15-alpine as node -FROM ruby:2.6-alpine3.8 +FROM ruby:2.6-alpine3.9 LABEL maintainer="https://github.com/tootsuite/mastodon" \ description="Your self-hosted, globally interconnected microblogging community" @@ -24,19 +24,18 @@ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=node /usr/local/bin/npm /usr/local/bin/npm COPY --from=node /opt/yarn-* /opt/yarn -RUN apk -U upgrade \ - && apk add -t build-dependencies \ +RUN apk add --no-cache -t build-dependencies \ build-base \ icu-dev \ libidn-dev \ - libressl \ + openssl \ libtool \ libxml2-dev \ libxslt-dev \ postgresql-dev \ protobuf-dev \ python \ - && apk add \ + && apk add --no-cache \ ca-certificates \ ffmpeg \ file \ @@ -64,7 +63,7 @@ RUN apk -U upgrade \ && make install \ && libtool --finish /usr/local/lib \ && cd /mastodon \ - && rm -rf /tmp/* /var/cache/apk/* + && rm -rf /tmp/* COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ COPY stack-fix.c /lib diff --git a/Gemfile b/Gemfile index 51595a758..fcf97b028 100644 --- a/Gemfile +++ b/Gemfile @@ -108,15 +108,15 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.12' + gem 'capybara', '~> 3.13' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.9' - gem 'microformats', '~> 4.0' + gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false gem 'webmock', '~> 3.5' - gem 'parallel_tests', '~> 2.27' + gem 'parallel_tests', '~> 2.28' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 76bcdeda5..957ca70a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,7 +126,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.12.0) + capybara (3.13.2) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -268,7 +268,7 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_accept_language (2.1.1) - httplog (1.2.0) + httplog (1.2.1) rack (>= 1.0) rainbow (>= 2.0.0) i18n (1.5.3) @@ -337,9 +337,9 @@ GEM redis (>= 3.0.5) memory_profiler (0.9.12) method_source (0.9.2) - microformats (4.0.7) - json - nokogiri + microformats (4.1.0) + json (~> 2.1) + nokogiri (~> 1.8, >= 1.8.3) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) @@ -392,7 +392,7 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.13.0) - parallel_tests (2.27.1) + parallel_tests (2.28.0) parallel parser (2.6.0.0) ast (~> 2.4.0) @@ -671,7 +671,7 @@ DEPENDENCIES capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 3.12) + capybara (~> 3.13) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.3) @@ -712,7 +712,7 @@ DEPENDENCIES makara (~> 0.4) mario-redis-lock (~> 1.2) memory_profiler - microformats (~> 4.0) + microformats (~> 4.1) mime-types (~> 3.2) net-ldap (~> 0.10) nokogiri (~> 1.10) @@ -725,7 +725,7 @@ DEPENDENCIES ox (~> 2.10) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) - parallel_tests (~> 2.27) + parallel_tests (~> 2.28) pg (~> 1.1) pghero (~> 2.2) pkg-config (~> 1.3) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 3a4382850..442e99089 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -53,11 +53,12 @@ class AccountsController < ApplicationController private def show_pinned_statuses? - [replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none? + [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end def filtered_statuses default_statuses.tap do |statuses| + statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? statuses.merge!(no_replies_scope) unless replies_requested? end @@ -79,12 +80,15 @@ class AccountsController < ApplicationController Status.without_replies end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id) + end + def set_account @account = Account.find_local!(params[:username]) end def older_url - ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") pagination_url(max_id: @statuses.last.id) end @@ -93,7 +97,9 @@ class AccountsController < ApplicationController end def pagination_url(max_id: nil, min_id: nil) - if media_requested? + if tag_requested? + short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) + elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) @@ -110,6 +116,10 @@ class AccountsController < ApplicationController request.path.ends_with?('/with_replies') end + def tag_requested? + request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + end + def filtered_status_page(params) if params[:min_id].present? filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6c2a5c141..6fdc827cb 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) + statuses.merge!(hashtag_scope) if params[:tagged].present? statuses end @@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController Status.without_reblogs end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tagged])&.id) + end + def pagination_params(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 1e420b3e7..4e45445df 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :set_pack + before_action :set_body_classes include Localized @@ -16,6 +17,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio private + def set_body_classes + @body_classes = 'admin' + end + def store_current_location store_location_for(:user, request.url) end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb new file mode 100644 index 000000000..3a3241425 --- /dev/null +++ b/app/controllers/settings/featured_tags_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Settings::FeaturedTagsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_featured_tags, only: :index + before_action :set_featured_tag, except: [:index, :create] + before_action :set_most_used_tags, only: :index + + def index + @featured_tag = FeaturedTag.new + end + + def create + @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag.reset_data + + if @featured_tag.save + redirect_to settings_featured_tags_path + else + set_featured_tags + set_most_used_tags + + render :index + end + end + + def destroy + @featured_tag.destroy! + redirect_to settings_featured_tags_path + end + + private + + def set_featured_tag + @featured_tag = current_account.featured_tags.find(params[:id]) + end + + def set_featured_tags + @featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?) + end + + def set_most_used_tags + @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) + end + + def featured_tag_params + params.require(:featured_tag).permit(:name) + end +end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 1a0b73d16..76d599f08 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -29,6 +29,6 @@ class Settings::ProfilesController < Settings::BaseController end def set_account - @account = current_user.account + @account = current_account end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 780ea64b4..d74db6000 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -2,6 +2,7 @@ # Intentionally does not inherit from BaseController class Settings::SessionsController < ApplicationController + before_action :authenticate_user! before_action :set_session, only: :destroy def destroy diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index 3f5d7ef46..50cd48a9e 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -22,7 +22,7 @@ export function clearAlert() { }; }; -export function showAlert(title, message) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { return { type: ALERT_SHOW, title, @@ -44,6 +44,6 @@ export function showAlertForError(error) { return showAlert(title, message); } else { console.error(error); - return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); + return showAlert(); } } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index a4352faab..0be2a5cd4 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image'; import { importFetchedAccounts } from './importer'; import { updateTimeline } from './timelines'; import { showAlertForError } from './alerts'; +import { showAlert } from './alerts'; +import { defineMessages } from 'react-intl'; let cancelFetchComposeSuggestionsAccounts; @@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; +const messages = defineMessages({ + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, +}); + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -184,20 +190,32 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function (dispatch, getState) { - if (getState().getIn(['compose', 'media_attachments']).size > 3) { + const uploadLimit = 4; + const media = getState().getIn(['compose', 'media_attachments']); + const total = Array.from(files).reduce((a, v) => a + v.size, 0); + const progress = new Array(files.length).fill(0); + + if (files.length + media.size > uploadLimit) { + dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } - dispatch(uploadComposeRequest()); - resizeImage(files[0]).then(file => { - const data = new FormData(); - data.append('file', file); - - return api(getState).post('/api/v1/media', data, { - onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); - }).catch(error => dispatch(uploadComposeFail(error))); + for (const [i, f] of Array.from(files).entries()) { + if (media.size + i > 3) break; + + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + + return api(getState).post('/api/v1/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); + }; }; }; diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js index de2203a4b..e453730ba 100644 --- a/app/javascript/mastodon/components/intersection_observer_article.js +++ b/app/javascript/mastodon/components/intersection_observer_article.js @@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component { } updateStateAfterIntersection = (prevState) => { - if (prevState.isIntersecting && !this.entry.isIntersecting) { + if (prevState.isIntersecting !== false && !this.entry.isIntersecting) { scheduleIdleTask(this.hideIfNotIntersecting); } return { diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js index 5ee1d2f14..7bc7bbaa4 100644 --- a/app/javascript/mastodon/containers/compose_container.js +++ b/app/javascript/mastodon/containers/compose_container.js @@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import Compose from '../features/standalone/compose'; import initialState from '../initial_state'; +import { fetchCustomEmojis } from '../actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); @@ -17,6 +18,8 @@ if (initialState) { store.dispatch(hydrateStore(initialState)); } +store.dispatch(fetchCustomEmojis()); + export default class TimelineContainer extends React.PureComponent { static propTypes = { diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index b6fe770ea..db55ad70b 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -63,7 +63,7 @@ class UploadButton extends ImmutablePureComponent { key={resetFileKey} ref={this.setRef} type='file' - multiple={false} + multiple accept={acceptContentTypes.toArray().join(',')} onChange={this.handleChange} disabled={disabled} diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index e277a73c7..e1f84de27 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -160,7 +160,7 @@ class GettingStarted extends ImmutablePureComponent { {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> - <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li> + <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js index e712b2f7d..754477bb9 100644 --- a/app/javascript/mastodon/features/introduction/index.js +++ b/app/javascript/mastodon/features/introduction/index.js @@ -89,7 +89,7 @@ const FrameInteractions = ({ onNext }) => ( </div> <div className='introduction__action'> - <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button> + <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish toot-orial!' /></button> </div> </div> ); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index d640033eb..2b7d9c56f 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -124,7 +124,7 @@ class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} + emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} shouldUpdateScroll={shouldUpdateScroll} /> </Column> diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index bc6b18664..2e41f784d 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -97,7 +97,7 @@ class ReportModal extends ImmutablePureComponent { <div className='report-modal__container'> <div className='report-modal__comment'> - <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p> + <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p> <textarea className='setting-text light' diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index f01c2bf24..93e45678f 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -263,7 +263,7 @@ class UI extends React.PureComponent { this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length === 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 60f481076..eb3a7bbde 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -15,6 +15,15 @@ { "descriptors": [ { + "defaultMessage": "File upload limit exceeded.", + "id": "upload_error.limit" + } + ], + "path": "app/javascript/mastodon/actions/compose.json" + }, + { + "descriptors": [ + { "defaultMessage": "{name} mentioned you", "id": "notification.mention" }, @@ -1275,7 +1284,7 @@ "id": "getting_started.security" }, { - "defaultMessage": "About this instance", + "defaultMessage": "About this server", "id": "navigation_bar.info" }, { @@ -1448,7 +1457,7 @@ "id": "introduction.interactions.favourite.text" }, { - "defaultMessage": "Finish tutorial!", + "defaultMessage": "Finish toot-orial!", "id": "introduction.interactions.action" } ], @@ -1828,7 +1837,7 @@ "id": "column.public" }, { - "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "id": "empty_column.public" } ], @@ -2188,7 +2197,7 @@ "id": "report.target" }, { - "defaultMessage": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "defaultMessage": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "id": "report.hint" }, { @@ -2298,4 +2307,4 @@ ], "path": "app/javascript/mastodon/features/video/index.json" } -] \ No newline at end of file +] diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3bb157aeb..b079f256d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -132,7 +132,7 @@ "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -228,7 +228,7 @@ "navigation_bar.favourites": "Favourites", "navigation_bar.filters": "Muted words", "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.info": "About this instance", + "navigation_bar.info": "About this server", "navigation_bar.keyboard_shortcuts": "Hotkeys", "navigation_bar.lists": "Lists", "navigation_bar.misc": "Misc", @@ -281,7 +281,7 @@ "reply_indicator.cancel": "Cancel", "report.forward": "Forward to {target}", "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting {target}", @@ -347,6 +347,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Change preview", "upload_form.undo": "Delete", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index e0cba7764..4bcc11d70 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -132,7 +132,7 @@ "empty_column.lists": "ãŸã ãªã¹ãããããŸããããªã¹ããäœããšããã«è¡šç€ºãããŸãã", "empty_column.mutes": "ãŸã 誰ããã¥ãŒãããŠããŸããã", "empty_column.notifications": "ãŸã éç¥ããããŸãããä»ã®äººãšãµãåã£ãŠäŒè©±ãå§ããŸãããã", - "empty_column.public": "ããã«ã¯ãŸã äœããããŸããïŒ å ¬éã§äœããæçš¿ããããä»ã®ã€ã³ã¹ã¿ã³ã¹ã®ãŠãŒã¶ãŒããã©ããŒãããããŠãã£ã±ãã«ããŸããã", + "empty_column.public": "ããã«ã¯ãŸã äœããããŸããïŒ å ¬éã§äœããæçš¿ããããä»ã®ãµãŒããŒã®ãŠãŒã¶ãŒããã©ããŒãããããŠãã£ã±ãã«ããŸããã", "follow_request.authorize": "èš±å¯", "follow_request.reject": "æåŠ", "getting_started.developers": "éçº", @@ -228,7 +228,7 @@ "navigation_bar.favourites": "ãæ°ã«å ¥ã", "navigation_bar.filters": "ãã£ã«ã¿ãŒèšå®", "navigation_bar.follow_requests": "ãã©ããŒãªã¯ãšã¹ã", - "navigation_bar.info": "ãã®ã€ã³ã¹ã¿ã³ã¹ã«ã€ããŠ", + "navigation_bar.info": "ãã®ãµãŒããŒã«ã€ããŠ", "navigation_bar.keyboard_shortcuts": "ãããããŒ", "navigation_bar.lists": "ãªã¹ã", "navigation_bar.logout": "ãã°ã¢ãŠã", @@ -280,8 +280,8 @@ "relative_time.seconds": "{number}ç§å", "reply_indicator.cancel": "ãã£ã³ã»ã«", "report.forward": "{target} ã«è»¢éãã", - "report.forward_hint": "ãã®ã¢ã«ãŠã³ãã¯å¥ã®ã€ã³ã¹ã¿ã³ã¹ã«æå±ããŠããŸããéå ±å 容ãå¿åã§è»¢éããŸããïŒ", - "report.hint": "éå ±å 容ã¯ããªãã®ã€ã³ã¹ã¿ã³ã¹ã®ã¢ãã¬ãŒã¿ãŒãžéä¿¡ãããŸããéå ±çç±ãå ¥åããŠãã ããã:", + "report.forward_hint": "ãã®ã¢ã«ãŠã³ãã¯å¥ã®ãµãŒããŒã«æå±ããŠããŸããéå ±å 容ãå¿åã§è»¢éããŸããïŒ", + "report.hint": "éå ±å 容ã¯ããªãã®ãµãŒããŒã®ã¢ãã¬ãŒã¿ãŒãžéä¿¡ãããŸããéå ±çç±ãå ¥åããŠãã ããã:", "report.placeholder": "远å ã³ã¡ã³ã", "report.submit": "éå ±ãã", "report.target": "{target}ãããéå ±ãã", @@ -347,6 +347,7 @@ "ui.beforeunload": "Mastodonããé¢ãããšéä¿¡åã®æçš¿ã¯å€±ãããŸãã", "upload_area.title": "ãã©ãã°ïŒããããã§ã¢ããããŒã", "upload_button.label": "ã¡ãã£ã¢ã远å (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "ã¢ããããŒãã§ããäžéãè¶ ããŠããŸãã", "upload_form.description": "èŠèŠé害è ã®ããã®èª¬æ", "upload_form.focus": "çŠç¹", "upload_form.undo": "åé€", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 9f78c430f..e9e0c768d 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -347,6 +347,7 @@ "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Ä (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "Przekroczono limit plików do wysÅania.", "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzÄ cych", "upload_form.focus": "Dopasuj podglÄ d", "upload_form.undo": "UsuÅ", diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index d1608094f..bbdbc865e 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }); const getOrientation = (img, type = 'image/png') => new Promise(resolve => { - if (type !== 'image/jpeg') { + if (!['image/jpeg', 'image/webp'].includes(type)) { resolve(1); return; } diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 63a5c61b8..f4f458cf4 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -288,3 +288,7 @@ border-bottom: 0; } } + +.directory__tag .trends__item__current { + width: auto; +} diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 4e969601b..4dbbaa1e8 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -153,10 +153,15 @@ $content-width: 840px; font-weight: 500; } - .directory__tag a { + .directory__tag > a, + .directory__tag > div { box-shadow: none; } + .directory__tag .table-action-link .fa { + color: inherit; + } + .directory__tag h4 { font-size: 18px; font-weight: 700; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8c1115e76..e29abf4f3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -638,7 +638,6 @@ font-weight: 400; overflow: hidden; text-overflow: ellipsis; - white-space: pre-wrap; padding-top: 2px; color: $primary-text-color; @@ -662,6 +661,7 @@ p { margin-bottom: 20px; + white-space: pre-wrap; &:last-child { margin-bottom: 0; diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index c97337e4e..1eaf30c5b 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -269,7 +269,8 @@ box-sizing: border-box; margin-bottom: 10px; - a { + & > a, + & > div { display: flex; align-items: center; justify-content: space-between; @@ -279,7 +280,9 @@ text-decoration: none; color: inherit; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + } + & > a { &:hover, &:active, &:focus { @@ -287,7 +290,7 @@ } } - &.active a { + &.active > a { background: $ui-highlight-color; cursor: default; } diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb index 5b4972674..ae3c11b6a 100644 --- a/app/lib/activity_tracker.rb +++ b/app/lib/activity_tracker.rb @@ -4,6 +4,8 @@ class ActivityTracker EXPIRE_AFTER = 90.days.seconds class << self + include Redisable + def increment(prefix) key = [prefix, current_week].join(':') @@ -20,10 +22,6 @@ class ActivityTracker private - def redis - Redis.current - end - def current_week Time.zone.today.cweek end diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 87318fb1c..919678618 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -2,6 +2,7 @@ class ActivityPub::Activity include JsonLdHelper + include Redisable def initialize(json, account, **options) @json = json @@ -70,10 +71,6 @@ class ActivityPub::Activity @object_uri ||= value_or_id(@object) end - def redis - Redis.current - end - def distribute(status) crawl_links(status) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index a1b186f1c..4bc75dae8 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -4,6 +4,7 @@ require 'singleton' class FeedManager include Singleton + include Redisable MAX_ITEMS = 400 @@ -35,7 +36,7 @@ class FeedManager def unpush_from_home(account, status) return false unless remove_from_feed(:home, account.id, status) - Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -54,7 +55,7 @@ class FeedManager def unpush_from_list(list, status) return false unless remove_from_feed(:list, list.id, status) - Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -143,10 +144,6 @@ class FeedManager private - def redis - Redis.current - end - def push_update_required?(timeline_id) redis.exists("subscribed:#{timeline_id}") end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 05fd9eeb1..0653214f5 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -99,7 +99,7 @@ class Formatter end def encode_and_link_urls(html, accounts = nil, options = {}) - entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false) + entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) if accounts.is_a?(Hash) options = accounts @@ -199,6 +199,53 @@ class Formatter result.flatten.join end + UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/ + + def utf8_friendly_extractor(text, options = {}) + old_to_new_index = [0] + + escaped = text.chars.map do |c| + output = begin + if c.ord.to_s(16).length > 2 && UNICODE_ESCAPE_BLACKLIST_RE.match(c).nil? + CGI.escape(c) + else + c + end + end + + old_to_new_index << old_to_new_index.last + output.length + + output + end.join + + # Note: I couldn't obtain list_slug with @user/list-name format + # for mention so this requires additional check + special = Extractor.extract_urls_with_indices(escaped, options).map do |extract| + # exactly one of :url, :hashtag, :screen_name, :cashtag keys is present + key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first + + new_indices = [ + old_to_new_index.find_index(extract[:indices].first), + old_to_new_index.find_index(extract[:indices].last), + ] + + has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key) + value_indices = [ + new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $ + new_indices.last - 1, + ] + + next extract.merge( + :indices => new_indices, + key => text[value_indices.first..value_indices.last] + ) + end + + standard = Extractor.extract_entities_with_indices(text, options) + + Extractor.remove_overlapping_entities(special + standard) + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener' } diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb index c5933f3ad..db70f1998 100644 --- a/app/lib/ostatus/activity/base.rb +++ b/app/lib/ostatus/activity/base.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class OStatus::Activity::Base + include Redisable + def initialize(xml, account = nil, **options) @xml = xml @account = account @@ -66,8 +68,4 @@ class OStatus::Activity::Base Status.find_by(uri: uri) end end - - def redis - Redis.current - end end diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb index 017a9748d..188aa4a27 100644 --- a/app/lib/potential_friendship_tracker.rb +++ b/app/lib/potential_friendship_tracker.rb @@ -11,6 +11,8 @@ class PotentialFriendshipTracker }.freeze class << self + include Redisable + def record(account_id, target_account_id, action) return if account_id == target_account_id @@ -31,11 +33,5 @@ class PotentialFriendshipTracker return [] if account_ids.empty? Account.searchable.where(id: account_ids) end - - private - - def redis - Redis.current - end end end diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index e352000c3..7c0d60379 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -12,6 +12,7 @@ class AccountDomainBlock < ApplicationRecord include Paginable + include DomainNormalizable belongs_to :account validates :domain, presence: true, uniqueness: { scope: :account_id } diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 4e730451a..3ab8a0daa 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -56,5 +56,6 @@ module AccountAssociations # Hashtags has_and_belongs_to_many :tags + has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account end end diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb index 2d5ebfca3..5fff3ef5d 100644 --- a/app/models/concerns/account_avatar.rb +++ b/app/models/concerns/account_avatar.rb @@ -3,7 +3,7 @@ module AccountAvatar extend ActiveSupport::Concern - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes class_methods do diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb index 067e166eb..a748fdff7 100644 --- a/app/models/concerns/account_header.rb +++ b/app/models/concerns/account_header.rb @@ -3,7 +3,7 @@ module AccountHeader extend ActiveSupport::Concern - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes MAX_PIXELS = 750_000 # 1500x500px diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index dff3e5414..fb84058fc 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -10,6 +10,6 @@ module DomainNormalizable private def normalize_domain - self.domain = TagManager.instance.normalize_domain(domain) + self.domain = TagManager.instance.normalize_domain(domain&.strip) end end diff --git a/app/models/concerns/redisable.rb b/app/models/concerns/redisable.rb new file mode 100644 index 000000000..c6cf97359 --- /dev/null +++ b/app/models/concerns/redisable.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Redisable + extend ActiveSupport::Concern + + private + + def redis + Redis.current + end +end diff --git a/app/models/export.rb b/app/models/export.rb index a2520e9c2..fc4bb6964 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'csv' class Export diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb new file mode 100644 index 000000000..b5a10ad2d --- /dev/null +++ b/app/models/featured_tag.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: featured_tags +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# tag_id :bigint(8) +# statuses_count :bigint(8) default(0), not null +# last_status_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class FeaturedTag < ApplicationRecord + belongs_to :account, inverse_of: :featured_tags, required: true + belongs_to :tag, inverse_of: :featured_tags, required: true + + delegate :name, to: :tag, allow_nil: true + + validates :name, presence: true + validate :validate_featured_tags_limit, on: :create + + def name=(str) + self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s) + end + + def increment(timestamp) + update(statuses_count: statuses_count + 1, last_status_at: timestamp) + end + + def decrement(deleted_status_id) + update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) + end + + def reset_data + self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count + self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at + end + + private + + def validate_featured_tags_limit + errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10 + end +end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5bce88f25..0e8943ff8 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Feed + include Redisable + def initialize(type, id) @type = type @id = id @@ -27,8 +29,4 @@ class Feed def key FeedManager.instance.key(@type, @id) end - - def redis - Redis.current - end end diff --git a/app/models/import.rb b/app/models/import.rb index 55e970b0d..a7a0d8065 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -13,20 +13,30 @@ # data_file_size :integer # data_updated_at :datetime # account_id :bigint(8) not null +# overwrite :boolean default(FALSE), not null # class Import < ApplicationRecord - FILE_TYPES = ['text/plain', 'text/csv'].freeze + FILE_TYPES = %w(text/plain text/csv).freeze + MODES = %i(merge overwrite).freeze self.inheritance_column = false belongs_to :account - enum type: [:following, :blocking, :muting] + enum type: [:following, :blocking, :muting, :domain_blocking] validates :type, presence: true has_attached_file :data validates_attachment_content_type :data, content_type: FILE_TYPES validates_attachment_presence :data + + def mode + overwrite? ? :overwrite : :merge + end + + def mode=(str) + self.overwrite = str.to_sym == :overwrite + end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 601b14223..81397a18e 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -25,11 +25,11 @@ class MediaAttachment < ApplicationRecord enum type: [:image, :gifv, :video, :audio, :unknown] - IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze + IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze @@ -105,8 +105,8 @@ class MediaAttachment < ApplicationRecord convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES - validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video? - validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video? + validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv? + validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv? remotable_attachment :file, VIDEO_LIMIT include Attachmentable @@ -129,6 +129,10 @@ class MediaAttachment < ApplicationRecord file.blank? && remote_url.present? end + def video_or_gifv? + video? || gifv? + end + def to_param shortcode end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index a792b352b..f26ea0c74 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -25,7 +25,7 @@ # class PreviewCard < ApplicationRecord - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 1.megabytes self.inheritance_column = false diff --git a/app/models/tag.rb b/app/models/tag.rb index 99830ae92..4373e967b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -14,6 +14,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :accounts has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' + has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' @@ -23,6 +24,7 @@ class Tag < ApplicationRecord scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, :accounts_count=, diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 3a8be2164..148535c21 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -7,6 +7,8 @@ class TrendingTags THRESHOLD = 5 class << self + include Redisable + def record_use!(tag, account, at_time = Time.now.utc) return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot? @@ -59,9 +61,5 @@ class TrendingTags @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) end - - def redis - Redis.current - end end end diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 859ef0d14..cc8b9a4d4 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -52,6 +52,14 @@ class ManifestSerializer < ActiveModel::Serializer end def share_target - { url_template: 'share?title={title}&text={text}&url={url}' } + { + url_template: 'share?title={title}&text={text}&url={url}', + action: 'share', + params: { + title: 'title', + text: 'text', + url: 'url', + }, + } end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 487456f3a..5e3308428 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -212,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService end def clear_tombstones! - Tombstone.delete_all(account_id: @account.id) + Tombstone.where(account_id: @account.id).delete_all end def protocol_changed? diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 61c408926..d78f506c6 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -2,6 +2,7 @@ class BatchedRemoveStatusService < BaseService include StreamEntryRenderer + include Redisable # Delete given statuses and reblogs of them # Dispatch PuSH updates of the deleted statuses, but only local ones @@ -120,10 +121,6 @@ class BatchedRemoveStatusService < BaseService end end - def redis - Redis.current - end - def build_xml(stream_entry) return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 9d36a1449..92d8c864a 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class FollowService < BaseService + include Redisable + # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) @@ -67,10 +69,6 @@ class FollowService < BaseService follow end - def redis - Redis.current - end - def build_follow_request_xml(follow_request) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request)) end diff --git a/app/services/import_service.rb b/app/services/import_service.rb new file mode 100644 index 000000000..3f558626e --- /dev/null +++ b/app/services/import_service.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'csv' + +class ImportService < BaseService + ROWS_PROCESSING_LIMIT = 20_000 + + def call(import) + @import = import + @account = @import.account + @data = CSV.new(import_data).reject(&:blank?) + + case @import.type + when 'following' + import_follows! + when 'blocking' + import_blocks! + when 'muting' + import_mutes! + when 'domain_blocking' + import_domain_blocks! + end + end + + private + + def import_follows! + import_relationships!('follow', 'unfollow', @account.following, follow_limit) + end + + def import_blocks! + import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT) + end + + def import_mutes! + import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT) + end + + def import_domain_blocks! + items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + @account.domain_blocks.find_each do |domain_block| + if presence_hash[domain_block.domain] + items.delete(domain_block.domain) + else + @account.unblock_domain!(domain_block.domain) + end + end + end + + items.each do |domain| + @account.block_domain!(domain) + end + + AfterAccountDomainBlockWorker.push_bulk(items) do |domain| + [@account.id, domain] + end + end + + def import_relationships!(action, undo_action, overwrite_scope, limit) + items = @data.take(limit).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + overwrite_scope.find_each do |target_account| + if presence_hash[target_account.acct] + items.delete(target_account.acct) + else + Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action) + end + end + end + + Import::RelationshipWorker.push_bulk(items) do |acct| + [@account.id, acct, action] + end + end + + def import_data + Paperclip.io_adapters.for(@import.data).read + end + + def follow_limit + FollowLimitValidator.limit_for_account(@account) + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 5d431c42a..cfb266fbb 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PostStatusService < BaseService + include Redisable + MIN_SCHEDULE_OFFSET = 5.minutes.freeze # Post a text status update, fetch and notify remote users mentioned @@ -115,10 +117,6 @@ class PostStatusService < BaseService ProcessHashtagsService.new end - def redis - Redis.current - end - def scheduled? @scheduled_at.present? end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index cf7471c98..d5ec076a8 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -2,12 +2,22 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) - tags = Extractor.extract_hashtags(status.text) if status.local? + tags = Extractor.extract_hashtags(status.text) if status.local? + records = [] tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + records << tag + TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? end + + return unless status.public_visibility? || status.unlisted_visibility? + + status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| + featured_tag.increment(status.created_at) + end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 4bee86c8a..99c8e6cbb 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -2,6 +2,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer + include Redisable def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @@ -56,7 +57,7 @@ class RemoveStatusService < BaseService def remove_from_affected @mentions.map(&:account).select(&:local?).each do |account| - Redis.current.publish("timeline:#{account.id}", @payload) + redis.publish("timeline:#{account.id}", @payload) end end @@ -131,26 +132,30 @@ class RemoveStatusService < BaseService end def remove_from_hashtags + @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag| + featured_tag.decrement(@status.id) + end + return unless @status.public_visibility? @tags.each do |hashtag| - Redis.current.publish("timeline:hashtag:#{hashtag}", @payload) - Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local? + redis.publish("timeline:hashtag:#{hashtag}", @payload) + redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local? end end def remove_from_public return unless @status.public_visibility? - Redis.current.publish('timeline:public', @payload) - Redis.current.publish('timeline:public:local', @payload) if @status.local? + redis.publish('timeline:public', @payload) + redis.publish('timeline:public:local', @payload) if @status.local? end def remove_from_media return unless @status.public_visibility? - Redis.current.publish('timeline:public:media', @payload) - Redis.current.publish('timeline:public:local:media', @payload) if @status.local? + redis.publish('timeline:public:media', @payload) + redis.publish('timeline:public:local:media', @payload) if @status.local? end def remove_from_direct @@ -159,8 +164,4 @@ class RemoveStatusService < BaseService end Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local? end - - def redis - Redis.current - end end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 0ee9dd7de..0da69728f 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -63,4 +63,17 @@ - @endorsed_accounts.each do |account| = account_link_to account + - @account.featured_tags.order(statuses_count: :desc).each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + = link_to short_account_tag_path(@account, featured_tag.tag) do + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true + = render 'application/sidebar' diff --git a/app/views/admin/change_emails/show.html.haml b/app/views/admin/change_emails/show.html.haml index 6febef9b1..6ff0d785e 100644 --- a/app/views/admin/change_emails/show.html.haml +++ b/app/views/admin/change_emails/show.html.haml @@ -3,7 +3,7 @@ = simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f| .fields-group - = f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email') + = f.input :email, wrapper: :with_label, hint: false, disabled: true, label: t('admin.accounts.change_email.current_email') .fields-group = f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email') diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml new file mode 100644 index 000000000..5f69517f3 --- /dev/null +++ b/app/views/settings/featured_tags/index.html.haml @@ -0,0 +1,27 @@ +- content_for :page_title do + = t('settings.featured_tags') + += simple_form_for @featured_tag, url: settings_featured_tags_path do |f| + = render 'shared/error_messages', object: @featured_tag + + .fields-group + = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ') + + .actions + = f.button :button, t('featured_tags.add_new'), type: :submit + +%hr.spacer/ + +- @featured_tags.each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + %div + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 4512fc714..7bb4beb01 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -5,8 +5,11 @@ .field-group = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface') - .field-group - = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-group.fields-row__column.fields-row__column-6 + = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' .actions = f.button :button, t('imports.upload'), type: :submit diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index 1dd8bf8fb..e9db20a46 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -13,11 +13,17 @@ class Import::RelationshipWorker case relationship when 'follow' - FollowService.new.call(from_account, target_account.acct) + FollowService.new.call(from_account, target_account) + when 'unfollow' + UnfollowService.new.call(from_account, target_account) when 'block' BlockService.new.call(from_account, target_account) + when 'unblock' + UnblockService.new.call(from_account, target_account) when 'mute' MuteService.new.call(from_account, target_account) + when 'unmute' + UnmuteService.new.call(from_account, target_account) end rescue ActiveRecord::RecordNotFound true diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index aeb221cf6..dfa71b29e 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -1,44 +1,14 @@ # frozen_string_literal: true -require 'csv' - class ImportWorker include Sidekiq::Worker sidekiq_options queue: 'pull', retry: false - attr_reader :import - def perform(import_id) - @import = Import.find(import_id) - - Import::RelationshipWorker.push_bulk(import_rows) do |row| - [@import.account_id, row.first, relationship_type] - end - - @import.destroy - end - - private - - def import_contents - Paperclip.io_adapters.for(@import.data).read - end - - def relationship_type - case @import.type - when 'following' - 'follow' - when 'blocking' - 'block' - when 'muting' - 'mute' - end - end - - def import_rows - rows = CSV.new(import_contents).reject(&:blank?) - rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following' - rows + import = Import.find(import_id) + ImportService.new.call(import) + ensure + import&.destroy end end diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb index cd2273418..bf5e20757 100644 --- a/app/workers/scheduler/feed_cleanup_scheduler.rb +++ b/app/workers/scheduler/feed_cleanup_scheduler.rb @@ -2,6 +2,7 @@ class Scheduler::FeedCleanupScheduler include Sidekiq::Worker + include Redisable sidekiq_options unique: :until_executed, retry: 0 @@ -57,8 +58,4 @@ class Scheduler::FeedCleanupScheduler def feed_manager FeedManager.instance end - - def redis - Redis.current - end end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index e9564692f..b2a621e85 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -35,11 +35,8 @@ ignore_missing: - 'activemodel.errors.*' - 'activerecord.attributes.*' - 'activerecord.errors.*' - - '{devise,pagination,doorkeeper}.*' + - '{pagination,doorkeeper}.*' - '{date,datetime,time,number}.*' - - 'simple_form.{yes,no}' - - 'simple_form.{placeholders,hints,labels}.*' - - 'simple_form.{error_notification,required}.:' - 'errors.messages.*' - 'activerecord.errors.models.doorkeeper/*' - 'sessions.{browsers,platforms}.*' diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb index 0e8f5bfeb..0ddbbee98 100644 --- a/config/initializers/twitter_regex.rb +++ b/config/initializers/twitter_regex.rb @@ -1,7 +1,7 @@ module Twitter class Regex - REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou - REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou + REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou + REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'ãã<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou REGEXEN[:valid_url_balanced_parens] = / \( (?: diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index bd0642b25..726c0504e 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -20,17 +20,17 @@ en: action: Verify email address action_with_app: Confirm and return to %{app} explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email. - extra_html: Please also check out <a href="%{terms_path}">the rules of the instance</a> and <a href="%{policy_path}">our terms of service</a>. + extra_html: Please also check out <a href="%{terms_path}">the rules of the server</a> and <a href="%{policy_path}">our terms of service</a>. subject: 'Mastodon: Confirmation instructions for %{instance}' title: Verify email address email_changed: explanation: 'The email address for your account is being changed to:' - extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account. + extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account. subject: 'Mastodon: Email changed' title: New email address password_change: explanation: The password for your account has been changed. - extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account. + extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account. subject: 'Mastodon: Password changed' title: Password changed reconfirmation_instructions: diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml index cae76d493..bd2bb71bb 100644 --- a/config/locales/devise.ja.yml +++ b/config/locales/devise.ja.yml @@ -20,17 +20,17 @@ ja: action: ã¡ãŒã«ã¢ãã¬ã¹ã®ç¢ºèª action_with_app: 確èªã %{app} ã«æ»ã explanation: ãã®ã¡ãŒã«ã¢ãã¬ã¹ã§%{host}ã«ã¢ã«ãŠã³ããäœæããŸãããæå¹ã«ãããŸã§ããšäžæ©ã§ããããå¿åœããããªãå Žåãç³ãèš³ãããŸããããã®ã¡ãŒã«ãç¡èŠããŠãã ããã - extra_html: ãŸã <a href="%{terms_path}">ã€ã³ã¹ã¿ã³ã¹ã®ã«ãŒã«</a> ãš <a href="%{policy_path}">å©çšèŠçŽ</a> ããèªã¿ãã ããã + extra_html: ãŸã <a href="%{terms_path}">ãµãŒããŒã®ã«ãŒã«</a> ãš <a href="%{policy_path}">å©çšèŠçŽ</a> ããèªã¿ãã ããã subject: 'Mastodon: ã¡ãŒã«ã¢ãã¬ã¹ã®ç¢ºèª %{instance}' title: ã¡ãŒã«ã¢ãã¬ã¹ã®ç¢ºèª email_changed: explanation: 'ã¢ã«ãŠã³ãã®ã¡ãŒã«ã¢ãã¬ã¹ã¯ä»¥äžã®ããã«å€æŽãããŸã:' - extra: ã¡ãŒã«ã¢ãã¬ã¹ã®å€æŽãè¡ã£ãŠããªãå Žåãä»ã®èª°ããããªãã®ã¢ã«ãŠã³ãã«ã¢ã¯ã»ã¹ããå¯èœæ§ããããŸããããã«ãã¹ã¯ãŒãã倿Žããããã¢ã«ãŠã³ããããã¯ãããŠããå Žåã¯ã€ã³ã¹ã¿ã³ã¹ç®¡çè ã«é£çµ¡ããŠãã ããã + extra: ã¡ãŒã«ã¢ãã¬ã¹ã®å€æŽãè¡ã£ãŠããªãå Žåãä»ã®èª°ããããªãã®ã¢ã«ãŠã³ãã«ã¢ã¯ã»ã¹ããå¯èœæ§ããããŸããããã«ãã¹ã¯ãŒãã倿Žããããã¢ã«ãŠã³ããããã¯ãããŠããå Žåã¯ãµãŒããŒç®¡çè ã«é£çµ¡ããŠãã ããã subject: 'Mastodon: ã¡ãŒã«ã¢ãã¬ã¹ã®å€æŽ' title: æ°ããã¡ãŒã«ã¢ãã¬ã¹ password_change: explanation: ãã¹ã¯ãŒãã倿ŽãããŸããã - extra: ãã¹ã¯ãŒãã®å€æŽãè¡ã£ãŠããªãå Žåãä»ã®èª°ããããªãã®ã¢ã«ãŠã³ãã«ã¢ã¯ã»ã¹ããå¯èœæ§ããããŸããããã«ãã¹ã¯ãŒãã倿Žããããã¢ã«ãŠã³ããããã¯ãããŠããå Žåã¯ã€ã³ã¹ã¿ã³ã¹ç®¡çè ã«é£çµ¡ããŠãã ããã + extra: ãã¹ã¯ãŒãã®å€æŽãè¡ã£ãŠããªãå Žåãä»ã®èª°ããããªãã®ã¢ã«ãŠã³ãã«ã¢ã¯ã»ã¹ããå¯èœæ§ããããŸããããã«ãã¹ã¯ãŒãã倿Žããããã¢ã«ãŠã³ããããã¯ãããŠããå Žåã¯ãµãŒããŒç®¡çè ã«é£çµ¡ããŠãã ããã subject: 'Mastodon: ãã¹ã¯ãŒãã倿ŽãããŸãã' title: ãã¹ã¯ãŒãã®å€æŽ reconfirmation_instructions: diff --git a/config/locales/en.yml b/config/locales/en.yml index b8d80a748..a9553aace 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,7 +7,7 @@ en: administered_by: 'Administered by:' api: API apps: Mobile apps - closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there. + closed_registrations: Registrations are currently closed on this server. However! You can find a different server to make an account on and get access to the very same network from there. contact: Contact contact_missing: Not set contact_unavailable: N/A @@ -27,7 +27,7 @@ en: generic_description: "%{domain} is one server in the network" hosted_on: Mastodon hosted on %{domain} learn_more: Learn more - other_instances: Instance list + other_instances: Server list privacy_policy: Privacy policy source_code: Source code status_count_after: @@ -386,7 +386,7 @@ en: desc_html: Modify the look with CSS loaded on every page title: Custom CSS hero: - desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to instance thumbnail + desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail title: Hero image hide_followers_count: desc_html: Do not show followers count on user profiles @@ -395,8 +395,8 @@ en: desc_html: Displayed on multiple pages. At least 293Ã205px recommended. When not set, falls back to default mascot title: Mascot image peers_api_enabled: - desc_html: Domain names this instance has encountered in the fediverse - title: Publish list of discovered instances + desc_html: Domain names this server has encountered in the fediverse + title: Publish list of discovered servers preview_sensitive_media: desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive title: Show sensitive media in OpenGraph previews @@ -424,20 +424,20 @@ en: title: Show staff badge site_description: desc_html: Introductory paragraph on the frontpage. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <code><a></code> and <code><em></code>. - title: Instance description + title: Server description site_description_extended: - desc_html: A good place for your code of conduct, rules, guidelines and other things that set your instance apart. You can use HTML tags + desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags title: Custom extended information site_short_description: - desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to instance description. - title: Short instance description + desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to server description. + title: Short server description site_terms: desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags title: Custom terms of service - site_title: Instance name + site_title: Server name thumbnail: desc_html: Used for previews via OpenGraph and API. 1200x630px recommended - title: Instance thumbnail + title: Server thumbnail timeline_preview: desc_html: Display public timeline on landing page title: Timeline preview @@ -498,7 +498,7 @@ en: warning: Be very careful with this data. Never share it with anyone! your_token: Your access token auth: - agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>. + agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the server</a> and <a href="%{terms_path}">our terms of service</a>. change_password: Password confirm_email: Confirm email delete_account: Delete account @@ -552,7 +552,7 @@ en: description_html: This will <strong>permanently, irreversibly</strong> remove content from your account and deactivate it. Your username will remain reserved to prevent future impersonations. proceed: Delete account success_msg: Your account was successfully deleted - warning_html: Only deletion of content from this particular instance is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases. + warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases. warning_title: Disseminated content availability directories: directory: Profile directory @@ -591,6 +591,10 @@ en: lists: Lists mutes: You mute storage: Media storage + featured_tags: + add_new: Add new + errors: + limit: You have already featured the maximum amount of hashtags filters: contexts: home: Home timeline @@ -609,7 +613,7 @@ en: title: Add new filter followers: domain: Domain - explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all instances where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those instances. + explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all servers where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those servers. followers_count: Number of followers lock_link: Lock your account purge: Remove from followers @@ -632,10 +636,16 @@ en: one: Something isn't quite right yet! Please review the error below other: Something isn't quite right yet! Please review %{count} errors below imports: - preface: You can import data that you have exported from another instance, such as a list of the people you are following or blocking. + modes: + merge: Merge + merge_long: Keep existing records and add new ones + overwrite: Overwrite + overwrite_long: Replace current records with the new ones + preface: You can import data that you have exported from another server, such as a list of the people you are following or blocking. success: Your data was successfully uploaded and will now be processed in due time types: blocking: Blocking list + domain_blocking: Domain blocking list following: Following list muting: Muting list upload: Upload @@ -657,7 +667,7 @@ en: one: 1 use other: "%{count} uses" max_uses_prompt: No limit - prompt: Generate and share links with others to grant access to this instance + prompt: Generate and share links with others to grant access to this server table: expires_at: Expires uses: Uses @@ -805,6 +815,7 @@ en: development: Development edit_profile: Edit profile export: Data export + featured_tags: Featured hashtags flavours: Flavours followers: Authorized followers import: Import @@ -985,7 +996,7 @@ en: final_action: Start posting final_step: 'Start posting! Even without followers your public messages may be seen by others, for example on the local timeline and in hashtags. You may want to introduce yourself on the #introductions hashtag.' full_handle: Your full handle - full_handle_hint: This is what you would tell your friends so they can message or follow you from another instance. + full_handle_hint: This is what you would tell your friends so they can message or follow you from another server. review_preferences_action: Change preferences review_preferences_step: Make sure to set your preferences, such as which emails you'd like to receive, or what privacy level youâd like your posts to default to. If you donât have motion sickness, you could choose to enable GIF autoplay. subject: Welcome to Mastodon diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 2b1b70639..e16ce31a9 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -7,7 +7,7 @@ ja: administered_by: '管çè :' api: API apps: ã¢ã㪠- closed_registrations: çŸåšãã®ã€ã³ã¹ã¿ã³ã¹ã§ã®æ°èŠç»é²ã¯åãä»ããŠããŸãããããããä»ã®ã€ã³ã¹ã¿ã³ã¹ã«ã¢ã«ãŠã³ããäœæããŠãå šãåããããã¯ãŒã¯ã«åå ããããšãã§ããŸãã + closed_registrations: çŸåšãã®ãµãŒããŒã§ã®æ°èŠç»é²ã¯åãä»ããŠããŸãããããããä»ã®ãµãŒããŒã«ã¢ã«ãŠã³ããäœæããŠãå šãåããããã¯ãŒã¯ã«åå ããããšãã§ããŸãã contact: é£çµ¡å contact_missing: æªèšå® contact_unavailable: N/A @@ -24,10 +24,10 @@ ja: real_conversation_title: æ¬åœã®ã³ãã¥ãã±ãŒã·ã§ã³ã®ããã« within_reach_body: ããããããŒãã¬ã³ããªãŒãª API ã«ããå®çŸããããiOS ã Androidããã®ä»æ§ã ãªãã©ãããã©ãŒã ã®ããã®ã¢ããªã§ã©ãã§ãå人ãšãããšãã§ããŸãã within_reach_title: ãã€ã§ã身è¿ã« - generic_description: "%{domain} ã¯ãMastodon ã€ã³ã¹ã¿ã³ã¹ã®äžã€ã§ã" + generic_description: "%{domain} ã¯ãMastodon ãµãŒããŒã®äžã€ã§ã" hosted_on: Mastodon hosted on %{domain} learn_more: ãã£ãšè©³ãã - other_instances: ä»ã®ã€ã³ã¹ã¿ã³ã¹ + other_instances: ä»ã®ãµãŒã㌠privacy_policy: ãã©ã€ãã·ãŒããªã·ãŒ source_code: ãœãŒã¹ã³ãŒã status_count_after: @@ -310,7 +310,7 @@ ja: all: ãã¹ãŠ limited: å¶éãã title: ã¢ãã¬ãŒã·ã§ã³ - title: æ¢ç¥ã®ã€ã³ã¹ã¿ã³ã¹ + title: æ¢ç¥ã®ãµãŒã㌠total_blocked_by_us: ãããã¯åèš total_followed_by_them: 被ãã©ããŒåèš total_followed_by_us: ãã©ããŒåèš @@ -392,8 +392,8 @@ ja: desc_html: è€æ°ã®ããŒãžã«è¡šç€ºãããŸãããµã€ãºã¯293x205pxä»¥äžæšå¥šã§ããæªèšå®ã®å Žåãæšæºã®ãã¹ã³ããã䜿çšãããŸã title: ãã¹ã³ããã€ã¡ãŒãž peers_api_enabled: - desc_html: é£åå ã§ãã®ã€ã³ã¹ã¿ã³ã¹ãééãããã¡ã€ã³ã®åå - title: æ¥ç¶ããŠããã€ã³ã¹ã¿ã³ã¹ã®ãªã¹ããå ¬éãã + desc_html: é£åå ã§ãã®ãµãŒããŒãééãããã¡ã€ã³ã®åå + title: æ¥ç¶ããŠãããµãŒããŒã®ãªã¹ããå ¬éãã preview_sensitive_media: desc_html: ä»ã®ãŠã§ããµã€ãã«ãªã³ã¯ã貌ã£ãéãã¡ãã£ã¢ãé²èŠ§æ³šæãšããŠããŒã¯ãããŠããŠããµã ãã€ã«ã衚瀺ãããŸã title: OpenGraphã«ãããã¬ãã¥ãŒã§é²èŠ§æ³šæã®ã¡ãã£ã¢ã衚瀺ãã @@ -420,21 +420,21 @@ ja: desc_html: ãŠãŒã¶ãŒããŒãžã«ã¹ã¿ããã®ãããžã衚瀺ããŸã title: ã¹ã¿ãããããžã衚瀺ãã site_description: - desc_html: ããã³ãããŒãžãžã®è¡šç€ºã«äœ¿çšãããçŽ¹ä»æã§ãããã®Mastodonã€ã³ã¹ã¿ã³ã¹ãç¹åŸŽä»ããããšããã®ä»éèŠãªããšãèšè¿°ããŠãã ãããHTMLã¿ã°ãç¹ã«<code><a></code> ãš <code><em></code>ã䜿ããŸãã - title: ã€ã³ã¹ã¿ã³ã¹ã®èª¬æ + desc_html: ããã³ãããŒãžãžã®è¡šç€ºã«äœ¿çšãããçŽ¹ä»æã§ãããã®MastodonãµãŒããŒãç¹åŸŽä»ããããšããã®ä»éèŠãªããšãèšè¿°ããŠãã ãããHTMLã¿ã°ãç¹ã«<code><a></code> ãš <code><em></code>ã䜿ããŸãã + title: ãµãŒããŒã®èª¬æ site_description_extended: - desc_html: ããªãã®ã€ã³ã¹ã¿ã³ã¹ã«ãããè¡åèŠç¯ãã«ãŒã«ãã¬ã€ãã©ã€ã³ããã®ã»ãã®èšè¿°ãããéã«æé©ãªå Žæã§ããHTMLã¿ã°ã䜿ããŸã + desc_html: ããªãã®ãµãŒããŒã«ãããè¡åèŠç¯ãã«ãŒã«ãã¬ã€ãã©ã€ã³ããã®ã»ãã®èšè¿°ãããéã«æé©ãªå Žæã§ããHTMLã¿ã°ã䜿ããŸã title: ã«ã¹ã¿ã 詳现説æ site_short_description: - desc_html: ãµã€ãããŒãš meta ã¿ã°ã«è¡šç€ºãããŸããMastodon ãšã¯äœãããããŠãã®ãµãŒããŒã®ç¹å¥ãªäœãã1段èœã§èšè¿°ããŠãã ããã空æ¬ã®å Žåãã€ã³ã¹ã¿ã³ã¹ã®èª¬æã䜿çšãããŸãã - title: çãã€ã³ã¹ã¿ã³ã¹ã®èª¬æ + desc_html: ãµã€ãããŒãš meta ã¿ã°ã«è¡šç€ºãããŸããMastodon ãšã¯äœãããããŠãã®ãµãŒããŒã®ç¹å¥ãªäœãã1段èœã§èšè¿°ããŠãã ããã空æ¬ã®å ŽåããµãŒããŒã®èª¬æã䜿çšãããŸãã + title: çããµãŒããŒã®èª¬æ site_terms: desc_html: ããªãã¯ç¬èªã®ãã©ã€ãã·ãŒããªã·ãŒãå©çšèŠçŽããã®ã»ãã®æ³çæ ¹æ ãæžãããšãã§ããŸããHTMLã¿ã°ã䜿ããŸã title: ã«ã¹ã¿ã å©çšèŠçŽ - site_title: ã€ã³ã¹ã¿ã³ã¹ã®åå + site_title: ãµãŒããŒã®åå thumbnail: desc_html: OpenGraphãšAPIã«ãããã¬ãã¥ãŒã«äœ¿çšãããŸãããµã€ãºã¯1200Ã630pxæšå¥šã§ã - title: ã€ã³ã¹ã¿ã³ã¹ã®ãµã ãã€ã« + title: ãµãŒããŒã®ãµã ãã€ã« timeline_preview: desc_html: ã©ã³ãã£ã³ã°ããŒãžã«å ¬éã¿ã€ã ã©ã€ã³ã衚瀺ããŸã title: ã¿ã€ã ã©ã€ã³ãã¬ãã¥ãŒ @@ -495,7 +495,7 @@ ja: warning: ãã®ããŒã¿ã¯æ°ãã€ããŠåãæ±ã£ãŠãã ãããä»ã®äººãšå ±æããªãã§ãã ããïŒ your_token: ã¢ã¯ã»ã¹ããŒã¯ã³ auth: - agreement_html: ç»é²ãããã¯ãªãã¯ãããš <a href="%{rules_path}">ã€ã³ã¹ã¿ã³ã¹ã®ã«ãŒã«</a> ãš <a href="%{terms_path}">ãã©ã€ãã·ãŒããªã·ãŒ</a> ã«åŸãããšã«åæããããšã«ãªããŸãã + agreement_html: ç»é²ãããã¯ãªãã¯ãããš <a href="%{rules_path}">ãµãŒããŒã®ã«ãŒã«</a> ãš <a href="%{terms_path}">ãã©ã€ãã·ãŒããªã·ãŒ</a> ã«åŸãããšã«åæããããšã«ãªããŸãã change_password: ãã¹ã¯ãŒã confirm_email: ã¡ãŒã«ã¢ãã¬ã¹ã®ç¢ºèª delete_account: ã¢ã«ãŠã³ãã®åé€ @@ -513,7 +513,7 @@ ja: cas: CAS saml: SAML register: ç»é²ãã - register_elsewhere: ä»ã®ã€ã³ã¹ã¿ã³ã¹ã§æ°èŠç»é² + register_elsewhere: ä»ã®ãµãŒããŒã§æ°èŠç»é² resend_confirmation: 確èªã¡ãŒã«ãåéãã reset_password: ãã¹ã¯ãŒããåçºè¡ security: ã»ãã¥ãªã㣠@@ -549,7 +549,7 @@ ja: description_html: ããªãã®ã¢ã«ãŠã³ãã«å«ãŸããã³ã³ãã³ãã¯å šãŠåé€ãããã¢ã«ãŠã³ãã¯ç¡å¹åãããŸããããã¯æä¹ çãªãã®ã§ã<strong>åãæ¶ãããšã¯ã§ããŸãã</strong>ããªãããŸããé²ãããã«ãåããŠãŒã¶ãŒåã§å床ç»é²ããããšã¯ã§ããªããªããŸãã proceed: ã¢ã«ãŠã³ããåé€ãã success_msg: ã¢ã«ãŠã³ãã¯æ£åžžã«åé€ãããŸãã - warning_html: åé€ãä¿èšŒãããã®ã¯ãã®ã€ã³ã¹ã¿ã³ã¹äžã®ã³ã³ãã³ãã®ã¿ã§ããä»ã®ã€ã³ã¹ã¿ã³ã¹çãå€éšã«åºãå ±æãããã³ã³ãã³ãã«ã€ããŠã¯çè·¡ãæ®ãããšããããŸãããŸããçŸåšæ¥ç¶ã§ããªããµãŒããŒããããªãã®æŽæ°ãåãåããªããªã£ããµãŒããŒã«å¯ŸããŠã¯ãåé€ã¯åæ ãããŸããã + warning_html: åé€ãä¿èšŒãããã®ã¯ãã®ãµãŒããŒäžã®ã³ã³ãã³ãã®ã¿ã§ããä»ã®ãµãŒããŒçãå€éšã«åºãå ±æãããã³ã³ãã³ãã«ã€ããŠã¯çè·¡ãæ®ãããšããããŸãããŸããçŸåšæ¥ç¶ã§ããªããµãŒããŒããããªãã®æŽæ°ãåãåããªããªã£ããµãŒããŒã«å¯ŸããŠã¯ãåé€ã¯åæ ãããŸããã warning_title: å ±æãããã³ã³ãã³ãã«ã€ã㊠directories: directory: ãã£ã¬ã¯ã㪠@@ -588,6 +588,10 @@ ja: lists: ãªã¹ã mutes: ãã¥ãŒã storage: ã¡ãã£ã¢ + featured_tags: + add_new: 远å + errors: + limit: 泚ç®ã®ããã·ã¥ã¿ã°ã®äžéã«éããŸãã filters: contexts: home: ããŒã ã¿ã€ã ã©ã€ã³ @@ -606,7 +610,7 @@ ja: title: æ°èŠãã£ã«ã¿ãŒã远å followers: domain: ãã¡ã€ã³ - explanation_html: ããªãã®æçš¿ã®ãã©ã€ãã·ãŒã確ä¿ãããå Žåã誰ãããªãããã©ããŒããŠããã®ããææ¡ããŠããå¿ èŠããããŸãã <strong>ãã©ã€ããŒãæçš¿ã¯ãããªãã®ãã©ãã¯ãŒãããå šãŠã®ã€ã³ã¹ã¿ã³ã¹ã«é ä¿¡ãããŸã</strong>ã ãã©ãã¯ãŒã®ã€ã³ã¹ã¿ã³ã¹ã®ç®¡çè ããœãããŠã§ã¢ãããªãã®ãã©ã€ãã·ãŒãå°éããŠããããã©ããæªããå Žåã¯ããã®ãã©ãã¯ãŒãåé€ããæ¹ããããããããŸããã + explanation_html: ããªãã®æçš¿ã®ãã©ã€ãã·ãŒã確ä¿ãããå Žåã誰ãããªãããã©ããŒããŠããã®ããææ¡ããŠããå¿ èŠããããŸãã <strong>ãã©ã€ããŒãæçš¿ã¯ãããªãã®ãã©ãã¯ãŒãããå šãŠã®ãµãŒããŒã«é ä¿¡ãããŸã</strong>ã ãã©ãã¯ãŒã®ãµãŒããŒã®ç®¡çè ããœãããŠã§ã¢ãããªãã®ãã©ã€ãã·ãŒãå°éããŠããããã©ããæªããå Žåã¯ããã®ãã©ãã¯ãŒãåé€ããæ¹ããããããããŸããã followers_count: ãã©ãã¯ãŒæ° lock_link: æ¿èªå¶ã¢ã«ãŠã³ãã«ãã purge: ãã©ãã¯ãŒããåé€ãã @@ -629,10 +633,16 @@ ja: one: ãšã©ãŒãçºçããŸããïŒ ä»¥äžã®ãšã©ãŒã確èªããŠãã ãã other: ãšã©ãŒãçºçããŸããïŒ ä»¥äžã®%{count}åã®ãšã©ãŒã確èªããŠãã ãã imports: - preface: ä»ã®ã€ã³ã¹ã¿ã³ã¹ã§ãšã¯ã¹ããŒãããããã¡ã€ã«ããããã©ããŒ/ãããã¯ããæ å ±ããã®ã€ã³ã¹ã¿ã³ã¹äžã®ã¢ã«ãŠã³ãã«ã€ã³ããŒãã§ããŸãã + modes: + merge: çµ±å + merge_long: çŸåšã®ã¬ã³ãŒããä¿æãããŸãŸæ°ãããã®ã远å ããŸã + overwrite: äžæžã + overwrite_long: çŸåšã®ã¬ã³ãŒããæ°ãããã®ã§çœ®ãæããŸã + preface: ä»ã®ãµãŒããŒã§ãšã¯ã¹ããŒãããããã¡ã€ã«ããããã©ããŒ/ãããã¯ããæ å ±ããã®ãµãŒããŒäžã®ã¢ã«ãŠã³ãã«ã€ã³ããŒãã§ããŸãã success: ãã¡ã€ã«ã¯æ£åžžã«ã¢ããããŒããããçŸåšåŠçäžã§ãããã°ããããŠãã確èªããŠãã ãã types: blocking: ãããã¯ããã¢ã«ãŠã³ããªã¹ã + domain_blocking: é衚瀺ã«ãããã¡ã€ã³ãªã¹ã following: ãã©ããŒäžã®ã¢ã«ãŠã³ããªã¹ã muting: ãã¥ãŒãããã¢ã«ãŠã³ããªã¹ã upload: ã¢ããããŒã @@ -654,7 +664,7 @@ ja: one: '1' other: "%{count}" max_uses_prompt: ç¡å¶é - prompt: ãªã³ã¯ãçæã»å ±æããŠãã®ã€ã³ã¹ã¿ã³ã¹ãžã®æ°èŠç»é²ãåãä»ããããšãã§ããŸã + prompt: ãªã³ã¯ãçæã»å ±æããŠãã®ãµãŒããŒãžã®æ°èŠç»é²ãåãä»ããããšãã§ããŸã table: expires_at: æå¹æé uses: äœ¿çš @@ -801,8 +811,9 @@ ja: development: éçº edit_profile: ãããã£ãŒã«ãç·šé export: ããŒã¿ã®ãšã¯ã¹ããŒã + featured_tags: 泚ç®ã®ããã·ã¥ã¿ã° flavours: ãã¬ãŒã㌠- followers: ä¿¡é Œæžã¿ã®ã€ã³ã¹ã¿ã³ã¹ + followers: ä¿¡é Œæžã¿ã®ãµãŒã㌠import: ããŒã¿ã®ã€ã³ããŒã migrate: ã¢ã«ãŠã³ãã®åŒã£è¶ã notifications: éç¥ @@ -980,13 +991,13 @@ ja: final_action: å§ããŸããã final_step: 'ããå§ããŸãããïŒ ããšããã©ãã¯ãŒãããªããŠããããªãã®å ¬éããæçš¿ã¯ããŒã«ã«ã¿ã€ã ã©ã€ã³ãããã·ã¥ã¿ã°ãªã©ã§èª°ãã®ç®ã«æ¢ãŸããããããŸãããèªå·±ç޹ä»ããããæã¯ #introductions ããã·ã¥ã¿ã°ã䜿ããšãããããããŸããã' full_handle: ããªãã®æ£åŒãªãŠãŒã¶ãŒå - full_handle_hint: ããã¯å¥ã®ã€ã³ã¹ã¿ã³ã¹ãããã©ããŒããŠããã£ããã¡ãã»ãŒãžã®ããåããããéã«ãåéã«äŒãããšããã§ãããã + full_handle_hint: ããã¯å¥ã®ãµãŒããŒãããã©ããŒããŠããã£ããã¡ãã»ãŒãžã®ããåããããéã«ãåéã«äŒãããšããã§ãããã review_preferences_action: èšå®ã®å€æŽ review_preferences_step: åãåãããã¡ãŒã«ãæçš¿ã®å ¬éç¯å²ãªã©ã®èšå®ãå¿ ãè¡ã£ãŠãã ãããäžå¿«ã§ãªããªãã¢ãã¡ãŒã·ã§ã³ GIF ã®èªååçãæå¹ã«ããããšãã§ããŸãã subject: Mastodon ãžãããã - tip_federated_timeline: é£åã¿ã€ã ã©ã€ã³ã¯ Mastodon ãããã¯ãŒã¯ã®æµããèŠããããã®ã§ãããã ãããªããšåãã€ã³ã¹ã¿ã³ã¹ã®äººããã©ããŒããŠãã人ã ããå«ãŸããã®ã§ããããå šãŠã§ã¯ãããŸããã - tip_following: æšæºã§ã¯èªåã§ã€ã³ã¹ã¿ã³ã¹ã®ç®¡çè ããã©ããŒããŠããŸãããã£ãšèå³ã®ãã人ãã¡ãèŠã€ããã«ã¯ãããŒã«ã«ã¿ã€ã ã©ã€ã³ãšé£åã¿ã€ã ã©ã€ã³ã確èªããŠãã ããã - tip_local_timeline: ããŒã«ã«ã¿ã€ã ã©ã€ã³ã¯ %{instance} ã«ãã人ã ã®æµããèŠããããã®ã§ãã圌ãã¯ããªããšåãã€ã³ã¹ã¿ã³ã¹ã«ããé£äººã®ãããªãã®ã§ãïŒ + tip_federated_timeline: é£åã¿ã€ã ã©ã€ã³ã¯ Mastodon ãããã¯ãŒã¯ã®æµããèŠããããã®ã§ãããã ãããªããšåããµãŒããŒã®äººããã©ããŒããŠãã人ã ããå«ãŸããã®ã§ããããå šãŠã§ã¯ãããŸããã + tip_following: æšæºã§ã¯èªåã§ãµãŒããŒã®ç®¡çè ããã©ããŒããŠããŸãããã£ãšèå³ã®ãã人ãã¡ãèŠã€ããã«ã¯ãããŒã«ã«ã¿ã€ã ã©ã€ã³ãšé£åã¿ã€ã ã©ã€ã³ã確èªããŠãã ããã + tip_local_timeline: ããŒã«ã«ã¿ã€ã ã©ã€ã³ã¯ %{instance} ã«ãã人ã ã®æµããèŠããããã®ã§ãã圌ãã¯ããªããšåããµãŒããŒã«ããé£äººã®ãããªãã®ã§ãïŒ tip_mobile_webapp: ããã¢ãã€ã«ç«¯æ«ã®ãã©ãŠã¶ã§ Mastodon ãããŒã ç»é¢ã«è¿œå ã§ããå Žåãããã·ã¥éç¥ãåãåãããšãã§ããŸããããã¯ãŸãã§ãã€ãã£ãã¢ããªã®ããã«åäœããŸãïŒ tips: è±ç¥è title: ããããã%{name} ïŒ diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 23fa10179..1567ac626 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -602,6 +602,10 @@ pl: lists: Listy mutes: Wyciszeni storage: UrzÄ dzenie przechowujÄ ce dane + featured_tags: + add_new: Dodaj nowy + errors: + limit: JuÅŒ przekroczyÅeÅ(-aÅ) maksymalnÄ liczbÄ wyróŌnionych hashtagów filters: contexts: home: Strona gÅówna @@ -647,10 +651,16 @@ pl: one: CoÅ jest wciÄ ÅŒ nie tak! Przyjrzyj siÄ poniÅŒszemu bÅÄdowi other: CoÅ jest wciÄ ÅŒ nie tak! Przejrzyj poniÅŒsze bÅÄdy (%{count}) imports: + modes: + merge: PoÅÄ cz + merge_long: Zachowaj obecne wpisy i dodaj nowe + overwrite: Nadpisz + overwrite_long: ZastÄ p obecne wpisy nowymi preface: MoÅŒesz zaimportowaÄ pewne dane (np. lista kont, które Åledzisz lub blokujesz) do swojego konta na tym serwerze, korzystajÄ c z danych wyeksportowanych z innego serwera. success: Twoje dane zostaÅy zaÅadowane i zostanÄ niebawem przetworzone types: blocking: Lista blokowanych + domain_blocking: Lista zablokowanych domen following: Lista Åledzonych muting: Lista wyciszonych upload: ZaÅaduj @@ -826,6 +836,7 @@ pl: development: Tworzenie aplikacji edit_profile: Edytuj profil export: Eksportowanie danych + featured_tags: WyróŌnione hashtagi flavours: Odmiany followers: Autoryzowani ÅledzÄ cy import: Importowanie danych diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 674abff63..ad9ae7417 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,8 +37,10 @@ en: setting_skin: Reskins the selected Mastodon flavour username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + featured_tag: + name: 'You might want to use one of these:' imports: - data: CSV file exported from another Mastodon instance + data: CSV file exported from another Mastodon server sessions: otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' user: @@ -112,6 +114,8 @@ en: username: Username username_or_email: Username or Email whole_word: Whole word + featured_tag: + name: Hashtag interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 9419331e4..2cade4301 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -33,11 +33,14 @@ ja: setting_display_media_show_all: é²èŠ§æ³šæãšããŠããŒã¯ãããã¡ãã£ã¢ãåžžã«è¡šç€ºãã setting_hide_network: ãã©ããŒãšãã©ãã¯ãŒã®æ å ±ããããã£ãŒã«ããŒãžã§èŠãããªãããã«ããŸã setting_noindex: å ¬éãããã£ãŒã«ããã³åæçš¿ããŒãžã«åœ±é¿ããŸã + setting_show_application: ãã¥ãŒãããã®ã«äœ¿çšããã¢ããªããã¥ãŒãã®è©³çްãã¥ãŒã«è¡šç€ºãããããã«ãªããŸã setting_theme: ãã°ã€ã³ããŠããå šãŠã®ããã€ã¹ã§é©çšããããã¶ã€ã³ã§ãã username: ããªãã®ãŠãŒã¶ãŒå㯠%{domain} ã®äžã§éè€ããŠããªãå¿ èŠããããŸã whole_word: ããŒã¯ãŒããŸãã¯ãã¬ãŒãºãè±æ°åã®ã¿ã®å Žåãåèªå šäœãšäžèŽããå Žåã®ã¿é©çšãããããã«ãªããŸã + featured_tag: + name: 'ãããã䜿ããšãããããããŸãã:' imports: - data: ä»ã® Mastodon ã€ã³ã¹ã¿ã³ã¹ãããšã¯ã¹ããŒãããCSVãã¡ã€ã«ãéžæããŠäžãã + data: ä»ã® Mastodon ãµãŒããŒãããšã¯ã¹ããŒãããCSVãã¡ã€ã«ãéžæããŠäžãã sessions: otp: 'æºåž¯é»è©±ã®ã¢ããªã§çæãããäºæ®µéèªèšŒã³ãŒããå ¥åãããããªã«ããªãŒã³ãŒãã䜿çšããŠãã ãã:' user: @@ -101,6 +104,7 @@ ja: setting_hide_network: ç¹ãããé ã setting_noindex: æ€çŽ¢ãšã³ãžã³ã«ããã€ã³ããã¯ã¹ãæåŠãã setting_reduce_motion: ã¢ãã¡ãŒã·ã§ã³ã®åããæžãã + setting_show_application: ãã¥ãŒãã®éä¿¡ã«äœ¿çšããã¢ããªãé瀺ãã setting_system_font_ui: ã·ã¹ãã ã®ããã©ã«ããã©ã³ãã䜿ã setting_theme: ãµã€ãããŒã setting_unfollow_modal: ãã©ããŒãè§£é€ããåã«ç¢ºèªãã€ã¢ãã°ã衚瀺ãã @@ -109,6 +113,8 @@ ja: username: ãŠãŒã¶ãŒå username_or_email: ãŠãŒã¶ãŒåãŸãã¯ã¡ãŒã«ã¢ãã¬ã¹ whole_word: åèªå šäœã«ããã + featured_tag: + name: ããã·ã¥ã¿ã° interactions: must_be_follower: ãã©ãã¯ãŒä»¥å€ããã®éç¥ãããã㯠must_be_following: ãã©ããŒããŠããªããŠãŒã¶ãŒããã®éç¥ãããã㯠diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 660841e06..f5b5a6ca5 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -33,9 +33,12 @@ pl: setting_display_media_show_all: Zawsze pokazuj zawartoÅÄ multimedialnÄ jako wraÅŒliwÄ setting_hide_network: Informacje o tym, kto CiÄ Åledzi i kogo Åledzisz nie bÄdÄ widoczne setting_noindex: WpÅywa na widocznoÅÄ strony profilu i Twoich wpisów + setting_show_application: W informacjach o wpisie bÄdzie widoczna informacja o aplikacji, z której zostaÅ wysÅany setting_skin: Zmienia wyglÄ d uÅŒywanej odmiany Mastodona username: Twoja nazwa uÅŒytkownika bÄdzie niepowtarzalna na %{domain} whole_word: JeÅli sÅowo lub fraza skÅada siÄ jedynie z liter lub cyfr, filtr bÄdzie zastosowany tylko do peÅnych wystÄ pieÅ + featured_tag: + name: 'Sugerujemy uÅŒycie jednego z nastÄpujÄ cych:' imports: data: Plik CSV wyeksportowany z innej instancji Mastodona sessions: @@ -102,6 +105,7 @@ pl: setting_hide_network: Ukryj swojÄ sieÄ setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych setting_reduce_motion: Ogranicz ruch w animacjach + setting_show_application: Informuj o aplikacji z której wysÅano wpisy setting_skin: Motyw setting_system_font_ui: UÅŒywaj domyÅlnej czcionki systemu setting_unfollow_modal: Pytaj o potwierdzenie przed cofniÄciem Åledzenia @@ -110,6 +114,8 @@ pl: username: Nazwa uÅŒytkownika username_or_email: Nazwa uÅŒytkownika lub adres e-mail whole_word: CaÅe sÅowo + featured_tag: + name: Hashtag interactions: must_be_follower: Nie wyÅwietlaj powiadomieÅ od osób, które CiÄ nie ÅledzÄ must_be_following: Nie wyÅwietlaj powiadomieÅ od osób, których nie Åledzisz diff --git a/config/navigation.rb b/config/navigation.rb index 5b0b5c343..f74c98ab2 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -6,6 +6,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings| settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} + settings.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} diff --git a/config/routes.rb b/config/routes.rb index 976b25812..447a22794 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ Rails.application.routes.draw do get '/@:username', to: 'accounts#show', as: :short_account get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies get '/@:username/media', to: 'accounts#show', as: :short_account_media + get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status @@ -119,6 +120,7 @@ Rails.application.routes.draw do resource :migration, only: [:show, :update] resources :sessions, only: [:destroy] + resources :featured_tags, only: [:index, :create, :destroy] end resources :media, only: [:show] do diff --git a/db/migrate/20171005102658_create_account_moderation_notes.rb b/db/migrate/20171005102658_create_account_moderation_notes.rb index d1802b5b3..974ed9940 100644 --- a/db/migrate/20171005102658_create_account_moderation_notes.rb +++ b/db/migrate/20171005102658_create_account_moderation_notes.rb @@ -7,6 +7,7 @@ class CreateAccountModerationNotes < ActiveRecord::Migration[5.1] t.timestamps end + add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id end end diff --git a/db/migrate/20190201012802_add_overwrite_to_imports.rb b/db/migrate/20190201012802_add_overwrite_to_imports.rb new file mode 100644 index 000000000..89b262cc7 --- /dev/null +++ b/db/migrate/20190201012802_add_overwrite_to_imports.rb @@ -0,0 +1,17 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddOverwriteToImports < ActiveRecord::Migration[5.2] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured do + add_column_with_default :imports, :overwrite, :boolean, default: false, allow_null: false + end + end + + def down + remove_column :imports, :overwrite, :boolean + end +end diff --git a/db/migrate/20190203180359_create_featured_tags.rb b/db/migrate/20190203180359_create_featured_tags.rb new file mode 100644 index 000000000..b08410a3a --- /dev/null +++ b/db/migrate/20190203180359_create_featured_tags.rb @@ -0,0 +1,12 @@ +class CreateFeaturedTags < ActiveRecord::Migration[5.2] + def change + create_table :featured_tags do |t| + t.references :account, foreign_key: { on_delete: :cascade } + t.references :tag, foreign_key: { on_delete: :cascade } + t.bigint :statuses_count, default: 0, null: false + t.datetime :last_status_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c6c94609f..05d4deb1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_01_17_114553) do +ActiveRecord::Schema.define(version: 2019_02_03_180359) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -260,6 +260,17 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do t.index ["status_id"], name: "index_favourites_on_status_id" end + create_table "featured_tags", force: :cascade do |t| + t.bigint "account_id" + t.bigint "tag_id" + t.bigint "statuses_count", default: 0, null: false + t.datetime "last_status_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_featured_tags_on_account_id" + t.index ["tag_id"], name: "index_featured_tags_on_tag_id" + end + create_table "follow_requests", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -300,6 +311,7 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do t.integer "data_file_size" t.datetime "data_updated_at" t.bigint "account_id", null: false + t.boolean "overwrite", default: false, null: false end create_table "invites", force: :cascade do |t| @@ -721,6 +733,8 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do add_foreign_key "custom_filters", "accounts", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade + add_foreign_key "featured_tags", "accounts", on_delete: :cascade + add_foreign_key "featured_tags", "tags", on_delete: :cascade add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb new file mode 100644 index 000000000..25cbdaac0 --- /dev/null +++ b/spec/fabricators/featured_tag_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:featured_tag) do + account + tag + statuses_count 1_337 + last_status_at Time.now.utc +end diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb index 0c1efe7c3..96d2fc7e0 100644 --- a/spec/lib/formatter_spec.rb +++ b/spec/lib/formatter_spec.rb @@ -74,10 +74,36 @@ RSpec.describe Formatter do end context 'given a URL with a query string' do - let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } + context 'with escaped unicode character' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } - it 'matches the full URL' do - is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"' + it 'matches the full URL' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"' + end + end + + context 'with unicode character' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=â&q=autolink' } + + it 'matches the full URL' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=â&q=autolink"' + end + end + + context 'with unicode character at the end' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=â' } + + it 'matches the full URL' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=â"' + end + end + + context 'with escaped and not escaped unicode characters' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=â&q=autolink' } + + it 'preserves escaped unicode characters' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=â&q=autolink"' + end end end @@ -89,6 +115,22 @@ RSpec.describe Formatter do end end + context 'given a URL in quotation marks' do + let(:text) { '"https://example.com/"' } + + it 'does not match the quotation marks' do + is_expected.to include 'href="https://example.com/"' + end + end + + context 'given a URL in angle brackets' do + let(:text) { '<https://example.com/>' } + + it 'does not match the angle brackets' do + is_expected.to include 'href="https://example.com/"' + end + end + context 'given a URL with Japanese path string' do let(:text) { 'https://ja.wikipedia.org/wiki/æ¥æ¬' } @@ -105,6 +147,22 @@ RSpec.describe Formatter do end end + context 'given a URL with a full-width space' do + let(:text) { 'https://example.com/ãabc123' } + + it 'does not match the full-width space' do + is_expected.to include 'href="https://example.com/"' + end + end + + context 'given a URL in Japanese quotation marks' do + let(:text) { 'ã[https://example.org/ã' } + + it 'does not match the quotation marks' do + is_expected.to include 'href="https://example.org/"' + end + end + context 'given a URL with Simplified Chinese path string' do let(:text) { 'https://baike.baidu.com/item/äžå人æ°å ±ååœ' } @@ -124,7 +182,11 @@ RSpec.describe Formatter do context 'given a URL containing unsafe code (XSS attack, visible part)' do let(:text) { %q{http://example.com/b<del>b</del>} } - it 'escapes the HTML in the URL' do + it 'does not include the HTML in the URL' do + is_expected.to include '"http://example.com/b"' + end + + it 'escapes the HTML' do is_expected.to include '<del>b</del>' end end @@ -132,7 +194,11 @@ RSpec.describe Formatter do context 'given a URL containing unsafe code (XSS attack, invisible part)' do let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} } - it 'escapes the HTML in the URL' do + it 'does not include the HTML in the URL' do + is_expected.to include '"http://example.com/blahblahblahblah/a"' + end + + it 'escapes the HTML' do is_expected.to include '<script>alert("Hello")</script>' end end @@ -168,6 +234,14 @@ RSpec.describe Formatter do is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>' end end + + context 'given text containing a hashtag with Unicode chars' do + let(:text) { '#hashtagã¿ã°' } + + it 'creates a hashtag link' do + is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagã¿ã°</span></a>' + end + end end describe '#format_spoiler' do diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 9c9b87daf..36e346f14 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -244,9 +244,9 @@ describe AccountInteractions do end describe '#block_domain!' do - let(:domain_block) { Fabricate(:domain_block) } + let(:domain) { 'example.com' } - subject { account.block_domain!(domain_block) } + subject { account.block_domain!(domain) } it 'creates and returns AccountDomainBlock' do expect do diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb new file mode 100644 index 000000000..07533e0b9 --- /dev/null +++ b/spec/models/featured_tag_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe FeaturedTag, type: :model do +end |