commit
a3ba28eb17
11
Dockerfile
11
Dockerfile
|
@ -1,5 +1,5 @@
|
||||||
FROM node:8.15-alpine as node
|
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" \
|
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||||
description="Your self-hosted, globally interconnected microblogging community"
|
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 /usr/local/bin/npm /usr/local/bin/npm
|
||||||
COPY --from=node /opt/yarn-* /opt/yarn
|
COPY --from=node /opt/yarn-* /opt/yarn
|
||||||
|
|
||||||
RUN apk -U upgrade \
|
RUN apk add --no-cache -t build-dependencies \
|
||||||
&& apk add -t build-dependencies \
|
|
||||||
build-base \
|
build-base \
|
||||||
icu-dev \
|
icu-dev \
|
||||||
libidn-dev \
|
libidn-dev \
|
||||||
libressl \
|
openssl \
|
||||||
libtool \
|
libtool \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxslt-dev \
|
libxslt-dev \
|
||||||
postgresql-dev \
|
postgresql-dev \
|
||||||
protobuf-dev \
|
protobuf-dev \
|
||||||
python \
|
python \
|
||||||
&& apk add \
|
&& apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
file \
|
file \
|
||||||
|
@ -64,7 +63,7 @@ RUN apk -U upgrade \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& libtool --finish /usr/local/lib \
|
&& libtool --finish /usr/local/lib \
|
||||||
&& cd /mastodon \
|
&& cd /mastodon \
|
||||||
&& rm -rf /tmp/* /var/cache/apk/*
|
&& rm -rf /tmp/*
|
||||||
|
|
||||||
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
|
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
|
||||||
COPY stack-fix.c /lib
|
COPY stack-fix.c /lib
|
||||||
|
|
6
Gemfile
6
Gemfile
|
@ -108,15 +108,15 @@ group :production, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.12'
|
gem 'capybara', '~> 3.13'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 1.9'
|
gem 'faker', '~> 1.9'
|
||||||
gem 'microformats', '~> 4.0'
|
gem 'microformats', '~> 4.1'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
gem 'simplecov', '~> 0.16', require: false
|
gem 'simplecov', '~> 0.16', require: false
|
||||||
gem 'webmock', '~> 3.5'
|
gem 'webmock', '~> 3.5'
|
||||||
gem 'parallel_tests', '~> 2.27'
|
gem 'parallel_tests', '~> 2.28'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -126,7 +126,7 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.12.0)
|
capybara (3.13.2)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
@ -268,7 +268,7 @@ GEM
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.1.1)
|
http-form_data (2.1.1)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
httplog (1.2.0)
|
httplog (1.2.1)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.5.3)
|
i18n (1.5.3)
|
||||||
|
@ -337,9 +337,9 @@ GEM
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
memory_profiler (0.9.12)
|
memory_profiler (0.9.12)
|
||||||
method_source (0.9.2)
|
method_source (0.9.2)
|
||||||
microformats (4.0.7)
|
microformats (4.1.0)
|
||||||
json
|
json (~> 2.1)
|
||||||
nokogiri
|
nokogiri (~> 1.8, >= 1.8.3)
|
||||||
mime-types (3.2.2)
|
mime-types (3.2.2)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2018.0812)
|
mime-types-data (3.2018.0812)
|
||||||
|
@ -392,7 +392,7 @@ GEM
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.13.0)
|
parallel (1.13.0)
|
||||||
parallel_tests (2.27.1)
|
parallel_tests (2.28.0)
|
||||||
parallel
|
parallel
|
||||||
parser (2.6.0.0)
|
parser (2.6.0.0)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
|
@ -671,7 +671,7 @@ DEPENDENCIES
|
||||||
capistrano-rails (~> 1.4)
|
capistrano-rails (~> 1.4)
|
||||||
capistrano-rbenv (~> 2.1)
|
capistrano-rbenv (~> 2.1)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.12)
|
capybara (~> 3.13)
|
||||||
charlock_holmes (~> 0.7.6)
|
charlock_holmes (~> 0.7.6)
|
||||||
chewy (~> 5.0)
|
chewy (~> 5.0)
|
||||||
cld3 (~> 3.2.3)
|
cld3 (~> 3.2.3)
|
||||||
|
@ -712,7 +712,7 @@ DEPENDENCIES
|
||||||
makara (~> 0.4)
|
makara (~> 0.4)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
microformats (~> 4.0)
|
microformats (~> 4.1)
|
||||||
mime-types (~> 3.2)
|
mime-types (~> 3.2)
|
||||||
net-ldap (~> 0.10)
|
net-ldap (~> 0.10)
|
||||||
nokogiri (~> 1.10)
|
nokogiri (~> 1.10)
|
||||||
|
@ -725,7 +725,7 @@ DEPENDENCIES
|
||||||
ox (~> 2.10)
|
ox (~> 2.10)
|
||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel_tests (~> 2.27)
|
parallel_tests (~> 2.28)
|
||||||
pg (~> 1.1)
|
pg (~> 1.1)
|
||||||
pghero (~> 2.2)
|
pghero (~> 2.2)
|
||||||
pkg-config (~> 1.3)
|
pkg-config (~> 1.3)
|
||||||
|
|
|
@ -53,11 +53,12 @@ class AccountsController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def show_pinned_statuses?
|
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
|
end
|
||||||
|
|
||||||
def filtered_statuses
|
def filtered_statuses
|
||||||
default_statuses.tap do |statuses|
|
default_statuses.tap do |statuses|
|
||||||
|
statuses.merge!(hashtag_scope) if tag_requested?
|
||||||
statuses.merge!(only_media_scope) if media_requested?
|
statuses.merge!(only_media_scope) if media_requested?
|
||||||
statuses.merge!(no_replies_scope) unless replies_requested?
|
statuses.merge!(no_replies_scope) unless replies_requested?
|
||||||
end
|
end
|
||||||
|
@ -79,12 +80,15 @@ class AccountsController < ApplicationController
|
||||||
Status.without_replies
|
Status.without_replies
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hashtag_scope
|
||||||
|
Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
|
||||||
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find_local!(params[:username])
|
@account = Account.find_local!(params[:username])
|
||||||
end
|
end
|
||||||
|
|
||||||
def older_url
|
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)
|
pagination_url(max_id: @statuses.last.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -93,7 +97,9 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_url(max_id: nil, min_id: nil)
|
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)
|
short_account_media_url(@account, max_id: max_id, min_id: min_id)
|
||||||
elsif replies_requested?
|
elsif replies_requested?
|
||||||
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
|
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')
|
request.path.ends_with?('/with_replies')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tag_requested?
|
||||||
|
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
|
end
|
||||||
|
|
||||||
def filtered_status_page(params)
|
def filtered_status_page(params)
|
||||||
if params[:min_id].present?
|
if params[:min_id].present?
|
||||||
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
|
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
|
||||||
|
|
|
@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
||||||
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
||||||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
||||||
|
statuses.merge!(hashtag_scope) if params[:tagged].present?
|
||||||
|
|
||||||
statuses
|
statuses
|
||||||
end
|
end
|
||||||
|
@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
Status.without_reblogs
|
Status.without_reblogs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hashtag_scope
|
||||||
|
Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
|
||||||
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
|
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
before_action :set_body_classes
|
||||||
|
|
||||||
include Localized
|
include Localized
|
||||||
|
|
||||||
|
@ -16,6 +17,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'admin'
|
||||||
|
end
|
||||||
|
|
||||||
def store_current_location
|
def store_current_location
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -29,6 +29,6 @@ class Settings::ProfilesController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = current_user.account
|
@account = current_account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
# Intentionally does not inherit from BaseController
|
# Intentionally does not inherit from BaseController
|
||||||
class Settings::SessionsController < ApplicationController
|
class Settings::SessionsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
before_action :set_session, only: :destroy
|
before_action :set_session, only: :destroy
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
|
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||||
|
});
|
||||||
|
|
||||||
export const ALERT_SHOW = 'ALERT_SHOW';
|
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||||
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
|
@ -15,10 +22,28 @@ export function clearAlert() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlert(title, message) {
|
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
|
||||||
return {
|
return {
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function showAlertForError(error) {
|
||||||
|
if (error.response) {
|
||||||
|
const { data, status, statusText } = error.response;
|
||||||
|
|
||||||
|
let message = statusText;
|
||||||
|
let title = `${status}`;
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
message = data.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return showAlert(title, message);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
return showAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import api from 'flavours/glitch/util/api';
|
import api from 'flavours/glitch/util/api';
|
||||||
import { CancelToken } from 'axios';
|
import { CancelToken, isCancel } from 'axios';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
|
||||||
import { useEmoji } from './emojis';
|
import { useEmoji } from './emojis';
|
||||||
|
@ -8,6 +8,9 @@ import { recoverHashtags } from 'flavours/glitch/util/hashtag';
|
||||||
import resizeImage from 'flavours/glitch/util/resize_image';
|
import resizeImage from 'flavours/glitch/util/resize_image';
|
||||||
|
|
||||||
import { updateTimeline } from './timelines';
|
import { updateTimeline } from './timelines';
|
||||||
|
import { showAlertForError } from './alerts';
|
||||||
|
import { showAlert } from './alerts';
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts;
|
let cancelFetchComposeSuggestionsAccounts;
|
||||||
|
|
||||||
|
@ -52,6 +55,10 @@ export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
||||||
|
|
||||||
export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET';
|
export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
|
});
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
|
@ -207,21 +214,33 @@ export function doodleSet(options) {
|
||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files) {
|
||||||
return function (dispatch, getState) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(uploadComposeRequest());
|
dispatch(uploadComposeRequest());
|
||||||
|
|
||||||
resizeImage(files[0]).then(file => {
|
for (const [i, f] of Array.from(files).entries()) {
|
||||||
|
if (media.size + i > 3) break;
|
||||||
|
|
||||||
|
resizeImage(f).then(file => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
|
|
||||||
return api(getState).post('/api/v1/media', data, {
|
return api(getState).post('/api/v1/media', data, {
|
||||||
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
|
onUploadProgress: function({ loaded }){
|
||||||
|
progress[i] = loaded;
|
||||||
|
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||||
|
},
|
||||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
||||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeUploadCompose(id, params) {
|
export function changeUploadCompose(id, params) {
|
||||||
|
@ -320,6 +339,10 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
if (!isCancel(error)) {
|
||||||
|
dispatch(showAlertForError(error));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, 200, { leading: true, trailing: true });
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import api from 'flavours/glitch/util/api';
|
import api from 'flavours/glitch/util/api';
|
||||||
|
import { showAlertForError } from './alerts';
|
||||||
|
|
||||||
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||||
|
@ -239,7 +240,8 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
api(getState).get('/api/v1/accounts/search', { params })
|
api(getState).get('/api/v1/accounts/search', { params })
|
||||||
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
|
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)))
|
||||||
|
.catch(error => dispatch(showAlertForError(error)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
export const fetchListSuggestionsReady = (query, accounts) => ({
|
||||||
|
|
|
@ -109,14 +109,11 @@ export function register () {
|
||||||
pushNotificationsSetting.remove(me);
|
pushNotificationsSetting.remove(me);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return getRegistration()
|
||||||
getRegistration()
|
|
||||||
.then(getPushSubscription)
|
.then(getPushSubscription)
|
||||||
.then(unsubscribe);
|
.then(unsubscribe);
|
||||||
} catch (e) {
|
})
|
||||||
|
.catch(console.warn);
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Your browser does not support Web Push Notifications.');
|
console.warn('Your browser does not support Web Push Notifications.');
|
||||||
}
|
}
|
||||||
|
@ -137,6 +134,6 @@ export function saveSettings() {
|
||||||
if (me) {
|
if (me) {
|
||||||
pushNotificationsSetting.set(me, data);
|
pushNotificationsSetting.set(me, data);
|
||||||
}
|
}
|
||||||
});
|
}).catch(console.warn);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api from 'flavours/glitch/util/api';
|
import api from 'flavours/glitch/util/api';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { showAlertForError } from './alerts';
|
||||||
|
|
||||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||||
export const SETTING_SAVE = 'SETTING_SAVE';
|
export const SETTING_SAVE = 'SETTING_SAVE';
|
||||||
|
@ -23,7 +24,9 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||||
|
|
||||||
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
||||||
|
|
||||||
api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
api(getState).put('/api/web/settings', { data })
|
||||||
|
.then(() => dispatch({ type: SETTING_SAVE }))
|
||||||
|
.catch(error => dispatch(showAlertForError(error)));
|
||||||
}, 5000, { trailing: true });
|
}, 5000, { trailing: true });
|
||||||
|
|
||||||
export function saveSettings() {
|
export function saveSettings() {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale } from 'mastodon/locales';
|
||||||
import Compose from 'flavours/glitch/features/standalone/compose';
|
import Compose from 'flavours/glitch/features/standalone/compose';
|
||||||
import initialState from 'flavours/glitch/util/initial_state';
|
import initialState from 'flavours/glitch/util/initial_state';
|
||||||
|
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
@ -17,6 +18,8 @@ if (initialState) {
|
||||||
store.dispatch(hydrateStore(initialState));
|
store.dispatch(hydrateStore(initialState));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
|
||||||
export default class TimelineContainer extends React.PureComponent {
|
export default class TimelineContainer extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { showAlertForError } from '../actions/alerts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
@ -134,7 +135,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
onEmbed (status) {
|
onEmbed (status) {
|
||||||
dispatch(openModal('EMBED', { url: status.get('url') }));
|
dispatch(openModal('EMBED', {
|
||||||
|
url: status.get('url'),
|
||||||
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
onDelete (status, history, withRedraft = false) {
|
onDelete (status, history, withRedraft = false) {
|
||||||
|
|
|
@ -214,6 +214,7 @@ export default class ComposerOptions extends React.PureComponent {
|
||||||
onChange={handleChangeFiles}
|
onChange={handleChangeFiles}
|
||||||
ref={handleRefFileElement}
|
ref={handleRefFileElement}
|
||||||
type='file'
|
type='file'
|
||||||
|
multiple
|
||||||
{...hiddenComponent}
|
{...hiddenComponent}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
<div className='getting-started__footer'>
|
<div className='getting-started__footer'>
|
||||||
<ul>
|
<ul>
|
||||||
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></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='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='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
|
||||||
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a></li>
|
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a></li>
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class PublicTimeline extends React.PureComponent {
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`public_timeline-${columnId}`}
|
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' />}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ export default class EmbedModal extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onError: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ export default class EmbedModal extends ImmutablePureComponent {
|
||||||
iframeDocument.body.style.margin = 0;
|
iframeDocument.body.style.margin = 0;
|
||||||
this.iframe.width = iframeDocument.body.scrollWidth;
|
this.iframe.width = iframeDocument.body.scrollWidth;
|
||||||
this.iframe.height = iframeDocument.body.scrollHeight;
|
this.iframe.height = iframeDocument.body.scrollHeight;
|
||||||
|
}).catch(error => {
|
||||||
|
this.props.onError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ export default class ReportModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='report-modal__container'>
|
<div className='report-modal__container'>
|
||||||
<div className='report-modal__comment'>
|
<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
|
<textarea
|
||||||
className='setting-text light'
|
className='setting-text light'
|
||||||
|
|
|
@ -186,7 +186,7 @@ export default class UI extends React.Component {
|
||||||
this.setState({ draggingOver: false });
|
this.setState({ draggingOver: false });
|
||||||
this.dragTargets = [];
|
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));
|
this.props.dispatch(uploadCompose(e.dataTransfer.files));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { showAlert } from 'flavours/glitch/actions/alerts';
|
import { showAlertForError } from 'flavours/glitch/actions/alerts';
|
||||||
|
|
||||||
const defaultFailSuffix = 'FAIL';
|
const defaultFailSuffix = 'FAIL';
|
||||||
|
|
||||||
|
@ -8,21 +8,7 @@ export default function errorsMiddleware() {
|
||||||
const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
|
const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
|
||||||
|
|
||||||
if (action.type.match(isFail)) {
|
if (action.type.match(isFail)) {
|
||||||
if (action.error.response) {
|
dispatch(showAlertForError(action.error));
|
||||||
const { data, status, statusText } = action.error.response;
|
|
||||||
|
|
||||||
let message = statusText;
|
|
||||||
let title = `${status}`;
|
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
message = data.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(showAlert(title, message));
|
|
||||||
} else {
|
|
||||||
console.error(action.error);
|
|
||||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -290,3 +290,7 @@
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.directory__tag .trends__item__current {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -153,10 +153,15 @@ $content-width: 840px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.directory__tag a {
|
.directory__tag > a,
|
||||||
|
.directory__tag > div {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.directory__tag .table-action-link .fa {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.directory__tag h4 {
|
.directory__tag h4 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
|
@ -269,7 +269,8 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
a {
|
& > a,
|
||||||
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -279,7 +280,9 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -287,7 +290,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active a {
|
&.active > a {
|
||||||
background: $ui-highlight-color;
|
background: $ui-highlight-color;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
|
const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
|
||||||
if (type !== 'image/jpeg') {
|
if (!['image/jpeg', 'image/webp'].includes(type)) {
|
||||||
resolve(1);
|
resolve(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function clearAlert() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlert(title, message) {
|
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
|
||||||
return {
|
return {
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
title,
|
||||||
|
@ -44,6 +44,6 @@ export function showAlertForError(error) {
|
||||||
return showAlert(title, message);
|
return showAlert(title, message);
|
||||||
} else {
|
} else {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
|
return showAlert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
import { updateTimeline } from './timelines';
|
import { updateTimeline } from './timelines';
|
||||||
import { showAlertForError } from './alerts';
|
import { showAlertForError } from './alerts';
|
||||||
|
import { showAlert } from './alerts';
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts;
|
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_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
||||||
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
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) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
|
@ -184,21 +190,33 @@ export function submitComposeFail(error) {
|
||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files) {
|
||||||
return function (dispatch, getState) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(uploadComposeRequest());
|
dispatch(uploadComposeRequest());
|
||||||
|
|
||||||
resizeImage(files[0]).then(file => {
|
for (const [i, f] of Array.from(files).entries()) {
|
||||||
|
if (media.size + i > 3) break;
|
||||||
|
|
||||||
|
resizeImage(f).then(file => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
|
|
||||||
return api(getState).post('/api/v1/media', data, {
|
return api(getState).post('/api/v1/media', data, {
|
||||||
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
|
onUploadProgress: function({ loaded }){
|
||||||
|
progress[i] = loaded;
|
||||||
|
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||||
|
},
|
||||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
||||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeUploadCompose(id, params) {
|
export function changeUploadCompose(id, params) {
|
||||||
|
|
|
@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStateAfterIntersection = (prevState) => {
|
updateStateAfterIntersection = (prevState) => {
|
||||||
if (prevState.isIntersecting && !this.entry.isIntersecting) {
|
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
|
||||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
scheduleIdleTask(this.hideIfNotIntersecting);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
import Compose from '../features/standalone/compose';
|
import Compose from '../features/standalone/compose';
|
||||||
import initialState from '../initial_state';
|
import initialState from '../initial_state';
|
||||||
|
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
@ -17,6 +18,8 @@ if (initialState) {
|
||||||
store.dispatch(hydrateStore(initialState));
|
store.dispatch(hydrateStore(initialState));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
|
||||||
export default class TimelineContainer extends React.PureComponent {
|
export default class TimelineContainer extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
|
@ -63,7 +63,7 @@ class UploadButton extends ImmutablePureComponent {
|
||||||
key={resetFileKey}
|
key={resetFileKey}
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
type='file'
|
type='file'
|
||||||
multiple={false}
|
multiple
|
||||||
accept={acceptContentTypes.toArray().join(',')}
|
accept={acceptContentTypes.toArray().join(',')}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -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>}
|
{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>}
|
{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='/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='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='/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>
|
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||||
|
|
|
@ -89,7 +89,7 @@ const FrameInteractions = ({ onNext }) => (
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='introduction__action'>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -124,7 +124,7 @@ class PublicTimeline extends React.PureComponent {
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`public_timeline-${columnId}`}
|
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}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -97,7 +97,7 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='report-modal__container'>
|
<div className='report-modal__container'>
|
||||||
<div className='report-modal__comment'>
|
<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
|
<textarea
|
||||||
className='setting-text light'
|
className='setting-text light'
|
||||||
|
|
|
@ -263,7 +263,7 @@ class UI extends React.PureComponent {
|
||||||
this.setState({ draggingOver: false });
|
this.setState({ draggingOver: false });
|
||||||
this.dragTargets = [];
|
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));
|
this.props.dispatch(uploadCompose(e.dataTransfer.files));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,15 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/actions/alerts.json"
|
"path": "app/javascript/mastodon/actions/alerts.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "File upload limit exceeded.",
|
||||||
|
"id": "upload_error.limit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/actions/compose.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -1275,7 +1284,7 @@
|
||||||
"id": "getting_started.security"
|
"id": "getting_started.security"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "About this instance",
|
"defaultMessage": "About this server",
|
||||||
"id": "navigation_bar.info"
|
"id": "navigation_bar.info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1448,7 +1457,7 @@
|
||||||
"id": "introduction.interactions.favourite.text"
|
"id": "introduction.interactions.favourite.text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Finish tutorial!",
|
"defaultMessage": "Finish toot-orial!",
|
||||||
"id": "introduction.interactions.action"
|
"id": "introduction.interactions.action"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1828,7 +1837,7 @@
|
||||||
"id": "column.public"
|
"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"
|
"id": "empty_column.public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -2188,7 +2197,7 @@
|
||||||
"id": "report.target"
|
"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"
|
"id": "report.hint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.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.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.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.authorize": "Authorize",
|
||||||
"follow_request.reject": "Reject",
|
"follow_request.reject": "Reject",
|
||||||
"getting_started.developers": "Developers",
|
"getting_started.developers": "Developers",
|
||||||
|
@ -228,7 +228,7 @@
|
||||||
"navigation_bar.favourites": "Favourites",
|
"navigation_bar.favourites": "Favourites",
|
||||||
"navigation_bar.filters": "Muted words",
|
"navigation_bar.filters": "Muted words",
|
||||||
"navigation_bar.follow_requests": "Follow requests",
|
"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.keyboard_shortcuts": "Hotkeys",
|
||||||
"navigation_bar.lists": "Lists",
|
"navigation_bar.lists": "Lists",
|
||||||
"navigation_bar.misc": "Misc",
|
"navigation_bar.misc": "Misc",
|
||||||
|
@ -281,7 +281,7 @@
|
||||||
"reply_indicator.cancel": "Cancel",
|
"reply_indicator.cancel": "Cancel",
|
||||||
"report.forward": "Forward to {target}",
|
"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.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.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting {target}",
|
"report.target": "Reporting {target}",
|
||||||
|
@ -347,6 +347,7 @@
|
||||||
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
|
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
|
||||||
"upload_area.title": "Drag & drop to upload",
|
"upload_area.title": "Drag & drop to upload",
|
||||||
"upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
|
"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.description": "Describe for the visually impaired",
|
||||||
"upload_form.focus": "Change preview",
|
"upload_form.focus": "Change preview",
|
||||||
"upload_form.undo": "Delete",
|
"upload_form.undo": "Delete",
|
||||||
|
|
|
@ -132,7 +132,7 @@
|
||||||
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
|
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
|
||||||
"empty_column.mutes": "まだ誰もミュートしていません。",
|
"empty_column.mutes": "まだ誰もミュートしていません。",
|
||||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||||
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
|
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
|
||||||
"follow_request.authorize": "許可",
|
"follow_request.authorize": "許可",
|
||||||
"follow_request.reject": "拒否",
|
"follow_request.reject": "拒否",
|
||||||
"getting_started.developers": "開発",
|
"getting_started.developers": "開発",
|
||||||
|
@ -228,7 +228,7 @@
|
||||||
"navigation_bar.favourites": "お気に入り",
|
"navigation_bar.favourites": "お気に入り",
|
||||||
"navigation_bar.filters": "フィルター設定",
|
"navigation_bar.filters": "フィルター設定",
|
||||||
"navigation_bar.follow_requests": "フォローリクエスト",
|
"navigation_bar.follow_requests": "フォローリクエスト",
|
||||||
"navigation_bar.info": "このインスタンスについて",
|
"navigation_bar.info": "このサーバーについて",
|
||||||
"navigation_bar.keyboard_shortcuts": "ホットキー",
|
"navigation_bar.keyboard_shortcuts": "ホットキー",
|
||||||
"navigation_bar.lists": "リスト",
|
"navigation_bar.lists": "リスト",
|
||||||
"navigation_bar.logout": "ログアウト",
|
"navigation_bar.logout": "ログアウト",
|
||||||
|
@ -280,8 +280,8 @@
|
||||||
"relative_time.seconds": "{number}秒前",
|
"relative_time.seconds": "{number}秒前",
|
||||||
"reply_indicator.cancel": "キャンセル",
|
"reply_indicator.cancel": "キャンセル",
|
||||||
"report.forward": "{target} に転送する",
|
"report.forward": "{target} に転送する",
|
||||||
"report.forward_hint": "このアカウントは別のインスタンスに所属しています。通報内容を匿名で転送しますか?",
|
"report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?",
|
||||||
"report.hint": "通報内容はあなたのインスタンスのモデレーターへ送信されます。通報理由を入力してください。:",
|
"report.hint": "通報内容はあなたのサーバーのモデレーターへ送信されます。通報理由を入力してください。:",
|
||||||
"report.placeholder": "追加コメント",
|
"report.placeholder": "追加コメント",
|
||||||
"report.submit": "通報する",
|
"report.submit": "通報する",
|
||||||
"report.target": "{target}さんを通報する",
|
"report.target": "{target}さんを通報する",
|
||||||
|
@ -347,6 +347,7 @@
|
||||||
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
|
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
|
||||||
"upload_area.title": "ドラッグ&ドロップでアップロード",
|
"upload_area.title": "ドラッグ&ドロップでアップロード",
|
||||||
"upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)",
|
"upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)",
|
||||||
|
"upload_error.limit": "アップロードできる上限を超えています。",
|
||||||
"upload_form.description": "視覚障害者のための説明",
|
"upload_form.description": "視覚障害者のための説明",
|
||||||
"upload_form.focus": "焦点",
|
"upload_form.focus": "焦点",
|
||||||
"upload_form.undo": "削除",
|
"upload_form.undo": "削除",
|
||||||
|
|
|
@ -347,6 +347,7 @@
|
||||||
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
|
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
|
||||||
"upload_area.title": "Przeciągnij i upuść aby wysłać",
|
"upload_area.title": "Przeciągnij i upuść aby wysłać",
|
||||||
"upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)",
|
"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.description": "Wprowadź opis dla niewidomych i niedowidzących",
|
||||||
"upload_form.focus": "Dopasuj podgląd",
|
"upload_form.focus": "Dopasuj podgląd",
|
||||||
"upload_form.undo": "Usuń",
|
"upload_form.undo": "Usuń",
|
||||||
|
|
|
@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
|
const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
|
||||||
if (type !== 'image/jpeg') {
|
if (!['image/jpeg', 'image/webp'].includes(type)) {
|
||||||
resolve(1);
|
resolve(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,3 +288,7 @@
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.directory__tag .trends__item__current {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -153,10 +153,15 @@ $content-width: 840px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.directory__tag a {
|
.directory__tag > a,
|
||||||
|
.directory__tag > div {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.directory__tag .table-action-link .fa {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.directory__tag h4 {
|
.directory__tag h4 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
|
@ -638,7 +638,6 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: pre-wrap;
|
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
|
|
||||||
|
@ -662,6 +661,7 @@
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -269,7 +269,8 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
a {
|
& > a,
|
||||||
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -279,7 +280,9 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -287,7 +290,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active a {
|
&.active > a {
|
||||||
background: $ui-highlight-color;
|
background: $ui-highlight-color;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ class ActivityTracker
|
||||||
EXPIRE_AFTER = 90.days.seconds
|
EXPIRE_AFTER = 90.days.seconds
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def increment(prefix)
|
def increment(prefix)
|
||||||
key = [prefix, current_week].join(':')
|
key = [prefix, current_week].join(':')
|
||||||
|
|
||||||
|
@ -20,10 +22,6 @@ class ActivityTracker
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_week
|
def current_week
|
||||||
Time.zone.today.cweek
|
Time.zone.today.cweek
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class ActivityPub::Activity
|
class ActivityPub::Activity
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def initialize(json, account, **options)
|
def initialize(json, account, **options)
|
||||||
@json = json
|
@json = json
|
||||||
|
@ -70,10 +71,6 @@ class ActivityPub::Activity
|
||||||
@object_uri ||= value_or_id(@object)
|
@object_uri ||= value_or_id(@object)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def distribute(status)
|
def distribute(status)
|
||||||
crawl_links(status)
|
crawl_links(status)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'singleton'
|
||||||
|
|
||||||
class FeedManager
|
class FeedManager
|
||||||
include Singleton
|
include Singleton
|
||||||
|
include Redisable
|
||||||
|
|
||||||
MAX_ITEMS = 400
|
MAX_ITEMS = 400
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class FeedManager
|
||||||
|
|
||||||
def unpush_from_home(account, status)
|
def unpush_from_home(account, status)
|
||||||
return false unless remove_from_feed(:home, account.id, 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
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ class FeedManager
|
||||||
|
|
||||||
def unpush_from_list(list, status)
|
def unpush_from_list(list, status)
|
||||||
return false unless remove_from_feed(:list, list.id, 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
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -143,10 +144,6 @@ class FeedManager
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def push_update_required?(timeline_id)
|
def push_update_required?(timeline_id)
|
||||||
redis.exists("subscribed:#{timeline_id}")
|
redis.exists("subscribed:#{timeline_id}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -99,7 +99,7 @@ class Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
def encode_and_link_urls(html, accounts = nil, options = {})
|
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)
|
if accounts.is_a?(Hash)
|
||||||
options = accounts
|
options = accounts
|
||||||
|
@ -199,6 +199,53 @@ class Formatter
|
||||||
result.flatten.join
|
result.flatten.join
|
||||||
end
|
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 = {})
|
def link_to_url(entity, options = {})
|
||||||
url = Addressable::URI.parse(entity[:url])
|
url = Addressable::URI.parse(entity[:url])
|
||||||
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class OStatus::Activity::Base
|
class OStatus::Activity::Base
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def initialize(xml, account = nil, **options)
|
def initialize(xml, account = nil, **options)
|
||||||
@xml = xml
|
@xml = xml
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -66,8 +68,4 @@ class OStatus::Activity::Base
|
||||||
Status.find_by(uri: uri)
|
Status.find_by(uri: uri)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,8 @@ class PotentialFriendshipTracker
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def record(account_id, target_account_id, action)
|
def record(account_id, target_account_id, action)
|
||||||
return if account_id == target_account_id
|
return if account_id == target_account_id
|
||||||
|
|
||||||
|
@ -31,11 +33,5 @@ class PotentialFriendshipTracker
|
||||||
return [] if account_ids.empty?
|
return [] if account_ids.empty?
|
||||||
Account.searchable.where(id: account_ids)
|
Account.searchable.where(id: account_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
class AccountDomainBlock < ApplicationRecord
|
class AccountDomainBlock < ApplicationRecord
|
||||||
include Paginable
|
include Paginable
|
||||||
|
include DomainNormalizable
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
validates :domain, presence: true, uniqueness: { scope: :account_id }
|
validates :domain, presence: true, uniqueness: { scope: :account_id }
|
||||||
|
|
|
@ -56,5 +56,6 @@ module AccountAssociations
|
||||||
|
|
||||||
# Hashtags
|
# Hashtags
|
||||||
has_and_belongs_to_many :tags
|
has_and_belongs_to_many :tags
|
||||||
|
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module AccountAvatar
|
module AccountAvatar
|
||||||
extend ActiveSupport::Concern
|
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
|
LIMIT = 2.megabytes
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module AccountHeader
|
module AccountHeader
|
||||||
extend ActiveSupport::Concern
|
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
|
LIMIT = 2.megabytes
|
||||||
MAX_PIXELS = 750_000 # 1500x500px
|
MAX_PIXELS = 750_000 # 1500x500px
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,6 @@ module DomainNormalizable
|
||||||
private
|
private
|
||||||
|
|
||||||
def normalize_domain
|
def normalize_domain
|
||||||
self.domain = TagManager.instance.normalize_domain(domain)
|
self.domain = TagManager.instance.normalize_domain(domain&.strip)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Redisable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def redis
|
||||||
|
Redis.current
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'csv'
|
require 'csv'
|
||||||
|
|
||||||
class Export
|
class Export
|
||||||
|
|
|
@ -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
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Feed
|
class Feed
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def initialize(type, id)
|
def initialize(type, id)
|
||||||
@type = type
|
@type = type
|
||||||
@id = id
|
@id = id
|
||||||
|
@ -27,8 +29,4 @@ class Feed
|
||||||
def key
|
def key
|
||||||
FeedManager.instance.key(@type, @id)
|
FeedManager.instance.key(@type, @id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,20 +13,30 @@
|
||||||
# data_file_size :integer
|
# data_file_size :integer
|
||||||
# data_updated_at :datetime
|
# data_updated_at :datetime
|
||||||
# account_id :bigint(8) not null
|
# account_id :bigint(8) not null
|
||||||
|
# overwrite :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Import < ApplicationRecord
|
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
|
self.inheritance_column = false
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
enum type: [:following, :blocking, :muting]
|
enum type: [:following, :blocking, :muting, :domain_blocking]
|
||||||
|
|
||||||
validates :type, presence: true
|
validates :type, presence: true
|
||||||
|
|
||||||
has_attached_file :data
|
has_attached_file :data
|
||||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
validates_attachment_content_type :data, content_type: FILE_TYPES
|
||||||
validates_attachment_presence :data
|
validates_attachment_presence :data
|
||||||
|
|
||||||
|
def mode
|
||||||
|
overwrite? ? :overwrite : :merge
|
||||||
|
end
|
||||||
|
|
||||||
|
def mode=(str)
|
||||||
|
self.overwrite = str.to_sym == :overwrite
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,11 +25,11 @@ class MediaAttachment < ApplicationRecord
|
||||||
|
|
||||||
enum type: [:image, :gifv, :video, :audio, :unknown]
|
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
|
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze
|
||||||
AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].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_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
|
||||||
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', '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
|
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' }
|
convert_options: { all: '-quality 90 -strip' }
|
||||||
|
|
||||||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
|
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: IMAGE_LIMIT, unless: :video_or_gifv?
|
||||||
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video?
|
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv?
|
||||||
remotable_attachment :file, VIDEO_LIMIT
|
remotable_attachment :file, VIDEO_LIMIT
|
||||||
|
|
||||||
include Attachmentable
|
include Attachmentable
|
||||||
|
@ -129,6 +129,10 @@ class MediaAttachment < ApplicationRecord
|
||||||
file.blank? && remote_url.present?
|
file.blank? && remote_url.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def video_or_gifv?
|
||||||
|
video? || gifv?
|
||||||
|
end
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
shortcode
|
shortcode
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class PreviewCard < ApplicationRecord
|
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
|
LIMIT = 1.megabytes
|
||||||
|
|
||||||
self.inheritance_column = false
|
self.inheritance_column = false
|
||||||
|
|
|
@ -14,6 +14,7 @@ class Tag < ApplicationRecord
|
||||||
has_and_belongs_to_many :accounts
|
has_and_belongs_to_many :accounts
|
||||||
has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
|
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
|
has_one :account_tag_stat, dependent: :destroy
|
||||||
|
|
||||||
HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
|
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 :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 :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,
|
delegate :accounts_count,
|
||||||
:accounts_count=,
|
:accounts_count=,
|
||||||
|
|
|
@ -7,6 +7,8 @@ class TrendingTags
|
||||||
THRESHOLD = 5
|
THRESHOLD = 5
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def record_use!(tag, account, at_time = Time.now.utc)
|
def record_use!(tag, account, at_time = Time.now.utc)
|
||||||
return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot?
|
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.split(' ') if @disallowed_hashtags.is_a? String
|
||||||
@disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
|
@disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,6 +52,14 @@ class ManifestSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def share_target
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -212,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_tombstones!
|
def clear_tombstones!
|
||||||
Tombstone.delete_all(account_id: @account.id)
|
Tombstone.where(account_id: @account.id).delete_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def protocol_changed?
|
def protocol_changed?
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class BatchedRemoveStatusService < BaseService
|
class BatchedRemoveStatusService < BaseService
|
||||||
include StreamEntryRenderer
|
include StreamEntryRenderer
|
||||||
|
include Redisable
|
||||||
|
|
||||||
# Delete given statuses and reblogs of them
|
# Delete given statuses and reblogs of them
|
||||||
# Dispatch PuSH updates of the deleted statuses, but only local ones
|
# Dispatch PuSH updates of the deleted statuses, but only local ones
|
||||||
|
@ -120,10 +121,6 @@ class BatchedRemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_xml(stream_entry)
|
def build_xml(stream_entry)
|
||||||
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class FollowService < BaseService
|
class FollowService < BaseService
|
||||||
|
include Redisable
|
||||||
|
|
||||||
# Follow a remote user, notify remote user about the follow
|
# Follow a remote user, notify remote user about the follow
|
||||||
# @param [Account] source_account From which to 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)
|
# @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
|
follow
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_follow_request_xml(follow_request)
|
def build_follow_request_xml(follow_request)
|
||||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
|
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PostStatusService < BaseService
|
class PostStatusService < BaseService
|
||||||
|
include Redisable
|
||||||
|
|
||||||
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
||||||
|
|
||||||
# Post a text status update, fetch and notify remote users mentioned
|
# Post a text status update, fetch and notify remote users mentioned
|
||||||
|
@ -115,10 +117,6 @@ class PostStatusService < BaseService
|
||||||
ProcessHashtagsService.new
|
ProcessHashtagsService.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def scheduled?
|
def scheduled?
|
||||||
@scheduled_at.present?
|
@scheduled_at.present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,21 @@
|
||||||
class ProcessHashtagsService < BaseService
|
class ProcessHashtagsService < BaseService
|
||||||
def call(status, tags = [])
|
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|
|
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
|
||||||
tag = Tag.where(name: name).first_or_create(name: name)
|
tag = Tag.where(name: name).first_or_create(name: name)
|
||||||
|
|
||||||
status.tags << tag
|
status.tags << tag
|
||||||
|
records << tag
|
||||||
|
|
||||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
|
TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class RemoveStatusService < BaseService
|
class RemoveStatusService < BaseService
|
||||||
include StreamEntryRenderer
|
include StreamEntryRenderer
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def call(status, **options)
|
def call(status, **options)
|
||||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||||
|
@ -56,7 +57,7 @@ class RemoveStatusService < BaseService
|
||||||
|
|
||||||
def remove_from_affected
|
def remove_from_affected
|
||||||
@mentions.map(&:account).select(&:local?).each do |account|
|
@mentions.map(&:account).select(&:local?).each do |account|
|
||||||
Redis.current.publish("timeline:#{account.id}", @payload)
|
redis.publish("timeline:#{account.id}", @payload)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -131,26 +132,30 @@ class RemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_hashtags
|
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?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
@tags.each do |hashtag|
|
@tags.each do |hashtag|
|
||||||
Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
|
redis.publish("timeline:hashtag:#{hashtag}", @payload)
|
||||||
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
|
redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_public
|
def remove_from_public
|
||||||
return unless @status.public_visibility?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
Redis.current.publish('timeline:public', @payload)
|
redis.publish('timeline:public', @payload)
|
||||||
Redis.current.publish('timeline:public:local', @payload) if @status.local?
|
redis.publish('timeline:public:local', @payload) if @status.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_media
|
def remove_from_media
|
||||||
return unless @status.public_visibility?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
Redis.current.publish('timeline:public:media', @payload)
|
redis.publish('timeline:public:media', @payload)
|
||||||
Redis.current.publish('timeline:public:local:media', @payload) if @status.local?
|
redis.publish('timeline:public:local:media', @payload) if @status.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_direct
|
def remove_from_direct
|
||||||
|
@ -159,8 +164,4 @@ class RemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local?
|
Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -63,4 +63,17 @@
|
||||||
- @endorsed_accounts.each do |account|
|
- @endorsed_accounts.each do |account|
|
||||||
= account_link_to 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'
|
= render 'application/sidebar'
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
||||||
.fields-group
|
.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
|
.fields-group
|
||||||
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
||||||
|
|
|
@ -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
|
|
@ -5,8 +5,11 @@
|
||||||
.field-group
|
.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')
|
= 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
|
.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')
|
= 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
|
.actions
|
||||||
= f.button :button, t('imports.upload'), type: :submit
|
= f.button :button, t('imports.upload'), type: :submit
|
||||||
|
|
|
@ -13,11 +13,17 @@ class Import::RelationshipWorker
|
||||||
|
|
||||||
case relationship
|
case relationship
|
||||||
when 'follow'
|
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'
|
when 'block'
|
||||||
BlockService.new.call(from_account, target_account)
|
BlockService.new.call(from_account, target_account)
|
||||||
|
when 'unblock'
|
||||||
|
UnblockService.new.call(from_account, target_account)
|
||||||
when 'mute'
|
when 'mute'
|
||||||
MuteService.new.call(from_account, target_account)
|
MuteService.new.call(from_account, target_account)
|
||||||
|
when 'unmute'
|
||||||
|
UnmuteService.new.call(from_account, target_account)
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
|
|
|
@ -1,44 +1,14 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'csv'
|
|
||||||
|
|
||||||
class ImportWorker
|
class ImportWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: false
|
sidekiq_options queue: 'pull', retry: false
|
||||||
|
|
||||||
attr_reader :import
|
|
||||||
|
|
||||||
def perform(import_id)
|
def perform(import_id)
|
||||||
@import = Import.find(import_id)
|
import = Import.find(import_id)
|
||||||
|
ImportService.new.call(import)
|
||||||
Import::RelationshipWorker.push_bulk(import_rows) do |row|
|
ensure
|
||||||
[@import.account_id, row.first, relationship_type]
|
import&.destroy
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Scheduler::FeedCleanupScheduler
|
class Scheduler::FeedCleanupScheduler
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
sidekiq_options unique: :until_executed, retry: 0
|
sidekiq_options unique: :until_executed, retry: 0
|
||||||
|
|
||||||
|
@ -57,8 +58,4 @@ class Scheduler::FeedCleanupScheduler
|
||||||
def feed_manager
|
def feed_manager
|
||||||
FeedManager.instance
|
FeedManager.instance
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,11 +35,8 @@ ignore_missing:
|
||||||
- 'activemodel.errors.*'
|
- 'activemodel.errors.*'
|
||||||
- 'activerecord.attributes.*'
|
- 'activerecord.attributes.*'
|
||||||
- 'activerecord.errors.*'
|
- 'activerecord.errors.*'
|
||||||
- '{devise,pagination,doorkeeper}.*'
|
- '{pagination,doorkeeper}.*'
|
||||||
- '{date,datetime,time,number}.*'
|
- '{date,datetime,time,number}.*'
|
||||||
- 'simple_form.{yes,no}'
|
|
||||||
- 'simple_form.{placeholders,hints,labels}.*'
|
|
||||||
- 'simple_form.{error_notification,required}.:'
|
|
||||||
- 'errors.messages.*'
|
- 'errors.messages.*'
|
||||||
- 'activerecord.errors.models.doorkeeper/*'
|
- 'activerecord.errors.models.doorkeeper/*'
|
||||||
- 'sessions.{browsers,platforms}.*'
|
- 'sessions.{browsers,platforms}.*'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module Twitter
|
module Twitter
|
||||||
class Regex
|
class Regex
|
||||||
REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/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_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
|
||||||
REGEXEN[:valid_url_balanced_parens] = /
|
REGEXEN[:valid_url_balanced_parens] = /
|
||||||
\(
|
\(
|
||||||
(?:
|
(?:
|
||||||
|
|
|
@ -20,17 +20,17 @@ en:
|
||||||
action: Verify email address
|
action: Verify email address
|
||||||
action_with_app: Confirm and return to %{app}
|
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.
|
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}'
|
subject: 'Mastodon: Confirmation instructions for %{instance}'
|
||||||
title: Verify email address
|
title: Verify email address
|
||||||
email_changed:
|
email_changed:
|
||||||
explanation: 'The email address for your account is being changed to:'
|
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'
|
subject: 'Mastodon: Email changed'
|
||||||
title: New email address
|
title: New email address
|
||||||
password_change:
|
password_change:
|
||||||
explanation: The password for your account has been changed.
|
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'
|
subject: 'Mastodon: Password changed'
|
||||||
title: Password changed
|
title: Password changed
|
||||||
reconfirmation_instructions:
|
reconfirmation_instructions:
|
||||||
|
|
|
@ -20,17 +20,17 @@ ja:
|
||||||
action: メールアドレスの確認
|
action: メールアドレスの確認
|
||||||
action_with_app: 確認し %{app} に戻る
|
action_with_app: 確認し %{app} に戻る
|
||||||
explanation: このメールアドレスで%{host}にアカウントを作成しました。有効にするまであと一歩です。もし心当たりがない場合、申し訳ありませんがこのメールを無視してください。
|
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}'
|
subject: 'Mastodon: メールアドレスの確認 %{instance}'
|
||||||
title: メールアドレスの確認
|
title: メールアドレスの確認
|
||||||
email_changed:
|
email_changed:
|
||||||
explanation: 'アカウントのメールアドレスは以下のように変更されます:'
|
explanation: 'アカウントのメールアドレスは以下のように変更されます:'
|
||||||
extra: メールアドレスの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はインスタンス管理者に連絡してください。
|
extra: メールアドレスの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はサーバー管理者に連絡してください。
|
||||||
subject: 'Mastodon: メールアドレスの変更'
|
subject: 'Mastodon: メールアドレスの変更'
|
||||||
title: 新しいメールアドレス
|
title: 新しいメールアドレス
|
||||||
password_change:
|
password_change:
|
||||||
explanation: パスワードが変更されました。
|
explanation: パスワードが変更されました。
|
||||||
extra: パスワードの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はインスタンス管理者に連絡してください。
|
extra: パスワードの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はサーバー管理者に連絡してください。
|
||||||
subject: 'Mastodon: パスワードが変更されました'
|
subject: 'Mastodon: パスワードが変更されました'
|
||||||
title: パスワードの変更
|
title: パスワードの変更
|
||||||
reconfirmation_instructions:
|
reconfirmation_instructions:
|
||||||
|
|
|
@ -7,7 +7,7 @@ en:
|
||||||
administered_by: 'Administered by:'
|
administered_by: 'Administered by:'
|
||||||
api: API
|
api: API
|
||||||
apps: Mobile apps
|
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: Contact
|
||||||
contact_missing: Not set
|
contact_missing: Not set
|
||||||
contact_unavailable: N/A
|
contact_unavailable: N/A
|
||||||
|
@ -27,7 +27,7 @@ en:
|
||||||
generic_description: "%{domain} is one server in the network"
|
generic_description: "%{domain} is one server in the network"
|
||||||
hosted_on: Mastodon hosted on %{domain}
|
hosted_on: Mastodon hosted on %{domain}
|
||||||
learn_more: Learn more
|
learn_more: Learn more
|
||||||
other_instances: Instance list
|
other_instances: Server list
|
||||||
privacy_policy: Privacy policy
|
privacy_policy: Privacy policy
|
||||||
source_code: Source code
|
source_code: Source code
|
||||||
status_count_after:
|
status_count_after:
|
||||||
|
@ -386,7 +386,7 @@ en:
|
||||||
desc_html: Modify the look with CSS loaded on every page
|
desc_html: Modify the look with CSS loaded on every page
|
||||||
title: Custom CSS
|
title: Custom CSS
|
||||||
hero:
|
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
|
title: Hero image
|
||||||
hide_followers_count:
|
hide_followers_count:
|
||||||
desc_html: Do not show followers count on user profiles
|
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
|
desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot
|
||||||
title: Mascot image
|
title: Mascot image
|
||||||
peers_api_enabled:
|
peers_api_enabled:
|
||||||
desc_html: Domain names this instance has encountered in the fediverse
|
desc_html: Domain names this server has encountered in the fediverse
|
||||||
title: Publish list of discovered instances
|
title: Publish list of discovered servers
|
||||||
preview_sensitive_media:
|
preview_sensitive_media:
|
||||||
desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive
|
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
|
title: Show sensitive media in OpenGraph previews
|
||||||
|
@ -424,20 +424,20 @@ en:
|
||||||
title: Show staff badge
|
title: Show staff badge
|
||||||
site_description:
|
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>.
|
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:
|
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
|
title: Custom extended information
|
||||||
site_short_description:
|
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.
|
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 instance description
|
title: Short server description
|
||||||
site_terms:
|
site_terms:
|
||||||
desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
|
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
|
title: Custom terms of service
|
||||||
site_title: Instance name
|
site_title: Server name
|
||||||
thumbnail:
|
thumbnail:
|
||||||
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
||||||
title: Instance thumbnail
|
title: Server thumbnail
|
||||||
timeline_preview:
|
timeline_preview:
|
||||||
desc_html: Display public timeline on landing page
|
desc_html: Display public timeline on landing page
|
||||||
title: Timeline preview
|
title: Timeline preview
|
||||||
|
@ -498,7 +498,7 @@ en:
|
||||||
warning: Be very careful with this data. Never share it with anyone!
|
warning: Be very careful with this data. Never share it with anyone!
|
||||||
your_token: Your access token
|
your_token: Your access token
|
||||||
auth:
|
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
|
change_password: Password
|
||||||
confirm_email: Confirm email
|
confirm_email: Confirm email
|
||||||
delete_account: Delete account
|
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.
|
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
|
proceed: Delete account
|
||||||
success_msg: Your account was successfully deleted
|
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
|
warning_title: Disseminated content availability
|
||||||
directories:
|
directories:
|
||||||
directory: Profile directory
|
directory: Profile directory
|
||||||
|
@ -591,6 +591,10 @@ en:
|
||||||
lists: Lists
|
lists: Lists
|
||||||
mutes: You mute
|
mutes: You mute
|
||||||
storage: Media storage
|
storage: Media storage
|
||||||
|
featured_tags:
|
||||||
|
add_new: Add new
|
||||||
|
errors:
|
||||||
|
limit: You have already featured the maximum amount of hashtags
|
||||||
filters:
|
filters:
|
||||||
contexts:
|
contexts:
|
||||||
home: Home timeline
|
home: Home timeline
|
||||||
|
@ -609,7 +613,7 @@ en:
|
||||||
title: Add new filter
|
title: Add new filter
|
||||||
followers:
|
followers:
|
||||||
domain: Domain
|
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
|
followers_count: Number of followers
|
||||||
lock_link: Lock your account
|
lock_link: Lock your account
|
||||||
purge: Remove from followers
|
purge: Remove from followers
|
||||||
|
@ -632,10 +636,16 @@ en:
|
||||||
one: Something isn't quite right yet! Please review the error below
|
one: Something isn't quite right yet! Please review the error below
|
||||||
other: Something isn't quite right yet! Please review %{count} errors below
|
other: Something isn't quite right yet! Please review %{count} errors below
|
||||||
imports:
|
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
|
success: Your data was successfully uploaded and will now be processed in due time
|
||||||
types:
|
types:
|
||||||
blocking: Blocking list
|
blocking: Blocking list
|
||||||
|
domain_blocking: Domain blocking list
|
||||||
following: Following list
|
following: Following list
|
||||||
muting: Muting list
|
muting: Muting list
|
||||||
upload: Upload
|
upload: Upload
|
||||||
|
@ -657,7 +667,7 @@ en:
|
||||||
one: 1 use
|
one: 1 use
|
||||||
other: "%{count} uses"
|
other: "%{count} uses"
|
||||||
max_uses_prompt: No limit
|
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:
|
table:
|
||||||
expires_at: Expires
|
expires_at: Expires
|
||||||
uses: Uses
|
uses: Uses
|
||||||
|
@ -805,6 +815,7 @@ en:
|
||||||
development: Development
|
development: Development
|
||||||
edit_profile: Edit profile
|
edit_profile: Edit profile
|
||||||
export: Data export
|
export: Data export
|
||||||
|
featured_tags: Featured hashtags
|
||||||
flavours: Flavours
|
flavours: Flavours
|
||||||
followers: Authorized followers
|
followers: Authorized followers
|
||||||
import: Import
|
import: Import
|
||||||
|
@ -985,7 +996,7 @@ en:
|
||||||
final_action: Start posting
|
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.'
|
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: 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_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.
|
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
|
subject: Welcome to Mastodon
|
||||||
|
|
|
@ -7,7 +7,7 @@ ja:
|
||||||
administered_by: '管理者:'
|
administered_by: '管理者:'
|
||||||
api: API
|
api: API
|
||||||
apps: アプリ
|
apps: アプリ
|
||||||
closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。しかし、他のインスタンスにアカウントを作成しても全く同じネットワークに参加することができます。
|
closed_registrations: 現在このサーバーでの新規登録は受け付けていません。しかし、他のサーバーにアカウントを作成しても全く同じネットワークに参加することができます。
|
||||||
contact: 連絡先
|
contact: 連絡先
|
||||||
contact_missing: 未設定
|
contact_missing: 未設定
|
||||||
contact_unavailable: N/A
|
contact_unavailable: N/A
|
||||||
|
@ -24,10 +24,10 @@ ja:
|
||||||
real_conversation_title: 本当のコミュニケーションのために
|
real_conversation_title: 本当のコミュニケーションのために
|
||||||
within_reach_body: デベロッパーフレンドリーな API により実現された、iOS や Android、その他様々なプラットフォームのためのアプリでどこでも友人とやりとりできます。
|
within_reach_body: デベロッパーフレンドリーな API により実現された、iOS や Android、その他様々なプラットフォームのためのアプリでどこでも友人とやりとりできます。
|
||||||
within_reach_title: いつでも身近に
|
within_reach_title: いつでも身近に
|
||||||
generic_description: "%{domain} は、Mastodon インスタンスの一つです"
|
generic_description: "%{domain} は、Mastodon サーバーの一つです"
|
||||||
hosted_on: Mastodon hosted on %{domain}
|
hosted_on: Mastodon hosted on %{domain}
|
||||||
learn_more: もっと詳しく
|
learn_more: もっと詳しく
|
||||||
other_instances: 他のインスタンス
|
other_instances: 他のサーバー
|
||||||
privacy_policy: プライバシーポリシー
|
privacy_policy: プライバシーポリシー
|
||||||
source_code: ソースコード
|
source_code: ソースコード
|
||||||
status_count_after:
|
status_count_after:
|
||||||
|
@ -310,7 +310,7 @@ ja:
|
||||||
all: すべて
|
all: すべて
|
||||||
limited: 制限あり
|
limited: 制限あり
|
||||||
title: モデレーション
|
title: モデレーション
|
||||||
title: 既知のインスタンス
|
title: 既知のサーバー
|
||||||
total_blocked_by_us: ブロック合計
|
total_blocked_by_us: ブロック合計
|
||||||
total_followed_by_them: 被フォロー合計
|
total_followed_by_them: 被フォロー合計
|
||||||
total_followed_by_us: フォロー合計
|
total_followed_by_us: フォロー合計
|
||||||
|
@ -392,8 +392,8 @@ ja:
|
||||||
desc_html: 複数のページに表示されます。サイズは293x205px以上推奨です。未設定の場合、標準のマスコットが使用されます
|
desc_html: 複数のページに表示されます。サイズは293x205px以上推奨です。未設定の場合、標準のマスコットが使用されます
|
||||||
title: マスコットイメージ
|
title: マスコットイメージ
|
||||||
peers_api_enabled:
|
peers_api_enabled:
|
||||||
desc_html: 連合内でこのインスタンスが遭遇したドメインの名前
|
desc_html: 連合内でこのサーバーが遭遇したドメインの名前
|
||||||
title: 接続しているインスタンスのリストを公開する
|
title: 接続しているサーバーのリストを公開する
|
||||||
preview_sensitive_media:
|
preview_sensitive_media:
|
||||||
desc_html: 他のウェブサイトにリンクを貼った際、メディアが閲覧注意としてマークされていてもサムネイルが表示されます
|
desc_html: 他のウェブサイトにリンクを貼った際、メディアが閲覧注意としてマークされていてもサムネイルが表示されます
|
||||||
title: OpenGraphによるプレビューで閲覧注意のメディアも表示する
|
title: OpenGraphによるプレビューで閲覧注意のメディアも表示する
|
||||||
|
@ -420,21 +420,21 @@ ja:
|
||||||
desc_html: ユーザーページにスタッフのバッジを表示します
|
desc_html: ユーザーページにスタッフのバッジを表示します
|
||||||
title: スタッフバッジを表示する
|
title: スタッフバッジを表示する
|
||||||
site_description:
|
site_description:
|
||||||
desc_html: フロントページへの表示に使用される紹介文です。このMastodonインスタンスを特徴付けることやその他重要なことを記述してください。HTMLタグ、特に<code><a></code> と <code><em></code>が使えます。
|
desc_html: フロントページへの表示に使用される紹介文です。このMastodonサーバーを特徴付けることやその他重要なことを記述してください。HTMLタグ、特に<code><a></code> と <code><em></code>が使えます。
|
||||||
title: インスタンスの説明
|
title: サーバーの説明
|
||||||
site_description_extended:
|
site_description_extended:
|
||||||
desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます
|
desc_html: あなたのサーバーにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます
|
||||||
title: カスタム詳細説明
|
title: カスタム詳細説明
|
||||||
site_short_description:
|
site_short_description:
|
||||||
desc_html: サイドバーと meta タグに表示されます。Mastodon とは何か、そしてこのサーバーの特別な何かを1段落で記述してください。空欄の場合、インスタンスの説明が使用されます。
|
desc_html: サイドバーと meta タグに表示されます。Mastodon とは何か、そしてこのサーバーの特別な何かを1段落で記述してください。空欄の場合、サーバーの説明が使用されます。
|
||||||
title: 短いインスタンスの説明
|
title: 短いサーバーの説明
|
||||||
site_terms:
|
site_terms:
|
||||||
desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます
|
desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます
|
||||||
title: カスタム利用規約
|
title: カスタム利用規約
|
||||||
site_title: インスタンスの名前
|
site_title: サーバーの名前
|
||||||
thumbnail:
|
thumbnail:
|
||||||
desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です
|
desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です
|
||||||
title: インスタンスのサムネイル
|
title: サーバーのサムネイル
|
||||||
timeline_preview:
|
timeline_preview:
|
||||||
desc_html: ランディングページに公開タイムラインを表示します
|
desc_html: ランディングページに公開タイムラインを表示します
|
||||||
title: タイムラインプレビュー
|
title: タイムラインプレビュー
|
||||||
|
@ -495,7 +495,7 @@ ja:
|
||||||
warning: このデータは気をつけて取り扱ってください。他の人と共有しないでください!
|
warning: このデータは気をつけて取り扱ってください。他の人と共有しないでください!
|
||||||
your_token: アクセストークン
|
your_token: アクセストークン
|
||||||
auth:
|
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: パスワード
|
change_password: パスワード
|
||||||
confirm_email: メールアドレスの確認
|
confirm_email: メールアドレスの確認
|
||||||
delete_account: アカウントの削除
|
delete_account: アカウントの削除
|
||||||
|
@ -513,7 +513,7 @@ ja:
|
||||||
cas: CAS
|
cas: CAS
|
||||||
saml: SAML
|
saml: SAML
|
||||||
register: 登録する
|
register: 登録する
|
||||||
register_elsewhere: 他のインスタンスで新規登録
|
register_elsewhere: 他のサーバーで新規登録
|
||||||
resend_confirmation: 確認メールを再送する
|
resend_confirmation: 確認メールを再送する
|
||||||
reset_password: パスワードを再発行
|
reset_password: パスワードを再発行
|
||||||
security: セキュリティ
|
security: セキュリティ
|
||||||
|
@ -549,7 +549,7 @@ ja:
|
||||||
description_html: あなたのアカウントに含まれるコンテンツは全て削除され、アカウントは無効化されます。これは恒久的なもので、<strong>取り消すことはできません</strong>。なりすましを防ぐために、同じユーザー名で再度登録することはできなくなります。
|
description_html: あなたのアカウントに含まれるコンテンツは全て削除され、アカウントは無効化されます。これは恒久的なもので、<strong>取り消すことはできません</strong>。なりすましを防ぐために、同じユーザー名で再度登録することはできなくなります。
|
||||||
proceed: アカウントを削除する
|
proceed: アカウントを削除する
|
||||||
success_msg: アカウントは正常に削除されました
|
success_msg: アカウントは正常に削除されました
|
||||||
warning_html: 削除が保証されるのはこのインスタンス上のコンテンツのみです。他のインスタンス等、外部に広く共有されたコンテンツについては痕跡が残ることがあります。また、現在接続できないサーバーや、あなたの更新を受け取らなくなったサーバーに対しては、削除は反映されません。
|
warning_html: 削除が保証されるのはこのサーバー上のコンテンツのみです。他のサーバー等、外部に広く共有されたコンテンツについては痕跡が残ることがあります。また、現在接続できないサーバーや、あなたの更新を受け取らなくなったサーバーに対しては、削除は反映されません。
|
||||||
warning_title: 共有されたコンテンツについて
|
warning_title: 共有されたコンテンツについて
|
||||||
directories:
|
directories:
|
||||||
directory: ディレクトリ
|
directory: ディレクトリ
|
||||||
|
@ -588,6 +588,10 @@ ja:
|
||||||
lists: リスト
|
lists: リスト
|
||||||
mutes: ミュート
|
mutes: ミュート
|
||||||
storage: メディア
|
storage: メディア
|
||||||
|
featured_tags:
|
||||||
|
add_new: 追加
|
||||||
|
errors:
|
||||||
|
limit: 注目のハッシュタグの上限に達しました
|
||||||
filters:
|
filters:
|
||||||
contexts:
|
contexts:
|
||||||
home: ホームタイムライン
|
home: ホームタイムライン
|
||||||
|
@ -606,7 +610,7 @@ ja:
|
||||||
title: 新規フィルターを追加
|
title: 新規フィルターを追加
|
||||||
followers:
|
followers:
|
||||||
domain: ドメイン
|
domain: ドメイン
|
||||||
explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 <strong>プライベート投稿は、あなたのフォロワーがいる全てのインスタンスに配信されます</strong>。 フォロワーのインスタンスの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。
|
explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 <strong>プライベート投稿は、あなたのフォロワーがいる全てのサーバーに配信されます</strong>。 フォロワーのサーバーの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。
|
||||||
followers_count: フォロワー数
|
followers_count: フォロワー数
|
||||||
lock_link: 承認制アカウントにする
|
lock_link: 承認制アカウントにする
|
||||||
purge: フォロワーから削除する
|
purge: フォロワーから削除する
|
||||||
|
@ -629,10 +633,16 @@ ja:
|
||||||
one: エラーが発生しました! 以下のエラーを確認してください
|
one: エラーが発生しました! 以下のエラーを確認してください
|
||||||
other: エラーが発生しました! 以下の%{count}個のエラーを確認してください
|
other: エラーが発生しました! 以下の%{count}個のエラーを確認してください
|
||||||
imports:
|
imports:
|
||||||
preface: 他のインスタンスでエクスポートされたファイルから、フォロー/ブロックした情報をこのインスタンス上のアカウントにインポートできます。
|
modes:
|
||||||
|
merge: 統合
|
||||||
|
merge_long: 現在のレコードを保持したまま新しいものを追加します
|
||||||
|
overwrite: 上書き
|
||||||
|
overwrite_long: 現在のレコードを新しいもので置き換えます
|
||||||
|
preface: 他のサーバーでエクスポートされたファイルから、フォロー/ブロックした情報をこのサーバー上のアカウントにインポートできます。
|
||||||
success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください
|
success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください
|
||||||
types:
|
types:
|
||||||
blocking: ブロックしたアカウントリスト
|
blocking: ブロックしたアカウントリスト
|
||||||
|
domain_blocking: 非表示にしたドメインリスト
|
||||||
following: フォロー中のアカウントリスト
|
following: フォロー中のアカウントリスト
|
||||||
muting: ミュートしたアカウントリスト
|
muting: ミュートしたアカウントリスト
|
||||||
upload: アップロード
|
upload: アップロード
|
||||||
|
@ -654,7 +664,7 @@ ja:
|
||||||
one: '1'
|
one: '1'
|
||||||
other: "%{count}"
|
other: "%{count}"
|
||||||
max_uses_prompt: 無制限
|
max_uses_prompt: 無制限
|
||||||
prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます
|
prompt: リンクを生成・共有してこのサーバーへの新規登録を受け付けることができます
|
||||||
table:
|
table:
|
||||||
expires_at: 有効期限
|
expires_at: 有効期限
|
||||||
uses: 使用
|
uses: 使用
|
||||||
|
@ -801,8 +811,9 @@ ja:
|
||||||
development: 開発
|
development: 開発
|
||||||
edit_profile: プロフィールを編集
|
edit_profile: プロフィールを編集
|
||||||
export: データのエクスポート
|
export: データのエクスポート
|
||||||
|
featured_tags: 注目のハッシュタグ
|
||||||
flavours: フレーバー
|
flavours: フレーバー
|
||||||
followers: 信頼済みのインスタンス
|
followers: 信頼済みのサーバー
|
||||||
import: データのインポート
|
import: データのインポート
|
||||||
migrate: アカウントの引っ越し
|
migrate: アカウントの引っ越し
|
||||||
notifications: 通知
|
notifications: 通知
|
||||||
|
@ -980,13 +991,13 @@ ja:
|
||||||
final_action: 始めましょう
|
final_action: 始めましょう
|
||||||
final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。'
|
final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。'
|
||||||
full_handle: あなたの正式なユーザー名
|
full_handle: あなたの正式なユーザー名
|
||||||
full_handle_hint: これは別のインスタンスからフォローしてもらったりメッセージのやり取りをする際に、友達に伝えるといいでしょう。
|
full_handle_hint: これは別のサーバーからフォローしてもらったりメッセージのやり取りをする際に、友達に伝えるといいでしょう。
|
||||||
review_preferences_action: 設定の変更
|
review_preferences_action: 設定の変更
|
||||||
review_preferences_step: 受け取りたいメールや投稿の公開範囲などの設定を必ず行ってください。不快でないならアニメーション GIF の自動再生を有効にすることもできます。
|
review_preferences_step: 受け取りたいメールや投稿の公開範囲などの設定を必ず行ってください。不快でないならアニメーション GIF の自動再生を有効にすることもできます。
|
||||||
subject: Mastodon へようこそ
|
subject: Mastodon へようこそ
|
||||||
tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じインスタンスの人がフォローしている人だけが含まれるので、それが全てではありません。
|
tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じサーバーの人がフォローしている人だけが含まれるので、それが全てではありません。
|
||||||
tip_following: 標準では自動でインスタンスの管理者をフォローしています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してください。
|
tip_following: 標準では自動でサーバーの管理者をフォローしています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してください。
|
||||||
tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じインスタンスにいる隣人のようなものです!
|
tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じサーバーにいる隣人のようなものです!
|
||||||
tip_mobile_webapp: もしモバイル端末のブラウザで Mastodon をホーム画面に追加できる場合、プッシュ通知を受け取ることができます。それはまるでネイティブアプリのように動作します!
|
tip_mobile_webapp: もしモバイル端末のブラウザで Mastodon をホーム画面に追加できる場合、プッシュ通知を受け取ることができます。それはまるでネイティブアプリのように動作します!
|
||||||
tips: 豆知識
|
tips: 豆知識
|
||||||
title: ようこそ、%{name} !
|
title: ようこそ、%{name} !
|
||||||
|
|
|
@ -602,6 +602,10 @@ pl:
|
||||||
lists: Listy
|
lists: Listy
|
||||||
mutes: Wyciszeni
|
mutes: Wyciszeni
|
||||||
storage: Urządzenie przechowujące dane
|
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:
|
filters:
|
||||||
contexts:
|
contexts:
|
||||||
home: Strona główna
|
home: Strona główna
|
||||||
|
@ -647,10 +651,16 @@ pl:
|
||||||
one: Coś jest wciąż nie tak! Przyjrzyj się poniższemu błędowi
|
one: Coś jest wciąż nie tak! Przyjrzyj się poniższemu błędowi
|
||||||
other: Coś jest wciąż nie tak! Przejrzyj poniższe błędy (%{count})
|
other: Coś jest wciąż nie tak! Przejrzyj poniższe błędy (%{count})
|
||||||
imports:
|
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.
|
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
|
success: Twoje dane zostały załadowane i zostaną niebawem przetworzone
|
||||||
types:
|
types:
|
||||||
blocking: Lista blokowanych
|
blocking: Lista blokowanych
|
||||||
|
domain_blocking: Lista zablokowanych domen
|
||||||
following: Lista śledzonych
|
following: Lista śledzonych
|
||||||
muting: Lista wyciszonych
|
muting: Lista wyciszonych
|
||||||
upload: Załaduj
|
upload: Załaduj
|
||||||
|
@ -826,6 +836,7 @@ pl:
|
||||||
development: Tworzenie aplikacji
|
development: Tworzenie aplikacji
|
||||||
edit_profile: Edytuj profil
|
edit_profile: Edytuj profil
|
||||||
export: Eksportowanie danych
|
export: Eksportowanie danych
|
||||||
|
featured_tags: Wyróżnione hashtagi
|
||||||
flavours: Odmiany
|
flavours: Odmiany
|
||||||
followers: Autoryzowani śledzący
|
followers: Autoryzowani śledzący
|
||||||
import: Importowanie danych
|
import: Importowanie danych
|
||||||
|
|
|
@ -37,8 +37,10 @@ en:
|
||||||
setting_skin: Reskins the selected Mastodon flavour
|
setting_skin: Reskins the selected Mastodon flavour
|
||||||
username: Your username will be unique on %{domain}
|
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
|
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:
|
imports:
|
||||||
data: CSV file exported from another Mastodon instance
|
data: CSV file exported from another Mastodon server
|
||||||
sessions:
|
sessions:
|
||||||
otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:'
|
otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:'
|
||||||
user:
|
user:
|
||||||
|
@ -112,6 +114,8 @@ en:
|
||||||
username: Username
|
username: Username
|
||||||
username_or_email: Username or Email
|
username_or_email: Username or Email
|
||||||
whole_word: Whole word
|
whole_word: Whole word
|
||||||
|
featured_tag:
|
||||||
|
name: Hashtag
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Block notifications from non-followers
|
must_be_follower: Block notifications from non-followers
|
||||||
must_be_following: Block notifications from people you don't follow
|
must_be_following: Block notifications from people you don't follow
|
||||||
|
|
|
@ -33,11 +33,14 @@ ja:
|
||||||
setting_display_media_show_all: 閲覧注意としてマークされたメディアも常に表示する
|
setting_display_media_show_all: 閲覧注意としてマークされたメディアも常に表示する
|
||||||
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
|
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
|
||||||
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
|
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
|
||||||
|
setting_show_application: トゥートするのに使用したアプリがトゥートの詳細ビューに表示されるようになります
|
||||||
setting_theme: ログインしている全てのデバイスで適用されるデザインです。
|
setting_theme: ログインしている全てのデバイスで適用されるデザインです。
|
||||||
username: あなたのユーザー名は %{domain} の中で重複していない必要があります
|
username: あなたのユーザー名は %{domain} の中で重複していない必要があります
|
||||||
whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります
|
whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります
|
||||||
|
featured_tag:
|
||||||
|
name: 'これらを使うといいかもしれません:'
|
||||||
imports:
|
imports:
|
||||||
data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい
|
data: 他の Mastodon サーバーからエクスポートしたCSVファイルを選択して下さい
|
||||||
sessions:
|
sessions:
|
||||||
otp: '携帯電話のアプリで生成された二段階認証コードを入力するか、リカバリーコードを使用してください:'
|
otp: '携帯電話のアプリで生成された二段階認証コードを入力するか、リカバリーコードを使用してください:'
|
||||||
user:
|
user:
|
||||||
|
@ -101,6 +104,7 @@ ja:
|
||||||
setting_hide_network: 繋がりを隠す
|
setting_hide_network: 繋がりを隠す
|
||||||
setting_noindex: 検索エンジンによるインデックスを拒否する
|
setting_noindex: 検索エンジンによるインデックスを拒否する
|
||||||
setting_reduce_motion: アニメーションの動きを減らす
|
setting_reduce_motion: アニメーションの動きを減らす
|
||||||
|
setting_show_application: トゥートの送信に使用したアプリを開示する
|
||||||
setting_system_font_ui: システムのデフォルトフォントを使う
|
setting_system_font_ui: システムのデフォルトフォントを使う
|
||||||
setting_theme: サイトテーマ
|
setting_theme: サイトテーマ
|
||||||
setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
|
setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
|
||||||
|
@ -109,6 +113,8 @@ ja:
|
||||||
username: ユーザー名
|
username: ユーザー名
|
||||||
username_or_email: ユーザー名またはメールアドレス
|
username_or_email: ユーザー名またはメールアドレス
|
||||||
whole_word: 単語全体にマッチ
|
whole_word: 単語全体にマッチ
|
||||||
|
featured_tag:
|
||||||
|
name: ハッシュタグ
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: フォロワー以外からの通知をブロック
|
must_be_follower: フォロワー以外からの通知をブロック
|
||||||
must_be_following: フォローしていないユーザーからの通知をブロック
|
must_be_following: フォローしていないユーザーからの通知をブロック
|
||||||
|
|
|
@ -33,9 +33,12 @@ pl:
|
||||||
setting_display_media_show_all: Zawsze pokazuj zawartość multimedialną jako wrażliwą
|
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_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_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
|
setting_skin: Zmienia wygląd używanej odmiany Mastodona
|
||||||
username: Twoja nazwa użytkownika będzie niepowtarzalna na %{domain}
|
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ń
|
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:
|
imports:
|
||||||
data: Plik CSV wyeksportowany z innej instancji Mastodona
|
data: Plik CSV wyeksportowany z innej instancji Mastodona
|
||||||
sessions:
|
sessions:
|
||||||
|
@ -102,6 +105,7 @@ pl:
|
||||||
setting_hide_network: Ukryj swoją sieć
|
setting_hide_network: Ukryj swoją sieć
|
||||||
setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
|
setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
|
||||||
setting_reduce_motion: Ogranicz ruch w animacjach
|
setting_reduce_motion: Ogranicz ruch w animacjach
|
||||||
|
setting_show_application: Informuj o aplikacji z której wysłano wpisy
|
||||||
setting_skin: Motyw
|
setting_skin: Motyw
|
||||||
setting_system_font_ui: Używaj domyślnej czcionki systemu
|
setting_system_font_ui: Używaj domyślnej czcionki systemu
|
||||||
setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia
|
setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia
|
||||||
|
@ -110,6 +114,8 @@ pl:
|
||||||
username: Nazwa użytkownika
|
username: Nazwa użytkownika
|
||||||
username_or_email: Nazwa użytkownika lub adres e-mail
|
username_or_email: Nazwa użytkownika lub adres e-mail
|
||||||
whole_word: Całe słowo
|
whole_word: Całe słowo
|
||||||
|
featured_tag:
|
||||||
|
name: Hashtag
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Nie wyświetlaj powiadomień od osób, które Cię nie śledzą
|
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
|
must_be_following: Nie wyświetlaj powiadomień od osób, których nie śledzisz
|
||||||
|
|
|
@ -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|
|
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 :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 :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 :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}
|
settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
|
||||||
|
|
|
@ -74,6 +74,7 @@ Rails.application.routes.draw do
|
||||||
get '/@:username', to: 'accounts#show', as: :short_account
|
get '/@:username', to: 'accounts#show', as: :short_account
|
||||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
||||||
get '/@:username/media', to: 'accounts#show', as: :short_account_media
|
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', to: 'statuses#show', as: :short_account_status
|
||||||
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_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]
|
resource :migration, only: [:show, :update]
|
||||||
|
|
||||||
resources :sessions, only: [:destroy]
|
resources :sessions, only: [:destroy]
|
||||||
|
resources :featured_tags, only: [:index, :create, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :media, only: [:show] do
|
resources :media, only: [:show] do
|
||||||
|
|
|
@ -7,6 +7,7 @@ class CreateAccountModerationNotes < ActiveRecord::Migration[5.1]
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id
|
add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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
|
16
db/schema.rb
16
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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"
|
t.index ["status_id"], name: "index_favourites_on_status_id"
|
||||||
end
|
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|
|
create_table "follow_requests", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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.integer "data_file_size"
|
||||||
t.datetime "data_updated_at"
|
t.datetime "data_updated_at"
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
|
t.boolean "overwrite", default: false, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "invites", force: :cascade do |t|
|
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 "custom_filters", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", 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 "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", 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 "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
|
||||||
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Fabricator(:featured_tag) do
|
||||||
|
account
|
||||||
|
tag
|
||||||
|
statuses_count 1_337
|
||||||
|
last_status_at Time.now.utc
|
||||||
|
end
|
|
@ -74,6 +74,7 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'given a URL with a query string' do
|
context 'given a URL with a query string' do
|
||||||
|
context 'with escaped unicode character' do
|
||||||
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
|
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
|
||||||
|
|
||||||
it 'matches the full URL' do
|
it 'matches the full URL' do
|
||||||
|
@ -81,6 +82,31 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
context 'given a URL with parentheses in it' do
|
context 'given a URL with parentheses in it' do
|
||||||
let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
|
let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
|
||||||
|
|
||||||
|
@ -89,6 +115,22 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
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
|
context 'given a URL with Japanese path string' do
|
||||||
let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
|
let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
|
||||||
|
|
||||||
|
@ -105,6 +147,22 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
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
|
context 'given a URL with Simplified Chinese path string' do
|
||||||
let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
|
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
|
context 'given a URL containing unsafe code (XSS attack, visible part)' do
|
||||||
let(:text) { %q{http://example.com/b<del>b</del>} }
|
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>'
|
is_expected.to include '<del>b</del>'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -132,7 +194,11 @@ RSpec.describe Formatter do
|
||||||
context 'given a URL containing unsafe code (XSS attack, invisible part)' 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>} }
|
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>'
|
is_expected.to include '<script>alert("Hello")</script>'
|
||||||
end
|
end
|
||||||
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>'
|
is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
describe '#format_spoiler' do
|
describe '#format_spoiler' do
|
||||||
|
|
|
@ -244,9 +244,9 @@ describe AccountInteractions do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#block_domain!' do
|
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
|
it 'creates and returns AccountDomainBlock' do
|
||||||
expect do
|
expect do
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue